diff --git a/.editorconfig b/.editorconfig index d82e66309b..2e438f15e1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,240 +1,198 @@ -############################### -# 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 +############################### +# Core EditorConfig Options # +############################### +root = true + +# All files +[*] +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 + +# util shell scripts +[*.sh] +end_of_line = lf + +[*.{cmd,bat}] +end_of_line = crlf + +# Code files +[*.cs] +indent_size = 4 +insert_final_newline = true +charset = utf-8 +end_of_line = crlf + +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] + +# MPL file header +file_header_template = This Source Code form is subject to the terms of the Mozilla Public\nLicense, v. 2.0. If a copy of the MPL was not distributed with this\nfile, You can obtain one at https://mozilla.org/MPL/2.0/. + +# Organize usings +dotnet_sort_system_directives_first = true:error +dotnet_separate_import_directive_groups = true +csharp_qualified_using_at_nested_scope = true:error +# usings before namespace +csharp_using_directive_placement = outside_namespace:error +# Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = error + +# don't do double newlines or pointless newlines +dotnet_diagnostic.IDE2000.severity = error +dotnet_diagnostic.IDE2002.severity = error + +# Yell at people when they don't include the file header template +dotnet_diagnostic.IDE0073.severity = error + +# this. preferences +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:silent +dotnet_style_qualification_for_method = true:silent +dotnet_style_qualification_for_event = true:silent + +# 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 = true: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 + +############################### +# 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 + +# Force namespaces to match their folder names +# We'll override this later in folders where we don't want this +dotnet_style_namespace_match_folder = true:error + +############################### +# 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 + +# 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:warning + +# 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 # +############################### + +# 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 +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 = true +csharp_preserve_single_line_blocks = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..2a64a7c366 --- /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/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..05cce13f4c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# for the time being, i want to be notified of every PR to v6 +* @akiraveliara \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index d600dcc199..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,17 +0,0 @@ -# These are supported funding model platforms - -#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -github: - - Naamloos - - Neuheit -patreon: OoLunar -#open_collective: # Replace with a single Open Collective username -#ko_fi: # Replace with a single Ko-fi username -#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -#liberapay: # Replace with a single Liberapay username -#issuehunt: # Replace with a single IssueHunt username -#otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] - - https://ko-fi.com/naamloos -# insert Neuheit here diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index a3f93c9016..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Bug Report -description: File a report for a bug in the library. Make sure to open one issue per bug. -labels: ["bug"] -body: - - type: markdown - attributes: - value: | - First, give a brief description of what's going wrong. Are you seeing errors you don't think you should be seeing? Is the library doing something - unexpected? Are you getting Discord to yell at you because you exceeded ratelimits? Keep in mind that this description will likely decide the - priority and effort spent on your issue. - - type: textarea - id: summary - attributes: - label: Summary - placeholder: What is going wrong? - validations: - required: true - - type: markdown - attributes: - value: | - Next, we'll take a look at some circumstancial information, like versions and operating systems, that will help us diagnose the issue. - - type: dropdown - id: version - attributes: - label: What version of the library are you using? - multiple: false - options: - - v4.5.1 (Stable) - - v5.0.0-nightly (make sure you are using the latest nightly!) - validations: - required: true - - type: dropdown - id: dotnet-version - attributes: - label: What .NET version are you using? Make sure to use the latest patch release for your major version. - multiple: false - options: - - .NET 8.0 - - .NET 9.0 - - .NET 10.0 - - .NET 11.0 Preview - validations: - required: true - - type: input - id: operating-system - attributes: - label: Operating System - placeholder: The operating system your bot was running on when you encountered the issue, eg. Windows 11 23H2 or Alpine Linux 3.19. - - type: markdown - attributes: - value: | - Now, we'll look at more specific information. This will further help us diagnose, reproduce and finally fix the issue. Please provide as much detail - as possible here, but be careful to not leak any sensitive information, such as tokens. - - type: textarea - id: repro - attributes: - label: Reproduction Steps - placeholder: Anything that can help us reproduce the issue. A step-by-step guide to reproducing the problem, or your code that caused the issue. - validations: - required: true - - type: textarea - id: logs - attributes: - label: Trace Logs - placeholder: The relevant section from a trace log. You can attach a full trace log as a file, or if you are uncomfortable sharing it on GitHub, you can contact us on Discord. - render: log - - type: textarea - id: exception - attributes: - label: Exceptions or other error messages - placeholder: The exception message and stacktrace, other error messages printed out by DSharpPlus, or something else in a similar vein. - render: txt - - type: textarea - id: anything-else - attributes: - label: Anything else you'd like to share - placeholder: This is where anything else related to your issue goes. A self-diagnosis, additional notes, or changes you made to library mechanics such as the websocket client. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 3350216546..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Feature Request -description: You have a feature you'd like implemented? There's a Discord feature we don't yet implement? Let us know! -labels: ["enhancement"] -body: - - type: markdown - attributes: - value: | - Describe the feature you want to request. This may include library convenience features, Discord features or your favourite shortcut for - writing more concise commands. Keep in mind that this description may decide the priority of your feature request. - - type: textarea - id: description - attributes: - label: Description - placeholder: Your feature request, described in detail. - validations: - required: true - - type: dropdown - id: libraries - attributes: - label: Specify the libraries you want this feature request to affect - multiple: true - options: - - DSharpPlus, the core library - - DSharpPlus.Commands - - DSharpPlus.Interactivity - - DSharpPlus.Lavalink - - DSharpPlus.Rest - - DSharpPlus.VoiceNext - validations: - required: true - - type: markdown - attributes: - value: | - Now, let's look at what you may have considered before making this feature request. Any alternatively considered designs, reasons why the current - implementation is insufficient, or anything else you'd like to tell us about your feature request. - - type: textarea - id: additional - attributes: - label: Other considerations - placeholder: Anything you considered or would like to tell us. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 0b3981ca5b..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,22 +0,0 @@ -Make sure you familiarize yourself with our contributing guidelines. (delete this line afterwards) - -- [ ] This pull request was written by AI -- [ ] This pull request was assisted by AI, but you wrote the final code -- [ ] This pull request did not involve AI in any way - -# Summary -Fixes #issue/Resolves #issue/Implements functionality. Short description of changes. - -# Details -You can put detailed description of the changes in here. - -# Changes proposed -* Outline -* Your -* Changes -* Here - -# Notes -Any additional notes go here. - -- [ ] All features in this pull request were tested. \ No newline at end of file diff --git a/.github/workflows/build-alpha.yml b/.github/workflows/build-alpha.yml deleted file mode 100644 index 1db4981475..0000000000 --- a/.github/workflows/build-alpha.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Build Alpha Version - -on: - push: - branches: - - alpha - -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 Alpha - runs-on: ubuntu-latest - 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: Package Project - run: | - dotnet pack -c Release -o build -p:Alpha=${{ github.run_number }} - dotnet nuget push "build/*" --skip-duplicate -k ${{ secrets.NUGET_ORG_API_KEY }} -s https://api.nuget.org/v3/index.json - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: DSharpPlus-Alpha-${{ github.run_number }}.zip - path: ./build/* \ No newline at end of file diff --git a/.github/workflows/build-commit.yml b/.github/workflows/build-commit.yml deleted file mode 100644 index b6d2ba92f6..0000000000 --- a/.github/workflows/build-commit.yml +++ /dev/null @@ -1,112 +0,0 @@ -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 install -g docfx --version 2.78.3 --allow-downgrade - 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 deleted file mode 100644 index ca91c500e6..0000000000 --- a/.github/workflows/build-pr.yml +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index b56a758d1c..0000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,92 +0,0 @@ -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..a6f497d62c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,490 +1,412 @@ -## 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 Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# 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 + +# Credentials for the DSharpPlus.Test bot +DSharpPlus.Test/config.json + +# Github Codespaces configuration file +.devcontainer/devcontainer.json + +# Docfx +docs/_site + +# tool test environment +.testenv +.testenvhost +/.vscode/settings.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..8776756586 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "bundles"] + path = lib/bundles + url = https://github.com/dsharpplus/bundles.git +[submodule "etf"] + path = lib/etf + url = https://github.com/discord-lib-common/etfkit.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..cbdaf98663 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: +- repo: https://github.com/dotnet/format + rev: "v5.1.225507" + hooks: + - id: dotnet-format + name: dotnet-format + language: system + entry: dotnet format --include + types_or: ["c#"] \ No newline at end of file diff --git a/BUILDING.md b/BUILDING.md deleted file mode 100644 index 7ea072e268..0000000000 --- a/BUILDING.md +++ /dev/null @@ -1,85 +0,0 @@ -# Building DSharpPlus - -These are detailed instructions on how to build the DSharpPlus library under various environmnets. - -It is recommended you have prior experience with multi-target .NET Core/Standard projects, as well as the `dotnet` CLI utility, and MSBuild. - -## Requirements - -In order to build the library, you will first need to install some software. - -### Windows - -On Windows, we only officially support Visual Studio 2017 15.3 or newer. Visual Studio Code and other IDEs might work, but are generally not supported or even guaranteed to work properly. - -* **Windows 10** - while we support running the library on Windows 7 and above, we only support building on Windows 10. -* [**Git for Windows**](https://git-scm.com/download/win) - required to clone the repository. -* [**Visual Studio 2017**](https://www.visualstudio.com/downloads/) - community edition or better. We do not support Visual Studio 2015 and older. Note that to build the library, you need Visual Studio 2017 version 15.3 or newer. - * **Workloads**: - * **.NET Framework Desktop** - required to build .NETFX (4.5, 4.6, and 4.7 targets) - * **.NET Core Cross-Platform Development** - required to build .NET Standard targets (1.1, 1.3, and 2.0) and the project overall. - * **Individual Components**: - * **.NET Framework 4.5 SDK** - required for .NETFX 4.5 target - * **.NET Framework 4.6 SDK** - required for .NETFX 4.6 target - * **.NET Framework 4.7 SDK** - required for .NETFX 4.7 target -* [**.NET Core SDK 2.0**](https://www.microsoft.com/net/download) - required to build the project. -* **Windows PowerShell** - required to run the build scripts. You need to make sure your script execution policy allows execution of unsigned scripts. - -### GNU/Linux - -On GNU/Linux, we support building via Visual Studio Code and .NET Core SDK. Other IDEs might work, but are not supported or guaranteed to work properly. - -While these should apply to any modern distribution, we only test against Debian 10. Your mileage may vary. - -When installing the below, make sure you install all the dependencies properly. We might ship a build environmnent as a docker container in the future. - -* **Any modern GNU/Linux distribution** - like Debian 9. -* **Git** - to clone the repository. -* [**Visual Studio Code**](https://code.visualstudio.com/Download) - a recent version is required. - * **C# for Visual Studio Code (powered by OmniSharp)** - required for syntax highlighting and basic Intellisense -* [**.NET Core SDK 2.0**](https://www.microsoft.com/net/download) - required to build the project. -* [**Mono 5.x**](http://www.mono-project.com/download/#download-lin) - required to build the .NETFX 4.5, 4.6, and 4.7 targets, as well as to build the docs. -* [**PowerShell Core**](https://docs.microsoft.com/en-us/powershell/scripting/setup/Installing-PowerShell-Core-on-macOS-and-Linux?view=powershell-6) - required to execute the build scripts. -* **p7zip-full** - required to package docs. - -## Instructions - -Once you install all the necessary prerequisites, you can proceed to building. These instructions assume you have already cloned the repository. - -### Windows - -Building on Windows is relatively easy. There's 2 ways to build the project: - -#### Building through Visual Studio - -Building through Visual Studio yields just binaries you can use in your projects. - -1. Open the solution in Visual Studio. -2. Set the configuration to Release. -3. Select Build > Build Solution to build the project. -4. Select Build > Publish DSharpPlus to publish the binaries. - -#### Building with the build script - -Building this way outputs NuGet packages, and a documentation package. Ensure you have an internet connection available, as the script will install programs necessary to build the documentation. - -1. Open PowerShell and navigate to the directory which you cloned DSharpPlus to. -2. Execute `.\rebuild-all.ps1 -configuration Release` and wait for the script to finish execution. -3. Once it's done, the artifacts will be available in *dsp-artifacts* directory, next to the directory to which the repository is cloned. - -### GNU/Linux - -When all necessary prerequisites are installed, you can proceed to building. There are technically 2 ways to build the library, though both of them perform the same steps, they are just invoked slightly differently. - -#### Through Visual Studio Code - -1. Open Visual Studio Code and open the folder to which you cloned DSharpPlus as your workspace. -2. Select Build > Run Task... -3. Select `buildRelease` task and wait for it to finish. -4. The artifacts will be placed in *dsp-artifacts* directory, next to whoch the repository is cloned. - -#### Through PowerShell - -1. Open PowerShell (`pwsh`) and navigate to the directory which you cloned DSharpPlus to. -2. Execute `.\rebuild-all.ps1 -configuration Release` and wait for the script to finish execution. -3. Once it's done, the artifacts will be available in *dsp-artifacts* directory, next to the directory to which the repository is cloned. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index d02571f90c..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,78 +0,0 @@ - -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at our Discord server. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 6be1773232..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,146 +0,0 @@ -# Contributing to DSharpPlus - -We're really happy to accept contributions. However we also ask that you follow several rules when doing so. - -> [!NOTE] -> We do not merge pull requests for undocumented or in-PR Discord features. Before we merge support for any Discord feature, we require the relevant docs PR to be merged first. - -# Authorship - -We require that you own the intellectual property to any contribution you choose to submit. Don't take code from other people, and don't submit AI-generated code. Additionally, please test your contributions before submitting them. - -# Proper base - -When opening a PR, please make sure your branch targets the latest master branch. Also make sure your branch is even with the target branch, to avoid unnecessary surprises. - -# Versioning - -We generally attempt to follow [semantic versioning](https://semver.org/) when it comes to pushing stable releases. Ideally, this means you should only be creating PRs for `patch` and `minor` changes. If you wish to introduce a `major` (breaking) change, please discuss it beforehand so we can determine how to integrate it into our next major version. If this involves removing a public facing property/method, mark it with the `Obsolete` attribute instead on the latest release branch. - -We may make exceptions to this rule to ease adoption of Discord features and/or changes. In particular, we allow minor versions to break existing code if the scope of such breaks is limited or the change is considered crucially important. - -# Proper titles - -When opening issues, make sure the title reflects the purpose of the issue or the pull request. Prefer past tense, and be brief. Further description belongs inside the issue or PR. - -# New additions - -When adding new features that do not correspond to API features, please attempt to add tests for them. Our tests follow a specific naming convention. If any changes are made to the `DSharpPlus.Commands` namespace, then the tests for those will be found in the `DSharpPlus.Tests.Commands` namespace and directory. - -# Descriptive changes - -We require the commits describe the change made. It can be a short description. If you fixed or resolved an open issue, please refer to it using Github's # links. - -Examples of good commit messages: - -* `Fixed a potential memory leak with cache entities. Fixes #142.` -* `Implemented new command extension. Resolves #169.` -* `Changed message cache behaviour. It's now global instead of per-channel.` -* `Fixed a potential NRE.` - -* ``` - Changed message cache behaviour: - - - Messages are now stored globally. - - Cache now deletes messages when they are deleted from discord. - - Cache itself is now a ring buffer. - ``` - -Examples of bad commit messages: - -* `I a bad.` -* `Tit and tat.` -* `Fixed.` -* `Oops.` - -# Code style - -We use [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) throughout the repository, with a series of exceptions: - -* Preference of `this`. While this one is not required, it's ill-advised to remove the existing instances thereof. -* In the same vein, we prefer you not use underscores for private fields in conjunction to using `this`. It is, again, ill-advised to go out of your way to change existing code. -* Do not use `var`. Always specify the type name. -* Do not use `ConfigureAwait(false)`. Other ConfigureAwait overloads may be warranted. -* Use the LINQ methods as opposed to the keyword constructs. -* Use file-scoped namespaces, and place using directives outside of them (ideally, order System.* directives first and separate groups of directives with empty lines). -* When working with async code, always await any tasks for the sake of good stack traces. For example: - - ```cs - public async Task DoSomethingAsync() - { - await this.DoAnotherThingAsync(); - } - - public async Task DoAnotherThingAsync() - { - Console.WriteLine("42"); - await this.DoYetAnotherThingAsync(42); - } - - public async Task DoYetAnotherThingAsync(int num) - { - if (num == 42) - await SuperAwesomeMethodAsync(); - } - ``` - -In addition to these, we also have several preferences: - -* Use initializer syntax or collection expressions when possible: - - ```cs - Class a = new() - { - StringNumber = "fourty-two", - Number = 42 - }; - - Dictionary b = new() - { - ["fourty-two"] = 42, - ["sixty-nine"] = 69 - }; - - int[] c = [42, 69]; - ``` - -* Inline `out` declarations when possible: `SomeOutMethod(42, out string stringified);` -* Members in classes should be ordered as follows (with few exceptions): - * Public `const` fields. - * Non-public `const` fields. - * Public static properties. - * Public static fields. - * Non-public static properties. - * Non-public static fields. - * Public properties. - * Public fields. - * Private properties. - * Private fields. - * Static constructor. - * Public constructors. - * Non-public constructors. - * Public methods (with the exception of methods overriden from `System.Object`). - * Non-public methods. - * Methods overriden from `System.Object`. - * Public static methods. - * Non-public static methods. - * Operator overloads. - * Public events. - * Non-public events. - -Use your own best judgement with regards to this ordering, and prefer intuitiveness over strict adherence. - -# Code changes - -One of our requirements is that all code change commits must build successfully. This is verified by our CI. When you open a pull request, Github will start an action which will perform a build and create PR artifacts. You can view its summary by visiting it from the checks section on -the PR overview page. - -PRs that do not build will not be accepted. - -Furthermore we require that methods you implement on Discord entities have a reflection in the Discord API, and that such entities must be documented in the currently live documentation (PR documentation does not count). - -In the event your code change is a style change, XML doc change, or otherwise does not change how the code works, tag the commit with `[ci skip]`. - -# Non-code changes - -In the event you change something outside of code (i.e. a meta-change or documentation), you must tag your commit with `[ci skip]`. diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/DSharpPlus.Analyzers.Test.csproj b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/DSharpPlus.Analyzers.Test.csproj deleted file mode 100644 index a7d8d64ec8..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/DSharpPlus.Analyzers.Test.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - false - net9.0 - true - true - false - - - - - - - - - - - - - - - - - - diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/HasPermissionTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/HasPermissionTest.cs deleted file mode 100644 index 24b01ae248..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/HasPermissionTest.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Core; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Core.HasPermissionAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier ->; - -namespace DSharpPlus.Analyzers.Test; - -public class HasPermissionTest -{ - /// - /// Unit test to see if HasPermissionAnalyzer reports - /// - [Test] - public static async Task HasPermissionNotEquals_DiagnosticAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class DoesIt - { - public static bool HaveAdmin(DiscordPermission perm) - { - if ((perm & DiscordPermission.Administrator) != 0) - { - return true; - } - return false; - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(7, 13) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'perm.HasPermission(DiscordPermission.Administrator)' instead") - ); - - await test.RunAsync(); - } - - [Test] - public async Task HasPermissionEquals_DiagnosticAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class DoesIt - { - public static bool HaveNoAdmin(DiscordPermission perm) - { - if ((perm & DiscordPermission.Administrator) == 0) - { - return true; - } - return false; - } - } - - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(7, 13) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'perm.HasPermission(DiscordPermission.Administrator)' instead") - ); - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/MultipleOverwriteTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/MultipleOverwriteTest.cs deleted file mode 100644 index a90cd037a3..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/MultipleOverwriteTest.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Core; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Core.MultipleOverwriteAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier ->; - -namespace DSharpPlus.Analyzers.Test; - -/// -/// -/// -public static class MultipleOverwriteTest -{ - /// - /// Single diagnostic report for multiple overwrite analyzer - /// - [Test] - public static async Task DiagnosticAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - using System.Threading.Tasks; - - public class OverwriteTest - { - public async Task AddOverwritesAsync(DiscordChannel channel, DiscordMember member, DiscordMember member2) - { - await channel.AddOverwriteAsync(member, DiscordPermission.BanMembers); - await channel.AddOverwriteAsync(member2, DiscordPermission.KickMembers); - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(9, 15) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use one 'channel.ModifyAsync(..)' instead of multiple 'channel.AddOverwriteAsync(..)'") - ); - await test.RunAsync(); - } - - [Test] - public static async Task MultipleErrorsScenarioTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - using System.Threading.Tasks; - - public class OverwriteTest - { - public async Task AddOverwritesAsync(DiscordChannel channel, DiscordMember member, DiscordMember member2) - { - await channel.AddOverwriteAsync(member, DiscordPermission.BanMembers); - await channel.AddOverwriteAsync(member2, DiscordPermission.KickMembers); - await channel.AddOverwriteAsync(member2, DiscordPermission.Administrator); - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(9, 15) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use one 'channel.ModifyAsync(..)' instead of multiple 'channel.AddOverwriteAsync(..)'") - ); - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(10, 15) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use one 'channel.ModifyAsync(..)' instead of multiple 'channel.AddOverwriteAsync(..)'") - ); - - await test.RunAsync(); - } - - [Test] - public static async Task ForEachLoopDiagnosticAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - using System.Threading.Tasks; - using System.Collections.Generic; - - public class OverwriteTest - { - public async Task AddOverwritesAsync(DiscordChannel channel, List members) - { - foreach (DiscordMember member in members) - { - await channel.AddOverwriteAsync(member, DiscordPermission.BanMembers); - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(11, 19) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use one 'channel.ModifyAsync(..)' instead of multiple 'channel.AddOverwriteAsync(..)'") - ); - - await test.RunAsync(); - } - - [Test] - public static async Task ForLoopDiagnosticAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - using System.Threading.Tasks; - using System.Collections.Generic; - - public class OverwriteTest - { - public async Task AddOverwritesAsync(DiscordChannel channel, List members) - { - for (int i = 0; i < members.Count; i++) - { - await channel.AddOverwriteAsync(members[i], DiscordPermission.BanMembers); - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(11, 19) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use one 'channel.ModifyAsync(..)' instead of multiple 'channel.AddOverwriteAsync(..)'") - ); - - await test.RunAsync(); - } - - [Test] - public static async Task WhileLoopDiagnosticAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - using System.Threading.Tasks; - using System.Collections.Generic; - - public class OverwriteTest - { - public async Task AddOverwritesAsync(DiscordChannel channel, List members) - { - int i = 0; - while (i < members.Count) - { - await channel.AddOverwriteAsync(members[i], DiscordPermission.BanMembers); - i++; - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(12, 19) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use one 'channel.ModifyAsync(..)' instead of multiple 'channel.AddOverwriteAsync(..)'") - ); - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/ProcessorCheckTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/ProcessorCheckTest.cs deleted file mode 100644 index 866c2c1919..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/ProcessorCheckTest.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Commands; -using DSharpPlus.Commands; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Commands.ProcessorCheckAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier ->; - -namespace DSharpPlus.Analyzers.Test; - -public static class ProcessorCheckTest -{ - [Test] - public static async Task DiagnosticTestAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - test.TestState.AdditionalReferences.Add(typeof(CommandContext).Assembly); - - test.TestCode = """ - using System.Threading.Tasks; - using DSharpPlus.Commands.Trees.Metadata; - using DSharpPlus.Commands.Processors.TextCommands; - using DSharpPlus.Commands.Processors.SlashCommands; - - public class Test - { - [AllowedProcessors()] - public async Task Tester(TextCommandContext context) - { - await context.RespondAsync("Tester!"); - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(9, 30) - .WithSeverity(DiagnosticSeverity.Error) - .WithMessage("All provided processors does not support context 'TextCommandContext'") - ); - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/RegisterNestedClassesTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/RegisterNestedClassesTest.cs deleted file mode 100644 index 1ed12205a7..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/RegisterNestedClassesTest.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Commands; -using DSharpPlus.Commands; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Commands.RegisterNestedClassesAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier ->; - -namespace DSharpPlus.Analyzers.Test; - -public static class RegisterNestedClassesTest -{ - [Test] - public static async Task TestNormalScenarioAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - test.TestState.AdditionalReferences.Add(typeof(CommandContext).Assembly); - - test.TestCode = """ - using System.Threading.Tasks; - using DSharpPlus.Commands; - - public class Test - { - public static void Register(CommandsExtension extension) - { - extension.AddCommands([typeof(ACommands.BCommands), typeof(ACommands)]); - } - } - - [Command("a")] - public class ACommands - { - [Command("b")] - public class BCommands - { - [Command("c")] - public static async ValueTask CAsync(CommandContext context) - { - await context.RespondAsync("C"); - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(8, 39) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Don't register 'BCommands', register 'ACommands' instead") - ); - - await test.RunAsync(); - } - - [Test] - public static async Task TestListScenarioAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - test.TestState.AdditionalReferences.Add(typeof(CommandContext).Assembly); - - test.TestCode = """ - using System; - using System.Threading.Tasks; - using System.Collections.Generic; - using DSharpPlus.Commands; - - public class Test - { - public static void Register(CommandsExtension extension) - { - List types = new() { typeof(ACommands.BCommands), typeof(ACommands) }; - extension.AddCommands(types); - } - } - - [Command("a")] - public class ACommands - { - [Command("b")] - public class BCommands - { - [Command("c")] - public static async ValueTask CAsync(CommandContext context) - { - await context.RespondAsync("C"); - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(10, 43) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Don't register 'BCommands', register 'ACommands' instead") - ); - - await test.RunAsync(); - } - - [Test] - public static async Task TestArrayScenarioAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - test.TestState.AdditionalReferences.Add(typeof(CommandContext).Assembly); - - test.TestCode = """ - using System; - using System.Threading.Tasks; - using DSharpPlus.Commands; - - public class Test - { - public static void Register(CommandsExtension extension) - { - Type[] types = new[] { typeof(ACommands.BCommands), typeof(ACommands) }; - extension.AddCommands(types); - } - } - - [Command("a")] - public class ACommands - { - [Command("b")] - public class BCommands - { - [Command("c")] - public static async ValueTask CAsync(CommandContext context) - { - await context.RespondAsync("C"); - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(9, 39) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Don't register 'BCommands', register 'ACommands' instead") - ); - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/SingleEntityGetTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/SingleEntityGetTest.cs deleted file mode 100644 index 71ab222994..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/SingleEntityGetTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Core; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Core.SingleEntityGetRequestAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier ->; - -namespace DSharpPlus.Analyzers.Test; - -/// -/// -/// -public static class SingleEntityGetTest -{ - [Test] - public static async Task DiagnosticTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using System; - using System.Threading.Tasks; - using System.Collections.Generic; - using DSharpPlus.Entities; - - public class Test - { - public async Task SomeLoopery(IEnumerable ids, DiscordChannel channel) - { - foreach (ulong id in ids) - { - DiscordMessage message = await channel.GetMessageAsync(id); - Console.WriteLine($"Author is: {message.Author.Username}"); - } - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(12, 44) - .WithSeverity(DiagnosticSeverity.Info) - .WithMessage( - "Use 'channel.GetMessagesAsync()' outside of the loop instead of 'channel.GetMessageAsync(id)' inside the loop") - ); - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/UseDiscordPermissionsTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/UseDiscordPermissionsTest.cs deleted file mode 100644 index 1f99e8f702..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/UseDiscordPermissionsTest.cs +++ /dev/null @@ -1,381 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Core; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Core.UseDiscordPermissionsAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier ->; - -namespace DSharpPlus.Analyzers.Test; - -public class UseDiscordPermissionsTest -{ - [Test] - public static async Task OrOperationTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm = perm | DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task ExclusiveOrOperationTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm = perm ^ DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task AndNotOperationTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm = perm & ~DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task AndNotParenthesizedOperationTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm = perm & (~DiscordPermission.Administrator); - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task OrAssignmentTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm |= DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task ExclusiveOrAssignmentTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm ^= DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task AndNotAssignmentTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm &= ~DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task AndNotParenthesizedAssignmentTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermission perm) - { - perm &= (~DiscordPermission.Administrator); - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task NoAssignmentTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static DiscordPermission AddAdmin(DiscordPermission perm) - { - return perm | DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0009") - .WithLocation(7, 16) - .WithSeverity(DiagnosticSeverity.Warning) - .WithMessage("Use 'DiscordPermissions' instead of operating on 'DiscordPermission'")); - - await test.RunAsync(); - } - - [Test] - public static async Task UsingBothTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermissions perm) - { - perm = perm | DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0010") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Info) - .WithMessage("Prefer using '+' instead of '|'")); - - await test.RunAsync(); - } - - [Test] - public static async Task UsingBothAssignmentTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermissions perm) - { - perm |= DiscordPermission.Administrator; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0010") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Info) - .WithMessage("Prefer using '+=' instead of '|='")); - - await test.RunAsync(); - } - - [Test] - public static async Task UsingOnlyDiscordPermissionsTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermissions perm1, DiscordPermissions perm2) - { - perm1 |= perm2; - } - } - """; - - test.ExpectedDiagnostics.Add( - Verifier.Diagnostic("DSP0010") - .WithLocation(7, 9) - .WithSeverity(DiagnosticSeverity.Info) - .WithMessage("Prefer using '+=' instead of '|='")); - - await test.RunAsync(); - } - - [Test] - public static async Task PlusAssignmentDiscordPermissionsTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermissions perm) - { - perm += DiscordPermission.Administrator; - } - } - """; - - await test.RunAsync(); - } - - [Test] - public static async Task PlusExpressionDiscordPermissionsTestAsync() - { - CSharpAnalyzerTest test = - Utility.CreateAnalyzerTest(); - - test.TestCode = """ - using DSharpPlus.Entities; - - public class PermissionsUtil - { - public static void AddAdmin(DiscordPermissions perm) - { - perm = perm + DiscordPermission.Administrator; - } - } - """; - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/Utility.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/Utility.cs deleted file mode 100644 index 1515b6ea2d..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/Utility.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; - -namespace DSharpPlus.Analyzers.Test; - -public static class Utility -{ - public static CSharpAnalyzerTest CreateAnalyzerTest() - where T : DiagnosticAnalyzer, new() - { - CSharpAnalyzerTest test = new() - { - ReferenceAssemblies = ReferenceAssemblies.Net.Net90 - }; - test.TestState.AdditionalReferences.Add(typeof(DiscordClient).Assembly); - return test; - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/ValidGuildInstallablesTest.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/ValidGuildInstallablesTest.cs deleted file mode 100644 index 430b6f9106..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers.Test/ValidGuildInstallablesTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Analyzers.Commands; -using DSharpPlus.Commands; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; -using NUnit.Framework; -using Verifier - = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< - DSharpPlus.Analyzers.Commands.ValidGuildInstallablesAnalyzer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier - >; - -namespace DSharpPlus.Analyzers.Test; - -public static class ValidGuildInstallablesTest -{ - [Test] - public static async Task DiagnoistTestAsync() - { - CSharpAnalyzerTest test - = Utility.CreateAnalyzerTest(); - test.TestState.AdditionalReferences.Add(typeof(CommandContext).Assembly); - - test.TestCode = """ - using System.Threading.Tasks; - using DSharpPlus; - using DSharpPlus.Entities; - using DSharpPlus.Commands; - using DSharpPlus.Commands.Processors.SlashCommands.Metadata; - - public class PingCommand - { - [Command("ping"), RegisterToGuilds(379378609942560770)] - [InteractionInstallType(DiscordApplicationIntegrationType.UserInstall)] - public static async ValueTask ExecuteAsync(CommandContext ctx) - { - await ctx.RespondAsync("Pong!"); - } - } - """; - - test.ExpectedDiagnostics.Add - ( - Verifier.Diagnostic() - .WithLocation(11, 35) - .WithSeverity(DiagnosticSeverity.Error) - .WithMessage( - "Cannot register 'ExecuteAsync' to the specified guilds because its installable context does not contain 'GuildInstall'" - ) - ); - - await test.RunAsync(); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/AnalyzerReleases.Shipped.md b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/AnalyzerReleases.Shipped.md deleted file mode 100644 index 7b6f49c8e6..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/AnalyzerReleases.Shipped.md +++ /dev/null @@ -1,14 +0,0 @@ -## Release 5.0 - -### New Rules - - Rule ID | Category | Severity | Notes ----------|----------|----------|------------------------------------------------------------------------------------------------------------------------------- - DSP0006 | Usage | Warning | `DiscordPermissions.HasPermission` should always be preferred over bitwise operations - DSP0007 | Design | Warning | Use `DiscordChannel#ModifyAsync` instead of `DiscordChannel#AddOverwriteAsnyc` - DSP0008 | Design | Info | Use a list request instead of fetching single entities inside of a loop - DSP0009 | Usage | Warning | Use `DiscordPermissions` instead of operating on `DiscordPermission` - DSP0010 | Usage | Info | Use `DiscordPermissions` methods and math operations instead of bitwise operations - DSP1001 | Usage | Error | A slash command explicitly registered to a guild should not specify DMs or user apps as installable context - DSP1002 | Usage | Warning | Do not explicitly register nested classes of elsewhere registered classes to DSharpPlus.Commands - DSP1003 | Usage | Error | A command taking a specific context type should not be registered as allowing processors whose contex type it doesn't support \ No newline at end of file diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/AnalyzerReleases.Unshipped.md b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/AnalyzerReleases.Unshipped.md deleted file mode 100644 index dd63383228..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/AnalyzerReleases.Unshipped.md +++ /dev/null @@ -1,4 +0,0 @@ -### New Rules - - Rule ID | Category | Severity | Notes ----------|----------|----------|------- \ No newline at end of file diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/ProcessorCheckAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/ProcessorCheckAnalyzer.cs deleted file mode 100644 index 52a018bac0..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/ProcessorCheckAnalyzer.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Commands; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ProcessorCheckAnalyzer : DiagnosticAnalyzer -{ - public const string DiagnosticId = "DSP1003"; - public const string Category = "Usage"; - - private static readonly LocalizableString title = new LocalizableResourceString - ( - nameof(Resources.DSP1003Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString description = new LocalizableResourceString - ( - nameof(Resources.DSP1003Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormat = new LocalizableResourceString - ( - nameof(Resources.DSP1003MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor rule = new - ( - DiagnosticId, - title, - messageFormat, - Category, - DiagnosticSeverity.Error, - true, - description, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/commands.html#usage-error-dsp1003" - ); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(rule); - - private static readonly Dictionary allowedContexts - = new() - { - { - "DSharpPlus.Commands.Processors.TextCommands.CommandContext", - ["UserCommandProcessor", "SlashCommandProcessor", "TextCommandProcessor"] - }, - { - "DSharpPlus.Commands.Processors.SlashCommands.SlashCommandContext", - ["UserCommandProcessor", "SlashCommandProcessor"] - }, - { "DSharpPlus.Commands.Processors.UserCommands.UserCommandContext", ["UserCommandProcessor"] }, - { "DSharpPlus.Commands.Processors.TextCommands.TextCommandContext", ["TextCommandProcessor"] }, - { "DSharpPlus.Commands.Processors.UserCommands.MessageCommandContext", ["MessageCommandProcessor"] }, - }; - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.MethodDeclaration); - } - - private void Analyze(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not MethodDeclarationSyntax methodDecl) - { - return; - } - - IEnumerable attributes = methodDecl.AttributeLists.SelectMany(al => al.Attributes); - - List types = []; - foreach (AttributeSyntax attribute in attributes) - { - TypeInfo typeInfo = ctx.SemanticModel.GetTypeInfo(attribute); - - if (typeInfo.Type is not INamedTypeSymbol namedTypeSymbol) - { - continue; - } - - if (namedTypeSymbol.Name != "AllowedProcessorsAttribute") - { - continue; - } - - if (namedTypeSymbol.ContainingNamespace.ToDisplayString() != "DSharpPlus.Commands.Trees.Metadata") - { - continue; - } - - foreach (ITypeSymbol typeArgument in namedTypeSymbol.TypeArguments) - { - types.Add(typeArgument); - } - - break; - } - - if (types.Count <= 0) - { - return; - } - - if (methodDecl.ParameterList.Parameters.Count <= 0) - { - return; - } - - ParameterSyntax contextParam = methodDecl.ParameterList.Parameters.First(); - TypeInfo contextType = ctx.SemanticModel.GetTypeInfo(contextParam.Type!); - - if (contextType.Type is null) - { - return; - } - - if (!allowedContexts.TryGetValue( - $"{contextType.Type.ContainingNamespace.ToDisplayString()}.{contextType.Type.MetadataName}", - out string[]? arr)) - { - return; - } - - bool containsAnyProcessor = false; - foreach (ITypeSymbol? t in types) - { - if (arr.Contains(t.Name)) - { - containsAnyProcessor = true; - break; - } - } - - if (containsAnyProcessor) - { - return; - } - - Diagnostic diagnostic = Diagnostic.Create(rule, - contextParam.Type?.GetLocation(), - contextType.Type.Name - ); - ctx.ReportDiagnostic(diagnostic); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/RegisterNestedClassesAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/RegisterNestedClassesAnalyzer.cs deleted file mode 100644 index 6cd54c9415..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/RegisterNestedClassesAnalyzer.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Commands; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class RegisterNestedClassesAnalyzer : DiagnosticAnalyzer -{ - public const string DiagnosticId = "DSP1002"; - public const string Category = "Usage"; - - private static readonly LocalizableString title = new LocalizableResourceString - ( - nameof(Resources.DSP1002Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString description = new LocalizableResourceString - ( - nameof(Resources.DSP1002Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormat = new LocalizableResourceString - ( - nameof(Resources.DSP1002MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor rule = new - ( - DiagnosticId, - title, - messageFormat, - Category, - DiagnosticSeverity.Warning, - true, - description, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/commands.html#usage-warning-dsp1002" - ); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(rule); - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); - } - - public void Analyze(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not InvocationExpressionSyntax invocation) - { - return; - } - - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) - { - return; - } - - if (memberAccess.Name.Identifier.ValueText != "AddCommands") - { - return; - } - - TypeInfo typeInfo = ctx.SemanticModel.GetTypeInfo(memberAccess.Expression); - if (!ctx.Compilation.CheckByName(typeInfo, "DSharpPlus.Commands.CommandsExtension")) - { - return; - } - - SymbolInfo invocationSymbolInfo = ctx.SemanticModel.GetSymbolInfo(invocation); - if (invocationSymbolInfo.Symbol is not IMethodSymbol methodSymbol) - { - return; - } - - INamedTypeSymbol? enumerableType - = ctx.Compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); - INamedTypeSymbol? typeType = ctx.Compilation.GetTypeByMetadataName("System.Type"); - INamedTypeSymbol enumerableTypesType = enumerableType!.Construct(typeType!); - - bool isEnumerableTypesType - = SymbolEqualityComparer.Default.Equals(methodSymbol.Parameters.FirstOrDefault()?.Type, - enumerableTypesType); - if (!isEnumerableTypesType) - { - return; - } - - ArgumentSyntax? firstArgument = invocation.ArgumentList.Arguments.FirstOrDefault(); - if (firstArgument is null) - { - return; - } - - List typeInfos = GetTypesFromArgument(ctx, firstArgument.Expression); - - foreach (TypeSyntax type in typeInfos) - { - TypeInfo collectionTypeInfo = ctx.SemanticModel.GetTypeInfo(type); - - if (collectionTypeInfo.Type?.ContainingSymbol is INamedTypeSymbol) - { - Diagnostic diagnostic = Diagnostic.Create(rule, - type.GetLocation(), - collectionTypeInfo.Type.Name, - collectionTypeInfo.Type.ContainingSymbol.Name - ); - ctx.ReportDiagnostic(diagnostic); - } - } - } - - private List GetTypesFromArgument(SyntaxNodeAnalysisContext ctx, ExpressionSyntax expression) - { - if (expression is CollectionExpressionSyntax collection) - { - List predefinedTypes = []; - foreach (CollectionElementSyntax elemenet in collection.Elements) - { - if (elemenet is ExpressionElementSyntax - { - Expression: TypeOfExpressionSyntax typeOf - }) - { - predefinedTypes.Add(typeOf.Type); - } - else if (elemenet is ExpressionElementSyntax expressionElement) - { - TypeSyntax? syntax = GetTypeInfoFromVar(ctx, expressionElement.Expression); - if (syntax is null) - { - continue; - } - - predefinedTypes.Add(syntax); - } - } - - return predefinedTypes; - } - else if (expression is IdentifierNameSyntax identifierName) - { - SyntaxNode? declaringSyntax = GetDeclaringSyntax(ctx, identifierName); - - InitializerExpressionSyntax? initializer = declaringSyntax switch - { - ObjectCreationExpressionSyntax o => o.Initializer, - ImplicitObjectCreationExpressionSyntax io => io.Initializer, - ArrayCreationExpressionSyntax a => a.Initializer, - ImplicitArrayCreationExpressionSyntax ia => ia.Initializer, - _ => null, - }; - - if (initializer is null) - { - return []; - } - - List types = []; - foreach (ExpressionSyntax element in initializer.Expressions) - { - if (element is TypeOfExpressionSyntax typeOf) - { - types.Add(typeOf.Type); - } - else - { - TypeSyntax? typeSyntax = GetTypeInfoFromVar(ctx, element); - if (typeSyntax is not null) - { - types.Add(typeSyntax); - } - } - } - - return types; - } - - return []; - } - - private TypeSyntax? GetTypeInfoFromVar(SyntaxNodeAnalysisContext ctx, ExpressionSyntax expression) - { - if (expression is not IdentifierNameSyntax identifierName) - { - return null; - } - - SyntaxNode? declaringSyntax = GetDeclaringSyntax(ctx, identifierName); - if (declaringSyntax is not TypeOfExpressionSyntax typeOf) - { - return null; - } - - return typeOf.Type; - } - - private SyntaxNode? GetDeclaringSyntax(SyntaxNodeAnalysisContext ctx, IdentifierNameSyntax identifierName) - { - SymbolInfo symbolInfo = ctx.SemanticModel.GetSymbolInfo(identifierName); - SyntaxNode declaringSyntax; - if (symbolInfo.Symbol is IFieldSymbol field) - { - declaringSyntax = field.DeclaringSyntaxReferences.First().GetSyntax(); - } - else if (symbolInfo.Symbol is ILocalSymbol local) - { - declaringSyntax = local.DeclaringSyntaxReferences.First().GetSyntax(); - } - else - { - return null; - } - - if (declaringSyntax is not VariableDeclaratorSyntax declarator) - { - return null; - } - - return declarator.Initializer?.Value; - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/ValidGuildInstallablesAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/ValidGuildInstallablesAnalyzer.cs deleted file mode 100644 index 346d0d44dd..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Commands/ValidGuildInstallablesAnalyzer.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Commands; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class ValidGuildInstallablesAnalyzer : DiagnosticAnalyzer -{ - public const string DiagnosticId = "DSP1001"; - public const string Category = "Usage"; - - private static readonly LocalizableString title = new LocalizableResourceString - ( - nameof(Resources.DSP1001Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString description = new LocalizableResourceString - ( - nameof(Resources.DSP1001Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormat = new LocalizableResourceString - ( - nameof(Resources.DSP1001MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor rule = new - ( - DiagnosticId, - title, - messageFormat, - Category, - DiagnosticSeverity.Error, - true, - description, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/commands.html#usage-error-dsp1001" - ); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(rule); - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.MethodDeclaration); - } - - private void Analyze(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not MethodDeclarationSyntax methodDecl) - { - return; - } - - IEnumerable attributes = methodDecl.AttributeLists.SelectMany(a => a.Attributes); - - AttributeSyntax? registerToGuild = null; - AttributeSyntax? interactionInstallType = null; - foreach (AttributeSyntax attributeSyntax in attributes) - { - TypeInfo typeInfo = ctx.SemanticModel.GetTypeInfo(attributeSyntax); - if (ctx.Compilation.CheckByName(typeInfo, "DSharpPlus.Commands.RegisterToGuildsAttribute")) - { - registerToGuild = attributeSyntax; - } - else if (ctx.Compilation.CheckByName(typeInfo, - "DSharpPlus.Commands.Processors.SlashCommands.Metadata.InteractionInstallTypeAttribute")) - - { - interactionInstallType = attributeSyntax; - } - } - - if (registerToGuild is null || interactionInstallType is null) - { - return; - } - - bool hasGuildInstall = false; - foreach (AttributeArgumentSyntax a in interactionInstallType.ArgumentList?.Arguments ?? []) - { - Optional constantValue = ctx.SemanticModel.GetConstantValue(a.Expression); - if (constantValue.HasValue) - { - if (constantValue.Value is not null && (int)constantValue.Value == 0) - { - hasGuildInstall = true; - break; - } - } - } - - if (hasGuildInstall) - { - return; - } - - Diagnostic diagnostic = Diagnostic.Create( - rule, - methodDecl.Identifier.GetLocation(), - methodDecl.Identifier - ); - ctx.ReportDiagnostic(diagnostic); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/HasPermissionAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/HasPermissionAnalyzer.cs deleted file mode 100644 index 81e5bc9281..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/HasPermissionAnalyzer.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Core; - -// This should get redesigned when #2152 gets merged -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class HasPermissionAnalyzer : DiagnosticAnalyzer -{ - public const string DiagnosticId = "DSP0006"; - - public const string Category = "Usage"; - - private static readonly LocalizableString title = new LocalizableResourceString - ( - nameof(Resources.DSP0006Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString description = new LocalizableResourceString - ( - nameof(Resources.DSP0006Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormat = new LocalizableResourceString - ( - nameof(Resources.DSP0006MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor rule = new - ( - DiagnosticId, - title, - messageFormat, - Category, - DiagnosticSeverity.Warning, - true, - description, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/core.html#usage-warning-dsp0006" - ); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(rule); - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.NotEqualsExpression, SyntaxKind.EqualsExpression); - } - - private static void Analyze(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not BinaryExpressionSyntax binaryExpression) - { - return; - } - - if (binaryExpression.Kind() != SyntaxKind.NotEqualsExpression && - binaryExpression.Kind() != SyntaxKind.EqualsExpression) - { - return; - } - - if (binaryExpression.Left is not ParenthesizedExpressionSyntax p || - p.Expression is not BinaryExpressionSyntax leftBinary) - { - return; - } - - if (leftBinary.Kind() != SyntaxKind.BitwiseAndExpression) - { - return; - } - - TypeInfo leftTypeInfo = ctx.SemanticModel.GetTypeInfo(leftBinary.Left); - if (!ctx.Compilation.CheckByName(leftTypeInfo, "DSharpPlus.Entities.DiscordPermission")) - { - return; - } - - TypeInfo rightTypeInfo = ctx.SemanticModel.GetTypeInfo(leftBinary.Right); - if (!ctx.Compilation.CheckByName(rightTypeInfo, "DSharpPlus.Entities.DiscordPermission")) - { - return; - } - - Diagnostic diagnostic = Diagnostic.Create( - rule, - binaryExpression.GetLocation(), - leftBinary.Left.GetText().ToString().Trim(), - leftBinary.Right.GetText().ToString().Trim()); - ctx.ReportDiagnostic(diagnostic); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/MultipleOverwriteAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/MultipleOverwriteAnalyzer.cs deleted file mode 100644 index 09fc30317d..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/MultipleOverwriteAnalyzer.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Core; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class MultipleOverwriteAnalyzer : DiagnosticAnalyzer -{ - public const string DiagnosticId = "DSP0007"; - public const string Category = "Design"; - - private static readonly LocalizableString title = new LocalizableResourceString - ( - nameof(Resources.DSP0007Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString description = new LocalizableResourceString - ( - nameof(Resources.DSP0007Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormat = new LocalizableResourceString - ( - nameof(Resources.DSP0007MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor rule = new( - DiagnosticId, - title, - messageFormat, - Category, - DiagnosticSeverity.Warning, - true, - description, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/core.html#usage-error-dsp0007" - ); - - // This might need to be a concurrent dictionary cause of line 51 - private readonly Dictionary> invocations = new(); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(rule); - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); - } - - private void Analyze(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not InvocationExpressionSyntax invocation) - { - return; - } - - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) - { - return; - } - - if (memberAccess.Name.Identifier.ValueText != "AddOverwriteAsync") - { - return; - } - - TypeInfo typeInfo = ctx.SemanticModel.GetTypeInfo(memberAccess.Expression); - if (!ctx.Compilation.CheckByName(typeInfo, "DSharpPlus.Entities.DiscordChannel")) - { - return; - } - - MethodDeclarationSyntax? method = FindMethodDecl(invocation); - if (method is null) - { - return; - } - - string memberText = memberAccess.GetText().ToString(); - bool isInLoop = IsInLoop(invocation); - if (!this.invocations.TryGetValue(method, out HashSet hashSet)) - { - this.invocations.Add(method, [memberText]); - if (isInLoop) - { - Diagnostic loopDiagnostic = Diagnostic.Create( - rule, - invocation.GetLocation(), - memberAccess.Expression - ); - - ctx.ReportDiagnostic(loopDiagnostic); - } - - return; - } - - if (hashSet.Add(memberText)) - { - if (isInLoop) - { - Diagnostic loopDiagnostic = Diagnostic.Create( - rule, - invocation.GetLocation(), - memberAccess.Expression - ); - - ctx.ReportDiagnostic(loopDiagnostic); - } - - return; - } - - Diagnostic diagnostic = Diagnostic.Create( - rule, - invocation.GetLocation(), - memberAccess.Expression - ); - - ctx.ReportDiagnostic(diagnostic); - } - - private MethodDeclarationSyntax? FindMethodDecl(SyntaxNode? syntax) - { - if (syntax is null) - { - return null; - } - - if (syntax is MethodDeclarationSyntax method) - { - return method; - } - - return FindMethodDecl(syntax.Parent); - } - - private bool IsInLoop(SyntaxNode? syntax) - { - if (syntax is null) - { - return false; - } - - if (syntax is ForEachStatementSyntax) - { - return true; - } - - if (syntax is ForStatementSyntax) - { - return true; - } - - if (syntax is WhileStatementSyntax) - { - return true; - } - - return IsInLoop(syntax.Parent); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/SingleEntityGetRequestAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/SingleEntityGetRequestAnalyzer.cs deleted file mode 100644 index 3619852dbc..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/SingleEntityGetRequestAnalyzer.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Core; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class SingleEntityGetRequestAnalyzer : DiagnosticAnalyzer -{ - public const string DiagnosticId = "DSP0008"; - public const string Category = "Design"; - - private static readonly LocalizableString title = new LocalizableResourceString - ( - nameof(Resources.DSP0008Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString description = new LocalizableResourceString - ( - nameof(Resources.DSP0008Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormat = new LocalizableResourceString - ( - nameof(Resources.DSP0008MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor rule = new( - DiagnosticId, - title, - messageFormat, - Category, - DiagnosticSeverity.Info, - true, - description, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/core.html#design-info-dsp0008" - ); - - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(rule); - - private static readonly IReadOnlyDictionary methods = new Dictionary() - { - { "GetMessageAsync", "DSharpPlus.Entities.DiscordChannel" }, - { "GetGuildAsync", "DSharpPlus.DiscordClient"}, - { "GetMemberAsync", "DSharpPlus.Entities.Channel"} - }; - - private static readonly IReadOnlyDictionary preferredMethods = new Dictionary() - { - { "GetMessageAsync", "GetMessagesAsync" }, - {"GetGuildAsync", "GetGuildsAsync"}, - { "GetMemberAsync", "GetAllMembersAsync"} - }; - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); - } - - private void Analyze(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not InvocationExpressionSyntax invocation) - { - return; - } - - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) - { - return; - } - - string methodName = memberAccess.Name.Identifier.ValueText; - if (!methods.TryGetValue(methodName, out string? typeName)) - { - return; - } - - TypeInfo typeInfo = ctx.SemanticModel.GetTypeInfo(memberAccess.Expression); - if (!ctx.Compilation.CheckByName(typeInfo, typeName)) - { - return; - } - - StatementSyntax? syntax = FindClosestLoopStatement(invocation); - if (syntax is null) - { - return; - } - - Diagnostic diagnostic = Diagnostic.Create( - rule, - invocation.GetLocation(), - $"{memberAccess.Expression}.{preferredMethods[methodName]}()", - invocation - ); - - ctx.ReportDiagnostic(diagnostic); - } - - private StatementSyntax? FindClosestLoopStatement(SyntaxNode? syntax) - { - if (syntax is null) - { - return null; - } - - if (syntax is ForEachStatementSyntax forEachStatement) - { - return forEachStatement.Statement; - } - - if (syntax is ForStatementSyntax forStatement) - { - return forStatement.Statement; - } - - return FindClosestLoopStatement(syntax.Parent); - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/UseDiscordPermissionsAnalyzer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/UseDiscordPermissionsAnalyzer.cs deleted file mode 100644 index f30ac26c0d..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Core/UseDiscordPermissionsAnalyzer.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DSharpPlus.Analyzers.Core; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class UseDiscordPermissionsAnalyzer : DiagnosticAnalyzer -{ - private static readonly LocalizableString titleDsp0009 = new LocalizableResourceString - ( - nameof(Resources.DSP0009Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString descriptionDsp0009 = new LocalizableResourceString - ( - nameof(Resources.DSP0009Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormatDsp0009 = new LocalizableResourceString - ( - nameof(Resources.DSP0009MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString titleDsp0010 = new LocalizableResourceString - ( - nameof(Resources.DSP0010Title), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString descriptionDsp0010 = new LocalizableResourceString - ( - nameof(Resources.DSP0010Description), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly LocalizableString messageFormatDsp0010 = new LocalizableResourceString - ( - nameof(Resources.DSP0010MessageFormat), - Resources.ResourceManager, - typeof(Resources) - ); - - private static readonly DiagnosticDescriptor ruleDsp0009 = new( - "DSP0009", - titleDsp0009, - messageFormatDsp0009, - "Usage", - DiagnosticSeverity.Warning, - true, - descriptionDsp0009, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/core.html#usage-warning-dsp0009" - ); - - private static readonly DiagnosticDescriptor ruleDsp0010 = new( - "DSP0010", - titleDsp0010, - messageFormatDsp0010, - "Usage", - DiagnosticSeverity.Info, - true, - descriptionDsp0010, - helpLinkUri: $"{Utility.BaseDocsUrl}/articles/analyzers/core.html#usage-warning-dsp0010" - ); - - public override ImmutableArray SupportedDiagnostics { get; } - = ImmutableArray.Create(ruleDsp0009, ruleDsp0010); - - public override void Initialize(AnalysisContext ctx) - { - ctx.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - ctx.EnableConcurrentExecution(); - ctx.RegisterSyntaxNodeAction( - AnalyzeBitOp, - SyntaxKind.BitwiseAndExpression, - SyntaxKind.BitwiseOrExpression, - SyntaxKind.ExclusiveOrExpression, - SyntaxKind.AddExpression, - SyntaxKind.SubtractExpression, - SyntaxKind.MultiplyExpression, - SyntaxKind.DivideExpression, - SyntaxKind.ModuloExpression, - SyntaxKind.LeftShiftExpression, - SyntaxKind.RightShiftAssignmentExpression, - SyntaxKind.UnsignedRightShiftExpression); - ctx.RegisterSyntaxNodeAction( - AnalyzeAssignment, - SyntaxKind.AndAssignmentExpression, - SyntaxKind.OrAssignmentExpression, - SyntaxKind.ExclusiveOrAssignmentExpression, - SyntaxKind.AddAssignmentExpression, - SyntaxKind.SubtractAssignmentExpression, - SyntaxKind.MultiplyAssignmentExpression, - SyntaxKind.DivideAssignmentExpression, - SyntaxKind.ModuloAssignmentExpression, - SyntaxKind.LeftShiftAssignmentExpression, - SyntaxKind.RightShiftAssignmentExpression, - SyntaxKind.UnsignedRightShiftAssignmentExpression); - } - - private static void AnalyzeBitOp(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not BinaryExpressionSyntax binaryExpression) - { - return; - } - - Location location; - if (binaryExpression.Parent is AssignmentExpressionSyntax assignment) - { - location = assignment.GetLocation(); - } - else - { - location = binaryExpression.GetLocation(); - } - - TypeInfo leftTypeInfo = ctx.SemanticModel.GetTypeInfo(binaryExpression.Left); - TypeInfo rightTypeInfo = ctx.SemanticModel.GetTypeInfo(binaryExpression.Right); - - bool leftTypeIsDiscordPermission - = ctx.Compilation.CheckByName(leftTypeInfo, "DSharpPlus.Entities.DiscordPermission"); - bool rightTypeIsDiscordPermission - = ctx.Compilation.CheckByName(rightTypeInfo, "DSharpPlus.Entities.DiscordPermission"); - bool leftTypeIsDiscordPermissions - = ctx.Compilation.CheckByName(leftTypeInfo, "DSharpPlus.Entities.DiscordPermissions"); - bool rightTypeIsDiscordPermissions - = ctx.Compilation.CheckByName(rightTypeInfo, "DSharpPlus.Entities.DiscordPermissions"); - - Diagnostic diagnostic; - if (leftTypeIsDiscordPermissions || rightTypeIsDiscordPermissions) - { - if (binaryExpression.Kind() != SyntaxKind.BitwiseAndExpression && - binaryExpression.Kind() != SyntaxKind.BitwiseOrExpression && - binaryExpression.Kind() != SyntaxKind.ExclusiveOrExpression) - { - return; - } - - if (binaryExpression.Kind() == SyntaxKind.AndAssignmentExpression && - !GetNotOperation(binaryExpression.Right)) - { - return; - } - - string equivalence = GetDiscordPermissionsEquivalence(binaryExpression.Kind()); - diagnostic = Diagnostic.Create(ruleDsp0010, location, equivalence, - binaryExpression.OperatorToken.ToString()); - ctx.ReportDiagnostic(diagnostic); - return; - } - - if (!leftTypeIsDiscordPermission && !rightTypeIsDiscordPermission) - { - return; - } - - diagnostic - = Diagnostic.Create(ruleDsp0009, location); - ctx.ReportDiagnostic(diagnostic); - } - - private static void AnalyzeAssignment(SyntaxNodeAnalysisContext ctx) - { - if (ctx.Node is not AssignmentExpressionSyntax assignmentExpression) - { - return; - } - - TypeInfo leftTypeInfo = ctx.SemanticModel.GetTypeInfo(assignmentExpression.Left); - TypeInfo rightTypeInfo = ctx.SemanticModel.GetTypeInfo(assignmentExpression.Right); - - bool leftTypeIsDiscordPermission - = ctx.Compilation.CheckByName(leftTypeInfo, "DSharpPlus.Entities.DiscordPermission"); - bool rightTypeIsDiscordPermission - = ctx.Compilation.CheckByName(rightTypeInfo, "DSharpPlus.Entities.DiscordPermission"); - bool leftTypeIsDiscordPermissions - = ctx.Compilation.CheckByName(leftTypeInfo, "DSharpPlus.Entities.DiscordPermissions"); - bool rightTypeIsDiscordPermissions - = ctx.Compilation.CheckByName(rightTypeInfo, "DSharpPlus.Entities.DiscordPermissions"); - - Diagnostic diagnostic; - if (leftTypeIsDiscordPermissions || rightTypeIsDiscordPermissions) - { - if (assignmentExpression.Kind() != SyntaxKind.AndAssignmentExpression && - assignmentExpression.Kind() != SyntaxKind.OrAssignmentExpression && - assignmentExpression.Kind() != SyntaxKind.ExclusiveOrAssignmentExpression) - { - return; - } - - if (assignmentExpression.Kind() == SyntaxKind.AndAssignmentExpression && - !GetNotOperation(assignmentExpression.Right)) - { - return; - } - - string equivalence = GetDiscordPermissionsEquivalence(assignmentExpression.Kind()); - diagnostic = Diagnostic.Create(ruleDsp0010, assignmentExpression.GetLocation(), equivalence, - assignmentExpression.OperatorToken.ToString()); - ctx.ReportDiagnostic(diagnostic); - return; - } - - if (!leftTypeIsDiscordPermission && - !rightTypeIsDiscordPermission) - { - return; - } - - diagnostic = Diagnostic.Create(ruleDsp0009, assignmentExpression.GetLocation()); - ctx.ReportDiagnostic(diagnostic); - } - - private static bool GetNotOperation(ExpressionSyntax expression) - { - return expression switch - { - ParenthesizedExpressionSyntax { Expression: PrefixUnaryExpressionSyntax unaryExpression } => - unaryExpression.Kind() == SyntaxKind.BitwiseNotExpression, - PrefixUnaryExpressionSyntax unaryExpression2 => unaryExpression2.Kind() == SyntaxKind.BitwiseNotExpression, - _ => false - }; - } - - private static string GetDiscordPermissionsEquivalence(SyntaxKind syntaxKind) - { - return syntaxKind switch - { - SyntaxKind.BitwiseAndExpression => "-", - SyntaxKind.BitwiseOrExpression => "+", - SyntaxKind.ExclusiveOrExpression => "DiscordPermissions#Toggle", - SyntaxKind.AndAssignmentExpression => "-=", - SyntaxKind.OrAssignmentExpression => "+=", - SyntaxKind.ExclusiveOrAssignmentExpression => "DiscordPermissions#Toggle", - _ => throw new ArgumentException("Syntax kind does not have a DiscordPermissions equivalence") - }; - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/DSharpPlus.Analyzers.csproj b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/DSharpPlus.Analyzers.csproj deleted file mode 100644 index 526fd7c353..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/DSharpPlus.Analyzers.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - netstandard2.0 - true - enable - latest - - true - true - - DSharpPlus.Analyzers - - DSharpPlus.Analyzers - $(PackageTags) - A package for analyzer designed for the DSharpPlus libraries - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - True - True - Resources.resx - - - - diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Properties/launchSettings.json b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Properties/launchSettings.json deleted file mode 100644 index 328ac23c77..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Properties/launchSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "DebugRoslynAnalyzers": { - "commandName": "DebugRoslynComponent", - "targetProject": "../DSharpPlus.Analyzers.Test/DSharpPlus.Analyzers.Test.csproj" - } - } -} \ No newline at end of file diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Resources.Designer.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Resources.Designer.cs deleted file mode 100644 index a3b3226860..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Resources.Designer.cs +++ /dev/null @@ -1,192 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace DSharpPlus.Analyzers { - using System; - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static System.Resources.ResourceManager resourceMan; - - private static System.Globalization.CultureInfo resourceCulture; - - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { - get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("DSharpPlus.Analyzers.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - internal static string DSP0006Title { - get { - return ResourceManager.GetString("DSP0006Title", resourceCulture); - } - } - - internal static string DSP0006Description { - get { - return ResourceManager.GetString("DSP0006Description", resourceCulture); - } - } - - internal static string DSP0006MessageFormat { - get { - return ResourceManager.GetString("DSP0006MessageFormat", resourceCulture); - } - } - - internal static string DSP0007Title { - get { - return ResourceManager.GetString("DSP0007Title", resourceCulture); - } - } - - internal static string DSP0007Description { - get { - return ResourceManager.GetString("DSP0007Description", resourceCulture); - } - } - - internal static string DSP0007MessageFormat { - get { - return ResourceManager.GetString("DSP0007MessageFormat", resourceCulture); - } - } - - internal static string DSP0008Title { - get { - return ResourceManager.GetString("DSP0008Title", resourceCulture); - } - } - - internal static string DSP0008Description { - get { - return ResourceManager.GetString("DSP0008Description", resourceCulture); - } - } - - internal static string DSP0008MessageFormat { - get { - return ResourceManager.GetString("DSP0008MessageFormat", resourceCulture); - } - } - - internal static string DSP1001Title { - get { - return ResourceManager.GetString("DSP1001Title", resourceCulture); - } - } - - internal static string DSP1001Description { - get { - return ResourceManager.GetString("DSP1001Description", resourceCulture); - } - } - - internal static string DSP1001MessageFormat { - get { - return ResourceManager.GetString("DSP1001MessageFormat", resourceCulture); - } - } - - internal static string DSP1002Title { - get { - return ResourceManager.GetString("DSP1002Title", resourceCulture); - } - } - - internal static string DSP1002Description { - get { - return ResourceManager.GetString("DSP1002Description", resourceCulture); - } - } - - internal static string DSP1002MessageFormat { - get { - return ResourceManager.GetString("DSP1002MessageFormat", resourceCulture); - } - } - - internal static string DSP1003Title { - get { - return ResourceManager.GetString("DSP1003Title", resourceCulture); - } - } - - internal static string DSP1003Description { - get { - return ResourceManager.GetString("DSP1003Description", resourceCulture); - } - } - - internal static string DSP1003MessageFormat { - get { - return ResourceManager.GetString("DSP1003MessageFormat", resourceCulture); - } - } - - internal static string DSP0009Description { - get { - return ResourceManager.GetString("DSP0009Description", resourceCulture); - } - } - - internal static string DSP0009Title { - get { - return ResourceManager.GetString("DSP0009Title", resourceCulture); - } - } - - internal static string DSP0009MessageFormat { - get { - return ResourceManager.GetString("DSP0009MessageFormat", resourceCulture); - } - } - - internal static string DSP0010Title { - get { - return ResourceManager.GetString("DSP0010Title", resourceCulture); - } - } - - internal static string DSP0010Description { - get { - return ResourceManager.GetString("DSP0010Description", resourceCulture); - } - } - - internal static string DSP0010MessageFormat { - get { - return ResourceManager.GetString("DSP0010MessageFormat", resourceCulture); - } - } - } -} diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Resources.resx b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Resources.resx deleted file mode 100644 index 5de8f5d481..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Resources.resx +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - Use 'Permissions#HasPermission' - - - Use 'Permissions#HasPermission' instead of bitwise operations. - - - Use '{0}.HasPermission({1})' instead - - - Use 'DiscordChannel#ModifyAsync' - - - Use one 'DiscordChannel#ModifyAsync' instead of multiple 'DiscordChannel#OverwriteAsync' calls. - - - Use one '{0}.ModifyAsync(..)' instead of multiple '{0}.AddOverwriteAsync(..)' - - - Use list request - - - Use list request instead of requesting single entities in a loop. - - - Use '{0}' outside of the loop instead of '{1}' inside the loop - - - Cannot register to guilds for this installable context - - - Cannot register this slash command to the specified guilds because its context disallows that. - - - Cannot register '{0}' to the specified guilds because its installable context does not contain 'GuildInstall' - - - Don't register nested classes - - - Don't register nested classes, register its parent instead. - - - Don't register '{0}', register '{1}' instead - - - Command processor doesn't support command context - - - The specified command processor doesn't support the specified command context. - - - All provided processors does not support context '{0}' - - - Use 'DiscordPermissions' and its operators instead of doing raw operations on 'DiscordPermission'. - - - Use 'DiscordPermissions' over operations on 'DiscordPermission' - - - Use 'DiscordPermissions' instead of operating on 'DiscordPermission' - - - Prefer named method 'Name' over bitwise operations - - - Prefer named method 'Name' over bitwise operations. - - - Prefer using '{0}' instead of '{1}' - - \ No newline at end of file diff --git a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Utility.cs b/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Utility.cs deleted file mode 100644 index 4be893f267..0000000000 --- a/DSharpPlus.Analyzers/DSharpPlus.Analyzers/Utility.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace DSharpPlus.Analyzers; - -public static class Utility -{ - public const string BaseDocsUrl = "https://dsharpplus.github.io/DSharpPlus"; - - /// - /// Checks if the type is equal to the name provided - /// - /// The compilation used to get types - /// The type the compare - /// The fully qualified name to the type - /// - public static bool CheckByName(this Compilation compilation, TypeInfo typeInfo, string fullyQualifiedName) => - typeInfo.Type?.Equals(compilation.GetTypeByMetadataName(fullyQualifiedName), SymbolEqualityComparer.Default) ?? false; -} diff --git a/DSharpPlus.Commands/AbstractContext.cs b/DSharpPlus.Commands/AbstractContext.cs deleted file mode 100644 index 7913ea7f2f..0000000000 --- a/DSharpPlus.Commands/AbstractContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index b1e14a8fee..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/ChannelTypesAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 0932f5f8b0..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/FromCode/CodeType.cs +++ /dev/null @@ -1,25 +0,0 @@ -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.LanguageList.cs b/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.LanguageList.cs deleted file mode 100644 index 6e147e7d37..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.LanguageList.cs +++ /dev/null @@ -1,387 +0,0 @@ -// -// Last modified on Tuesday, 12 March 2024 22:52:02 -using System; -using System.Collections.Frozen; -using System.Collections.Generic; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -partial class FromCodeAttribute : Attribute -{ - /// - /// A list of default languages from Highlight.Js that Discord uses for their codeblocks. - /// - public static readonly FrozenSet CodeBlockLanguages = new List() - { - "1c", - "abnf", - "accesslog", - "actionscript", - "as", - "ada", - "angelscript", - "asc", - "apache", - "apacheconf", - "applescript", - "osascript", - "arcade", - "arduino", - "ino", - "armasm", - "arm", - "xml", - "html", - "xhtml", - "rss", - "atom", - "xjb", - "xsd", - "xsl", - "plist", - "wsf", - "svg", - "asciidoc", - "adoc", - "aspectj", - "autohotkey", - "ahk", - "autoit", - "avrasm", - "awk", - "axapta", - "x++", - "bash", - "sh", - "basic", - "bnf", - "brainfuck", - "bf", - "c", - "h", - "cal", - "capnproto", - "capnp", - "ceylon", - "clean", - "icl", - "dcl", - "clojure", - "clj", - "edn", - "clojure-repl", - "cmake", - "cmake.in", - "coffeescript", - "coffee", - "cson", - "iced", - "coq", - "cos", - "cls", - "cpp", - "cc", - "c++", - "h++", - "hpp", - "hh", - "hxx", - "cxx", - "crmsh", - "crm", - "pcmk", - "crystal", - "cr", - "csharp", - "cs", - "c#", - "csp", - "css", - "d", - "markdown", - "md", - "mkdown", - "mkd", - "dart", - "delphi", - "dpr", - "dfm", - "pas", - "pascal", - "diff", - "patch", - "django", - "jinja", - "dns", - "bind", - "zone", - "dockerfile", - "docker", - "dos", - "bat", - "cmd", - "dsconfig", - "dts", - "dust", - "dst", - "ebnf", - "elixir", - "ex", - "exs", - "elm", - "ruby", - "rb", - "gemspec", - "podspec", - "thor", - "irb", - "erb", - "erlang-repl", - "erlang", - "erl", - "excel", - "xlsx", - "xls", - "fix", - "flix", - "fortran", - "f90", - "f95", - "fsharp", - "fs", - "f#", - "gams", - "gms", - "gauss", - "gss", - "gcode", - "nc", - "gherkin", - "feature", - "glsl", - "gml", - "go", - "golang", - "golo", - "gradle", - "graphql", - "gql", - "groovy", - "haml", - "handlebars", - "hbs", - "html.hbs", - "html.handlebars", - "htmlbars", - "haskell", - "hs", - "haxe", - "hx", - "hsp", - "http", - "https", - "hy", - "hylang", - "inform7", - "i7", - "ini", - "toml", - "irpf90", - "isbl", - "java", - "jsp", - "javascript", - "js", - "jsx", - "mjs", - "cjs", - "jboss-cli", - "wildfly-cli", - "json", - "julia", - "julia-repl", - "jldoctest", - "kotlin", - "kt", - "kts", - "lasso", - "ls", - "lassoscript", - "latex", - "tex", - "ldif", - "leaf", - "less", - "lisp", - "livecodeserver", - "livescript", - "ls", - "llvm", - "lsl", - "lua", - "makefile", - "mk", - "mak", - "make", - "mathematica", - "mma", - "wl", - "matlab", - "maxima", - "mel", - "mercury", - "m", - "moo", - "mipsasm", - "mips", - "mizar", - "perl", - "pl", - "pm", - "mojolicious", - "monkey", - "moonscript", - "moon", - "n1ql", - "nestedtext", - "nt", - "nginx", - "nginxconf", - "nim", - "nix", - "nixos", - "node-repl", - "nsis", - "objectivec", - "mm", - "objc", - "obj-c", - "obj-c++", - "objective-c++", - "ocaml", - "ml", - "openscad", - "scad", - "oxygene", - "parser3", - "pf", - "pf.conf", - "pgsql", - "postgres", - "postgresql", - "php", - "php-template", - "plaintext", - "text", - "txt", - "pony", - "powershell", - "pwsh", - "ps", - "ps1", - "processing", - "pde", - "profile", - "prolog", - "properties", - "protobuf", - "proto", - "puppet", - "pp", - "purebasic", - "pb", - "pbi", - "python", - "py", - "gyp", - "ipython", - "python-repl", - "pycon", - "q", - "k", - "kdb", - "qml", - "qt", - "r", - "reasonml", - "re", - "rib", - "roboconf", - "graph", - "instances", - "routeros", - "mikrotik", - "rsl", - "ruleslanguage", - "rust", - "rs", - "sas", - "scala", - "scheme", - "scm", - "scilab", - "sci", - "scss", - "shell", - "console", - "shellsession", - "smali", - "smalltalk", - "st", - "sml", - "ml", - "sqf", - "sql", - "stan", - "stanfuncs", - "stata", - "do", - "ado", - "step21", - "p21", - "step", - "stp", - "stylus", - "styl", - "subunit", - "swift", - "taggerscript", - "yaml", - "yml", - "tap", - "tcl", - "tk", - "thrift", - "tp", - "twig", - "craftcms", - "typescript", - "ts", - "tsx", - "mts", - "cts", - "vala", - "vbnet", - "vb", - "vbscript", - "vbs", - "vbscript-html", - "verilog", - "v", - "sv", - "svh", - "vhdl", - "vim", - "wasm", - "wren", - "x86asm", - "xl", - "tao", - "xquery", - "xpath", - "xq", - "xqm", - "zephir", - "zep" - }.ToFrozenSet(); -} diff --git a/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs deleted file mode 100644 index a330a13ca4..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index df6a4bcd09..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/MinMaxLengthAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 5876555122..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/MinMaxValueAttribute.cs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 5c901ce30e..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/RemainingTextAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class RemainingTextAttribute : Attribute; diff --git a/DSharpPlus.Commands/ArgumentModifiers/VariadicArgumentAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/VariadicArgumentAttribute.cs deleted file mode 100644 index 4a31b0734b..0000000000 --- a/DSharpPlus.Commands/ArgumentModifiers/VariadicArgumentAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -/// -/// Specifies that a parameter can accept multiple arguments. -/// This attribute is only valid on parameters of type . -/// -[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public sealed class VariadicArgumentAttribute : Attribute -{ - /// - /// The maximum number of arguments that this parameter can accept. - /// - public int MaximumArgumentCount { get; } - - /// - /// The minimum number of arguments that this parameter can accept. - /// - public int MinimumArgumentCount { get; } - - /// - /// The number of arguments that this parameter can accept. - /// - /// The maximum number of arguments that this parameter can accept. - /// The minimum number of arguments that this parameter can accept. - public VariadicArgumentAttribute(int maximumArgumentCount, int minimumArgumentCount = 1) - { - ArgumentOutOfRangeException.ThrowIfLessThan(minimumArgumentCount, 1, nameof(minimumArgumentCount)); - ArgumentOutOfRangeException.ThrowIfLessThan(maximumArgumentCount, 1, nameof(maximumArgumentCount)); - ArgumentOutOfRangeException.ThrowIfLessThan(maximumArgumentCount, minimumArgumentCount, nameof(maximumArgumentCount)); - - this.MaximumArgumentCount = maximumArgumentCount; - this.MinimumArgumentCount = minimumArgumentCount; - } -} diff --git a/DSharpPlus.Commands/CommandAttribute.cs b/DSharpPlus.Commands/CommandAttribute.cs deleted file mode 100644 index a51ba0a563..0000000000 --- a/DSharpPlus.Commands/CommandAttribute.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 249e728a96..0000000000 --- a/DSharpPlus.Commands/CommandContext.cs +++ /dev/null @@ -1,138 +0,0 @@ -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 deleted file mode 100644 index c019abb573..0000000000 --- a/DSharpPlus.Commands/CommandsConfiguration.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index ffdb77b93f..0000000000 --- a/DSharpPlus.Commands/CommandsExtension.cs +++ /dev/null @@ -1,594 +0,0 @@ -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()); - } - - // Configure processors in a specific order to ensure dependencies are met - 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 deleted file mode 100644 index befc6002c1..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ContextCheckAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 369eb3d348..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ContextCheckFailedData.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index c1bb815c44..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ContextCheckMapEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 72a568ec51..0000000000 --- a/DSharpPlus.Commands/ContextChecks/DirectMessageUsageAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 90e1806709..0000000000 --- a/DSharpPlus.Commands/ContextChecks/DirectMessageUsageCheck.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 366e7e530d..0000000000 --- a/DSharpPlus.Commands/ContextChecks/IContextCheck.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index df65e9e426..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/IParameterCheck.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 804096cee9..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 917da55df1..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckFailedData.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 5a66b3d572..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 2a52221a44..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckMapEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 25bd094599..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHierarchyCheck.cs +++ /dev/null @@ -1,40 +0,0 @@ -#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 deleted file mode 100644 index 7d32b5974a..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherBotHierarchyAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 14dd0ffa4a..0000000000 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherUserHierarchyAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 4fd79587ce..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index f4bb016cee..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerCheck.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 6b4700e72a..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequireGuildAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index ac66b6e33a..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequireGuildCheck.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 977bc79080..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequireNsfwAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index c692a963f6..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequireNsfwCheck.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 7cb05cd8a1..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequirePermissionsAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 111ae160ec..0000000000 --- a/DSharpPlus.Commands/ContextChecks/RequirePermissionsCheck.cs +++ /dev/null @@ -1,37 +0,0 @@ -#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 deleted file mode 100644 index 55f4142cad..0000000000 --- a/DSharpPlus.Commands/ContextChecks/UnconditionalCheckAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 181bdf75a8..0000000000 --- a/DSharpPlus.Commands/Converters/BooleanConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 6140b33aa5..0000000000 --- a/DSharpPlus.Commands/Converters/ByteConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index d7f358068c..0000000000 --- a/DSharpPlus.Commands/Converters/ConverterContext.cs +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 9990e91b84..0000000000 --- a/DSharpPlus.Commands/Converters/ConverterDelegate`1.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 6388b4dc8f..0000000000 --- a/DSharpPlus.Commands/Converters/DateTimeOffsetConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index ae84909c01..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordAttachmentConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index bba7ad9cf4..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordChannelConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 1ade3eabc4..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordEmojiConverter.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index da5d7f38ef..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordMemberConverter.cs +++ /dev/null @@ -1,129 +0,0 @@ -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; -using DSharpPlus.Exceptions; - -using Raffinert.FuzzySharp; - -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 username first - if (context.Parameter.Attributes.Any(x => x.GetType() == typeof(DisableUsernameFuzzyMatchingAttribute))) - { - if (context.Guild.Members.Values.FirstOrDefault(member => member.Username.Equals(value, StringComparison.InvariantCultureIgnoreCase)) - is DiscordMember memberByUsername) - { - return Optional.FromValue(memberByUsername); - } - - // then try display name - DiscordMember? fuzzyDisplayNameMember = context.Guild.Members.Values.Select(x => new - { - Member = x, - Ratio = Fuzz.Ratio(x.DisplayName, value) - }) - .OrderByDescending(x => x.Ratio) - .FirstOrDefault(x => x.Ratio >= 85) - ?.Member; - - if (fuzzyDisplayNameMember is not null) - { - return Optional.FromValue(fuzzyDisplayNameMember); - } - } - else - { - // match them all and return the highest matching member at a score of 85 or higher - // unfortunately, the tuple loses its names, so we can't give this readable names - IEnumerable<(DiscordMember?, int, int)> sortedMembers = context.Guild.Members.Values.Select(x => - ( - Item1: x, - Item2: Fuzz.Ratio(x.Username, value), - Item3: Fuzz.Ratio(x.DisplayName, value) - )); - - DiscordMember? highestScoringUsername = sortedMembers.OrderByDescending(x => x.Item2) - .FirstOrDefault(x => x.Item2 >= 85) - .Item1; - - if (highestScoringUsername is not null) - { - return Optional.FromValue(highestScoringUsername); - } - - // then by display name - DiscordMember? highestScoringDisplayName = sortedMembers.OrderByDescending(x => x.Item3) - .FirstOrDefault(x => x.Item3 >= 85) - .Item1; - - if (highestScoringDisplayName is not null) - { - return Optional.FromValue(highestScoringDisplayName); - } - } - } - } - - 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 deleted file mode 100644 index a6c5881db0..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordMessageConverter.cs +++ /dev/null @@ -1,114 +0,0 @@ -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 deleted file mode 100644 index 5e1a863aa0..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordRoleConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index da3f29b79f..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordSnowflakeObjectConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 354f15d609..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordThreadChannelConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index f53c9e002d..0000000000 --- a/DSharpPlus.Commands/Converters/DiscordUserConverter.cs +++ /dev/null @@ -1,133 +0,0 @@ -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; -using DSharpPlus.Exceptions; - -using Raffinert.FuzzySharp; - -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) - { - // try username first - if (context.Parameter.Attributes.Any(x => x.GetType() == typeof(DisableUsernameFuzzyMatchingAttribute))) - { - if (context.Guild.Members.Values.FirstOrDefault(member => member.Username.Equals(value, StringComparison.InvariantCultureIgnoreCase)) - is DiscordMember memberByUsername) - { - return Optional.FromValue(memberByUsername); - } - - // then try display name - DiscordMember? fuzzyDisplayNameMember = context.Guild.Members.Values.Select(x => new - { - Member = x, - Ratio = Fuzz.Ratio(x.DisplayName, value) - }) - .OrderByDescending(x => x.Ratio) - .FirstOrDefault(x => x.Ratio >= 85) - ?.Member; - - if (fuzzyDisplayNameMember is not null) - { - return Optional.FromValue(fuzzyDisplayNameMember); - } - } - else - { - // match them all and return the highest matching member at a score of 85 or higher - // unfortunately, that tuple loses its names, but its okay, still helps with readability here - IEnumerable<(DiscordMember?, int, int)> sortedMembers = context.Guild.Members.Values.Select(x => - ( - Item1: x, - Item2: Fuzz.Ratio(x.Username, value), - Item3: Fuzz.Ratio(x.DisplayName, value) - )); - - DiscordMember? highestScoringUsername = sortedMembers.OrderByDescending(x => x.Item2) - .FirstOrDefault(x => x.Item2 >= 85) - .Item1; - - if (highestScoringUsername is not null) - { - return Optional.FromValue(highestScoringUsername); - } - - // then by display name - DiscordMember? highestScoringDisplayName = sortedMembers.OrderByDescending(x => x.Item3) - .FirstOrDefault(x => x.Item3 >= 85) - .Item1; - - if (highestScoringDisplayName is not null) - { - return Optional.FromValue(highestScoringDisplayName); - } - } - } - - // 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 deleted file mode 100644 index b3c0cb33db..0000000000 --- a/DSharpPlus.Commands/Converters/DoubleConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 6951acdbb6..0000000000 --- a/DSharpPlus.Commands/Converters/EnumArgumentConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -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/EnumArgumentConverter`1.cs b/DSharpPlus.Commands/Converters/EnumArgumentConverter`1.cs deleted file mode 100644 index 6165e6d1e2..0000000000 --- a/DSharpPlus.Commands/Converters/EnumArgumentConverter`1.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 where T : struct, Enum -{ - private static readonly Type baseEnumType = Enum.GetUnderlyingType(typeof(T)); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Multiple Choice"; - - public Task> ConvertAsync(ConverterContext context) - { - // Null check for nullability warnings, however I think this is redundant as the base processor should handle this - if (context.Argument is null) - { - return Task.FromResult(Optional.FromNoValue()); - } - else if (context.Argument is string stringArgument) - { - return Enum.TryParse(stringArgument, true, out T result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); - } - - // 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. - // 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. - T value = (T)Convert.ChangeType(context.Argument, baseEnumType, CultureInfo.InvariantCulture); - return Enum.IsDefined(value) - ? Task.FromResult(Optional.FromValue(value)) - : Task.FromResult(Optional.FromNoValue()); - } -} diff --git a/DSharpPlus.Commands/Converters/FloatConverter.cs b/DSharpPlus.Commands/Converters/FloatConverter.cs deleted file mode 100644 index b57c980797..0000000000 --- a/DSharpPlus.Commands/Converters/FloatConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 4976f31311..0000000000 --- a/DSharpPlus.Commands/Converters/IArgumentConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index df34f940a6..0000000000 --- a/DSharpPlus.Commands/Converters/Int16Converter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 48f4011010..0000000000 --- a/DSharpPlus.Commands/Converters/Int32Converter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index ecd2ab3238..0000000000 --- a/DSharpPlus.Commands/Converters/Int64Converter.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 1c1c7f0f1b..0000000000 --- a/DSharpPlus.Commands/Converters/Results/ArgumentFailedConversionResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 50ca2ff504..0000000000 --- a/DSharpPlus.Commands/Converters/Results/ArgumentNotParsedResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index bcf2ec8eef..0000000000 --- a/DSharpPlus.Commands/Converters/SByteConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 7d27f5ede1..0000000000 --- a/DSharpPlus.Commands/Converters/StringConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index 4ed90f4a66..0000000000 --- a/DSharpPlus.Commands/Converters/TimeSpanConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index b6945b8cb6..0000000000 --- a/DSharpPlus.Commands/Converters/UInt16Converter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index e83cb0f13a..0000000000 --- a/DSharpPlus.Commands/Converters/UInt32Converter.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index ce92697bf3..0000000000 --- a/DSharpPlus.Commands/Converters/UInt64Converter.cs +++ /dev/null @@ -1,22 +0,0 @@ -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/DSharpPlus.Commands.csproj b/DSharpPlus.Commands/DSharpPlus.Commands.csproj deleted file mode 100644 index ce0ae14e86..0000000000 --- a/DSharpPlus.Commands/DSharpPlus.Commands.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - DSharpPlus.Commands - An all in one package for managing commands. - $(PackageTags), commands, commandsnext, slash-commands, interactions - true - - - - - - - - - - \ No newline at end of file diff --git a/DSharpPlus.Commands/DefaultCommandExecutor.cs b/DSharpPlus.Commands/DefaultCommandExecutor.cs deleted file mode 100644 index 795c02b7b1..0000000000 --- a/DSharpPlus.Commands/DefaultCommandExecutor.cs +++ /dev/null @@ -1,366 +0,0 @@ -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 failedChecks; - } - - // 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 deleted file mode 100644 index 71cc505fe7..0000000000 --- a/DSharpPlus.Commands/EventArgs/CommandErroredEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2567f1def4..0000000000 --- a/DSharpPlus.Commands/EventArgs/CommandExecutedEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 499555e1b8..0000000000 --- a/DSharpPlus.Commands/EventArgs/ConfigureCommandsEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 2f1a4b0d0d..0000000000 --- a/DSharpPlus.Commands/Exceptions/ChecksFailedException.cs +++ /dev/null @@ -1,17 +0,0 @@ -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/ChoiceProviderFailedException.cs b/DSharpPlus.Commands/Exceptions/ChoiceProviderFailedException.cs deleted file mode 100644 index 7e5068d31a..0000000000 --- a/DSharpPlus.Commands/Exceptions/ChoiceProviderFailedException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace DSharpPlus.Commands.Exceptions; - -/// -/// Thrown if a choice provider failed to execute. -/// -public class ChoiceProviderFailedException : Exception -{ - public Type ProviderType { get; private set; } - - public ChoiceProviderFailedException(Type providerType, Exception? innerException) - : base($"Choice provider {providerType} failed.", innerException) - => this.ProviderType = providerType; - - public override string ToString() - => $"Choice provider {this.ProviderType} failed with exception {this.InnerException}"; -} diff --git a/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs b/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs deleted file mode 100644 index 2ee15decc0..0000000000 --- a/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 4474035131..0000000000 --- a/DSharpPlus.Commands/Exceptions/CommandNotFoundException.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/CommandRegistrationFailedException.cs b/DSharpPlus.Commands/Exceptions/CommandRegistrationFailedException.cs deleted file mode 100644 index 3ccac34f11..0000000000 --- a/DSharpPlus.Commands/Exceptions/CommandRegistrationFailedException.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace DSharpPlus.Commands.Exceptions; - -/// -/// Thrown if the extension failed to register application commands and an attempt was made to execute an application command. -/// -public class CommandRegistrationFailedException : Exception; diff --git a/DSharpPlus.Commands/Exceptions/CommandsException.cs b/DSharpPlus.Commands/Exceptions/CommandsException.cs deleted file mode 100644 index 32175b960a..0000000000 --- a/DSharpPlus.Commands/Exceptions/CommandsException.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 7049bc419f..0000000000 --- a/DSharpPlus.Commands/Exceptions/ParameterChecksFailedException.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index c487c93c76..0000000000 --- a/DSharpPlus.Commands/Exceptions/ParseArgumentException.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 832505b8ef..0000000000 --- a/DSharpPlus.Commands/ExtensionMethods.cs +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 400c14ab92..0000000000 --- a/DSharpPlus.Commands/ICommandExecutor.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index ac2d804631..0000000000 --- a/DSharpPlus.Commands/Invocation/AnonymousDelegateUtil.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 3a5b859b27..0000000000 --- a/DSharpPlus.Commands/Invocation/CommandEmitUtil.cs +++ /dev/null @@ -1,134 +0,0 @@ -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 deleted file mode 100644 index 774d905e7b..0000000000 --- a/DSharpPlus.Commands/ParameterAttribute.cs +++ /dev/null @@ -1,30 +0,0 @@ -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/ProcessorInvokingHandlers.cs b/DSharpPlus.Commands/ProcessorInvokingHandlers.cs deleted file mode 100644 index 324db5ff8e..0000000000 --- a/DSharpPlus.Commands/ProcessorInvokingHandlers.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.MessageCommands; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Processors.UserCommands; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Commands; - -// this is a temporary measure until we can land proper IoC support -internal sealed class ProcessorInvokingHandlers : - IEventHandler, - IEventHandler, - IEventHandler -{ - private readonly CommandsExtension extension; - - public ProcessorInvokingHandlers(CommandsExtension extension) => this.extension = extension; - - // user and message context menu commands - public async Task HandleEventAsync(DiscordClient sender, ContextMenuInteractionCreatedEventArgs eventArgs) - { - if (this.extension.TryGetProcessor(out UserCommandProcessor? userProcessor)) - { - await userProcessor.ExecuteInteractionAsync(sender, eventArgs); - } - - if (this.extension.TryGetProcessor(out MessageCommandProcessor? messageProcessor)) - { - await messageProcessor.ExecuteInteractionAsync(sender, eventArgs); - } - } - - // slash commands - public async Task HandleEventAsync(DiscordClient sender, InteractionCreatedEventArgs eventArgs) - { - if (this.extension.TryGetProcessor(out SlashCommandProcessor? slashProcessor)) - { - await slashProcessor.ExecuteInteractionAsync(sender, eventArgs); - } - } - - public async Task HandleEventAsync(DiscordClient sender, MessageCreatedEventArgs eventArgs) - { - if (this.extension.TryGetProcessor(out TextCommandProcessor? processor)) - { - await processor.ExecuteTextCommandAsync(sender, eventArgs); - } - } -} diff --git a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs deleted file mode 100644 index 73b1b9c2bc..0000000000 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index a21ae4cccd..0000000000 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.ConverterDelegateFactory.cs +++ /dev/null @@ -1,186 +0,0 @@ -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 deleted file mode 100644 index 3f8ce43f26..0000000000 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.cs +++ /dev/null @@ -1,394 +0,0 @@ -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); - } - - // 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()); - - 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 deleted file mode 100644 index 7394660526..0000000000 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/ICommandProcessor.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index f655dfe8ce..0000000000 --- a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 0460cc8f22..0000000000 --- a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandLogging.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index d1b078325f..0000000000 --- a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandProcessor.cs +++ /dev/null @@ -1,208 +0,0 @@ -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); - - if(command.GuildIds.Count == 0) - { - applicationCommands.Add(await ToApplicationCommandAsync(command)); - continue; - } - - DiscordApplicationCommand applicationCommand = await ToApplicationCommandAsync(command); - foreach (ulong guildId in command.GuildIds) - { - this.slashCommandProcessor.AddGuildApplicationCommand(guildId, applicationCommand); - } - } - - this.slashCommandProcessor.AddGlobalApplicationCommands(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 SlashCommandProcessor.ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name", this.extension!.ServiceProvider); - } - - 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 deleted file mode 100644 index 3109527ce5..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/ChoiceDisplayNameAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -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/SimpleAutoCompleteProvider.cs b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SimpleAutoCompleteProvider.cs deleted file mode 100644 index 26c8dbe73d..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SimpleAutoCompleteProvider.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using Raffinert.FuzzySharp; - -namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; - -/// -/// An abstract class that may be derived to provide a simple -/// autocomplete implementation that filters from a list of options -/// based on user input. -/// -public class SimpleAutoCompleteProvider : IAutoCompleteProvider -{ - /// - /// The list of available choices for this autocomplete provider, - /// without any filtering applied. - /// - protected virtual IEnumerable Choices { get; } = []; - - /// - /// The string comparison used between user input and option names. - /// - protected virtual StringComparison Comparison { get; } = StringComparison.OrdinalIgnoreCase; - - /// - /// If , when multiple choices have the same - /// Value, only - /// the first such choice is presented to the user. Otherwise (if - /// ), has no effect. - /// - protected virtual bool AllowDuplicateValues { get; } = true; - - /// - /// The method by which choices are matched to the user input. - /// - protected virtual SimpleAutoCompleteStringMatchingMethod MatchingMethod { get; } = SimpleAutoCompleteStringMatchingMethod.Contains; - - /// - public ValueTask> AutoCompleteAsync(AutoCompleteContext context) - { - IEnumerable results; - - switch (this.MatchingMethod) - { - case SimpleAutoCompleteStringMatchingMethod.Contains: - results = this.Choices - .Select(c => (Choice: c, Index: c.Name.IndexOf(context.UserInput ?? "", this.Comparison))) - .Where(ci => ci.Index != -1) - .OrderBy(ci => ci.Index) - .Select(c => c.Choice); - break; - case SimpleAutoCompleteStringMatchingMethod.StartsWith: - results = this.Choices - .Where(c => c.Name.StartsWith(context.UserInput ?? "", this.Comparison)); - break; - case SimpleAutoCompleteStringMatchingMethod.Fuzzy: - if (context.UserInput == null || context.UserInput == "") - results = this.Choices; - else - { - Func caseMatching = Comparison switch - { - StringComparison.CurrentCultureIgnoreCase => s => s.ToLower(), - StringComparison.InvariantCultureIgnoreCase or StringComparison.OrdinalIgnoreCase => s => s.ToLowerInvariant(), - _ => s => s - }; - string userInput = caseMatching(context.UserInput ?? ""); - results = this.Choices - .Select(c => (Choice: c, Score: Fuzz.PartialRatio(userInput, caseMatching(c.Name)))) - .Where(cs => cs.Score >= 50) - .OrderByDescending(cs => cs.Score) - .Select(c => c.Choice); - } - break; - default: - throw new InvalidOperationException($"The value {this.MatchingMethod} is not valid."); - } - - if (!this.AllowDuplicateValues) - { - results = results.DistinctBy(c => c.Value); - } - - return ValueTask.FromResult(results.Take(25)); - } - - /// - /// Converts a sequence of objects into autocomplete choices. - /// - /// The input sequence. - /// The sequence of autocomplete choices. - public static IEnumerable Convert(params IEnumerable options) - { - return options.Select(o => new DiscordAutoCompleteChoice(o?.ToString() ?? "", o)); - } - - /// - /// Converts a sequence of objects into autocomplete choices. - /// - /// The input sequence. - /// The sequence of autocomplete choices. - public static IEnumerable Convert(params IEnumerable> options) - { - return options.Select(kvp => new DiscordAutoCompleteChoice(kvp.Key, kvp.Value)); - } - - /// - /// Converts a sequence of objects into autocomplete choices. - /// - /// The input sequence. - /// The sequence of autocomplete choices. - public static IEnumerable Convert(params IEnumerable<(string Key, object Value)> options) - { - return options.Select(t => new DiscordAutoCompleteChoice(t.Key, t.Value)); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SimpleAutoCompleteStringMatchingMethod.cs b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SimpleAutoCompleteStringMatchingMethod.cs deleted file mode 100644 index baacc44495..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SimpleAutoCompleteStringMatchingMethod.cs +++ /dev/null @@ -1,28 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; - -/// -/// Represents the string matching method for a -/// . -/// -public enum SimpleAutoCompleteStringMatchingMethod -{ - /// - /// The starts with the - /// user input. - /// - StartsWith, - - /// - /// The contains the - /// user input. - /// - Contains, - - /// - /// The fuzzy matches - /// the user input. - /// - Fuzzy -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs deleted file mode 100644 index 736abfd4db..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index e7e2c0832d..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashChoiceProviderAttribute.cs +++ /dev/null @@ -1,91 +0,0 @@ -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 deleted file mode 100644 index 8730397c66..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/AutoCompleteContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -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/EnumAutoCompleteProvider.cs b/DSharpPlus.Commands/Processors/SlashCommands/EnumAutoCompleteProvider.cs deleted file mode 100644 index 6c54c824d4..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/EnumAutoCompleteProvider.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Reflection; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -/// -/// Provides a cached list of choices for the enum type. -/// -/// The enum type to provide choices for. -public class EnumAutoCompleteProvider : IAutoCompleteProvider where T : struct, Enum -{ - private static readonly DiscordAutoCompleteChoice[] choices; - - static EnumAutoCompleteProvider() - { - List choiceList = []; - foreach (FieldInfo fieldInfo in typeof(T).GetFields()) - { - if (fieldInfo.IsSpecialName || !fieldInfo.IsStatic) - { - continue; - } - - // Add support for ChoiceDisplayNameAttribute - string displayName = fieldInfo.GetCustomAttribute() is ChoiceDisplayNameAttribute displayNameAttribute - ? displayNameAttribute.DisplayName - : fieldInfo.Name; - - object? obj = fieldInfo.GetValue(null); - if (obj is not T) - { - // Hey what the fuck - continue; - } - - // Put ulong as a string, bool, byte, short and int as int, uint and long as long. - choiceList.Add(Convert.ChangeType(obj, Enum.GetUnderlyingType(typeof(T)), CultureInfo.InvariantCulture) switch - { - bool value => new DiscordAutoCompleteChoice(displayName, value ? 1 : 0), - byte value => new DiscordAutoCompleteChoice(displayName, value), - sbyte value => new DiscordAutoCompleteChoice(displayName, value), - short value => new DiscordAutoCompleteChoice(displayName, value), - ushort value => new DiscordAutoCompleteChoice(displayName, value), - int value => new DiscordAutoCompleteChoice(displayName, value), - uint value => new DiscordAutoCompleteChoice(displayName, value), - long value => new DiscordAutoCompleteChoice(displayName, value), - ulong value => new DiscordAutoCompleteChoice(displayName, value), - double value => new DiscordAutoCompleteChoice(displayName, value), - float value => new DiscordAutoCompleteChoice(displayName, value), - _ => throw new UnreachableException($"Unknown enum base type encountered: {obj.GetType()}") - }); - } - - choices = [.. choiceList]; - } - - /// - public ValueTask> AutoCompleteAsync(AutoCompleteContext context) - { - List results = []; - foreach (DiscordAutoCompleteChoice choice in choices) - { - if (choice.Name.Contains(context.UserInput ?? "", StringComparison.OrdinalIgnoreCase)) - { - results.Add(choice); - if (results.Count == 25) - { - break; - } - } - } - - return ValueTask.FromResult>(results); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/EnumOptionChoiceProvider.cs b/DSharpPlus.Commands/Processors/SlashCommands/EnumOptionChoiceProvider.cs deleted file mode 100644 index 7a98a56540..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/EnumOptionChoiceProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -public class EnumOptionChoiceProvider : IChoiceProvider -{ - public ValueTask> ProvideAsync(CommandParameter parameter) - { - List enumNames = []; - Type baseType = IArgumentConverter.GetConverterFriendlyBaseType(parameter.Type); - foreach (FieldInfo fieldInfo in baseType.GetFields()) - { - if (fieldInfo.IsSpecialName || !fieldInfo.IsStatic) - { - continue; - } - else if (fieldInfo.GetCustomAttribute() is ChoiceDisplayNameAttribute displayNameAttribute) - { - enumNames.Add(displayNameAttribute.DisplayName); - } - else - { - enumNames.Add(fieldInfo.Name); - } - } - - // We can use `enumNames.Count` here since `SlashCommandProcessor.Registration.ConfigureCommands` - // will use autocomplete for enums that have more than 25 values. If the user decides to use - // this class manually, `IChoiceProvider.ProvideAsync` will be called and truncate the list - // automatically, warning the user that the list is too long. - List choices = new(enumNames.Count); - Array enumValues = Enum.GetValuesAsUnderlyingType(baseType); - for (int i = 0; i < enumNames.Count; i++) - { - object? obj = enumValues.GetValue(i); - choices.Add(obj switch - { - null => throw new InvalidOperationException($"Failed to get the value of the enum {parameter.Type.Name} for element {enumNames[i]}"), - bool value => new DiscordApplicationCommandOptionChoice(enumNames[i], value ? 1 : 0), - byte value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - sbyte value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - short value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - ushort value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - int value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - uint value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - long value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - ulong value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - float value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - double value => new DiscordApplicationCommandOptionChoice(enumNames[i], value), - _ => new DiscordApplicationCommandOptionChoice(enumNames[i], obj.ToString()!), - }); - } - - return ValueTask.FromResult>(choices); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs b/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs deleted file mode 100644 index 2a1c3da569..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index c90421dbdd..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionConverterContext.cs +++ /dev/null @@ -1,55 +0,0 @@ -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/CaseImplHelpers.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/CaseImplHelpers.cs deleted file mode 100644 index f531c745c3..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/CaseImplHelpers.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Globalization; - -using CommunityToolkit.HighPerformance; -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; - -internal static class CaseImplHelpers -{ - public static void LowercaseCore(ReadOnlySpan raw, ArrayPoolBufferWriter result, CultureInfo culture) - { - for (int i = 0; i < raw.Length; i++) - { - char character = raw[i]; - result.Write(char.ToLower(character, culture)); - } - } - - public static void KebabCaseCore(ReadOnlySpan raw, ArrayPoolBufferWriter result, CultureInfo culture) - { - for (int i = 0; i < raw.Length; i++) - { - char character = raw[i]; - - // camelCase, PascalCase - if (i != 0 && char.IsUpper(character) && result.WrittenSpan[^1] is not ('-' or '_')) - { - result.Write('-'); - } - - result.Write(char.ToLower(character, culture)); - } - } - - public static void SnakeCaseCore(ReadOnlySpan raw, ArrayPoolBufferWriter result, CultureInfo culture) - { - for (int i = 0; i < raw.Length; i++) - { - char character = raw[i]; - - // camelCase, PascalCase - if (i != 0 && char.IsUpper(character) && result.WrittenSpan[^1] is not ('-' or '_')) - { - result.Write('_'); - } - - result.Write(char.ToLower(character, culture)); - } - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs deleted file mode 100644 index 4888c4da68..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs +++ /dev/null @@ -1,33 +0,0 @@ -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/KebabCaseNamingFixer.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingFixer.cs deleted file mode 100644 index 84746fdc9b..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingFixer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -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 KebabCaseNamingFixer : 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); - - return new string(writer.WrittenSpan); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs deleted file mode 100644 index 4382ef8602..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs +++ /dev/null @@ -1,48 +0,0 @@ -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/LowercaseNamingFixer.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingFixer.cs deleted file mode 100644 index 7d97ee1c13..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingFixer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -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 LowercaseNamingFixer : 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); - - return new string(writer.WrittenSpan); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs deleted file mode 100644 index 75842f514e..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs +++ /dev/null @@ -1,47 +0,0 @@ -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/SnakeCaseNamingFixer.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingFixer.cs deleted file mode 100644 index 35e08d852b..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingFixer.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -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 SnakeCaseNamingFixer : 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); - - return new string(writer.WrittenSpan); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs deleted file mode 100644 index 5adf4dbb1f..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 72afe72f34..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionTypes.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 91a4026c35..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/DiscordLocale.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 7b80f2c38f..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/IInteractionLocalizer.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 248dfba6fe..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/InteractionLocalizerAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index f7c1a8ce6d..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/LocalesHelper.cs +++ /dev/null @@ -1,94 +0,0 @@ -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 deleted file mode 100644 index 7fd1b13cb8..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionAllowedContextsAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 49e2313aa0..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionInstallTypeAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -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/RemoteRecordRetentionPolicies/DefaultRemoteRecordRetentionPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/RemoteRecordRetentionPolicies/DefaultRemoteRecordRetentionPolicy.cs deleted file mode 100644 index b0b3fc5b1d..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/RemoteRecordRetentionPolicies/DefaultRemoteRecordRetentionPolicy.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands.RemoteRecordRetentionPolicies; - -internal sealed class DefaultRemoteRecordRetentionPolicy : IRemoteRecordRetentionPolicy -{ - public Task CheckDeletionStatusAsync(DiscordApplicationCommand command) - => Task.FromResult(command.Type != DiscordApplicationCommandType.ActivityEntryPoint); -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/RemoteRecordRetentionPolicies/IRemoteRecordRetentionPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/RemoteRecordRetentionPolicies/IRemoteRecordRetentionPolicy.cs deleted file mode 100644 index 5c8178d847..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/RemoteRecordRetentionPolicies/IRemoteRecordRetentionPolicy.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands.RemoteRecordRetentionPolicies; - -/// -/// Provides a means to customize when and which application commands get deleted from your bot. -/// -public interface IRemoteRecordRetentionPolicy -{ - /// - /// Returns a value indicating whether the application command should be deleted or not. - /// - public Task CheckDeletionStatusAsync(DiscordApplicationCommand command); -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs deleted file mode 100644 index 64d5eecf98..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index eef1583b7b..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandContext.cs +++ /dev/null @@ -1,162 +0,0 @@ -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 - ( - DiscordModalBuilder builder - ) - { - if (this.Interaction.ResponseState is not DiscordInteractionResponseState.Unacknowledged) - { - throw new InvalidOperationException("Modal must be the first response to the interaction."); - } - - if (string.IsNullOrWhiteSpace(builder.CustomId)) - { - throw new ArgumentException("Modal response has to have a custom id"); - } - - 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.Remote.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.Remote.cs deleted file mode 100644 index e412d89f27..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.Remote.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Net.Models; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -public sealed partial class SlashCommandProcessor -{ - private async Task> VerifyAndUpdateRemoteCommandsAsync - ( - IReadOnlyList local, - IReadOnlyList remoteCommands - ) - { - int added = 0, edited = 0, unchanged = 0, deleted = 0; - List updated = []; - List remoteTracking = new(remoteCommands); - - foreach (DiscordApplicationCommand command in local) - { - DiscordApplicationCommand remote; - - // if name and type didnt change check for updates to command, else delete and recreate the command with new name/type - if ((remote = remoteTracking.SingleOrDefault(x => x.Name == command.Name && x.Type == command.Type)) is not null) - { - if (command.WeakEquals(remote)) - { - unchanged++; - updated.Add(remote); - remoteTracking.Remove(remote); - continue; - } - else - { - edited++; - updated.Add(await ModifyGlobalCommandAsync(remote.Id, command)); - remoteTracking.Remove(remote); - continue; - } - } - else - { - added++; - updated.Add(await CreateGlobalCommandAsync(command)); - } - } - - deleted = remoteTracking.Count; - - foreach (DiscordApplicationCommand toDelete in remoteTracking) - { - if (await this.Configuration.RemoteRecordRetentionPolicy.CheckDeletionStatusAsync(toDelete)) - { - await DeleteGlobalCommandAsync(toDelete); - } - } - - if (added != 0 || edited != 0 || deleted != 0) - { - SlashLogging.detectedCommandRecordChanges(this.logger, unchanged, added, edited, deleted, null); - } - else - { - SlashLogging.commandRecordsUnchanged(this.logger, null); - } - - return updated; - } - - private async ValueTask CreateGlobalCommandAsync(DiscordApplicationCommand command) - { - return this.extension.DebugGuildId == 0 - ? await this.extension.Client.CreateGlobalApplicationCommandAsync(command) - : await this.extension.Client.CreateGuildApplicationCommandAsync(this.extension.DebugGuildId, command); - } - -#pragma warning disable IDE0046 - private async ValueTask ModifyGlobalCommandAsync(ulong id, DiscordApplicationCommand command) - { - if (this.extension.DebugGuildId == 0) - { - return await this.extension.Client.EditGlobalApplicationCommandAsync(id, x => CopyToEditModel(command, x)); - } - else - { - return await this.extension.Client.EditGuildApplicationCommandAsync - ( - this.extension.DebugGuildId, - id, - x => CopyToEditModel(command, x) - ); - } - } -#pragma warning restore IDE0046 - - private async ValueTask DeleteGlobalCommandAsync(DiscordApplicationCommand command) - { - if (this.extension.DebugGuildId == 0) - { - await this.extension.Client.DeleteGlobalApplicationCommandAsync(command.Id); - } - else - { - await this.extension.Client.DeleteGuildApplicationCommandAsync(this.extension.DebugGuildId, command.Id); - } - } - - private static void CopyToEditModel(DiscordApplicationCommand command, ApplicationCommandEditModel editModel) - { - editModel.AllowDMUsage = command.AllowDMUsage.HasValue - ? new(command.AllowDMUsage.Value) - : Optional.FromNoValue(); - editModel.DefaultMemberPermissions = command.DefaultMemberPermissions; - editModel.Description = command.Description; - editModel.NameLocalizations = command.NameLocalizations; - editModel.DescriptionLocalizations = command.DescriptionLocalizations; - editModel.IntegrationTypes = command.IntegrationTypes is not null - ? new(command.IntegrationTypes) - : Optional.FromNoValue>(); - editModel.AllowedContexts = command.Contexts is not null - ? new(command.Contexts) - : Optional.FromNoValue>(); - editModel.NSFW = command.NSFW; - editModel.Options = command.Options is not null - ? new(command.Options) - : Optional.FromNoValue>(); - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs deleted file mode 100644 index 236876b68c..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs +++ /dev/null @@ -1,721 +0,0 @@ -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 globalApplicationCommands = []; - private static readonly Dictionary> guildsApplicationCommands = []; - - // 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 AddGlobalApplicationCommands(params DiscordApplicationCommand[] commands) => globalApplicationCommands.AddRange(commands); - public void AddGlobalApplicationCommands(IEnumerable commands) => globalApplicationCommands.AddRange(commands); - public void AddGuildApplicationCommand(ulong guildId, DiscordApplicationCommand command) - { - if (!guildsApplicationCommands.TryGetValue(guildId, out List? guildCommands)) - { - guildCommands = []; - guildsApplicationCommands.Add(guildId, guildCommands); - } - guildCommands.Add(command); - } - - /// - /// 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 globalCommands = []; - Dictionary> guildsCommands = guildsApplicationCommands; - globalCommands.AddRange(globalApplicationCommands); - - 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) - { - globalCommands.Add(applicationCommand); - continue; - } - - foreach (ulong guildId in command.GuildIds) - { - if (!guildsCommands.TryGetValue(guildId, out List? guildCommands)) - { - guildCommands = []; - guildsCommands.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(globalCommands) - : await this.extension.Client.BulkOverwriteGuildApplicationCommandsAsync - ( - this.extension.DebugGuildId, - globalCommands - ) - ); - } - 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(globalCommands, preexisting)); - } - - // for the time being, we still overwrite guilds by force - foreach (KeyValuePair> kv in guildsCommands) - { - 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", this.extension!.ServiceProvider); - descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.description", this.extension!.ServiceProvider); - } - - 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", this.extension!.ServiceProvider)) - { - 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", this.extension!.ServiceProvider); - } - - 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 + ".name", this.extension!.ServiceProvider)) - { - nameLocalizations[ietfTag] = this.Configuration.NamingPolicy.TransformText - ( - name, - ietfTag == "en-US" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(ietfTag) - ); - } - - descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, localeIdBuilder + ".description", this.extension!.ServiceProvider); - } - - 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 static async ValueTask> ExecuteLocalizerAsync(Type localizer, string name, IServiceProvider serviceProvider) - { - using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); - IInteractionLocalizer instance; - try - { - instance = (IInteractionLocalizer)ActivatorUtilities.CreateInstance(scope.ServiceProvider, localizer); - } - catch (Exception) - { - ILogger logger = 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/SlashCommandProcessor.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.cs deleted file mode 100644 index 699d75a55d..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.Converters.Results; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -public sealed partial class SlashCommandProcessor : BaseCommandProcessor -{ - // Required for GuildDownloadCompleted event - public const DiscordIntents RequiredIntents = DiscordIntents.Guilds; - - /// - public override IReadOnlyList Commands => applicationCommandMapping.Values; - - /// - /// The configuration values being used for this processor. - /// - public SlashCommandConfiguration Configuration { get; init; } - - /// - /// Whether the application commands have been registered. - /// - private bool isApplicationCommandsRegistered; - - /// - /// Creates a new instance of . - /// - public SlashCommandProcessor() : this(new()) { } - - /// - /// Creates a new instance of . - /// - /// The configuration values to use for this processor. - public SlashCommandProcessor(SlashCommandConfiguration configuration) => this.Configuration = configuration; - - /// - public override async ValueTask ConfigureAsync(CommandsExtension extension) - { - // Find all converters and prepare other helpful data. - await base.ConfigureAsync(extension); - - // Register the commands if the user wants to. - if (!this.isApplicationCommandsRegistered && this.Configuration.RegisterCommands) - { - await RegisterSlashCommandsAsync(extension); - } - } - - public async Task ExecuteInteractionAsync(DiscordClient client, InteractionCreatedEventArgs eventArgs) - { - if (this.extension is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - else if (eventArgs.Interaction.Type - is not DiscordInteractionType.ApplicationCommand - and not DiscordInteractionType.AutoComplete - || eventArgs.Interaction.Data.Type is not DiscordApplicationCommandType.SlashCommand) - { - return; - } - else if (this.ConverterDelegates.Count == 0) - { - SlashLogging.interactionReceivedBeforeConfigured(this.logger, null); - return; - } - - AsyncServiceScope serviceScope = this.extension.ServiceProvider.CreateAsyncScope(); - - if (this.registrationFailed) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = new SlashCommandContext() - { - Arguments = new Dictionary(), - Channel = eventArgs.Interaction.Channel, - Command = null!, - Extension = this.extension, - ServiceScope = serviceScope, - User = eventArgs.Interaction.User, - Interaction = eventArgs.Interaction, - Options = eventArgs.Interaction.Data.Options ?? [], - }, - CommandObject = null, - Exception = new CommandRegistrationFailedException(), - }); - - await serviceScope.DisposeAsync(); - return; - } - - if (!TryFindCommand(eventArgs.Interaction, out Command? command, out IReadOnlyList? options)) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = new SlashCommandContext() - { - Arguments = new Dictionary(), - Channel = eventArgs.Interaction.Channel, - Command = null!, - Extension = this.extension, - ServiceScope = serviceScope, - User = eventArgs.Interaction.User, - Interaction = eventArgs.Interaction, - Options = eventArgs.Interaction.Data.Options ?? [], - }, - CommandObject = null, - Exception = new CommandNotFoundException(eventArgs.Interaction.Data.Name), - }); - - await serviceScope.DisposeAsync(); - return; - } - - InteractionConverterContext converterContext = new() - { - Channel = eventArgs.Interaction.Channel, - Command = command, - Extension = this.extension, - Interaction = eventArgs.Interaction, - Options = options, - ParameterNamePolicy = this.Configuration.NamingPolicy, - ServiceScope = serviceScope, - User = eventArgs.Interaction.User, - }; - - if (eventArgs.Interaction.Type is DiscordInteractionType.AutoComplete) - { - AutoCompleteContext? autoCompleteContext = await ParseAutoCompleteArgumentsAsync(converterContext); - if (autoCompleteContext is not null) - { - foreach (Attribute attribute in autoCompleteContext.Parameter.Attributes) - { - if (attribute is SlashAutoCompleteProviderAttribute autoCompleteProviderAttribute) - { - await eventArgs.Interaction.CreateResponseAsync( - DiscordInteractionResponseType.AutoCompleteResult, - new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(await autoCompleteProviderAttribute.AutoCompleteAsync(autoCompleteContext)) - ); - - break; - } - } - } - - converterContext.ServiceScope.Dispose(); - return; - } - - IReadOnlyDictionary parsedArguments = await ParseParametersAsync(converterContext); - SlashCommandContext 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 argumentFailedConversionResult) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = commandContext, - CommandObject = null, - Exception = new ArgumentParseException(argument.Key, argumentFailedConversionResult), - }); - - await serviceScope.DisposeAsync(); - return; - } - else if (argument.Value is ArgumentNotParsedResult) - { - 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 bool TryFindCommand(DiscordInteraction interaction, [NotNullWhen(true)] out Command? command, [NotNullWhen(true)] out IReadOnlyList? options) - { - if (!applicationCommandMapping.TryGetValue(interaction.Data.Id, out command)) - { - options = null; - return false; - } - - // Cache this for later use. - CultureInfo culture = ResolveCulture(interaction); - - // Resolve subcommands, which do not have subcommand id's. - options = interaction.Data.Options ?? []; - while (options.Any()) - { - DiscordInteractionDataOption option = options[0]; - if (option.Type is not DiscordApplicationCommandOptionType.SubCommandGroup and not DiscordApplicationCommandOptionType.SubCommand) - { - break; - } - - command = command.Subcommands.First(subcommandName => this.Configuration.NamingPolicy.TransformText(subcommandName.Name, culture) == option.Name); - options = option.Options ?? []; - } - - return true; - } - - public async ValueTask ClearDiscordSlashCommandsAsync(bool clearGuildCommands = false) - { - if (this.extension is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - await this.extension.Client.BulkOverwriteGlobalApplicationCommandsAsync(new List()); - if (!clearGuildCommands) - { - return; - } - - foreach (ulong guildId in this.extension.Client.Guilds.Keys) - { - await this.extension.Client.BulkOverwriteGuildApplicationCommandsAsync(guildId, new List()); - } - } - - private async ValueTask ParseAutoCompleteArgumentsAsync(InteractionConverterContext converterContext) - { - if (this.extension is null) - { - return null; - } - - CultureInfo culture = ResolveCulture(converterContext.Interaction); - Dictionary parsedArguments = []; - CommandParameter? autoCompleteParameter = null; - DiscordInteractionDataOption? autoCompleteOption = null; - try - { - // Parse until we find the parameter that the user is currently typing - while (converterContext.NextParameter()) - { - DiscordInteractionDataOption? option = converterContext.Options.FirstOrDefault(discordOption => discordOption.Name.Equals( - converterContext.Extension.GetProcessor().Configuration.NamingPolicy.GetParameterName(converterContext.Parameter, culture, -1), - StringComparison.OrdinalIgnoreCase) - ); - - if (option is null) - { - continue; - } - else if (option.Focused) - { - autoCompleteParameter = converterContext.Parameter; - autoCompleteOption = option; - break; - } - - IOptional optional = await this.ConverterDelegates[IArgumentConverter.GetConverterFriendlyBaseType(converterContext.Parameter.Type)](converterContext); - parsedArguments.Add(converterContext.Parameter, optional.HasValue ? optional.RawValue : converterContext.Parameter.DefaultValue); - } - - if (autoCompleteParameter is null || autoCompleteOption is null) - { - this.logger.LogWarning("Cannot find the auto complete parameter that the user is currently typing - this should be reported to library developers."); - return null; - } - } - catch (Exception error) - { - await this.extension.commandErrored.InvokeAsync(converterContext.Extension, new CommandErroredEventArgs() - { - Context = new SlashCommandContext() - { - Arguments = parsedArguments, - Channel = converterContext.Interaction.Channel, - Command = converterContext.Command, - Extension = converterContext.Extension, - Interaction = converterContext.Interaction, - Options = converterContext.Options, - ServiceScope = converterContext.ServiceScope, - User = converterContext.Interaction.User, - }, - Exception = new ArgumentParseException(converterContext.Parameter, new() - { - Error = error, - Value = converterContext.Argument?.RawValue - }), - CommandObject = null, - }); - - return null; - } - - return new AutoCompleteContext() - { - Arguments = parsedArguments, - Parameter = autoCompleteParameter, - Channel = converterContext.Interaction.Channel, - Command = converterContext.Command, - Extension = converterContext.Extension, - Interaction = converterContext.Interaction, - Options = converterContext.Options, - ServiceScope = converterContext.ServiceScope, - User = converterContext.Interaction.User, - UserInput = autoCompleteOption.RawValue, - }; - } - - public override SlashCommandContext CreateCommandContext(InteractionConverterContext converterContext, IReadOnlyDictionary parsedArguments) - { - return new() - { - Arguments = parsedArguments, - Channel = converterContext.Interaction.Channel, - Command = converterContext.Command, - Extension = this.extension ?? throw new InvalidOperationException("SlashCommandProcessor has not been configured."), - Interaction = converterContext.Interaction, - Options = converterContext.Options, - ServiceScope = converterContext.ServiceScope, - User = converterContext.Interaction.User, - }; - } -} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs deleted file mode 100644 index 2c3dfabeb0..0000000000 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 991cbd101c..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextChannelTypesCheck.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 2215d65f49..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 0f527f4433..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyCheck.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 228e00e1ea..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxLengthCheck.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index e61b91b3d8..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxValueCheck.cs +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 96a0ed8c79..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ConverterRequiresText.cs +++ /dev/null @@ -1,27 +0,0 @@ -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/DisableUsernameFuzzyMatchingAttribute.cs b/DSharpPlus.Commands/Processors/TextCommands/DisableUsernameFuzzyMatchingAttribute.cs deleted file mode 100644 index 71d20a2826..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/DisableUsernameFuzzyMatchingAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -/// -/// Applied to parameters of types and , prevents DSharpPlus from fuzzy-matching -/// against their username and requires an exact, case-insensitive match instead. -/// -[AttributeUsage(AttributeTargets.Parameter)] -public class DisableUsernameFuzzyMatchingAttribute : Attribute; diff --git a/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs b/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs deleted file mode 100644 index aa8d78c4f9..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 3cb3403580..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultPrefixResolver.cs +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 1eb3bcb479..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicer.cs +++ /dev/null @@ -1,119 +0,0 @@ -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 deleted file mode 100644 index c829907bc5..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/Parsing/IPrefixResolver.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 7a3c2662e6..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/TextCommandConfiguration.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 8489cf27b6..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/TextCommandContext.cs +++ /dev/null @@ -1,149 +0,0 @@ -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 deleted file mode 100644 index c9ebb39315..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/TextCommandProcessor.cs +++ /dev/null @@ -1,377 +0,0 @@ -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 deleted file mode 100644 index 8289bdcb5a..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/TextConverterContext.cs +++ /dev/null @@ -1,143 +0,0 @@ -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 deleted file mode 100644 index 52df1319f0..0000000000 --- a/DSharpPlus.Commands/Processors/TextCommands/TextLogging.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2a972e9e97..0000000000 --- a/DSharpPlus.Commands/Processors/UserCommands/UserCommandContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 7ffca0ed62..0000000000 --- a/DSharpPlus.Commands/Processors/UserCommands/UserCommandLogging.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 5619fe7976..0000000000 --- a/DSharpPlus.Commands/Processors/UserCommands/UserCommandProcessor.cs +++ /dev/null @@ -1,208 +0,0 @@ -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); - - if (command.GuildIds.Count == 0) - { - applicationCommands.Add(await ToApplicationCommandAsync(command)); - continue; - } - - DiscordApplicationCommand applicationCommand = await ToApplicationCommandAsync(command); - foreach (ulong guildId in command.GuildIds) - { - this.slashCommandProcessor.AddGuildApplicationCommand(guildId, applicationCommand); - } - } - - this.slashCommandProcessor.AddGlobalApplicationCommands(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 SlashCommandProcessor.ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name", this.extension!.ServiceProvider); - } - - 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 deleted file mode 100644 index f6564baedb..0000000000 --- a/DSharpPlus.Commands/Properties/AssemblyProperties.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DSharpPlus.Tests")] diff --git a/DSharpPlus.Commands/RefreshEventHandler.cs b/DSharpPlus.Commands/RefreshEventHandler.cs deleted file mode 100644 index 1febe59a03..0000000000 --- a/DSharpPlus.Commands/RefreshEventHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Commands; - -internal sealed class RefreshEventHandler : IEventHandler -{ - private readonly CommandsExtension extension; - - public RefreshEventHandler(CommandsExtension extension) - => this.extension = extension; - - public async Task HandleEventAsync(DiscordClient sender, ClientStartedEventArgs eventArgs) - => await this.extension.RefreshAsync(); -} diff --git a/DSharpPlus.Commands/RegisterToGuildsAttribute.cs b/DSharpPlus.Commands/RegisterToGuildsAttribute.cs deleted file mode 100644 index 584004fa32..0000000000 --- a/DSharpPlus.Commands/RegisterToGuildsAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 75e01e8f16..0000000000 --- a/DSharpPlus.Commands/Trees/Command.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index f9f61d07ef..0000000000 --- a/DSharpPlus.Commands/Trees/CommandBuilder.cs +++ /dev/null @@ -1,288 +0,0 @@ -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 deleted file mode 100644 index d8be7b4e27..0000000000 --- a/DSharpPlus.Commands/Trees/CommandParameter.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 497153ec29..0000000000 --- a/DSharpPlus.Commands/Trees/CommandParameterBuilder.cs +++ /dev/null @@ -1,161 +0,0 @@ -#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 deleted file mode 100644 index cd472726da..0000000000 --- a/DSharpPlus.Commands/Trees/Metadata/AllowDMUsageAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index a0cf4fd7fc..0000000000 --- a/DSharpPlus.Commands/Trees/Metadata/AllowedProcessorsAttribute.cs +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index 05ae9c98bb..0000000000 --- a/DSharpPlus.Commands/Trees/Metadata/DefaultGroupCommandAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index cbeb5af058..0000000000 --- a/DSharpPlus.Commands/Trees/Metadata/TextAliasAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 14d8871ba0..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/AliasesAttribute.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 = aliases; - } -} diff --git a/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs b/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs deleted file mode 100644 index ef4790ac31..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 9bfaa64830..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/CheckBaseAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 477220c3fb..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/CommandAttribute.cs +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 0ba44e6452..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/CooldownAttribute.cs +++ /dev/null @@ -1,330 +0,0 @@ -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 deleted file mode 100644 index b14c44ad7a..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/DescriptionAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 9a2982f13a..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/DontInjectAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 5122db727a..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/GroupAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index b98a9b7972..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/HiddenAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 7d4490cfcf..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/ModuleLifespanAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 034eede1f8..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/PriorityAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index fac04a4022..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RemainingTextAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index e83fffbd97..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index aa0e7aeae3..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireDirectMessageAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 30bb238a07..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireGuildAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 7747f676a4..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireNsfwAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index e41d6fbf7a..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireOwnerAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 2ef7ccb209..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 1b0c284126..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequirePrefixesAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index d57c0ec2f5..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 4ba95363ba..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireRolesAttribute.cs +++ /dev/null @@ -1,141 +0,0 @@ -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 = roleIds; - this.RoleNames = 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 deleted file mode 100644 index 2c2e0175ac..0000000000 --- a/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index 422afb9552..0000000000 --- a/DSharpPlus.CommandsNext/BaseCommandModule.cs +++ /dev/null @@ -1,25 +0,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 deleted file mode 100644 index 3c74d27628..0000000000 --- a/DSharpPlus.CommandsNext/CommandsNextConfiguration.cs +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index 2daef22c2f..0000000000 --- a/DSharpPlus.CommandsNext/CommandsNextEvents.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index a707fc15be..0000000000 --- a/DSharpPlus.CommandsNext/CommandsNextExtension.cs +++ /dev/null @@ -1,1219 +0,0 @@ -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 deleted file mode 100644 index 6076752d20..0000000000 --- a/DSharpPlus.CommandsNext/CommandsNextUtilities.cs +++ /dev/null @@ -1,439 +0,0 @@ -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 deleted file mode 100644 index d3656febae..0000000000 --- a/DSharpPlus.CommandsNext/Converters/ArgumentBindingResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index d500bfd674..0000000000 --- a/DSharpPlus.CommandsNext/Converters/BaseHelpFormatter.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index b8d28c3e16..0000000000 --- a/DSharpPlus.CommandsNext/Converters/DefaultHelpFormatter.cs +++ /dev/null @@ -1,115 +0,0 @@ -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 deleted file mode 100644 index ed847da5ba..0000000000 --- a/DSharpPlus.CommandsNext/Converters/EntityConverters.cs +++ /dev/null @@ -1,329 +0,0 @@ -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 deleted file mode 100644 index b813312ce0..0000000000 --- a/DSharpPlus.CommandsNext/Converters/EnumConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 7c383b84be..0000000000 --- a/DSharpPlus.CommandsNext/Converters/HelpFormatterFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 7472c4239f..0000000000 --- a/DSharpPlus.CommandsNext/Converters/IArgumentConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 7f79a55ad6..0000000000 --- a/DSharpPlus.CommandsNext/Converters/NullableConverter.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 28a35549cc..0000000000 --- a/DSharpPlus.CommandsNext/Converters/NumericConverters.cs +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 66246570a9..0000000000 --- a/DSharpPlus.CommandsNext/Converters/StringConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index c38f1f49f4..0000000000 --- a/DSharpPlus.CommandsNext/Converters/TimeConverters.cs +++ /dev/null @@ -1,60 +0,0 @@ -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/DSharpPlus.CommandsNext.csproj b/DSharpPlus.CommandsNext/DSharpPlus.CommandsNext.csproj deleted file mode 100644 index b3e80ee3ec..0000000000 --- a/DSharpPlus.CommandsNext/DSharpPlus.CommandsNext.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - DSharpPlus.CommandsNext - Advanced command framework for DSharpPlus. - $(PackageTags), commands, commandsnext - true - - - - - - - - \ No newline at end of file diff --git a/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs b/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs deleted file mode 100644 index b410ebb190..0000000000 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs +++ /dev/null @@ -1,297 +0,0 @@ -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 = this.aliasList; - - this.executionCheckList = []; - this.ExecutionChecks = this.executionCheckList; - - this.overloadArgumentSets = []; - this.overloadList = []; - this.Overloads = this.overloadList; - - this.Module = module; - - this.customAttributeList = []; - this.CustomAttributes = 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 = 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 deleted file mode 100644 index 6517824475..0000000000 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandGroupBuilder.cs +++ /dev/null @@ -1,70 +0,0 @@ -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 = 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 = 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 = cs; - return cmd; - } -} diff --git a/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs b/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs deleted file mode 100644 index 05a3efd82b..0000000000 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 4e02bb4356..0000000000 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs +++ /dev/null @@ -1,162 +0,0 @@ -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 = 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 = 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 deleted file mode 100644 index f67ca29360..0000000000 --- a/DSharpPlus.CommandsNext/Entities/Command.cs +++ /dev/null @@ -1,207 +0,0 @@ -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 deleted file mode 100644 index 262660aa30..0000000000 --- a/DSharpPlus.CommandsNext/Entities/CommandArgument.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 IReadOnlyList CustomAttributes { get; internal set; } = []; -} diff --git a/DSharpPlus.CommandsNext/Entities/CommandGroup.cs b/DSharpPlus.CommandsNext/Entities/CommandGroup.cs deleted file mode 100644 index 8f8a01bb03..0000000000 --- a/DSharpPlus.CommandsNext/Entities/CommandGroup.cs +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 6450929b83..0000000000 --- a/DSharpPlus.CommandsNext/Entities/CommandHelpMessage.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 91982d4b8e..0000000000 --- a/DSharpPlus.CommandsNext/Entities/CommandModule.cs +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 1f936c6524..0000000000 --- a/DSharpPlus.CommandsNext/Entities/CommandOverload.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index ca2a37cf94..0000000000 --- a/DSharpPlus.CommandsNext/Entities/CommandResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 0e93e76de1..0000000000 --- a/DSharpPlus.CommandsNext/EventArgs/CommandContext.cs +++ /dev/null @@ -1,154 +0,0 @@ -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 deleted file mode 100644 index fa944a09c1..0000000000 --- a/DSharpPlus.CommandsNext/EventArgs/CommandErrorEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 2cc8496214..0000000000 --- a/DSharpPlus.CommandsNext/EventArgs/CommandEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index c4d99ba78d..0000000000 --- a/DSharpPlus.CommandsNext/EventArgs/CommandExecutionEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 5d8c4ca7e2..0000000000 --- a/DSharpPlus.CommandsNext/Exceptions/ChecksFailedException.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -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 = failedChecks.ToList(); - } -} diff --git a/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs b/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs deleted file mode 100644 index ce4282e77e..0000000000 --- a/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 9fdb77b8b4..0000000000 --- a/DSharpPlus.CommandsNext/Exceptions/DuplicateCommandException.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index f940368d71..0000000000 --- a/DSharpPlus.CommandsNext/Exceptions/DuplicateOverloadException.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -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 = argumentTypes.ToList(); - 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 deleted file mode 100644 index a97fc6be02..0000000000 --- a/DSharpPlus.CommandsNext/Exceptions/InvalidOverloadException.cs +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 88564ae864..0000000000 --- a/DSharpPlus.CommandsNext/Executors/AsynchronousCommandExecutor.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index b502399cfe..0000000000 --- a/DSharpPlus.CommandsNext/Executors/ICommandExecutor.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index f24b22ab40..0000000000 --- a/DSharpPlus.CommandsNext/Executors/ParallelQueuedCommandExecutor.cs +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index e8a4d4f1d7..0000000000 --- a/DSharpPlus.CommandsNext/Executors/SynchronousCommandExecutor.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index d4ccbf2136..0000000000 --- a/DSharpPlus.CommandsNext/ExtensionMethods.cs +++ /dev/null @@ -1,61 +0,0 @@ -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.CommandsNext/MessageHandler.cs b/DSharpPlus.CommandsNext/MessageHandler.cs deleted file mode 100644 index e4ae51f68b..0000000000 --- a/DSharpPlus.CommandsNext/MessageHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.CommandsNext; - -internal sealed class MessageHandler : IEventHandler -{ - private readonly CommandsNextExtension extension; - - public MessageHandler(CommandsNextExtension ext) - => this.extension = ext; - - public async Task HandleEventAsync(DiscordClient sender, MessageCreatedEventArgs eventArgs) - => await this.extension.HandleCommandsAsync(sender, eventArgs); -} diff --git a/DSharpPlus.Http.AspNetCore/DSharpPlus.Http.AspNetCore.csproj b/DSharpPlus.Http.AspNetCore/DSharpPlus.Http.AspNetCore.csproj deleted file mode 100644 index 9130501ef3..0000000000 --- a/DSharpPlus.Http.AspNetCore/DSharpPlus.Http.AspNetCore.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - Library - DSharpPlus.Http.AspNetCore - A package to easily use HTTP-based Discord interactions with DSharpPlus in an ASP.NET Core project. - $(PackageTags), interactions, slash-commands, http-interactions - enable - enable - true - - - - - - - - diff --git a/DSharpPlus.Http.AspNetCore/EndpointRouteBuilderExtensions.cs b/DSharpPlus.Http.AspNetCore/EndpointRouteBuilderExtensions.cs deleted file mode 100644 index a7b9eb89c1..0000000000 --- a/DSharpPlus.Http.AspNetCore/EndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Mime; - -using DSharpPlus.Net.InboundWebhooks; -using DSharpPlus.Net.InboundWebhooks.Transport; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace DSharpPlus.Http.AspNetCore; - -public static class EndpointRouteBuilderExtensions -{ - /// - /// Registers an endpoint to handle HTTP-based interactions from Discord - /// - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder AddDiscordHttpInteractions - ( - this IEndpointRouteBuilder builder, - [StringSyntax("Route")] string url = "/interactions" - ) - => builder.MapPost(url, HandleDiscordInteractionAsync); - - private static async Task HandleDiscordInteractionAsync - ( - HttpContext httpContext, - CancellationToken cancellationToken, - - [FromServices] - DiscordClient client, - - [FromServices] - IInteractionTransportService transportService - ) - { - (int length, byte[]? bodyBuffer) = await ExtractAndValidateBodyAsync(httpContext, cancellationToken, client); - - if (length == -1 || bodyBuffer == null) - { - return; - } - - ArraySegment body = new(bodyBuffer, 0, length); - - byte[] result = await transportService.HandleHttpInteractionAsync(body, cancellationToken); - - ArrayPool.Shared.Return(bodyBuffer); - - httpContext.Response.StatusCode = (int) HttpStatusCode.OK; - httpContext.Response.ContentLength = result.Length; - httpContext.Response.ContentType = MediaTypeNames.Application.Json; - - await httpContext.Response.Body.WriteAsync(result, cancellationToken); - } - - /// - /// Registers an endpoint to handle HTTP-based events from Discord - /// - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder AddDiscordWebhookEvents - ( - this IEndpointRouteBuilder builder, - [StringSyntax("Route")] string url = "/webhook-events" - ) - => builder.MapPost(url, HandleDiscordWebhookEventAsync); - - private static async Task HandleDiscordWebhookEventAsync - ( - HttpContext httpContext, - CancellationToken cancellationToken, - - [FromServices] - DiscordClient client, - - [FromServices] - IWebhookTransportService transportService - ) - { - (int length, byte[]? bodyBuffer) = await ExtractAndValidateBodyAsync(httpContext, cancellationToken, client); - - if (length == -1 || bodyBuffer == null) - { - return; - } - - ArraySegment body = new(bodyBuffer, 0, length); - - - // ReSharper disable MethodSupportsCancellation => we dont care if the request was canceld and always want to return the buffer - _ = transportService.HandleWebhookEventAsync(body).ContinueWith(_ => ArrayPool.Shared.Return(bodyBuffer)); - // ReSharper restore MethodSupportsCancellation - - httpContext.Response.StatusCode = (int) HttpStatusCode.NoContent; - } - - private static async Task<(int length, byte[]? bodyBuffer)> ExtractAndValidateBodyAsync - ( - HttpContext httpContext, - CancellationToken cancellationToken, - DiscordClient client - ) - { - if (!httpContext.Request.Headers.TryGetValue(HeaderNames.ContentLength, out StringValues lengthString) - || !int.TryParse(lengthString, out int length)) - { - httpContext.Response.StatusCode = 400; - return (-1, null); - } - - byte[] bodyBuffer = ArrayPool.Shared.Rent(length); - await httpContext.Request.Body.ReadExactlyAsync(bodyBuffer.AsMemory(..length), cancellationToken); - - if (!TryExtractHeaders(httpContext.Request.Headers, out string? timestamp, out string? key)) - { - httpContext.Response.StatusCode = 401; - ArrayPool.Shared.Return(bodyBuffer); - return (-1, null); - } - - if (!DiscordHeaders.VerifySignature(bodyBuffer.AsSpan(..length), timestamp!, key!, client.CurrentApplication.VerifyKey)) - { - httpContext.Response.StatusCode = 401; - ArrayPool.Shared.Return(bodyBuffer); - return (-1, null); - } - - return (length, bodyBuffer); - } - - internal static bool TryExtractHeaders(IDictionary headers, out string? timestamp, out string? key) - { - timestamp = null; - key = null; - if (headers.TryGetValue(DiscordHeaders.TimestampHeaderName, out StringValues svTimestamp)) - { - timestamp = svTimestamp; - } - - if (headers.TryGetValue(DiscordHeaders.SignatureHeaderName, out StringValues svKey)) - { - key = svKey; - } - - return timestamp is not null && key is not null; - } -} diff --git a/DSharpPlus.Interactivity/DSharpPlus.Interactivity.csproj b/DSharpPlus.Interactivity/DSharpPlus.Interactivity.csproj deleted file mode 100644 index 11d3032dc0..0000000000 --- a/DSharpPlus.Interactivity/DSharpPlus.Interactivity.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - DSharpPlus.Interactivity - An addon that adds interactivity capabilities to commands. - $(PackageTags), interactive, pagination, reactions - true - - - - - - - - \ No newline at end of file diff --git a/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs b/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs deleted file mode 100644 index e6869f024b..0000000000 --- a/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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 deleted file mode 100644 index aeb6c496ae..0000000000 --- a/DSharpPlus.Interactivity/Enums/ButtonPaginationBehavior.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 1c5e63c915..0000000000 --- a/DSharpPlus.Interactivity/Enums/InteractionResponseBehavior.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 1b4e5ee843..0000000000 --- a/DSharpPlus.Interactivity/Enums/PaginationBehaviour.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 3d72c0cc1a..0000000000 --- a/DSharpPlus.Interactivity/Enums/PaginationButtonType.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index 6c6b690c34..0000000000 --- a/DSharpPlus.Interactivity/Enums/PaginationDeletion.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index e39a746678..0000000000 --- a/DSharpPlus.Interactivity/Enums/PollBehaviour.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index eb3ac9f604..0000000000 --- a/DSharpPlus.Interactivity/Enums/SplitType.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 6b614f9435..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentEventWaiter.cs +++ /dev/null @@ -1,160 +0,0 @@ -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 DiscordInteractionResponseBuilder { Content = responseMessage, IsEphemeral = true } - ); - } - else if (args.Interaction.ResponseState is DiscordInteractionResponseState.Deferred) - { - 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 DiscordInteractionResponseBuilder() { Content = responseMessage, IsEphemeral = true } - ); - } - else if (args.Interaction.ResponseState is DiscordInteractionResponseState.Deferred) - { - 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 deleted file mode 100644 index 748eaa0a5c..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentPaginator.cs +++ /dev/null @@ -1,139 +0,0 @@ -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 deleted file mode 100644 index ccf534541a..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/PaginationButtons.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 73676e0986..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ButtonPaginationRequest.cs +++ /dev/null @@ -1,181 +0,0 @@ -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 deleted file mode 100644 index c8d1a0f5a4..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentCollectRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index b8578ff95f..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentMatchRequest.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 7cab3f4bc4..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/InteractionPaginationRequest.cs +++ /dev/null @@ -1,195 +0,0 @@ -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 deleted file mode 100644 index 1b7078c9e5..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs +++ /dev/null @@ -1,134 +0,0 @@ -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) - { - IReadOnlyList 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 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 deleted file mode 100644 index d080da7b71..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/IPaginator.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index a344460f06..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ModalEventWaiter.cs +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index 562499388f..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Paginator.cs +++ /dev/null @@ -1,276 +0,0 @@ -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 deleted file mode 100644 index 962c8fbdad..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Poller.cs +++ /dev/null @@ -1,126 +0,0 @@ -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) - { - IReadOnlyList 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 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 deleted file mode 100644 index f8ca085299..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs +++ /dev/null @@ -1,201 +0,0 @@ -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); - IReadOnlyList? result; - - try - { - await request.tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityCollectorError, ex, "Exception occurred while collecting reactions"); - } - finally - { - result = 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 deleted file mode 100644 index 6d643a1624..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 16ec22a7d2..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/IPaginationRequest.cs +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 98e8ad355b..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 4bd2cfaa3c..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/ModalMatchRequest.cs +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index f459dfa807..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationEmojis.cs +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index f277118947..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs +++ /dev/null @@ -1,195 +0,0 @@ -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 deleted file mode 100644 index 32ca845135..0000000000 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index d93463d09d..0000000000 --- a/DSharpPlus.Interactivity/Extensions/ChannelExtensions.cs +++ /dev/null @@ -1,116 +0,0 @@ -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 deleted file mode 100644 index fe5568f70a..0000000000 --- a/DSharpPlus.Interactivity/Extensions/ClientExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 628c1c3c41..0000000000 --- a/DSharpPlus.Interactivity/Extensions/InteractionExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 9df89cf50f..0000000000 --- a/DSharpPlus.Interactivity/Extensions/MessageExtensions.cs +++ /dev/null @@ -1,222 +0,0 @@ -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 deleted file mode 100644 index 52f64c49ef..0000000000 --- a/DSharpPlus.Interactivity/InteractivityConfiguration.cs +++ /dev/null @@ -1,95 +0,0 @@ -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/InteractivityEventHandler.cs b/DSharpPlus.Interactivity/InteractivityEventHandler.cs deleted file mode 100644 index 9020eb5553..0000000000 --- a/DSharpPlus.Interactivity/InteractivityEventHandler.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Reflection; -using System.Threading.Tasks; - -using DSharpPlus.AsyncEvents; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Interactivity; - -internal sealed class InteractivityEventHandler - : IEventHandler, - IEventHandler, - IEventHandler, - IEventHandler, - IEventHandler, - IEventHandler -{ - private readonly InteractivityExtension extension; - - public InteractivityEventHandler(InteractivityExtension ext) - => this.extension = ext; - - public async Task HandleEventAsync(DiscordClient sender, DiscordEventArgs eventArgs) - { - if (this.extension.eventDistributor.TryGetValue(eventArgs.GetType(), out AsyncEvent? value)) - { - MethodInfo invoke = typeof(AsyncEvent<,>) - .MakeGenericType(typeof(DiscordClient), eventArgs.GetType()) - .GetMethod("InvokeAsync")!; - - await (Task)invoke.Invoke(value, [sender, eventArgs])!; - } - } - - public async Task HandleEventAsync(DiscordClient sender, ComponentInteractionCreatedEventArgs eventArgs) - { - await Task.WhenAll - ( - this.extension.ComponentEventWaiter.HandleAsync(sender, eventArgs), - this.extension.compPaginator.HandleAsync(sender, eventArgs) - ); - } - - public async Task HandleEventAsync(DiscordClient sender, MessageReactionAddedEventArgs eventArgs) - { - await Task.WhenAll - ( - this.extension.Paginator.HandleReactionAdd(sender, eventArgs), - this.extension.Poller.HandleReactionAdd(sender, eventArgs) - ); - } - - public async Task HandleEventAsync(DiscordClient sender, MessageReactionRemovedEventArgs eventArgs) - { - await Task.WhenAll - ( - this.extension.Paginator.HandleReactionRemove(sender, eventArgs), - this.extension.Poller.HandleReactionRemove(sender, eventArgs) - ); - } - - public async Task HandleEventAsync(DiscordClient sender, MessageReactionsClearedEventArgs eventArgs) - { - await Task.WhenAll - ( - this.extension.Paginator.HandleReactionClear(sender, eventArgs), - this.extension.Poller.HandleReactionClear(sender, eventArgs) - ); - } - - public async Task HandleEventAsync(DiscordClient sender, ModalSubmittedEventArgs eventArgs) - => await this.extension.ModalEventWaiter.Handle(sender, eventArgs); -} diff --git a/DSharpPlus.Interactivity/InteractivityEvents.cs b/DSharpPlus.Interactivity/InteractivityEvents.cs deleted file mode 100644 index e63fad3e07..0000000000 --- a/DSharpPlus.Interactivity/InteractivityEvents.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 271e435f10..0000000000 --- a/DSharpPlus.Interactivity/InteractivityExtension.cs +++ /dev/null @@ -1,1174 +0,0 @@ -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); - } - - IReadOnlyList 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 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; - IReadOnlyList 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); - IReadOnlyList 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 deleted file mode 100644 index 6f0f35d46f..0000000000 --- a/DSharpPlus.Interactivity/InteractivityResult.cs +++ /dev/null @@ -1,24 +0,0 @@ -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.Old.License b/DSharpPlus.Old.License new file mode 100644 index 0000000000..d584925892 --- /dev/null +++ b/DSharpPlus.Old.License @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Mike Santiago +Copyright (c) 2016-2023 DSharpPlus Development Team + +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. diff --git a/DSharpPlus.Tests/Commands/.editorconfig b/DSharpPlus.Tests/Commands/.editorconfig deleted file mode 100644 index 833e59b99c..0000000000 --- a/DSharpPlus.Tests/Commands/.editorconfig +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 5c4e42fdcf..0000000000 --- a/DSharpPlus.Tests/Commands/Cases/Commands/TestMultiLevelSubCommands.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index e402be9a7c..0000000000 --- a/DSharpPlus.Tests/Commands/Cases/Commands/TestSingleLevelSubCommands.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index e2d9ff89b0..0000000000 --- a/DSharpPlus.Tests/Commands/Cases/Commands/TestTopLevelCommands.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index f332a5cb3f..0000000000 --- a/DSharpPlus.Tests/Commands/Cases/UserInput.cs +++ /dev/null @@ -1,50 +0,0 @@ -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/TestMultiLevelSubCommandsFiltered.cs b/DSharpPlus.Tests/Commands/CommandFiltering/TestMultiLevelSubCommandsFiltered.cs deleted file mode 100644 index d71fee040e..0000000000 --- a/DSharpPlus.Tests/Commands/CommandFiltering/TestMultiLevelSubCommandsFiltered.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Entities; - -namespace DSharpPlus.Tests.Commands.CommandFiltering; - -public class TestMultiLevelSubCommandsFiltered -{ - [Command("root")] - public class RootCommand - { - [Command("subgroup")] - public class GroupCommand - { - [Command("command-text-only-attribute"), AllowedProcessors(typeof(TextCommandProcessor))] - public static ValueTask TextOnlyAsync(CommandContext context) => default; - - [Command("command-text-only-parameter")] - public static ValueTask TextOnlyAsync2(TextCommandContext context) => default; - - [Command("command-slash-only-attribute"), AllowedProcessors(typeof(SlashCommandProcessor))] - public static ValueTask SlashOnlyAsync(CommandContext context) => default; - - [Command("command-slash-only-parameter")] - public static ValueTask SlashOnlyAsync2(SlashCommandContext context) => default; - } - - [Command("subgroup-slash-only"), AllowedProcessors(typeof(SlashCommandProcessor))] - public class SlashGroupCommand - { - [Command("slash-only-group")] - public static ValueTask SlashOnlyAsync(CommandContext context, DiscordChannel channel) => default; - - [Command("slash-only-group2")] - public static ValueTask SlashOnlyAsync2(SlashCommandContext context, DiscordChannel channel) => default; - } - - [Command("subgroup-text-only"), AllowedProcessors(typeof(TextCommandProcessor))] - public class TextGroupCommand - { - [Command("text-only-group")] - public static ValueTask SlashOnlyAsync(CommandContext context, DiscordChannel channel) => default; - - [Command("text-only-group2")] - public static ValueTask SlashOnlyAsync2(TextCommandContext context, DiscordChannel channel) => default; - } - } - - public class ContextMenues - { - [Command("UserContextOnly"), SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - public static ValueTask UserCommand(SlashCommandContext context, DiscordUser user) => default; - - [Command("SlashUserContext"), SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu, DiscordApplicationCommandType.SlashCommand)] - public static ValueTask SlashUserCommand(SlashCommandContext context, DiscordUser user) => default; - - [Command("MessageContextOnly"), SlashCommandTypes(DiscordApplicationCommandType.MessageContextMenu)] - public static ValueTask MessageCommand(SlashCommandContext context, DiscordMessage message) => default; - - [Command("SlashMessageContext"), SlashCommandTypes(DiscordApplicationCommandType.MessageContextMenu, DiscordApplicationCommandType.SlashCommand)] - public static ValueTask SlashMessageCommand(SlashCommandContext context, DiscordMessage message) => default; - } - - [Command("group")] - public class ContextMenuesInGroup - { - [Command("UserContextOnly"), SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - public static ValueTask UserCommand(SlashCommandContext context, DiscordUser user) => default; - - [Command("SlashUserContext"), SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu, DiscordApplicationCommandType.SlashCommand)] - public static ValueTask SlashUserCommand(SlashCommandContext context, DiscordUser user) => default; - - [Command("MessageContextOnly"), SlashCommandTypes(DiscordApplicationCommandType.MessageContextMenu)] - public static ValueTask MessageCommand(SlashCommandContext context, DiscordMessage message) => default; - - [Command("SlashMessageContext"), SlashCommandTypes(DiscordApplicationCommandType.MessageContextMenu, DiscordApplicationCommandType.SlashCommand)] - public static ValueTask SlashMessageCommand(SlashCommandContext context, DiscordMessage message) => default; - } -} diff --git a/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs b/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs deleted file mode 100644 index 1031834f67..0000000000 --- a/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs +++ /dev/null @@ -1,197 +0,0 @@ -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 deleted file mode 100644 index ba957734f8..0000000000 --- a/DSharpPlus.Tests/Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicerTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index 294f8b31e0..0000000000 --- a/DSharpPlus.Tests/Commands/Trees/CommandBuilderTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -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 deleted file mode 100644 index 9b276ec1ee..0000000000 --- a/DSharpPlus.Tests/DSharpPlus.Tests.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - net9.0 - enable - false - true - false - - - - - - - - - - - - \ No newline at end of file diff --git a/DSharpPlus.VoiceNext/AudioFormat.cs b/DSharpPlus.VoiceNext/AudioFormat.cs deleted file mode 100644 index a8503bc91d..0000000000 --- a/DSharpPlus.VoiceNext/AudioFormat.cs +++ /dev/null @@ -1,126 +0,0 @@ -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 IReadOnlyList AllowedSampleRates { get; } = [8000, 12000, 16000, 24000, 48000]; - - /// - /// Gets the collection of channel counts the Opus encoder can use. - /// - public static IReadOnlyList AllowedChannelCounts { get; } =[1, 2]; - - /// - /// Gets the collection of sample durations (in ms) the Opus encoder can use. - /// - public static IReadOnlyList AllowedSampleDurations { get; } = [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 deleted file mode 100644 index cb62d3c639..0000000000 --- a/DSharpPlus.VoiceNext/Codec/Helpers.cs +++ /dev/null @@ -1,30 +0,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 deleted file mode 100644 index 3a9dd44771..0000000000 --- a/DSharpPlus.VoiceNext/Codec/Interop.cs +++ /dev/null @@ -1,262 +0,0 @@ -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 deleted file mode 100644 index 11cd696a2f..0000000000 --- a/DSharpPlus.VoiceNext/Codec/Opus.cs +++ /dev/null @@ -1,215 +0,0 @@ -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 deleted file mode 100644 index db15596cce..0000000000 --- a/DSharpPlus.VoiceNext/Codec/Rtp.cs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index fb27d50ae0..0000000000 --- a/DSharpPlus.VoiceNext/Codec/Sodium.cs +++ /dev/null @@ -1,181 +0,0 @@ -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/DSharpPlus.VoiceNext.csproj b/DSharpPlus.VoiceNext/DSharpPlus.VoiceNext.csproj deleted file mode 100644 index 7884fa647f..0000000000 --- a/DSharpPlus.VoiceNext/DSharpPlus.VoiceNext.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - true - DSharpPlus.VoiceNext - Voice implementation for DSharpPlus. - $(PackageTags), audio, voice, radio, music - true - - - - - - - - \ No newline at end of file diff --git a/DSharpPlus.VoiceNext/DiscordClientExtensions.cs b/DSharpPlus.VoiceNext/DiscordClientExtensions.cs deleted file mode 100644 index 6154d3214e..0000000000 --- a/DSharpPlus.VoiceNext/DiscordClientExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index 914c4ab021..0000000000 --- a/DSharpPlus.VoiceNext/Entities/AudioSender.cs +++ /dev/null @@ -1,112 +0,0 @@ -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 deleted file mode 100644 index cd86defd34..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceDispatch.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 5ce0ebd279..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceIdentifyPayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 9d746032da..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoicePacket.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 69a883610a..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceReadyPayload.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 0b6008130d..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayload.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 21ef63d7b5..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayloadData.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 6fe4a65a79..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceServerUpdatePayload.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 95e230f8fd..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceSessionDescriptionPayload.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 3ce3b034c2..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceSpeakingPayload.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 6532fce858..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceStateUpdatePayload.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index b17a156ed6..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceUserJoinPayload.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index de3b164d29..0000000000 --- a/DSharpPlus.VoiceNext/Entities/VoiceUserLeavePayload.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 11984f8cc4..0000000000 --- a/DSharpPlus.VoiceNext/EventArgs/VoiceReceiveEventArgs.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 25d05f49d5..0000000000 --- a/DSharpPlus.VoiceNext/EventArgs/VoiceUserJoinEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 54a6b27955..0000000000 --- a/DSharpPlus.VoiceNext/EventArgs/VoiceUserLeaveEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 44915b9367..0000000000 --- a/DSharpPlus.VoiceNext/IVoiceFilter.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index b446f06bbd..0000000000 --- a/DSharpPlus.VoiceNext/RawVoicePacket.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 3a51c0dd10..0000000000 --- a/DSharpPlus.VoiceNext/StreamExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 4f07f8fc6e..0000000000 --- a/DSharpPlus.VoiceNext/VoiceApplication.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index adb006511e..0000000000 --- a/DSharpPlus.VoiceNext/VoiceNextConfiguration.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 57df1e0667..0000000000 --- a/DSharpPlus.VoiceNext/VoiceNextConnection.cs +++ /dev/null @@ -1,1112 +0,0 @@ -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/VoiceNextEventHandler.cs b/DSharpPlus.VoiceNext/VoiceNextEventHandler.cs deleted file mode 100644 index 1b873d28d7..0000000000 --- a/DSharpPlus.VoiceNext/VoiceNextEventHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.VoiceNext; - -internal sealed class VoiceNextEventHandler - : IEventHandler, - IEventHandler -{ - private readonly VoiceNextExtension extension; - - public VoiceNextEventHandler(VoiceNextExtension ext) - => this.extension = ext; - - public async Task HandleEventAsync(DiscordClient sender, VoiceStateUpdatedEventArgs eventArgs) - => await this.extension.Client_VoiceStateUpdate(sender, eventArgs); - - public async Task HandleEventAsync(DiscordClient sender, VoiceServerUpdatedEventArgs eventArgs) - => await this.extension.Client_VoiceServerUpdateAsync(sender, eventArgs); -} diff --git a/DSharpPlus.VoiceNext/VoiceNextEvents.cs b/DSharpPlus.VoiceNext/VoiceNextEvents.cs deleted file mode 100644 index a6e97f7e47..0000000000 --- a/DSharpPlus.VoiceNext/VoiceNextEvents.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index ccc69e2a66..0000000000 --- a/DSharpPlus.VoiceNext/VoiceNextExtension.cs +++ /dev/null @@ -1,236 +0,0 @@ -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 deleted file mode 100644 index 64f4d0cd20..0000000000 --- a/DSharpPlus.VoiceNext/VoiceTransmitSink.cs +++ /dev/null @@ -1,235 +0,0 @@ -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.slnx b/DSharpPlus.slnx index 663def2cbb..eae872a936 100644 --- a/DSharpPlus.slnx +++ b/DSharpPlus.slnx @@ -1,36 +1,34 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DSharpPlus/AnsiColor.cs b/DSharpPlus/AnsiColor.cs deleted file mode 100644 index 0b04e019f3..0000000000 --- a/DSharpPlus/AnsiColor.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 0b88bbe4f1..0000000000 --- a/DSharpPlus/AsyncEvents/AsyncEvent.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 16a6c462c6..0000000000 --- a/DSharpPlus/AsyncEvents/AsyncEventArgs.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index d2a598c145..0000000000 --- a/DSharpPlus/AsyncEvents/AsyncEventHandler.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 489169b48d..0000000000 --- a/DSharpPlus/AsyncEvents/AsyncEvent`2.cs +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index b51ebecd48..0000000000 --- a/DSharpPlus/AsyncManualResetEvent.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 76afb30359..0000000000 --- a/DSharpPlus/Clients/BaseDiscordClient.cs +++ /dev/null @@ -1,184 +0,0 @@ -#pragma warning disable CS0618 -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -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 DiscordRestApiClient ApiClient { get; internal set; } - 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 assembly = typeof(DiscordClient).GetTypeInfo().Assembly; - - AssemblyInformationalVersionAttribute? versionAttribute = assembly.GetCustomAttribute(); - if (versionAttribute != null) - { - this.VersionString = versionAttribute.InformationalVersion; - } - else - { - Version? version = assembly.GetName().Version; - string versionString = version.ToString(3); - - if (version.Revision > 0) - { - this.VersionString = $"{versionString}, CI build {version.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 transportApplication = await this.ApiClient.GetCurrentApplicationInfoAsync(); - return new DiscordApplication(transportApplication, 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. - /// - /// - [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] - 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 voiceRegions = await ListVoiceRegionsAsync(); - foreach (DiscordVoiceRegion voiceRegion in voiceRegions) - { - this.InternalVoiceRegions.TryAdd(voiceRegion.Id, voiceRegion); - } - } - } - - /// - /// Gets the current gateway info. - /// - /// A gateway info object. - public async Task GetGatewayInfoAsync() - => await this.ApiClient.GetGatewayInfoAsync(); - - internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId) - { - TryGetCachedUserInternal(userId, out DiscordUser? user); - return user; - } - - internal bool TryGetCachedUserInternal(ulong userId, [NotNullWhen(true)] out DiscordUser? user) - { - if (this.UserCache.TryGetValue(userId, out user)) - { - return true; - } - - user = new DiscordUser { Id = userId, 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/DefaultEventDispatcher.cs b/DSharpPlus/Clients/DefaultEventDispatcher.cs deleted file mode 100644 index bc66defe92..0000000000 --- a/DSharpPlus/Clients/DefaultEventDispatcher.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; -using Microsoft.Extensions.Caching.Memory; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace DSharpPlus.Clients; - -/// -/// The default DSharpPlus event dispatcher, dispatching events asynchronously and using a shared scope. Catch-all event -/// handlers referencing are supported. -/// -public sealed class DefaultEventDispatcher : IEventDispatcher, IDisposable -{ - private readonly IServiceProvider serviceProvider; - private readonly EventHandlerCollection handlers; - private readonly IClientErrorHandler errorHandler; - private readonly ILogger logger; - private readonly IMemoryCache cache; - private readonly ConcurrentDictionary inFlightWaiters; - - - private bool disposed = false; - - public DefaultEventDispatcher - ( - IServiceProvider serviceProvider, - IOptions handlers, - IClientErrorHandler errorHandler, - ILogger logger, - IMemoryCache cache - ) - { - this.serviceProvider = serviceProvider; - this.handlers = handlers.Value; - this.errorHandler = errorHandler; - this.logger = logger; - this.cache = cache; - this.inFlightWaiters = []; - } - - /// - public EventWaiter CreateEventWaiter(Func condition, TimeSpan timeout) - where T : DiscordEventArgs - { - EventWaiter waiter = new() - { - Id = Ulid.NewUlid(), - Condition = condition, - CompletionSource = new(), - Timeout = timeout - }; - - InFlightEventWaiter inFlight = new(waiter.Id, Unsafe.As>(condition), typeof(T)); - - this.cache.CreateEntry(waiter.Id) - .SetAbsoluteExpiration(timeout) - .SetValue(waiter) - .RegisterPostEvictionCallback((key, value, reason, state) => - { - if (reason == EvictionReason.Expired) - { - this.inFlightWaiters.Remove((Ulid)key, out _); - ((EventWaiter)value!).CompletionSource.SetResult(EventWaiterResult.FromTimedOut()); - } - }) - .Dispose(); - - this.inFlightWaiters.AddOrUpdate(waiter.Id, inFlight, (_, _) => inFlight); - - return waiter; - } - - - /// - public ValueTask DispatchAsync(DiscordClient client, T eventArgs) - where T : DiscordEventArgs - { - if (this.disposed) - { - return ValueTask.CompletedTask; - } - - _ = Parallel.ForEachAsync(this.inFlightWaiters.Values, (waiter, ct) => - { - if (waiter.EventType != typeof(T)) - { - return ValueTask.CompletedTask; - } - - if (!waiter.Condition(eventArgs)) - { - return ValueTask.CompletedTask; - } - - EventWaiter? fullWaiter = this.cache.Get>(waiter.Id); - - fullWaiter?.CompletionSource.SetResult(new() - { - TimedOut = false, - Value = eventArgs - }); - - this.inFlightWaiters.Remove(waiter.Id, out _); - this.cache.Remove(waiter.Id); - - return ValueTask.CompletedTask; - }); - - IReadOnlyList general = this.handlers[typeof(DiscordEventArgs)]; - IReadOnlyList specific = this.handlers[typeof(T)]; - - if (general.Count == 0 && specific.Count == 0) - { - return ValueTask.CompletedTask; - } - - try - { - IServiceScope scope = this.serviceProvider.CreateScope(); - _ = Task.WhenAll - ( - general.Concat(specific) - .Select(async handler => - { - try - { - await ((Func)handler)(client, eventArgs, - scope.ServiceProvider); - } - catch (Exception e) - { - await this.errorHandler.HandleEventHandlerError(typeof(T).ToString(), e, - (Delegate)handler, client, eventArgs); - } - }) - ) - .ContinueWith((_) => scope.Dispose()); - } - catch (ObjectDisposedException) - { - // ObjectDisposedException can be thrown from the this.serviceProvider.CreateScope() call above, - // when the serviceProvider is already disposed externally. - // This *should* only happen when the hosting application is shutting down, - // so it should be safe to just ignore it. - // One option would be to show that exception as debug log, but I would guess that just causes confusion. - } - - return ValueTask.CompletedTask; - } - - /// - public void Dispose() - { - this.logger.LogInformation("Detecting shutdown. All further incoming or enqueued events will not dispatch."); - this.disposed = true; - } - - private readonly record struct InFlightEventWaiter(Ulid Id, Func Condition, Type EventType); -} diff --git a/DSharpPlus/Clients/DiscordClient.Dispatch.cs b/DSharpPlus/Clients/DiscordClient.Dispatch.cs deleted file mode 100644 index f0c7dd1703..0000000000 --- a/DSharpPlus/Clients/DiscordClient.Dispatch.cs +++ /dev/null @@ -1,3180 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -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; - JToken rlMetadata; - GatewayOpCode rlOpcode; - - 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; - - case "rate_limited": - rlMetadata = dat["meta"]; - rlOpcode = (GatewayOpCode)(int)dat["opcode"]; - await OnRatelimitedAsync(dat.ToDiscordObject(), rlOpcode, rlMetadata); - 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 is null) - { - return; - } - - channel.Discord = this; - - DiscordGuild? gld = channel.Guild; - - DiscordChannel? channel_new = InternalGetCachedChannel(channel.Id, channel.GuildId); - DiscordChannel channel_old = null!; - - if(channel is DiscordForumChannel forum_channel_update && (channel_new is null || channel_new is DiscordForumChannel)) - { - DiscordForumChannel? forum_channel_new = channel_new as DiscordForumChannel; - await OnForumChannelUpdateAsync(forum_channel_new , forum_channel_update, gld); - return; - } - - if (channel_new is not 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 = [..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 - { - gld?.channels[channel.Id] = channel; - } - - await this.dispatcher.DispatchAsync(this, new ChannelUpdatedEventArgs - { - ChannelAfter = channel_new, - Guild = gld, - ChannelBefore = channel_old - }); - } - - internal async Task OnForumChannelUpdateAsync(DiscordForumChannel? channel_new, DiscordForumChannel updateChannel, DiscordGuild? guild) - { - DiscordForumChannel channel_old = null!; - if(channel_new is not null) - { - channel_old = new DiscordForumChannel - { - Bitrate = channel_new.Bitrate, - Discord = this, - GuildId = channel_new.GuildId, - Id = channel_new.Id, - LastMessageId = channel_new.LastMessageId, - Name = channel_new.Name, - permissionOverwrites = [.. 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, - availableTagsInternal = [.. channel_new.AvailableTags], - DefaultLayout = channel_new.DefaultLayout, - DefaultPerUserRateLimit = channel_new.DefaultPerUserRateLimit, - DefaultReaction = channel_new.DefaultReaction, - DefaultSortOrder = channel_new.DefaultSortOrder - }; - - channel_new.Bitrate = updateChannel.Bitrate; - channel_new.Name = updateChannel.Name; - channel_new.Position = updateChannel.Position; - channel_new.Topic = updateChannel.Topic; - channel_new.UserLimit = updateChannel.UserLimit; - channel_new.ParentId = updateChannel.ParentId; - channel_new.IsNSFW = updateChannel.IsNSFW; - channel_new.PerUserRateLimit = updateChannel.PerUserRateLimit; - channel_new.Type = updateChannel.Type; - channel_new.RtcRegionId = updateChannel.RtcRegionId; - channel_new.QualityMode = updateChannel.QualityMode; - - channel_new.permissionOverwrites.Clear(); - - foreach (DiscordOverwrite po in updateChannel.permissionOverwrites) - { - po.Discord = this; - po.channelId = updateChannel.Id; - } - - channel_new.permissionOverwrites.AddRange(updateChannel.permissionOverwrites); - channel_new.availableTagsInternal.Clear(); - - foreach (DiscordForumTag tag in updateChannel.AvailableTags) - { - tag.Discord = this; - } - channel_new.availableTagsInternal.AddRange(updateChannel.AvailableTags); - channel_new.DefaultLayout = updateChannel.DefaultLayout; - channel_new.DefaultPerUserRateLimit = updateChannel.DefaultPerUserRateLimit; - channel_new.DefaultReaction = updateChannel.DefaultReaction; - channel_new.DefaultSortOrder = updateChannel.DefaultSortOrder; - } - else - { - guild?.channels[updateChannel.Id] = updateChannel; - } - - await this.dispatcher.DispatchAsync(this, new ChannelUpdatedEventArgs - { - ChannelAfter = channel_new, - Guild = guild, - 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 scheduledEvent in guild.scheduledEvents.Values) - { - scheduledEvent.Discord = this; - - if (scheduledEvent.Creator != null) - { - scheduledEvent.Creator.Discord = this; - } - } - - foreach (DiscordChannel channel in guild.channels.Values) - { - channel.GuildId = guild.Id; - channel.Discord = this; - foreach (DiscordOverwrite xo in channel.permissionOverwrites) - { - xo.Discord = this; - xo.channelId = channel.Id; - } - } - - foreach (DiscordThreadChannel thread in guild.threads.Values) - { - thread.GuildId = guild.Id; - thread.Discord = this; - } - - foreach (DiscordEmoji emoji in guild.emojis.Values) - { - emoji.Discord = this; - } - - foreach (DiscordMessageSticker sticker in guild.stickers.Values) - { - sticker.Discord = this; - } - - foreach (DiscordVoiceState voicestate in guild.voiceStates.Values) - { - voicestate.Discord = this; - voicestate.GuildId = guild.Id; - } - - foreach (DiscordRole role in guild.roles.Values) - { - role.Discord = this; - role.guild_id = guild.Id; - } - - foreach (DiscordStageInstance stageInstance in guild.stageInstances.Values) - { - stageInstance.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, - Colors = newRole.Colors, - 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.Colors = role.Colors; - 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; - } - - if (message.MessageType == DiscordMessageType.PollResult) - { - message = DiscordPollCompletionMessage.Parse(message) ?? message; - - MessagePollCompletedEventArgs pollEventArgs = new() - { - PollCompletion = message as DiscordPollCompletionMessage, - Message = message - }; - - await this.dispatcher.DispatchAsync(this, pollEventArgs); - } - - MessageCreatedEventArgs ea = new() - { - Message = message, - - MentionedUsers = message.mentionedUsers, - MentionedRoles = message.mentionedRoles != null ? message.mentionedRoles : null, - MentionedChannels = message.mentionedChannels != null ? 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; - } - if (event_message.Flags is not null) - { - message.Flags = event_message.Flags; - } - - message.embeds.Clear(); - message.embeds.AddRange(event_message.embeds); - - message.attachments.Clear(); - message.attachments.AddRange(event_message.attachments); - - message.Components = event_message.Components; - 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 = message.mentionedUsers, - MentionedRoles = message.mentionedRoles != null ? message.mentionedRoles : null, - MentionedChannels = message.mentionedChannels != null ? 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 = 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 (channel != null) - { - channel.Discord = this; - interaction.ContextChannel = channel; - } - - 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 OnRatelimitedAsync(RatelimitedEventArgs ratelimitedEventArgs, GatewayOpCode rlOpcode, JToken rlMetadata) - { - RatelimitMetadata metadata = rlOpcode switch - { - GatewayOpCode.RequestGuildMembers => rlMetadata.ToDiscordObject(), - _ => new UnknownRatelimitMetadata - { - Json = rlMetadata.ToString() - } - }; - - ratelimitedEventArgs.Metadata = metadata; - - if (rlOpcode == GatewayOpCode.RequestGuildMembers) - { - RequestGuildMembersRatelimitMetadata rgmMetadata = (RequestGuildMembersRatelimitMetadata)metadata; - CancelGuildMemberEnumeration(rgmMetadata.GuildId, rgmMetadata.Nonce, ratelimitedEventArgs.RetryAfter); - } - - await this.dispatcher.DispatchAsync(this, ratelimitedEventArgs); - } - - 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 deleted file mode 100644 index d7ba0698a3..0000000000 --- a/DSharpPlus/Clients/DiscordClient.cs +++ /dev/null @@ -1,1363 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Tracing; -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, - DiscordRestApiClientFactory 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.GetCurrentApplicationClient(); - 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. - /// 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) - => await this.ApiClient.GetInviteAsync(code, withCounts); - - /// - /// 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; - } - - DiscordUser 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(); - } - } - - private void CancelGuildMemberEnumeration(ulong guildId, string? nonce, TimeSpan retryAfter) - { - if (this.guildMembersChunkedEvents.Count is 0) - { - return; - } - - Int128 code = new(guildId, (ulong)(nonce?.GetHashCode() ?? 0)); - - if (!this.guildMembersChunkedEvents.TryGetValue(code, out Channel? eventChannel)) - { - return; - } - - eventChannel.Writer.Complete(new GatewayRatelimitedException(retryAfter)); - } - - #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/DiscordClientBuilder.cs b/DSharpPlus/Clients/DiscordClientBuilder.cs deleted file mode 100644 index 6413eda06d..0000000000 --- a/DSharpPlus/Clients/DiscordClientBuilder.cs +++ /dev/null @@ -1,274 +0,0 @@ -using System; -using System.Threading.Tasks; - -using DSharpPlus.Clients; -using DSharpPlus.Extensions; -using DSharpPlus.Logging; -using DSharpPlus.Net; -using DSharpPlus.Net.Gateway; -using DSharpPlus.Net.Gateway.Compression; -using DSharpPlus.Net.Gateway.Compression.Zlib; -using DSharpPlus.Net.Gateway.Compression.Zstd; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -/// -/// Enables building a DiscordClient from complex configuration, registering extensions and fine-tuning behavioural aspects. -/// -public sealed class DiscordClientBuilder -{ - private readonly IServiceCollection serviceCollection; - private bool addDefaultLogging = true; - private bool sharding = false; - private LogLevel minimumLogLevel = LogLevel.Information; - - /// - /// Creates a new DiscordClientBuilder from the provided service collection. This is private in favor of static - /// methods that control creation based on certain presets. IServiceCollection-based configuration occurs separate - /// from this type. - /// - private DiscordClientBuilder(IServiceCollection serviceCollection) - => this.serviceCollection = serviceCollection; - - /// - /// Creates a new DiscordClientBuilder without sharding, using the specified token. - /// - /// The token to use for this application. - /// The intents to connect to the gateway with. - /// The service collection to base this builder on. - /// A new DiscordClientBuilder. - public static DiscordClientBuilder CreateDefault - ( - string token, - DiscordIntents intents, - IServiceCollection? serviceCollection = null - ) - { - serviceCollection ??= new ServiceCollection(); - - DiscordClientBuilder builder = new(serviceCollection); - builder.serviceCollection.Configure(x => x.GetToken = () => token); - builder.serviceCollection.AddDSharpPlusDefaultsSingleShard(intents); - - return builder; - } - - /// - /// Creates a new sharding DiscordClientbuilder using the specified token. - /// - /// - /// DSharpPlus supports more advanced sharding over just specifying the amount of shards, which can be accessed - /// through the underlying service collection:
- /// - /// builder.ConfigureServices(services => - /// { - /// services.Configure<ShardingOptions>(x => ...); - /// - /// // The default orchestrator supports a shard count and a "stride" (offset from shard 0), which requires - /// // a total shard count. If you wish to customize sharding further, you can specify your own orchestrator: - /// services.AddSingleton<IShardOrchestrator, MyCustomShardOrchestrator>(); - /// } - /// - ///
- /// The token to use for this application. - /// The intents to connect to the gateway with. - /// The amount of shards to start. - /// The service collection to base this builder on. - /// A new DiscordClientBuilder. - public static DiscordClientBuilder CreateSharded - ( - string token, - DiscordIntents intents, - uint? shardCount = null, - IServiceCollection? serviceCollection = null - ) - { - serviceCollection ??= new ServiceCollection(); - - DiscordClientBuilder builder = new(serviceCollection); - builder.serviceCollection.Configure(x => x.ShardCount = shardCount); - builder.serviceCollection.Configure(x => x.GetToken = () => token); - builder.serviceCollection.AddDSharpPlusDefaultsMultiShard(intents); - - builder.sharding = true; - - return builder; - } - - /// - /// Sets the gateway compression used to zstd. This requires zstd natives to be available to the application. - /// - /// The current instance for chaining. - public DiscordClientBuilder UseZstdCompression() - { - this.serviceCollection.Replace(); - return this; - } - - /// - /// Sets the gateway compression used to zlib. This is the default compression mode. - /// - /// The current instance for chaining. - public DiscordClientBuilder UseZlibCompression() - { - this.serviceCollection.Replace(); - return this; - } - - /// - /// Disables gateway compression entirely. - /// - /// The current instance for chaining. - public DiscordClientBuilder DisableGatewayCompression() - { - this.serviceCollection.Replace(); - return this; - } - - /// - /// Disables the DSharpPlus default logger for this DiscordClientBuilder. - /// - /// The current instance for chaining. - public DiscordClientBuilder DisableDefaultLogging() - { - this.addDefaultLogging = false; - return this; - } - - /// - /// Sets the log level for the default logger, should it be used. - /// - /// - /// This does not affect custom logging configurations. - /// - /// The current instance for chaining. - public DiscordClientBuilder SetLogLevel(LogLevel minimum) - { - this.minimumLogLevel = minimum; - return this; - } - - /// - /// Configures logging for this DiscordClientBuilder and disables the default DSharpPlus logger. - /// - /// The configuration delegate. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureLogging(Action configure) - { - this.addDefaultLogging = false; - this.serviceCollection.AddLogging(configure); - return this; - } - - /// - /// Configures services on this DiscordClientBuilder, enabling you to customize the library or your own services. - /// - /// The configuration delegate. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureServices(Action configure) - { - configure(this.serviceCollection); - return this; - } - - /// - /// Configures event handlers on the present client builder. - /// - /// A configuration delegate enabling specific configuration. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureEventHandlers(Action configure) - { - this.serviceCollection.ConfigureEventHandlers(configure); - return this; - } - - /// - /// Configures the rest client used by DSharpPlus. - /// - /// A configuration delegate for the rest client. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureRestClient(Action configure) - { - this.serviceCollection.Configure(configure); - return this; - } - - /// - /// Configures the gateway client used by DSharpPlus. - /// - /// A configuration delegate for the gateway client. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureGatewayClient(Action configure) - { - this.serviceCollection.Configure(configure); - return this; - } - - /// - /// Configures the sharding attempted by DSharpPlus. Throws if the builder was not set up for sharding. - /// - /// A configuration delegate for sharding. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureSharding(Action configure) - { - if (!this.sharding) - { - throw new InvalidOperationException("This client builder is not set up for sharding."); - } - - this.serviceCollection.Configure(configure); - return this; - } - - /// - /// Tweaks assorted extra configuration knobs around the library. - /// - /// A configuration delegate for the remaining library. - /// The current instance for chaining. - public DiscordClientBuilder ConfigureExtraFeatures(Action configure) - { - this.serviceCollection.Configure(configure); - return this; - } - - /// - /// Instructs DSharpPlus to try reconnecting when encountering a fatal gateway error. By default, DSharpPlus will - /// leave the decision on fatal gateway errors to the user. - /// - /// The current instance for chaining. - public DiscordClientBuilder SetReconnectOnFatalGatewayErrors() - { - this.serviceCollection.AddOrReplace(ServiceLifetime.Singleton); - return this; - } - - /// - /// Builds a new client from the present builder. - /// - public DiscordClient Build() - { - if (this.addDefaultLogging) - { - this.serviceCollection.AddLogging(builder => - { - builder.AddProvider(new DefaultLoggerProvider()) - .SetMinimumLevel(this.minimumLogLevel); - }); - } - - IServiceProvider provider = this.serviceCollection.BuildServiceProvider(); - return provider.GetRequiredService(); - } - - /// - /// Builds the client and connects to Discord. The client instance will be unobtainable in user code. - /// - public async Task ConnectAsync() - { - DiscordClient client = Build(); - await client.ConnectAsync(); - } -} diff --git a/DSharpPlus/Clients/DiscordRestApiClientFactory.cs b/DSharpPlus/Clients/DiscordRestApiClientFactory.cs deleted file mode 100644 index 37db18db64..0000000000 --- a/DSharpPlus/Clients/DiscordRestApiClientFactory.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Threading.Tasks; - -using DSharpPlus.Net; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace DSharpPlus.Clients; - -/// -/// Provides a way to get access to either the current application's REST client or OAuth2-based REST clients. -/// -public sealed class DiscordRestApiClientFactory -{ - private readonly DiscordRestApiClient primaryClient; - private readonly IServiceProvider services; - private readonly IMemoryCache clientCache; - - private readonly MemoryCacheEntryOptions options; - - public DiscordRestApiClientFactory - ( - DiscordRestApiClient primaryClient, - IOptions tokenContainer, - IMemoryCache cache, - IServiceProvider services - ) - { - this.services = services; - this.primaryClient = primaryClient; - this.clientCache = cache; - - this.primaryClient.SetToken(TokenType.Bot, tokenContainer.Value.GetToken()); - - this.options = new() - { - SlidingExpiration = TimeSpan.FromMinutes(5) - }; - } - - /// - /// Gets the REST API client for the current bot. - /// - public DiscordRestApiClient GetCurrentApplicationClient() - => this.primaryClient; - - /// - /// Gets a REST API client for a given bearer token. Not all features of the API might be available. - /// - public async Task GetOAuth2ClientAsync(string token) - { - if (this.clientCache.TryGetValue($"dsharpplus.oauth-2-client:bearer-{token}", out OAuth2DiscordClient? cachedClient)) - { - return cachedClient.ApiClient; - } - - OAuth2DiscordClient client = new(); - DiscordRestApiClient apiClient = this.services.GetRequiredService(); - - apiClient.SetToken(TokenType.Bearer, token); - - apiClient.SetClient(client); - client.ApiClient = apiClient; - - this.clientCache.Set($"dsharpplus.oauth-2-client:bearer-{token}", client, this.options); - - await client.InitializeAsync(); - - return client.ApiClient; - } -} diff --git a/DSharpPlus/Clients/DiscordWebhookClient.cs b/DSharpPlus/Clients/DiscordWebhookClient.cs deleted file mode 100644 index abfc1394db..0000000000 --- a/DSharpPlus/Clients/DiscordWebhookClient.cs +++ /dev/null @@ -1,275 +0,0 @@ -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 DiscordRestApiClient 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 DiscordRestApiClient(parsedTimeout, this.Logger); - this.hooks = []; - this.Webhooks = 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/EventHandlerCollection.cs b/DSharpPlus/Clients/EventHandlerCollection.cs deleted file mode 100644 index e274a954d8..0000000000 --- a/DSharpPlus/Clients/EventHandlerCollection.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -using Microsoft.Extensions.DependencyInjection; - -using Handler = System.Func; -using SimpleHandler = System.Func; - -namespace DSharpPlus; - -/// -/// Contains an in-construction, mutable, list of event handlers filtered by event name. -/// -public sealed class EventHandlerCollection -{ - /// - /// Gets the registered handlers for a type, or an empty collection if none were configured. These are returned as - /// , and individual elements must be casted to - /// Func<DiscordClient, TEventArgs, IServiceProvider, Task>. - /// - public IReadOnlyList this[Type type] - { - get - { - return this.Handlers.TryGetValue(type, out IReadOnlyList? handlers) - ? handlers - : []; - } - } - - /// - /// The event handlers configured for this application. - /// - private FrozenDictionary> Handlers - => this.immutableHandlers ?? AsImmutable(); - - private FrozenDictionary>? immutableHandlers = null; - - private readonly Dictionary> handlers = []; - - /// - /// Registers a single simple event handler. - /// - /// The type of event args this handler consumes. - /// The event handler. - public void Register(Func handler) - where T : DiscordEventArgs - { - if (this.handlers.TryGetValue(typeof(T), out List? value)) - { - value.Add(CanonicalizeSimpleDelegateHandler(Unsafe.As(handler))); - } - else - { - List temporary = []; - temporary.Add(CanonicalizeSimpleDelegateHandler(Unsafe.As(handler))); - - this.handlers.Add(typeof(T), temporary); - } - } - - /// - /// Registers all type-wise event handlers implemented by a specific type. - /// - public void Register(Type t) - { - if (!t.IsAssignableTo(typeof(IEventHandler))) - { - throw new InvalidOperationException($"The presented type {t} is not an event handler: it does not implement IEventHandler."); - } - - foreach ((Type args, Handler handler) in CanonicalizeTypeHandlerImplementations(t)) - { - if (this.handlers.TryGetValue(args, out List? value)) - { - value.Add(handler); - } - else - { - List temporary = [handler]; - this.handlers.Add(args, temporary); - } - } - } - - /// - /// Produces a delegate matching the canonical representation from a simple, delegate-based handler. - /// - private static Handler CanonicalizeSimpleDelegateHandler(SimpleHandler simpleHandler) - => (client, args, _) => simpleHandler(client, args); - - /// - /// Produces a delegate matching the canonical representation from an implementation of . - /// - private static IEnumerable<(Type, Handler)> CanonicalizeTypeHandlerImplementations(Type targetType) - { - return targetType.GetInterfaces() - .Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEventHandler<>)) - .Select(x => x.GetGenericArguments()[0]) - .Select(argumentType => - { - ParameterExpression client = Expression.Parameter(typeof(DiscordClient), "client"); - ParameterExpression eventArgs = Expression.Parameter(argumentType, "eventArgs"); - ParameterExpression services = Expression.Parameter(typeof(IServiceProvider), "services"); - - MethodInfo serviceProviderMethod = typeof(ServiceProviderServiceExtensions).GetMethod - ( - name: "GetRequiredService", - bindingAttr: BindingFlags.Public | BindingFlags.Static, - types: [typeof(IServiceProvider), typeof(Type)] - )!; - - MethodInfo handlerMethod = targetType.GetMethod - ( - name: "HandleEventAsync", - bindingAttr: BindingFlags.Public | BindingFlags.Instance, - types: [typeof(DiscordClient), argumentType] - )!; - - MethodCallExpression handlerCall = Expression.Call - ( - Expression.Convert - ( - Expression.Call(null, serviceProviderMethod, services, Expression.Constant(targetType)), - targetType - ), - handlerMethod, - client, eventArgs - ); - - LambdaExpression lambda = Expression.Lambda(handlerCall, client, eventArgs, services); - - return (argumentType, Unsafe.As(lambda.Compile())); - }); - } - - private FrozenDictionary> AsImmutable() - { - Dictionary> copy = new(this.handlers.Count); - - foreach (KeyValuePair> handler in this.handlers) - { - copy.Add(handler.Key, handler.Value); - } - - return this.immutableHandlers = copy.ToFrozenDictionary(); - } -} diff --git a/DSharpPlus/Clients/EventHandlingBuilder.cs b/DSharpPlus/Clients/EventHandlingBuilder.cs deleted file mode 100644 index 941d051018..0000000000 --- a/DSharpPlus/Clients/EventHandlingBuilder.cs +++ /dev/null @@ -1,929 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus; - -/// -/// Provides an API for configuring delegate-based event handlers for your application. -/// -public sealed class EventHandlingBuilder -{ - /// - /// The underlying service collection for this application. - /// - public IServiceCollection Services { get; } - - internal EventHandlingBuilder(IServiceCollection services) - => this.Services = services; - - /// - /// Registers all event handlers implemented on the provided type. - /// - public EventHandlingBuilder AddEventHandlers(ServiceLifetime lifetime = ServiceLifetime.Transient) - where T : IEventHandler - { - this.Services.Configure(c => c.Register(typeof(T))); - this.Services.Add(ServiceDescriptor.Describe(typeof(T), typeof(T), lifetime)); - return this; - } - - /// - /// Registers all event handlers implemented on the provided types. - /// - public EventHandlingBuilder AddEventHandlers - ( - IReadOnlyList types, - ServiceLifetime lifetime = ServiceLifetime.Transient - ) - { - foreach(Type t in types) - { - this.Services.Configure(c => c.Register(t)); - this.Services.Add(ServiceDescriptor.Describe(t, t, lifetime)); - } - - return this; - } - - /// - /// Fired whenever the underlying websocket connection is established. - /// - public EventHandlingBuilder HandleSocketOpened(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired whenever the underlying websocket connection is terminated. - /// - public EventHandlingBuilder HandleSocketClosed(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when this client has successfully completed its handshake with the websocket gateway. - /// - /// - /// will not be populated when this event is fired.
- /// See also: , - ///
- public EventHandlingBuilder HandleSessionCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired whenever a session is resumed. - /// - public EventHandlingBuilder HandleSessionResumed(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when too many consecutive heartbeats fail and the library considers the connection zombied. - /// - public EventHandlingBuilder HandleZombied(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the permissions for an application command are updated. - /// - public EventHandlingBuilder HandleApplicationCommandPermissionsUpdated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a new channel is created. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleChannelCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a channel is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleChannelUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a channel is deleted. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleChannelDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a DM channel is deleted. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleDmChannelDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired whenever a channels pinned message list is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleChannelPinsUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the user joins a new guild. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild is becoming available. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildAvailable(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the user leaves or is removed from a guild. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild becomes unavailable. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildUnavailable(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when all guilds finish streaming from Discord upon initial connection. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildDownloadCompleted - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guilds emojis get updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildEmojisUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guilds stickers get updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildStickersUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild integration is updated. - /// - public EventHandlingBuilder HandleGuildIntegrationsUpdated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a audit log entry is created. - /// - public EventHandlingBuilder HandleGuildAuditLogCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a scheduled event is created. - /// - public EventHandlingBuilder HandleScheduledGuildEventCreated - ( - Func handler - ) - { - this.Services.Configure - ( - c => c.Register(handler) - ); - - return this; - } - - /// - /// Fired when a scheduled event is updated. - /// - public EventHandlingBuilder HandleScheduledGuildEventUpdated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a scheduled event is deleted. - /// - public EventHandlingBuilder HandleScheduledGuildEventDeleted - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a scheduled event is completed. - /// - public EventHandlingBuilder HandleScheduledGuildEventCompleted - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an user is registered to a scheduled event. - /// - public EventHandlingBuilder HandleScheduledGuildEventUserAdded - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - - return this; - } - - /// - /// Fired when an user removes themselves from a scheduled event. - /// - public EventHandlingBuilder HandleScheduledGuildEventUserRemoved - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - - return this; - } - - /// - /// Fired when a guild ban gets added. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildBanAdded(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild ban gets removed. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildBanRemoved(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a new user joins a guild. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildMemberAdded(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a user is removed from a guild, by leaving, a kick or a ban. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildMemberRemoved(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild member is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildMemberUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired in response to requesting guild members over the gateway. - /// - public EventHandlingBuilder HandleGuildMembersChunked(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild role is created. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildRoleCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild role is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildRoleUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild role is deleted. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleGuildRoleDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an invite is created. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleInviteCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an invite is deleted. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleInviteDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a message is created. - /// For this event to fire you need the or - /// intent. - /// - public EventHandlingBuilder HandleMessageCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a poll completes and a poll result message is created. For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleMessagePollCompleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a message is updated. - /// For this event to fire you need the or - /// intent. - /// - public EventHandlingBuilder HandleMessageUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a message is deleted. - /// For this event to fire you need the or - /// intent. - /// - public EventHandlingBuilder HandleMessageDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when multiple messages are deleted at once. - /// For this event to fire you need the or - /// intent. - /// - public EventHandlingBuilder HandleMessagesBulkDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a vote was cast on a poll. - /// - public EventHandlingBuilder HandleMessagePollVoted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a reaction gets added to a message. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleMessageReactionAdded(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a reaction gets removed from a message. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleMessageReactionRemoved - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when all reactions get removed from a message. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleMessageReactionsCleared - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when all reactions of a specific emoji are removed from a message. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleMessageReactionRemovedEmoji - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a user presence has been updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandlePresenceUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the current user updates their settings. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleUserSettingsUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when properties about the current user change. - /// - /// - /// Note that this event only applies for changes to the current user, the client that is connected to Discord. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleUserUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when someone joins, leaves or moves voice channels. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleVoiceStateUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a guild's voice server is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleVoiceServerUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a thread is created. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleThreadCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a thread is updated. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleThreadUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a thread is deleted. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleThreadDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the current member gains access to channels that contain threads. - /// For this event to fire you need the intent. - /// - public EventHandlingBuilder HandleThreadListSynced(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the thread member for the current user is updated. - /// For this event to fire you need the intent. - /// - /// - /// This event is primarily implemented for completeness and unlikely to be useful to bots. - /// - public EventHandlingBuilder HandleThreadMemberUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when the thread members are updated. - /// For this event to fire you need the or intent. - /// - public EventHandlingBuilder HandleThreadMembersUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an integration is created. - /// - public EventHandlingBuilder HandleIntegrationCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an integration is updated. - /// - public EventHandlingBuilder HandleIntegrationUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an integration is deleted. - /// - public EventHandlingBuilder HandleIntegrationDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a stage instance is created. - /// - public EventHandlingBuilder HandleStageInstanceCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a stage instance is updated. - /// - public EventHandlingBuilder HandleStageInstanceUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a stage instance is deleted. - /// - public EventHandlingBuilder HandleStageInstanceDeleted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when any interaction is invoked. - /// - public EventHandlingBuilder HandleInteractionCreated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a component is interacted with. - /// - public EventHandlingBuilder HandleComponentInteractionCreated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a modal is submitted. If a modal is closed, this event is not fired. - /// - public EventHandlingBuilder HandleModalSubmitted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a user uses a context menu. - /// - public EventHandlingBuilder HandleContextMenuInteractionCreated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a user starts typing in a channel. - /// - public EventHandlingBuilder HandleTypingStarted(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an unknown event gets received. - /// - public EventHandlingBuilder HandleUnknownEvent(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired whenever webhooks update. - /// - public EventHandlingBuilder HandleWebhooksUpdated(Func handler) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when a new auto-moderation rule is created. - /// - public EventHandlingBuilder HandleAutoModerationRuleCreated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an auto-moderation rule is updated. - /// - public EventHandlingBuilder HandleAutoModerationRuleUpdated - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an auto-moderation rule is deleted. - /// - public EventHandlingBuilder HandleAutoModerationRuleDeleted - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an auto-moderation is executed. - /// - public EventHandlingBuilder HandleAutoModerationRuleExecuted - ( - Func handler - ) - { - this.Services.Configure(c => c.Register(handler)); - return this; - } - - /// - /// Fired when an entitlement was created. - /// - public EventHandlingBuilder HandleEntitlementCreated - ( - Func handler - ) - { - this.Services.Configure - ( - c => c.Register(handler) - ); - - return this; - } - - /// - /// Fired when an entitlement was updated. - /// - public EventHandlingBuilder HandleEntitlementUpdated - ( - Func handler - ) - { - this.Services.Configure - ( - c => c.Register(handler) - ); - - return this; - } - - /// - /// Fired when an entitlement was deleted. - /// - public EventHandlingBuilder HandleEntitlementDeleted - ( - Func handler - ) - { - this.Services.Configure - ( - c => c.Register(handler) - ); - - return this; - } - - /// - /// Fired when the application was authorized. This is only available through webhook events. - /// - public EventHandlingBuilder HandleApplicationAuthorized - ( - Func handler - ) - { - this.Services.Configure - ( - c => c.Register(handler) - ); - - return this; - } - - /// - /// Fired if a request made over the gateway got ratelimited. - /// - public EventHandlingBuilder HandleRatelimited(Func handler) - { - this.Services.Configure - ( - c => c.Register(handler) - ); - - return this; - } -} diff --git a/DSharpPlus/Clients/EventWaiter.cs b/DSharpPlus/Clients/EventWaiter.cs deleted file mode 100644 index 5c156a9955..0000000000 --- a/DSharpPlus/Clients/EventWaiter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Clients; - -/// -/// Provides a mechanism to wait for an instance of an event matching the specified condition. -/// -/// -public sealed record EventWaiter - where T : DiscordEventArgs -{ - /// - /// The tracking identifier for this waiter. - /// - public required Ulid Id { get; init; } - - /// - /// The condition an event needs to meet before it is considered to fulfil this event waiter. - /// - public required Func Condition { get; init; } - - /// - /// An awaitable task completion source for this event waiter. - /// - /// - /// Do not manipulate this manually - it is exposed for implementers of . - /// - public required TaskCompletionSource> CompletionSource { get; init; } - - /// - /// An awaitable task for this event waiter. - /// - public Task> Task => this.CompletionSource.Task; - - /// - /// The timeout for this event waiter. - /// - public required TimeSpan Timeout { get; init; } -} \ No newline at end of file diff --git a/DSharpPlus/Clients/EventWaiterResult.cs b/DSharpPlus/Clients/EventWaiterResult.cs deleted file mode 100644 index 6ae5b74b3e..0000000000 --- a/DSharpPlus/Clients/EventWaiterResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Clients; - -// really, this should just be a result type -/// -/// Represents either the received event or an indication that the event waiter timed out. -/// -public readonly record struct EventWaiterResult - where T : DiscordEventArgs -{ - /// - /// Indicates that this event waiter timed out. - /// - public bool TimedOut { get; init; } - - /// - /// The event this event waiter sought to receive. - /// - [MemberNotNullWhen(false, nameof(TimedOut))] - public T? Value { get; init; } - - /// - /// Creates a new result that indicates having timed out. - /// - public static EventWaiterResult FromTimedOut() - => new() { TimedOut = true }; -} \ No newline at end of file diff --git a/DSharpPlus/Clients/IEventDispatcher.cs b/DSharpPlus/Clients/IEventDispatcher.cs deleted file mode 100644 index 7117dbc417..0000000000 --- a/DSharpPlus/Clients/IEventDispatcher.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Clients; - -/// -/// Represents a service dispatching events to registered event handlers. -/// -public interface IEventDispatcher -{ - /// - /// Dispatches the given event. - /// - /// The type of event to dispatch. - /// The event to dispatch. - /// The origin DiscordClient instance. - public ValueTask DispatchAsync(DiscordClient client, T eventArgs) - where T : DiscordEventArgs; - - /// - /// Creates a new event waiter for an event of the specified type. - /// - /// The condition an event needs to match before it fulfils the event waiter. - /// A timeout for this event waiter. - public EventWaiter CreateEventWaiter(Func condition, TimeSpan timeout) - where T : DiscordEventArgs; -} diff --git a/DSharpPlus/Clients/IShardOrchestrator.cs b/DSharpPlus/Clients/IShardOrchestrator.cs deleted file mode 100644 index 6fad38aae6..0000000000 --- a/DSharpPlus/Clients/IShardOrchestrator.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -using DSharpPlus.Entities; - -namespace DSharpPlus.Clients; - -/// -/// Represents a mechanism for orchestrating one or more shards in one or more processes. -/// -public interface IShardOrchestrator -{ - /// - /// Starts all shards associated with this orchestrator. - /// - public ValueTask StartAsync(DiscordActivity? activity, DiscordUserStatus? status, DateTimeOffset? idleSince); - - /// - /// Stops all shards associated with this orchestrator. - /// - public ValueTask StopAsync(); - - /// - /// Reconnects all shards associated with this orchestrator. - /// - public ValueTask ReconnectAsync(); - - /// - /// Sends an outbound event to Discord from the specified guild. Pass 0 to send a guild-independent outbound event - /// to shard 0. - /// - public ValueTask SendOutboundEventAsync(byte[] payload, ulong guildId); - - /// - /// Sends an outbound event to Discord on all shards. - /// - public ValueTask BroadcastOutboundEventAsync(byte[] payload); - - /// - /// Indicates whether the bot's connection to the given guild is functional. - /// - public bool IsConnected(ulong guildId); - - /// - /// Gets the connection latency to a specific guild, otherwise known as ping. - /// - public TimeSpan GetConnectionLatency(ulong guildId); - - /// - /// Get the list of Shard ID's this orcestrator is responsible for. - /// - /// - public IEnumerable GetShardIds(); - - /// - /// Indicates whether the bot's shard connection is functional. - /// - /// - /// - public bool IsConnected(int shardId); - - /// - /// Gets the connection latency specific to a shard, otherwise known as ping. - /// - /// - /// - public TimeSpan GetConnectionLatency(int shardId); - - /// - /// Indicates whether all shards are connected. - /// - public bool AllShardsConnected { get; } - - /// - /// Gets the total amount of shards connected to this bot. - /// - public int TotalShardCount { get; } - - /// - /// Gets the amount of shards handled by this orchestrator. - /// - public int ConnectedShardCount { get; } -} diff --git a/DSharpPlus/Clients/MultiShardOrchestrator.cs b/DSharpPlus/Clients/MultiShardOrchestrator.cs deleted file mode 100644 index 3c392e83cf..0000000000 --- a/DSharpPlus/Clients/MultiShardOrchestrator.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Gateway; -using DSharpPlus.Net.Gateway.Compression; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace DSharpPlus.Clients; - -/// -/// Orchestrates multiple shards within this process. -/// -public sealed class MultiShardOrchestrator : IShardOrchestrator -{ - private IGatewayClient[]? shards; - private readonly DiscordRestApiClient apiClient; - private readonly ShardingOptions options; - private readonly IServiceProvider serviceProvider; - private readonly IPayloadDecompressor decompressor; - - private uint shardCount; - private uint stride; - private uint totalShards; - - /// - public bool AllShardsConnected => this.shards?.All(shard => shard.IsConnected) == true; - - /// - /// - /// - /// - /// This value may be inaccurate before startup. It is guaranteed to be correct by the time the first SessionCreated event - /// is fired. - /// - public int TotalShardCount => (int)this.totalShards; - - /// - /// - /// - /// - /// This value may be inaccurate before startup. It is guaranteed to be correct by the time the first SessionCreated event - /// is fired. - /// - public int ConnectedShardCount => (int)this.shardCount; - - public MultiShardOrchestrator - ( - IServiceProvider serviceProvider, - IOptions options, - DiscordRestApiClientFactory apiClientFactory, - IPayloadDecompressor decompressor - ) - { - this.apiClient = apiClientFactory.GetCurrentApplicationClient(); - this.options = options.Value; - this.serviceProvider = serviceProvider; - this.decompressor = decompressor; - } - - /// - public async ValueTask StartAsync(DiscordActivity? activity, DiscordUserStatus? status, DateTimeOffset? idleSince) - { - uint startShards, totalShards, stride; - GatewayInfo info = await this.apiClient.GetGatewayInfoAsync(); - - if (this.options.ShardCount is null) - { - startShards = totalShards = (uint)info.ShardCount; - stride = 0; - } - else - { - totalShards = this.options.TotalShards == 0 ? this.options.ShardCount.Value : this.options.TotalShards; - startShards = this.options.ShardCount.Value; - stride = this.options.Stride; - - if (stride != 0 && totalShards == 0) - { - throw new ArgumentOutOfRangeException - ( - paramName: "options", - message: "The sharded client was set up for multi-process sharding but did not specify a total shard count." - ); - } - } - - this.stride = stride; - this.shardCount = startShards; - this.totalShards = totalShards; - - QueryUriBuilder gwuri = new(info.Url); - - gwuri.AddParameter("v", "10") - .AddParameter("encoding", "json"); - - if (this.decompressor.IsTransportCompression) - { - gwuri.AddParameter("compress", this.decompressor.Name); - } - - this.shards = new IGatewayClient[startShards]; - - // create all shard instances before starting any of them - for (int i = 0; i < startShards; i++) - { - this.shards[i] = this.serviceProvider.GetRequiredService(); - } - - for (int i = 0; i < startShards; i += info.SessionBucket.MaxConcurrency) - { - DateTimeOffset startTime = DateTimeOffset.UtcNow; - - for (int j = i; j < i + info.SessionBucket.MaxConcurrency && j < startShards; j++) - { - await this.shards[j].ConnectAsync - ( - gwuri.Build(), - activity, - status, - idleSince, - new ShardInfo - { - ShardCount = (int)totalShards, - ShardId = (int)stride + j - } - ); - } - - TimeSpan diff = DateTimeOffset.UtcNow - startTime; - - if (diff < TimeSpan.FromSeconds(5)) - { - await Task.Delay(TimeSpan.FromSeconds(5) - diff); - } - } - } - - /// - public async ValueTask StopAsync() - { - foreach (IGatewayClient client in this.shards) - { - await client.DisconnectAsync(); - } - } - - /// - public bool IsConnected(int shardId) - { - ArgumentOutOfRangeException.ThrowIfLessThan(shardId, (int)this.stride); - ArgumentOutOfRangeException.ThrowIfGreaterThan(shardId, (int)this.stride + this.shardCount); - - return this.shards[shardId - this.stride].IsConnected; - } - - /// - public bool IsConnected(ulong guildId) - { - int shardId = (int)GetShardIdForGuildId(guildId); - return IsConnected(shardId); - } - - /// - public TimeSpan GetConnectionLatency(int shardId) - { - ArgumentOutOfRangeException.ThrowIfLessThan(shardId, (int)this.stride); - ArgumentOutOfRangeException.ThrowIfGreaterThan(shardId, (int)this.stride + this.shardCount); - - return this.shards[shardId - this.stride].Ping; - } - - /// - public TimeSpan GetConnectionLatency(ulong guildId) - { - int shardId = (int)GetShardIdForGuildId(guildId); - return GetConnectionLatency(shardId); - } - - private uint GetShardIdForGuildId(ulong guildId) - => (uint)((guildId >> 22) % this.totalShards); - - /// - public async ValueTask ReconnectAsync() - { - // don't parallelize this, we can't start shards too wildly out of order - foreach(IGatewayClient shard in this.shards) - { - await shard.ReconnectAsync(); - } - } - - /// - public async ValueTask SendOutboundEventAsync(byte[] payload, ulong guildId) - { - if (guildId == 0) - { - await this.shards[0].WriteAsync(payload); - } - - uint shardId = GetShardIdForGuildId(guildId); - - ArgumentOutOfRangeException.ThrowIfLessThan(shardId, this.stride); - ArgumentOutOfRangeException.ThrowIfGreaterThan(shardId, this.stride + this.shardCount); - - await this.shards[shardId].WriteAsync(payload); - } - - /// - public async ValueTask BroadcastOutboundEventAsync(byte[] payload) - { - if (!this.AllShardsConnected) - { - throw new InvalidOperationException("Broadcast is only possible when all shards are connected"); - } - - await Parallel.ForEachAsync(this.shards, async (shard, _) => await shard.WriteAsync(payload)); - } - - /// - public IEnumerable GetShardIds() - { - if (this.stride == 0 || this.totalShards == 0) - { - // No striding, use linear IDs - uint count = this.shardCount == default ? 1 : this.shardCount; - for (int i = 0; i < count; i++) - { - yield return i; - } - } - else - { - // Strided IDs - uint count = this.shardCount == default ? 1 : this.shardCount; - for (int i = 0; i < count; i++) - { - int shardId = (int)(i * this.stride); - if (shardId < this.totalShards) - { - yield return shardId; - } - } - } - } -} diff --git a/DSharpPlus/Clients/NullShardOrchestrator.cs b/DSharpPlus/Clients/NullShardOrchestrator.cs deleted file mode 100644 index 9b436558ef..0000000000 --- a/DSharpPlus/Clients/NullShardOrchestrator.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -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 TimeSpan GetConnectionLatency(int shardId) => TimeSpan.Zero; - - /// - public IEnumerable GetShardIds() => []; - - /// - public bool IsConnected(ulong guildId) => this.AllShardsConnected; - public bool IsConnected(int shardId) => 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/Clients/OAuth2DiscordClient.cs b/DSharpPlus/Clients/OAuth2DiscordClient.cs deleted file mode 100644 index 5b063e3acc..0000000000 --- a/DSharpPlus/Clients/OAuth2DiscordClient.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -using DSharpPlus.Entities; - -namespace DSharpPlus.Clients; - -/// -/// Represents a thin client to serve as the base for OAuth2 usage. -/// -public sealed class OAuth2DiscordClient : BaseDiscordClient -{ - private readonly Dictionary guilds = []; - - /// - public override IReadOnlyDictionary Guilds => this.guilds; - - /// - public override void Dispose() => throw new System.NotImplementedException(); - - /// - public override async Task InitializeAsync() - { - if (this.CurrentUser is null) - { - this.CurrentUser = await this.ApiClient.GetCurrentUserAsync(); - UpdateUserCache(this.CurrentUser); - } - } -} diff --git a/DSharpPlus/Clients/ShardingOptions.cs b/DSharpPlus/Clients/ShardingOptions.cs deleted file mode 100644 index cd732e86f8..0000000000 --- a/DSharpPlus/Clients/ShardingOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace DSharpPlus.Clients; - -/// -/// Contains configuration options for sharding within DSharpPlus. -/// -public sealed class ShardingOptions -{ - /// - /// Specifies the amount of shards to start. If left default, the library will decide automatically. - /// - public uint? ShardCount { get; set; } - - /// - /// Specifies the amount of shards IDs to skip when starting up, designed for multi-process sharding. - /// - public uint Stride { get; set; } - - /// - /// Specifies the amount of total shards associated with this bot. This is only considered in combination with - /// . - /// - public uint TotalShards { get; set; } -} diff --git a/DSharpPlus/Clients/SingleShardOrchestrator.cs b/DSharpPlus/Clients/SingleShardOrchestrator.cs deleted file mode 100644 index 1590840a29..0000000000 --- a/DSharpPlus/Clients/SingleShardOrchestrator.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Net; -using DSharpPlus.Net.Gateway; -using DSharpPlus.Net.Gateway.Compression; - -namespace DSharpPlus.Clients; - -/// -/// Orchestrates a single "shard". -/// -public sealed class SingleShardOrchestrator : IShardOrchestrator -{ - private readonly IGatewayClient gatewayClient; - private readonly DiscordRestApiClient apiClient; - private readonly IPayloadDecompressor decompressor; - - /// - /// Creates a new instance of this type. - /// - public SingleShardOrchestrator - ( - IGatewayClient gatewayClient, - DiscordRestApiClientFactory apiClientFactory, - IPayloadDecompressor decompressor - ) - { - this.gatewayClient = gatewayClient; - this.apiClient = apiClientFactory.GetCurrentApplicationClient(); - this.decompressor = decompressor; - } - - /// - public bool AllShardsConnected => this.gatewayClient.IsConnected; - - /// - public int TotalShardCount => 1; - - /// - public int ConnectedShardCount => 1; - - /// - public async ValueTask BroadcastOutboundEventAsync(byte[] payload) - { - if (!this.AllShardsConnected) - { - throw new InvalidOperationException("Broadcast is only possible when the shard is connected"); - } - - await SendOutboundEventAsync(payload, 0); - } - - /// - public TimeSpan GetConnectionLatency(ulong _) => this.gatewayClient.Ping; - - /// - public TimeSpan GetConnectionLatency(int _) => this.gatewayClient.Ping; - - /// - public IEnumerable GetShardIds() => [this.gatewayClient.ShardId]; - - /// - public bool IsConnected(ulong _) => this.gatewayClient.IsConnected; - - /// - public bool IsConnected(int _) => this.gatewayClient.IsConnected; - - /// - public async ValueTask ReconnectAsync() => await this.gatewayClient.ReconnectAsync(); - - // guild ID doesn't matter here, since we only have a single shard - /// - /// Sends an outbound event to Discord. - /// - public async ValueTask SendOutboundEventAsync(byte[] payload, ulong _) - => await this.gatewayClient.WriteAsync(payload); - - /// - public async ValueTask StartAsync(DiscordActivity? activity, DiscordUserStatus? status, DateTimeOffset? idleSince) - { - GatewayInfo info = await this.apiClient.GetGatewayInfoAsync(); - - QueryUriBuilder gwuri = new(info.Url); - - gwuri.AddParameter("v", "10") - .AddParameter("encoding", "json"); - - if (this.decompressor.IsTransportCompression) - { - gwuri.AddParameter("compress", this.decompressor.Name); - } - - await this.gatewayClient.ConnectAsync(gwuri.Build(), activity, status, idleSince); - } - - /// - public async ValueTask StopAsync() => await this.gatewayClient.DisconnectAsync(); -} diff --git a/DSharpPlus/DSharpPlus.csproj b/DSharpPlus/DSharpPlus.csproj deleted file mode 100644 index c739166229..0000000000 --- a/DSharpPlus/DSharpPlus.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - DSharpPlus - A C# API for Discord based off DiscordSharp, but rewritten to fit the API standards. - $(PackageTags), webhooks - true - true - true - Preview - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DSharpPlus/DefaultClientErrorHandler.cs b/DSharpPlus/DefaultClientErrorHandler.cs deleted file mode 100644 index 1f87f77f08..0000000000 --- a/DSharpPlus/DefaultClientErrorHandler.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.ComponentModel; -using System.Threading.Tasks; - -using DSharpPlus.Exceptions; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -/// -/// Represents the default implementation of -/// -public sealed class DefaultClientErrorHandler : IClientErrorHandler -{ - private readonly ILogger logger; - - /// - /// Creates a new instance of this type. - /// - [ActivatorUtilitiesConstructor] - public DefaultClientErrorHandler(ILogger logger) - => this.logger = logger; - - /// - /// Don't use this. - /// - // glue code for legacy handling - [EditorBrowsable(EditorBrowsableState.Never)] - public DefaultClientErrorHandler(ILogger logger) - => this.logger = logger; - - /// - public ValueTask HandleEventHandlerError - ( - string name, - Exception exception, - Delegate invokedDelegate, - object sender, - object args - ) - { - if (exception is BadRequestException badRequest) - { - this.logger.LogError - ( - "Event handler exception for event {Event} thrown from {Method} (defined in {DeclaryingType}):\n" + - "A request was rejected by the Discord API.\n" + - " Errors: {Errors}\n" + - " Message: {JsonMessage}\n" + - " Stack trace: {Stacktrace}", - name, - invokedDelegate.Method, - invokedDelegate.Method.DeclaringType, - badRequest.Errors, - badRequest.JsonMessage, - badRequest.StackTrace - ); - - return ValueTask.CompletedTask; - } - - this.logger.LogError - ( - exception, - "Event handler exception for event {Event} thrown from {Method} (defined in {DeclaryingType}).", - name, - invokedDelegate.Method, - invokedDelegate.Method.DeclaringType - ); - - return ValueTask.CompletedTask; - } - - /// - public ValueTask HandleGatewayError(Exception exception) - { - this.logger.LogError - ( - exception, - "An error occurred in the DSharpPlus gateway." - ); - - return ValueTask.CompletedTask; - } -} diff --git a/DSharpPlus/DiscordConfiguration.cs b/DSharpPlus/DiscordConfiguration.cs deleted file mode 100644 index d7cc475366..0000000000 --- a/DSharpPlus/DiscordConfiguration.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 531f1a379c..0000000000 --- a/DSharpPlus/DiscordIntents.cs +++ /dev/null @@ -1,196 +0,0 @@ -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/AddFileOptions.cs b/DSharpPlus/Entities/AddFileOptions.cs deleted file mode 100644 index b9c48cbadc..0000000000 --- a/DSharpPlus/Entities/AddFileOptions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace DSharpPlus.Entities; - -/// -/// Additional flags for handling file upload streams. -/// -[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 sending. - /// - 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, -} diff --git a/DSharpPlus/Entities/Application/DiscordApplication.cs b/DSharpPlus/Entities/Application/DiscordApplication.cs deleted file mode 100644 index 2152f1ab4f..0000000000 --- a/DSharpPlus/Entities/Application/DiscordApplication.cs +++ /dev/null @@ -1,640 +0,0 @@ -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 IReadOnlyList? 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 - ? transportApplication.RpcOrigins.ToList() - : 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 = [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 = owners; - this.Team.Owner = owners.First(x => x.Id == transportApplication.Team.OwnerId); - this.Team.Members = 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, entitlementsThisRequest); - - 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 deleted file mode 100644 index e49ded74f1..0000000000 --- a/DSharpPlus/Entities/Application/DiscordApplicationAsset.cs +++ /dev/null @@ -1,83 +0,0 @@ -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 deleted file mode 100644 index 019cca347e..0000000000 --- a/DSharpPlus/Entities/Application/DiscordApplicationAssetType.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 711e8cc1c0..0000000000 --- a/DSharpPlus/Entities/Application/DiscordApplicationFlags.cs +++ /dev/null @@ -1,59 +0,0 @@ - -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/DiscordApplicationOAuth2InstallParams.cs b/DSharpPlus/Entities/Application/DiscordApplicationOAuth2InstallParams.cs deleted file mode 100644 index cee635edb0..0000000000 --- a/DSharpPlus/Entities/Application/DiscordApplicationOAuth2InstallParams.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the default installation configuration for an integration. -/// -/// -/// is ignored in the case of . -/// -public sealed class DiscordApplicationOAuth2InstallParams -{ - /// - /// Represents permissions that the integration requires. - /// - [JsonProperty("permissions")] - public DiscordPermissions Permissions { get; internal set; } - - /// - /// Represents scopes granted to the integration. - /// - [JsonProperty("scopes")] - public IReadOnlyList Scopes { get; internal set; } - - public DiscordApplicationOAuth2InstallParams() { } -} diff --git a/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs b/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs deleted file mode 100644 index caa04c0308..0000000000 --- a/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 569a6988ef..0000000000 --- a/DSharpPlus/Entities/Application/DiscordAsset.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/DiscordEntitlement.cs b/DSharpPlus/Entities/Application/DiscordEntitlement.cs deleted file mode 100644 index 6ca8e7c8b1..0000000000 --- a/DSharpPlus/Entities/Application/DiscordEntitlement.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Entitlement owned by a user or guild -/// -public sealed class DiscordEntitlement -{ - /// - /// ID of the entitlement - /// - [JsonProperty("id")] - public ulong Id { get; internal set; } - - /// - /// ID of the SKU - /// - [JsonProperty("sku_id")] - public ulong StockKeepingUnitId { get; internal set; } - - /// - /// ID of the parent application - /// - [JsonProperty("application_id")] - public ulong ApplicationId { get; internal set; } - - /// - /// ID of the user that is granted access to the entitlement's sku - /// - [JsonProperty("user_id")] - public ulong? UserId { get; internal set; } - - /// - /// Type of entitlement - /// - [JsonProperty("type")] - public DiscordEntitlementType Type { get; internal set; } - - /// - /// Entitlement was deleted - /// - [JsonProperty("deleted")] - public bool Deleted { get; internal set; } - - /// - /// Start date at which the entitlement is valid. Not present when using test entitlements. - /// - [JsonProperty("starts_at")] - public DateTimeOffset? StartsAt { get; internal set; } - - /// - /// Date at which the entitlement is no longer valid. Not present when using test entitlements. - /// - [JsonProperty("ends_at")] - public DateTimeOffset? EndsAt { get; internal set; } - - /// - /// ID of the guild that is granted access to the entitlement's sku - /// - [JsonProperty("guild_id")] - public ulong? GuildId { get; internal set; } - - /// - /// For consumable items, whether the entitlement has been consumed - /// - [JsonProperty("consumed")] - public bool? Consumed { get; internal set; } -} diff --git a/DSharpPlus/Entities/Application/DiscordEntitlementType.cs b/DSharpPlus/Entities/Application/DiscordEntitlementType.cs deleted file mode 100644 index ada14939a9..0000000000 --- a/DSharpPlus/Entities/Application/DiscordEntitlementType.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace DSharpPlus.Entities; - -/// -/// Type of Entitlement -/// -public enum DiscordEntitlementType -{ - /// - /// Entitlement was purchased by user - /// - Purchase, - - /// - /// Entitlement for Discord Nitro subscription - /// - PremiumSubscription, - - /// - /// Entitlement was gifted by developer - /// - DeveloperGift, - - /// - /// Entitlement was purchased by a dev in application test mode - /// - TestModePurchase, - - /// - /// Entitlement was granted when the SKU was free - /// - FreePurchase, - - /// - /// Entitlement was gifted by another user - /// - UserGift, - - /// - /// Entitlement was claimed by user for free as a Nitro Subscriber - /// - PremiumPurchase, - - /// - /// Entitlement was purchased as an app subscription - /// - ApplicationSubscription -} diff --git a/DSharpPlus/Entities/Application/DiscordOAuthScope.cs b/DSharpPlus/Entities/Application/DiscordOAuthScope.cs deleted file mode 100644 index a65adba7ce..0000000000 --- a/DSharpPlus/Entities/Application/DiscordOAuthScope.cs +++ /dev/null @@ -1,151 +0,0 @@ -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 deleted file mode 100644 index c4f7e07918..0000000000 --- a/DSharpPlus/Entities/Application/DiscordSpotifyAsset.cs +++ /dev/null @@ -1,23 +0,0 @@ -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/Application/DiscordStockKeepingUnit.cs b/DSharpPlus/Entities/Application/DiscordStockKeepingUnit.cs deleted file mode 100644 index c8d8fef6b6..0000000000 --- a/DSharpPlus/Entities/Application/DiscordStockKeepingUnit.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// SKUs (stock-keeping units) in Discord represent premium offerings that can be made available to your application's users or guilds. -/// -public sealed class DiscordStockKeepingUnit -{ - /// - /// Id of this entity - /// - [JsonProperty("id")] - public ulong Id { get; internal set; } - - /// - /// Type of stock keeping unit - /// - [JsonProperty("type")] - public DiscordStockKeepingUnitType Type { get; internal set; } - - /// - /// ID of the parent application - /// - [JsonProperty("application_id")] - public ulong ApplicationId { get; internal set; } - - /// - /// Customer-facing name of your premium offering - /// - [JsonProperty("name")] - public string Name { get; internal set; } - - /// - /// System-generated URL slug based on the SKU's name - /// - [JsonProperty("slug")] - public string Slug { get; internal set; } - - /// - /// Stock keeping unit flags - /// - [JsonProperty("flags")] - public DiscordStockKeepingUnitFlags Flags { get; internal set; } -} diff --git a/DSharpPlus/Entities/Application/DiscordStockKeepingUnitFlags.cs b/DSharpPlus/Entities/Application/DiscordStockKeepingUnitFlags.cs deleted file mode 100644 index fdc1bf909a..0000000000 --- a/DSharpPlus/Entities/Application/DiscordStockKeepingUnitFlags.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace DSharpPlus.Entities; - -/// -/// Represent additional information about the SKU -/// -[Flags] -public enum DiscordStockKeepingUnitFlags -{ - /// - /// SKU is available for purchase - /// - Available = 1 << 2, - - /// - /// Recurring SKU that can be purchased by a user and applied to a single server. Grants access to every user in that server. - /// - GuildSubscription = 1 << 7, - - /// - /// Recurring SKU purchased by a user for themselves. Grants access to the purchasing user in every server. - /// - UserSubscription = 1 << 8 -} diff --git a/DSharpPlus/Entities/Application/DiscordStockKeepingUnitType.cs b/DSharpPlus/Entities/Application/DiscordStockKeepingUnitType.cs deleted file mode 100644 index f372234eb2..0000000000 --- a/DSharpPlus/Entities/Application/DiscordStockKeepingUnitType.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace DSharpPlus.Entities; - -/// -/// For subscriptions, SKUs will have a type of either Subscription represented by type: 5 or -/// SubscriptionGroup represented by type:6 . -///
-/// For any current implementations, you will want to use the SKU -/// defined by type: 5 . A SubscriptionGroup is automatically created for each Subscription SKU -/// and are not used at this time. -///
-public enum DiscordStockKeepingUnitType -{ - /// - /// Durable one-time purchase - /// - Durable = 2, - - /// - /// Consumable one-time purchase - /// - Consumable = 3, - - /// - /// Represents a recurring subscription - /// - Subscription = 5, - - /// - /// System-generated group for each SUBSCRIPTION SKU created - /// - SubscriptionGroup = 6 -} diff --git a/DSharpPlus/Entities/Application/DiscordTestEntitlementOwnerType.cs b/DSharpPlus/Entities/Application/DiscordTestEntitlementOwnerType.cs deleted file mode 100644 index 823fa9e529..0000000000 --- a/DSharpPlus/Entities/Application/DiscordTestEntitlementOwnerType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DSharpPlus.Entities; - -/// -/// Defines what type of owner the test entitlement should have -/// -public enum DiscordTestEntitlementOwnerType -{ - /// - /// The test entitlement should belong to a guild - /// - Guild = 1, - - /// - /// The test entitlement should belong to a user - /// - User = 2 -} diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs b/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs deleted file mode 100644 index 7b0c998bbd..0000000000 --- a/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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 deleted file mode 100644 index 1c89074622..0000000000 --- a/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs +++ /dev/null @@ -1,304 +0,0 @@ -// 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 deleted file mode 100644 index bfd3f6747c..0000000000 --- a/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs +++ /dev/null @@ -1,1600 +0,0 @@ -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 - ? olds.ToList() - : null, - After = news != null - ? news.ToList() - : 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 = - change.NewValues.Select(xo => (ulong)xo["id"]!) - .Select(gx => guild.roles.GetValueOrDefault(gx)!).ToList(); - break; - - case "$remove": - entry.RemovedRoles = - 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 deleted file mode 100644 index f2f927e07c..0000000000 --- a/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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 deleted file mode 100644 index 1eedd1abaa..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index d26d243d3b..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index f9b880d846..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs +++ /dev/null @@ -1,115 +0,0 @@ -// 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 deleted file mode 100644 index 7f04a05e78..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 2c27b8a04e..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index a082fabb02..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index ed9db37361..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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 deleted file mode 100644 index 71ba021acf..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index e4d770c225..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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 deleted file mode 100644 index 8e6f899eca..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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 deleted file mode 100644 index 18dd7caac9..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs +++ /dev/null @@ -1,70 +0,0 @@ -// 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 deleted file mode 100644 index 49b527f416..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index 52c101f984..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index d38ad774a1..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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 deleted file mode 100644 index 14a3237155..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs +++ /dev/null @@ -1,67 +0,0 @@ -// 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 deleted file mode 100644 index 996afb14d6..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs +++ /dev/null @@ -1,50 +0,0 @@ -// 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 deleted file mode 100644 index a38bf7cc5f..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs +++ /dev/null @@ -1,45 +0,0 @@ -// 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 deleted file mode 100644 index 077cc1e863..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index 144549991f..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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 deleted file mode 100644 index ba17f94eae..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 49fb13bdd9..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs +++ /dev/null @@ -1,80 +0,0 @@ -// 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 deleted file mode 100644 index ebbd7c4d37..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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 deleted file mode 100644 index fa3e3228c4..0000000000 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs +++ /dev/null @@ -1,60 +0,0 @@ -// 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 deleted file mode 100644 index b67ec5fdca..0000000000 --- a/DSharpPlus/Entities/AuditLogs/PropertyChange.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index 36fe6cca4c..0000000000 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationAction.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 8c64c2a55e..0000000000 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionBuilder.cs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index a7a08acfd4..0000000000 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionExecution.cs +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 7c97bee974..0000000000 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadata.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord rule action metadata. -/// -public class DiscordRuleActionMetadata -{ - /// - /// Gets the ID of the channel where blocked content or moderation events 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 custom message that will be shown to a user when their content 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 deleted file mode 100644 index f161626488..0000000000 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadataBuilder.cs +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 7a0a87ef36..0000000000 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionType.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 46b408fcc0..0000000000 --- a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs +++ /dev/null @@ -1,118 +0,0 @@ -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 deleted file mode 100644 index 35dc364b2e..0000000000 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleEventType.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 13b9120e49..0000000000 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleKeywordPresetType.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 6120b5a40f..0000000000 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadata.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 55c2e88884..0000000000 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadataBuilder.cs +++ /dev/null @@ -1,135 +0,0 @@ -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 deleted file mode 100644 index 0df6bd1ee9..0000000000 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerType.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 0ffe6555f5..0000000000 --- a/DSharpPlus/Entities/BaseDiscordMessageBuilder.cs +++ /dev/null @@ -1,958 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -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; } - - /// - /// Suppresses notifications for this message. - /// - /// The builder to chain calls with. - public T SuppressNotifications() - { - this.Flags |= DiscordMessageFlags.SuppressNotifications; - return (T)this; - } - - /// - /// Suppresses embeds for this message. - /// - /// The builder to chain calls with. - public T SuppressEmbeds() - { - this.Flags |= DiscordMessageFlags.SuppressEmbeds; - 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(this.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 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 DiscordFile(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 DiscordFile(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; - } - - /// - /// 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; - bool isCV2 = this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2); - int maxTopComponents = isCV2 ? CV2_MAX_TOTAL_COMPONENTS : 5; - int maxAllComponents = isCV2 ? CV2_MAX_TOTAL_COMPONENTS : 25; - - int allComponentCount = this.Components.Sum - ( - c => - { - return c switch - { - DiscordActionRowComponent arc when isCV2 => 1 + arc.Components.Count, - DiscordActionRowComponent arc => 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 when isCV2 => arc.Components.Count + 1, // Action row + components - DiscordActionRowComponent arc => arc.Components.Count, // CV1 doesn't count the action row as a real component - 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() => EnableV2Components(); - IDiscordMessageBuilder IDiscordMessageBuilder.DisableV2Components() => DisableV2Components(); - IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(DiscordActionRowComponent component) => AddActionRowComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(DiscordSelectComponent selectMenu) => AddActionRowComponent(selectMenu); - IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(params IEnumerable buttons) => AddActionRowComponent(buttons); - IDiscordMessageBuilder IDiscordMessageBuilder.AddMediaGalleryComponent(params IEnumerable galleryItems) => AddMediaGalleryComponent(galleryItems); - IDiscordMessageBuilder IDiscordMessageBuilder.AddSectionComponent(DiscordSectionComponent section) => AddSectionComponent(section); - IDiscordMessageBuilder IDiscordMessageBuilder.AddTextDisplayComponent(DiscordTextDisplayComponent component) => AddTextDisplayComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddTextDisplayComponent(string content) => AddTextDisplayComponent(content); - IDiscordMessageBuilder IDiscordMessageBuilder.AddSeparatorComponent(DiscordSeparatorComponent component) => AddSeparatorComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFileComponent(DiscordFileComponent component) => AddFileComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddContainerComponent(DiscordContainerComponent component) => AddContainerComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.SuppressNotifications() => SuppressNotifications(); - IDiscordMessageBuilder IDiscordMessageBuilder.SuppressEmbeds() => SuppressEmbeds(); - 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); -} - -/// -/// Base interface for any discord message builder. -/// -public interface IDiscordMessageBuilder -{ - /// - /// 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 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(); - - IDiscordMessageBuilder SuppressEmbeds(); -} - -/* -* 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 deleted file mode 100644 index 714d742c51..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordAutoArchiveDuration.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index ce195d8e63..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordChannel.cs +++ /dev/null @@ -1,1291 +0,0 @@ -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.Rest; -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 and stage channels") : - 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)]; - DiscordChannelPosition[] pmds = new DiscordChannelPosition[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. - /// The role IDs for roles given to the user when accepting the invite. - /// - /// Thrown when the client does not have the - /// permission or in case if roleIds are used and 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, IEnumerable roleIds = null) - => await this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId, roleIds); - - /// - /// 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 deleted file mode 100644 index f7f05f5621..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordChannelFlags.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 7c3f04c21f..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordChannelType.cs +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index 4bbaaa56b4..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordDmChannel.cs +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 34f280c617..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordFollowedChannel.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 05fd64513f..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordOverwriteType.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 90d54a1368..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordPartialChannel.cs +++ /dev/null @@ -1,125 +0,0 @@ -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 deleted file mode 100644 index 192879d2a5..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordSystemChannelFlags.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 6b68c2e85f..0000000000 --- a/DSharpPlus/Entities/Channel/DiscordVideoQualityMode.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index a1e02aef3f..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordAttachment.cs +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index a281607f8f..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMentions.cs +++ /dev/null @@ -1,121 +0,0 @@ -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 deleted file mode 100644 index 5c9269d8e9..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs +++ /dev/null @@ -1,1066 +0,0 @@ -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.MentionEveryone = other.MentionEveryone; - 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.Activity = other.Activity; - this.Application = other.Application; - this.Flags = other.Flags; - this.ReferencedMessage = other.ReferencedMessage; - this.MessageSnapshots = other.MessageSnapshots; - this.Interaction = other.Interaction; - this.Components = other.Components; - this.internalReference = other.internalReference; - this.guildId = other.guildId; - this.channel = other.channel; - } - - /// - /// 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 = []; - - if (this.Components is null || this.Components.Count == 0) - { - return []; - } - - foreach (DiscordComponent component in this.Components) - { - switch (component) - { - case DiscordActionRowComponent actionRowComponent: - components.AddRange(FilterComponents(actionRowComponent.Components)); - break; - case DiscordContainerComponent containerComponent: - components.AddRange(FilterComponents(containerComponent.Components)); - break; - case DiscordSectionComponent sectionComponent: - components.AddRange(FilterComponents(sectionComponent.Components)); - - if (sectionComponent.Accessory is T filteredAccessory) - { - components.Add(filteredAccessory); - } - break; - } - - if (component is T filteredComponent) - { - components.Add(filteredComponent); - } - } - - return components; - } - - private static List FilterComponents(IReadOnlyList components) - where T : DiscordComponent - { - List filteredComponents = []; - - foreach (DiscordComponent component in components) - { - switch (component) - { - case DiscordActionRowComponent actionRowComponent: - filteredComponents.AddRange(FilterComponents(actionRowComponent.Components)); - break; - case DiscordContainerComponent containerComponent: - filteredComponents.AddRange(FilterComponents(containerComponent.Components)); - break; - case DiscordSectionComponent sectionComponent: - filteredComponents.AddRange(FilterComponents(sectionComponent.Components)); - - if (sectionComponent.Accessory is T filteredAccessory) - { - filteredComponents.Add(filteredAccessory); - } - break; - } - - if (component is T filteredComponent) - { - filteredComponents.Add(filteredComponent); - } - } - - return filteredComponents; - } - - /// - /// 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.SuppressEmbeds : 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.SuppressEmbeds : 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.SuppressEmbeds : 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 deleted file mode 100644 index 6669aaeee5..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageActivity.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 2983b1e8bf..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageActivityType.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 5aa5cb661c..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageApplication.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index c501c5df48..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs +++ /dev/null @@ -1,288 +0,0 @@ -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."); - } - - 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/DiscordMessageFlags.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageFlags.cs deleted file mode 100644 index 2e373efb7d..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageFlags.cs +++ /dev/null @@ -1,90 +0,0 @@ -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. - /// - SuppressEmbeds = 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, - - /// - /// This message has an associated thread, with the same id as the message - /// - HasThread = 1 << 5, - - /// - /// 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, - - /// - /// This message is a voice message - /// - IsVoiceMessage = 1 << 13, - - /// - /// This message has a snapshot (via Message Forwarding) - /// - HasSnapshot = 1 << 14, - - /// - /// 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 deleted file mode 100644 index 4d8cd1a6c0..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageInteraction.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 5337b5214d..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs +++ /dev/null @@ -1,52 +0,0 @@ -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/DiscordMessageReferenceType.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageReferenceType.cs deleted file mode 100644 index 10f3da96d7..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageReferenceType.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of a message reference. -/// -public enum DiscordMessageReferenceType : int -{ - /// - /// A standard reference used by replies. - /// - Default = 0, - - /// - /// Reference used to point to a message at a point in time. - /// - Forward = 1, -} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageSnapshot.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageSnapshot.cs deleted file mode 100644 index 9af14960c9..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageSnapshot.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord message snapshot. -/// -public sealed class DiscordMessageSnapshot -{ - /// - /// Gets the message object for the message snapshot. - /// - [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageSnapshotContent Message { get; set; } -} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageSnapshotContent.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageSnapshotContent.cs deleted file mode 100644 index db4bfa4b8e..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageSnapshotContent.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord text message snapshot. -/// -public class DiscordMessageSnapshotContent -{ - internal DiscordMessageSnapshotContent() - { - - } - - internal DiscordMessageSnapshotContent(DiscordMessageSnapshotContent other) - : this() - { - 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.stickers = new List(other.stickers); - - this.Content = other.Content; - this.EditedTimestamp = other.EditedTimestamp; - this.MessageType = other.MessageType; - this.Timestamp = other.Timestamp; - this.Components = other.Components; - } - - /// - /// 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 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 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 the type of the message. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageType? MessageType { get; internal set; } - - /// - /// Gets the bitwise flags for this message. - /// - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageFlags? Flags { get; internal set; } - - /// - /// Gets stickers for this message. - /// - [JsonIgnore] - public IReadOnlyList? Stickers - => this.stickers; - - [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] - internal List stickers = []; - - internal ulong? guildId { get; set; } - - private IMention[] GetMentions() - { - List 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() - { - 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) - { - mentionedUsers.Add(usr); - } - - // Replace 'this.mentionedUsers'. - this.mentionedUsers = [.. mentionedUsers]; - - if (!string.IsNullOrWhiteSpace(this.Content)) - { - this.mentionedChannels = this.mentionedChannels.Union(Utilities.GetChannelMentions(this.Content).Select(x => new DiscordChannel() { Id = x })).ToList(); - this.mentionedRoles = this.mentionedRoles.Union(this.mentionedRoleIds.Select(x => new DiscordRole() { Id = x })).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; - } - - /// - /// Returns a string representation of this message. - /// - /// String representation of this message. - public override string ToString() => $"Message Snapshot; Attachment count: {this.attachments.Count}; Embed count: {this.embeds.Count}; Contents: {this.Content}"; -} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs deleted file mode 100644 index 948f6bc7a2..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs +++ /dev/null @@ -1,141 +0,0 @@ -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 deleted file mode 100644 index c429398a3d..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageStickerPack.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index bfee09dd59..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageType.cs +++ /dev/null @@ -1,142 +0,0 @@ -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, - GuildIncidentAlertModeEnabled = 36, - GuildIncidentAlertModeDisabled = 37, - GuildIncidentReportRaid = 38, - GuildIncidentReportFalseAlarm = 39, - PurchaseNotification = 44, - PollResult = 46 -} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs b/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs deleted file mode 100644 index 78ab21733d..0000000000 --- a/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index cd2029fe8f..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbed.cs +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index bbad4a9759..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedAuthor.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 5ac0d137b2..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedBuilder.cs +++ /dev/null @@ -1,606 +0,0 @@ -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 = 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 = this.fields.ToList(); // 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 deleted file mode 100644 index 3f5fda88e6..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedField.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 33aab4823e..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedFooter.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 837abcbb86..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedImage.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 89de526e43..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 923ffcc27e..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedThumbnail.cs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 8caaf05ef4..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedVideo.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 0736c52033..0000000000 --- a/DSharpPlus/Entities/Channel/Message/MentionType.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 4464571880..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Mentions.cs +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index aca8d5e595..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPoll.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 862ce12d8c..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswer.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 361ea039a1..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswerCount.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 5c14a45a98..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollBuilder.cs +++ /dev/null @@ -1,115 +0,0 @@ -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/DiscordPollCompletionMessage.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollCompletionMessage.cs deleted file mode 100644 index 77b0c16122..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollCompletionMessage.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Diagnostics; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class DiscordPollCompletionMessage : DiscordMessage -{ - internal DiscordPollCompletionMessage(DiscordMessage other) : base(other) - { - } - - internal static DiscordPollCompletionMessage? Parse(DiscordMessage message) - { - Debug.Assert(message.MessageType == DiscordMessageType.PollResult); - - if (message.Embeds is not [DiscordEmbed embed]) - { - message.Discord.Logger.LogInformation( - "Received a poll completion message without an embed. This is likely due to missing the MessageContent intent."); - return null; - } - - DiscordPollCompletionMessage result = new(message); - - if (embed.Type != "poll_result" || embed.Fields.Count == 0) - { - throw new ArgumentException( - "The provided poll completion message's embed was malformed and does not represent poll results."); - } - - ulong winningAnswerEmojiId = 0; - string? winningAnswerEmojiName = null; - bool winningAnswerEmojiIsAnimated = false; - - // https://discord.com/developers/docs/resources/message#embed-fields-by-embed-type-poll-result-embed-fields - // i asked the devil whether they understood what the thought process here was, they said no. - foreach (DiscordEmbedField field in embed.Fields) - { - switch (field.Name) - { - case "poll_question_text": - result.PollQuestionText = field.Value!; - break; - - case "victor_answer_votes": - result.WinningAnswerVoteCount = int.Parse(field.Value!); - break; - - case "total_votes": - result.TotalVotes = int.Parse(field.Value!); - break; - - case "victor_answer_id": - result.WinningAnswerId = int.Parse(field.Value!); - break; - - case "victor_answer_text": - result.WinningAnswerText = field.Value!; - break; - - case "victor_answer_emoji_id": - winningAnswerEmojiId = ulong.Parse(field.Value!); - break; - - case "victor_answer_emoji_name": - winningAnswerEmojiName = field.Value!; - break; - - case "victor_answer_emoji_animated": - winningAnswerEmojiIsAnimated = bool.TryParse(field.Value!, out bool animated) && animated; - break; - - default: - continue; - } - } - - if (winningAnswerEmojiId != 0) - { - result.WinningAnswerEmoji = new DiscordEmoji - { - Id = winningAnswerEmojiId, - Name = winningAnswerEmojiName ?? "", - IsAnimated = winningAnswerEmojiIsAnimated, - IsManaged = false, - Discord = result.Discord - }; - } - else if (winningAnswerEmojiName is not null) - { - result.WinningAnswerEmoji = DiscordEmoji.FromUnicode(result.Discord, winningAnswerEmojiName); - } - else - { - result.WinningAnswerEmoji = null; - } - - return result; - } - - /// - /// The text of the original poll question. - /// - [JsonIgnore] - public string PollQuestionText { get; private set; } - - /// - /// Indicates whether the poll ended in a draw - /// - [JsonIgnore] - public bool IsDraw => this.WinningAnswerId is null; - - /// - /// The amount of votes cast for the winning answer. - /// - [JsonIgnore] - public int WinningAnswerVoteCount { get; private set; } - - /// - /// The amounts of votes cast in total, across all answers. - /// - [JsonIgnore] - public int TotalVotes { get; private set; } - - /// - /// The of the winning answer. - /// - [JsonIgnore] - public int? WinningAnswerId { get; private set; } - - /// - /// The text of the winning answer. - /// - [JsonIgnore] - public string? WinningAnswerText { get; private set; } - - /// - /// The emoji associated with the winning answer. - /// - [JsonIgnore] - public DiscordEmoji? WinningAnswerEmoji { get; private set; } - - /// - /// The original poll message. - /// - [JsonIgnore] - public DiscordMessage? PollMessage => this.Reference!.Message; - - /// - /// A message link pointing to the poll message. - /// - [JsonIgnore] - public string PollMessageLink - => $"https://discord.com/channels/{this.guildId}/{this.ChannelId}/{this.Reference!.Message.Id}"; -} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs deleted file mode 100644 index b2f3239ae5..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 25409908ca..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollMedia.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 412b3d1c38..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index a89faff476..0000000000 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollVoteUpdate.cs +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index a1666229db..0000000000 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index 637e6a650c..0000000000 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs +++ /dev/null @@ -1,131 +0,0 @@ -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 deleted file mode 100644 index f1697e0242..0000000000 --- a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 1b3e0cd3a0..0000000000 --- a/DSharpPlus/Entities/Channel/Stage/DiscordStagePrivacyLevel.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 48231e71d8..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs +++ /dev/null @@ -1,196 +0,0 @@ -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 deleted file mode 100644 index 513c9a0930..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index e941038723..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMetadata.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 45bf698b67..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DefaultReaction.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index f09dd2671e..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DefaultSortOrder.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index fd76793787..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordDefaultForumLayout.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 3ca237a106..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs +++ /dev/null @@ -1,64 +0,0 @@ -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.availableTagsInternal; - - // Justification: Used by JSON.NET - [JsonProperty("available_tags")] - internal List availableTagsInternal; - - /// - /// 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 deleted file mode 100644 index cd303623bf..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumPostStarter.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index f819659bbd..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumTag.cs +++ /dev/null @@ -1,112 +0,0 @@ -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 deleted file mode 100644 index 3ac62d9bef..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/Forum/ForumPostBuilder.cs +++ /dev/null @@ -1,128 +0,0 @@ -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 deleted file mode 100644 index 146920a726..0000000000 --- a/DSharpPlus/Entities/Channel/Thread/ThreadQueryResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index f594e743a8..0000000000 --- a/DSharpPlus/Entities/Color/DiscordColor.Colors.cs +++ /dev/null @@ -1,252 +0,0 @@ -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 deleted file mode 100644 index 9af02e71c9..0000000000 --- a/DSharpPlus/Entities/Color/DiscordColor.cs +++ /dev/null @@ -1,121 +0,0 @@ -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 deleted file mode 100644 index c71b947e1a..0000000000 --- a/DSharpPlus/Entities/DiscordConnection.cs +++ /dev/null @@ -1,72 +0,0 @@ -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/DiscordFile.cs b/DSharpPlus/Entities/DiscordFile.cs deleted file mode 100644 index 1d3a0d2902..0000000000 --- a/DSharpPlus/Entities/DiscordFile.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.IO; - -namespace DSharpPlus.Entities; - -/// -/// Represents a file to be sent to Discord. -/// -public record struct DiscordFile -{ - public DiscordFile - ( - 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/DiscordPermissionLevel.cs b/DSharpPlus/Entities/DiscordPermissionLevel.cs deleted file mode 100644 index f541fc1674..0000000000 --- a/DSharpPlus/Entities/DiscordPermissionLevel.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace DSharpPlus.Entities; - -/// -/// Specifies whether a permission in an overwrite is allowed, denied or unset. -/// -public enum DiscordPermissionLevel -{ - /// - /// The specified permission is allowed. This supersedes all other overwrites. - /// - Allowed, - - /// - /// The specified permission is denied. - /// - Denied, - - /// - /// The specified permission is unset and falls back to role permissions. - /// - Unset -} diff --git a/DSharpPlus/Entities/DiscordTeam.cs b/DSharpPlus/Entities/DiscordTeam.cs deleted file mode 100644 index cca8d38047..0000000000 --- a/DSharpPlus/Entities/DiscordTeam.cs +++ /dev/null @@ -1,164 +0,0 @@ -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 deleted file mode 100644 index 9c60e4e8ae..0000000000 --- a/DSharpPlus/Entities/DiscordUri.cs +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index fde1ce9b84..0000000000 --- a/DSharpPlus/Entities/Emoji/DiscordEmoji.EmojiUtils.cs +++ /dev/null @@ -1,11716 +0,0 @@ -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 deleted file mode 100644 index 098c6af84f..0000000000 --- a/DSharpPlus/Entities/Emoji/DiscordEmoji.cs +++ /dev/null @@ -1,366 +0,0 @@ -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 deleted file mode 100644 index 59e0b44fc9..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordBan.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 8be52a933e..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordBulkBan.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 238df2a96e..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuild.cs +++ /dev/null @@ -1,2772 +0,0 @@ -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.Abstractions.Rest; -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; internal set; } - - /// - /// Gets the guild's voice region. - /// - public async ValueTask GetVoiceRegionAsync() - { - if (this.Discord.VoiceRegions.TryGetValue(this.VoiceRegionId, out DiscordVoiceRegion? currentRegion)) - { - return currentRegion; - } - - IReadOnlyList regions = await this.Discord.ApiClient.ListVoiceRegionsAsync(); - foreach (DiscordVoiceRegion region in regions) - { - this.Discord.InternalVoiceRegions.TryAdd(region.Id, region); - } - - return this.Discord.InternalVoiceRegions[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, skipCache); - } - - /// - /// 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 apiRoles = await this.Discord.ApiClient.GetGuildRolesAsync(this.Id); - this.roles = new ConcurrentDictionary(apiRoles.ToDictionary(x => x.Id)); - return apiRoles; - } - /// - /// Gets the member counts of all roles except the @everyone role in this guild. - /// - /// Role ids and their corresponding member counts. - public async Task> GetRoleMemberCountsAsync() - => await this.Discord.ApiClient.GetGuildRoleMemberCountsAsync(this.Id); - - - /// - /// 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 DiscordRolePosition() { 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 ConcurrentDictionary(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 is null) - { - return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days); - } - - IEnumerable rawRoleIds = includedRoles - .Where(r => this.roles.ContainsKey(r.Id)) - .Select(x => x.Id); - - return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); - - } - - /// - /// 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 members = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : last); - recievedLastCall = members.Count; - - foreach (DiscordMember member in members) - { - this.Discord.UpdateUserCache(member.User); - - yield return member; - } - - DiscordMember? lastMember = members.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 (GuildMembersChunkedEventArgs 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 DiscordFile(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 deleted file mode 100644 index ddaaa32504..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildEmbed.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index a88254649e..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildEmoji.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 18bd0ab326..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreening.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 431c87292d..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreeningField.cs +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 4c82dc3074..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildPreview.cs +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 83b8565ca6..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildTemplate.cs +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index fdace0e6e7..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreen.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 6ecfce1c53..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreenChannel.cs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 71f13841bb..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordMember.cs +++ /dev/null @@ -1,651 +0,0 @@ -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 DiscordRoleColors Color - { - get - { - DiscordRole? role = this.Roles.OrderByDescending(xr => xr.Position).FirstOrDefault(xr => xr.Colors.PrimaryColor.Value != 0); - return role is not null ? role.Colors : new DiscordRoleColors(); - } - } - - /// - /// 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; - } - - /// - [JsonIgnore] - public override DiscordUserPrimaryGuild? PrimaryGuild - { - get => this.User.PrimaryGuild; - internal set => this.User.PrimaryGuild = 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)); - } - - bool hasCurrentMemberFields = mdl.Nickname.HasValue || mdl.Banner.HasValue || mdl.Avatar.HasValue || mdl.Bio.HasValue; - if (hasCurrentMemberFields && this.Discord.CurrentUser.Id == this.Id) - { - Optional avatarBase64 = Utilities.ConvertStreamToBase64(mdl.Avatar); - Optional bannerBase64 = Utilities.ConvertStreamToBase64(mdl.Banner); - - await this.Discord.ApiClient.ModifyCurrentMemberAsync(this.Guild.Id, mdl.Nickname, - bannerBase64, avatarBase64, mdl.Bio, 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/DiscordMemberFlags.cs b/DSharpPlus/Entities/Guild/DiscordMemberFlags.cs deleted file mode 100644 index b59b70e34d..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordMemberFlags.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; - -namespace DSharpPlus.Entities; - -/// -/// Represents additional details of a guild member. -/// -[Flags] -public enum DiscordMemberFlags -{ - /// - /// Member has left and rejoined the guild - /// - DidRejoin = 1 << 0, - - /// - /// Member has completed onboarding - /// - CompletedOnboarding = 1 << 1, - - /// - /// Member is exempt from guild verification requirements. Allows a member who does not meet verification requirements to participate in a server. - /// - BypassesVerification = 1 << 2, - - /// - /// Member has started onboarding - /// - StartedOnboarding = 1 << 3, - - /// - /// Member is a guest and can only access the voice channel they were invited to - /// - IsGuest = 1 << 4, - - /// - /// Member has started Server Guide new member actions - /// - StartedHomeActions = 1 << 5, - - /// - /// Member has completed Server Guide new member actions - /// - CompletedHomeActions = 1 << 6, - - /// - /// Member's username, display name, or nickname is blocked by AutoMod - /// - AutomodQuarantinedUsername = 1 << 7, - - /// - /// Member has dismissed the DM settings upsell - /// - DmSettingsUpsellAcknowledged = 1 << 9, - - /// - /// Member's guild tag is blocked by AutoMod - /// - AutomodQuarantinedGuildTag = 1 << 10, -} diff --git a/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs b/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs deleted file mode 100644 index 3927b0bdb7..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 49a4134b42..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordNsfwLevel.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 43c038b317..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordPremiumTier.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index e5bbf58354..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordRole.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Net.Abstractions.Rest; -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. - /// - [JsonProperty("colors", NullValueHandling = NullValueHandling.Ignore)] - public DiscordRoleColors Colors { get; internal set; } - - /// - /// 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; } - - /// - /// Gets the flags this role has. - /// - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordRoleFlags Flags { 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)]; - DiscordRolePosition[] pmds = new DiscordRolePosition[roles.Length]; - for (int i = 0; i < roles.Length; i++) - { - pmds[i] = new DiscordRolePosition - { - 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/DiscordRoleColors.cs b/DSharpPlus/Entities/Guild/DiscordRoleColors.cs deleted file mode 100644 index 1df6c142de..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordRoleColors.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the colors of a role with the new role color system. -/// -public class DiscordRoleColors -{ - /// - /// Gets the primary color of this role. - /// - [JsonIgnore] - public DiscordColor PrimaryColor - => new(this.primaryColor); - - [JsonProperty("primary_color", NullValueHandling = NullValueHandling.Ignore)] - internal int primaryColor; - - /// - /// Gets the secondary color of this role. - /// - [JsonIgnore] - public DiscordColor SecondaryColor - => new(this.secondaryColor); - - [JsonProperty("secondary_color", NullValueHandling = NullValueHandling.Ignore)] - internal int secondaryColor; - - /// - /// Gets the tertiary color of this role. This is only set when the role has the holographic style. - /// And the values will be predefined by Discord. - /// - [JsonIgnore] - public DiscordColor TertiaryColor - => new(this.tertiaryColor); - - [JsonProperty("tertiary_color", NullValueHandling = NullValueHandling.Ignore)] - internal int tertiaryColor; -} diff --git a/DSharpPlus/Entities/Guild/DiscordRoleFlags.cs b/DSharpPlus/Entities/Guild/DiscordRoleFlags.cs deleted file mode 100644 index 449218294b..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordRoleFlags.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace DSharpPlus.Entities; - -/// -/// Flags describing a role's properties. -/// -[Flags] -public enum DiscordRoleFlags -{ - /// - /// No flags are set. - /// - None = 0, - - /// - /// Role can be selected by members in an onboarding prompt. - /// - InPrompt = 1 << 0, -} diff --git a/DSharpPlus/Entities/Guild/DiscordRoleTags.cs b/DSharpPlus/Entities/Guild/DiscordRoleTags.cs deleted file mode 100644 index 9a082a3e44..0000000000 --- a/DSharpPlus/Entities/Guild/DiscordRoleTags.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 179a0432c6..0000000000 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs +++ /dev/null @@ -1,114 +0,0 @@ -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 deleted file mode 100644 index 8091266cae..0000000000 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventMetadata.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 9bcb1d4506..0000000000 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventPrivacyLevel.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 1f7b80b60f..0000000000 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventStatus.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 7851d867d3..0000000000 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventType.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 7545077d8c..0000000000 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index c40dfa3935..0000000000 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetMember.cs +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 6bab10bcd8..0000000000 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index 22d850258d..0000000000 --- a/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationType.cs +++ /dev/null @@ -1,18 +0,0 @@ -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/DiscordApplicationIntegrationTypeConfiguration.cs b/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationTypeConfiguration.cs deleted file mode 100644 index ca0a7d806c..0000000000 --- a/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationTypeConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the configuration for an integration type. -/// -public sealed class DiscordApplicationIntegrationTypeConfiguration -{ - /// - /// The install parameters for the integration. - /// - [JsonProperty("oauth2_install_params")] - public DiscordApplicationOAuth2InstallParams OAuth2InstallParams { get; internal set; } = default!; - - public DiscordApplicationIntegrationTypeConfiguration() { } -} diff --git a/DSharpPlus/Entities/Integration/DiscordIntegration.cs b/DSharpPlus/Entities/Integration/DiscordIntegration.cs deleted file mode 100644 index 86106fa81d..0000000000 --- a/DSharpPlus/Entities/Integration/DiscordIntegration.cs +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 84f7943450..0000000000 --- a/DSharpPlus/Entities/Integration/DiscordIntegrationAccount.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 54429cf8d0..0000000000 --- a/DSharpPlus/Entities/Integration/DiscordInteractionContextType.cs +++ /dev/null @@ -1,23 +0,0 @@ -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.WeakEquals.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.WeakEquals.cs deleted file mode 100644 index e5fb7d869f..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.WeakEquals.cs +++ /dev/null @@ -1,301 +0,0 @@ -#pragma warning disable IDE0040 // this shouldn't even be reported on a partial like this - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace DSharpPlus.Entities; - -partial class DiscordApplicationCommand -{ - /// - /// Implements a weak-equality check for application commands, treating null and default values the same - /// and only comparing fields known locally. - /// - public bool WeakEquals(DiscordApplicationCommand other) - { - return this.Name == other.Name - && this.Description == other.Description - && (this.DefaultMemberPermissions ?? DiscordPermissions.None) - == (other.DefaultMemberPermissions ?? DiscordPermissions.None) - && (this.AllowDMUsage ?? true) == (other.AllowDMUsage ?? true) - && (this.NSFW ?? false) == (other.NSFW ?? false) - && this.Type == other.Type - && IntegrationTypesMatch(this.IntegrationTypes, other.IntegrationTypes) - && EnumListsMatch(this.Contexts, other.Contexts) - && LocalizationsMatch(this.NameLocalizations, other.NameLocalizations) - && LocalizationsMatch(this.DescriptionLocalizations, other.DescriptionLocalizations) - && OptionsMatch(this.Options, other.Options); - } - - private static bool LocalizationsMatch - ( - IReadOnlyDictionary a, - IReadOnlyDictionary b - ) - { - // if both are null or empty, they are equivalent - if (a is null or { Count: 0 } && b is null or { Count: 0 }) - { - return true; - } - - // if one of the two is null, but not both (as per above), they are not equivalent - if (a is null || b is null) - { - return false; - } - - // they are both not-null, so let's go check - // if they aren't evenly long, they by necessity cannot be equivalent - if (a.Count != b.Count) - { - return false; - } - - foreach (KeyValuePair val in a) - { - if (b.TryGetValue(val.Key, out string? remoteValue)) - { - // this looks really stupid, and that's because it is. discord sends non-ASCII escaped, - // and this is one way to unescape it - since this only happens at startup, the overhead - // isn't catastrophic, albeit annoying to have to do regardless. - // - // for example, "Wählen Sie Ihren Gameserver" might be sent by discord as - // "W\u00e4hlen Sie Ihren Gameserver", which fails direct value equality since "\\u00e4" is - // not the same as "ä", even though the escape code matches. - byte[] temp = Encoding.UTF8.GetBytes(remoteValue); - string canonicalizedRemoteValue = Encoding.UTF8.GetString(temp); - - if (val.Value != canonicalizedRemoteValue) - { - return false; - } - } - else - { - return false; - } - } - - return true; - } - - private static bool OptionsMatch - ( - IReadOnlyList? a, - IReadOnlyList? b - ) - { - // if both are null or empty, they are equivalent - if (a is null or { Count: 0 } && b is null or { Count: 0 }) - { - return true; - } - - // if one of the two is null, but not both (as per above), they are not equivalent - if (a is null || b is null) - { - return false; - } - - // they are both not-null, so let's go check - // if they aren't evenly long, they by necessity cannot be equivalent - if (a.Count != b.Count) - { - return false; - } - - foreach (DiscordApplicationCommandOption option in a) - { - DiscordApplicationCommandOption other; - - if ((other = b.SingleOrDefault(x => x.Name == option.Name)) is not null) - { - // we have a remote record, check whether its surface matches - bool match = option.Description == other.Description - && option.Type == other.Type - && (option.AutoComplete ?? false) == (other.AutoComplete ?? false) - && (option.MaxLength ?? 4000) == (other.MaxLength ?? 4000) - && (option.MinLength ?? 0) == (other.MinLength ?? 0) - && (option.Required ?? false) == (other.Required ?? false) - && BoxedIntegersMatch(option.MinValue, other.MinValue) - && BoxedIntegersMatch(option.MaxValue, other.MaxValue) - && EnumListsMatch(option.ChannelTypes, other.ChannelTypes) - && ChoicesMatch(option.Choices, other.Choices) - && LocalizationsMatch(option.NameLocalizations, other.NameLocalizations) - && LocalizationsMatch(option.DescriptionLocalizations, other.DescriptionLocalizations) - && OptionsMatch(option.Options, other.Options); - - if (!match) - { - return false; - } - } - else - { - return false; - } - } - - return true; - } - - private static bool ChoicesMatch - ( - IReadOnlyList? a, - IReadOnlyList? b - ) - { - if (a is null or { Count: 0 } && b is null or { Count: 0 }) - { - return true; - } - - if (a is null || b is null) - { - return false; - } - - if (a.Count != b.Count) - { - return false; - } - - foreach (DiscordApplicationCommandOptionChoice choice in a) - { - if (!b.Any(c => - { - return c.Name == choice.Name - && ChoiceValuesMatch(c, choice) - && LocalizationsMatch(c.NameLocalizations, choice.NameLocalizations); - }) - ) - { - return false; - } - } - - return true; - } - - private static bool ChoiceValuesMatch(object a, object b) - { - return a switch - { - int value => int.TryParse(b.ToString(), out int other) && value == other, - long value => long.TryParse(b.ToString(), out long other) && value == other, - double value => double.TryParse(b.ToString(), out double other) && value == other, - string value => b is string other && value == other, - DiscordApplicationCommandOptionChoice value => b is DiscordApplicationCommandOptionChoice other && ChoiceValuesMatch(value.Value, other.Value), - _ => false - }; - } - - private static bool IntegrationTypesMatch - ( - IReadOnlyList? a, - IReadOnlyList? b - ) - { - if - ( - a is null or { Count: 0 } or [DiscordApplicationIntegrationType.GuildInstall] - && b is null or { Count: 0 } or [DiscordApplicationIntegrationType.GuildInstall] - ) - { - return true; - } - - if (a is null || b is null) - { - return false; - } - - if (a.Count != b.Count) - { - return false; - } - - foreach (DiscordApplicationIntegrationType type in a) - { - if (!b.Contains(type)) - { - return false; - } - } - - return true; - } - - private static bool EnumListsMatch - ( - IReadOnlyList? a, - IReadOnlyList? b - ) - where TEnum : Enum - { - if (a is null or { Count: 0 } && b is null or { Count: 0 }) - { - return true; - } - - if (a is null || b is null) - { - return false; - } - - if (a.Count != b.Count) - { - return false; - } - - foreach (TEnum type in a) - { - if (!b.Contains(type)) - { - return false; - } - } - - return true; - } - - private static bool BoxedIntegersMatch(object? a, object? b) - { - return a switch - { - sbyte value => b is sbyte other && value == other, - byte value => b is byte other && value == other, - short value => b is short other && value == other, - ushort value => b is ushort other && value == other, - int value => b is int other && value == other, - uint value => b is uint other && value == other, - long value => b is long other && value == other, - ulong value => b is ulong other && value == other, - float value => b is float other && value == other, - double value => b is double other && value == other, - null => b is null || IsZero(b), - _ => false - }; - } - - private static bool IsZero(object boxed) - { - return boxed switch - { - sbyte value when value is 0 => true, - byte value when value is 0 => true, - short value when value is 0 => true, - ushort value when value is 0 => true, - int value when value is 0 => true, - uint value when value is 0 => true, - long value when value is 0 => true, - ulong value when value is 0 => true, - float value when value is 0.0f => true, - double value when value is 0.0 => true, - _ => false - }; - } -} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs deleted file mode 100644 index 9d505df153..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs +++ /dev/null @@ -1,227 +0,0 @@ -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 - ) - { - List? optionsList = options?.ToList(); - - 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 (optionsList?.Any() ?? false) - { - throw new ArgumentException("Context menus do not support options."); - } - } - - 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 deleted file mode 100644 index 0d97029f44..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOption.cs +++ /dev/null @@ -1,156 +0,0 @@ -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."); - } - - IReadOnlyList? choiceList = choices != null ? choices.ToList() : null; - IReadOnlyList? optionList = options != null ? options.ToList() : null; - IReadOnlyList? channelTypeList = channelTypes != null ? 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 deleted file mode 100644 index 000873c849..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionChoice.cs +++ /dev/null @@ -1,133 +0,0 @@ -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 deleted file mode 100644 index b060ac518e..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionType.cs +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index 847f8464ae..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandPermissionType.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 6b5bfe813d..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandType.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index f0bf2877c1..0000000000 --- a/DSharpPlus/Entities/Interaction/Application/DiscordAutoCompleteChoice.cs +++ /dev/null @@ -1,69 +0,0 @@ -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 deleted file mode 100644 index b75a410966..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordActionRowComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index ef4b6ea1d1..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordButtonComponent.cs +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index d9e140e763..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordButtonStyle.cs +++ /dev/null @@ -1,28 +0,0 @@ -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/DiscordCheckboxComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxComponent.cs deleted file mode 100644 index 6b24655629..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxComponent.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a single checkbox for a binary choice. Only available in modals. -/// -/// -/// This component cannot be marked as required. To create a checkbox that must be checked for submitting a modal, use a -/// with one option and the minValues parameter set to 1. -/// -public class DiscordCheckboxComponent : DiscordComponent -{ - /// - /// Indicates whether this checkbox is selected by default. Defaults to false. - /// - [JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)] - public bool? SelectedByDefault { get; internal set; } - - // this is there for the modal submitted event because our entity modeling is too abysmal to understand that submitted components - // are, actually, not the same type as sent components - [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] - internal bool? Value { get; private set; } - - /// - /// Creates a new checkbox component. - /// - /// The custom ID for this component. - /// Indicates whether this component is selected by default. - public DiscordCheckboxComponent(string customId, bool? selectedByDefault = null) : this() - { - this.CustomId = customId; - this.SelectedByDefault = selectedByDefault; - } - - internal DiscordCheckboxComponent() - => this.Type = DiscordComponentType.Checkbox; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxGroupComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxGroupComponent.cs deleted file mode 100644 index 569503fe95..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxGroupComponent.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a group of up to ten multi-select checkboxes. Available in modals. -/// -public class DiscordCheckboxGroupComponent : DiscordComponent -{ - /// - /// The checkboxes within this group that can be selected from. - /// - [JsonProperty("options")] - public IReadOnlyList Options { get; internal set; } - - /// - /// The minimum amount of checkboxes that must be checked within this group. - /// - [JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)] - public int? MinValues { get; internal set; } - - /// - /// The maximum amount of checkboxes that may be checked within this group. - /// - [JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)] - public int? MaxValues { get; internal set; } - - /// - /// Indicates whether checking at least one box is required to submit the modal. - /// - [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsRequired { get; internal set; } - - // for the modal submit event - [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)] - internal IReadOnlyList? Values { get; private set; } - - /// - /// Creates a new checkbox group. - /// - /// The custom ID of this component. - /// The checkboxes to include within this group, up to 10. - /// The minimum amount of checkboxes that must be checked within this group, 0-10. - /// The maximum amount of checkboxes that may be checked within this group, 1-10. - /// Indicates whether responding to this component is required. - public DiscordCheckboxGroupComponent - ( - string customId, - IReadOnlyList options, - int? minValues = null, - int? maxValues = null, - bool? required = null - ) - : this() - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Count, 10, "options.Count"); - - ArgumentOutOfRangeException.ThrowIfGreaterThan(minValues ?? 0, 10, nameof(minValues)); - ArgumentOutOfRangeException.ThrowIfLessThan(minValues ?? 0, 0, nameof(minValues)); - - ArgumentOutOfRangeException.ThrowIfGreaterThan(maxValues ?? 1, 10, nameof(maxValues)); - ArgumentOutOfRangeException.ThrowIfLessThan(maxValues ?? 1, 1, nameof(maxValues)); - - ArgumentOutOfRangeException.ThrowIfGreaterThan(minValues ?? 0, maxValues ?? 10); - - if (required ?? false) - { - ArgumentOutOfRangeException.ThrowIfEqual(minValues ?? 1, 0); - } - - this.CustomId = customId; - this.Options = options; - this.MinValues = minValues; - this.MaxValues = maxValues; - this.IsRequired = required ?? minValues != 0; - } - - internal DiscordCheckboxGroupComponent() - => this.Type = DiscordComponentType.CheckboxGroup; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxGroupOption.cs b/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxGroupOption.cs deleted file mode 100644 index 6dcaad85a3..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordCheckboxGroupOption.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a single checkbox within a . -/// -public class DiscordCheckboxGroupOption -{ - /// - /// The developer-defined value that will be returned to the bot when the modal is submitted. - /// - [JsonProperty("value")] - public string Value { get; internal set; } - - /// - /// The user-facing label of the checkbox. Maximum 100 characters. - /// - [JsonProperty("label")] - public string Label { get; internal set; } - - /// - /// An optional user-facing description of this checkbox. Maximum 100 characters. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string? Description { get; internal set; } - - /// - /// Indicates whether this option is selected by default. - /// - [JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)] - public bool? SelectedByDefault { get; internal set; } - - /// - /// Creates a new checkbox for a checkbox group. - /// - /// The developer-defined value that will be returned to the bot when the modal is submitted. - /// The user-facing label of the checkbox, max 100 characters. - /// The user-facing description of the checkbox, max 100 characters. - /// Indicates whether this checkbox is selected by default. - public DiscordCheckboxGroupOption(string value, string label, string? description = null, bool? selectedByDefault = null) - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, 100, "value.Length"); - ArgumentOutOfRangeException.ThrowIfGreaterThan(label.Length, 100, "label.Length"); - ArgumentOutOfRangeException.ThrowIfGreaterThan(description?.Length ?? 0, 100, "description.Length"); - - this.Value = value; - this.Label = label; - this.Description = description; - this.SelectedByDefault = selectedByDefault; - } -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs deleted file mode 100644 index b44fdbcd76..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 7d2acf73ac..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordComponentType.cs +++ /dev/null @@ -1,108 +0,0 @@ -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, - - /// - /// A text input field in a modal. - /// - TextInput = 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, - - /// - /// A label component containing a title, component, and optionally description. Only used in Modals. - /// - Label = 18, - - /// - /// A component for uploading files to a bot. Only used in modals. - /// - FileUpload = 19, - - /// - /// A component containing a single-choice group of up to ten options. - /// - RadioGroup = 21, - - /// - /// A component containing a multiple-choice group of checkboxes. - /// - CheckboxGroup = 22, - - /// - /// A single checkbox component. - /// - Checkbox = 23 -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordContainerComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordContainerComponent.cs deleted file mode 100644 index 293489824d..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordContainerComponent.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A container for other components. -/// -public class DiscordContainerComponent : DiscordComponent -{ - - /// - /// The accent color for this container, similar to an embed. - /// - public DiscordColor? Color - { - get - { - return this.color.IsDefined(out int? colorValue) - ? (DiscordColor)colorValue - : null; - } - } - - [JsonProperty("accent_color", NullValueHandling = NullValueHandling.Include)] - internal Optional color; - - /// - /// Gets whether this container is spoilered. - /// - [JsonProperty("spoiler", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSpoilered { get; internal set; } - - /// - /// Gets the components of this container. - /// - /// - /// At this time, only the following components are allowed: - /// - /// - /// - /// - /// - /// - /// - /// - /// - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Components { get; internal set; } - - public DiscordContainerComponent - ( - IReadOnlyList components, - bool isSpoilered = false, - DiscordColor? color = null - ) - : this() - { - this.Components = components; - this.IsSpoilered = isSpoilered; - this.color = color?.Value; - - ThrowIfUnwrappedComponentsDetected(); - } - - internal DiscordContainerComponent() => this.Type = DiscordComponentType.Container; - - [StackTraceHidden] - [DebuggerStepThrough] - private void ThrowIfUnwrappedComponentsDetected() - { - for (int i = 0; i < this.Components.Count; i++) - { - DiscordComponent comp = this.Components[i]; - - if (comp is not (DiscordButtonComponent or DiscordSelectComponent)) - { - continue; - } - - string compType = comp is DiscordButtonComponent ? "Buttons" : "Selects"; - throw new ArgumentException($"{compType} must be wrapped in an action row. Index: {i}"); - } - } -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs deleted file mode 100644 index 013091b8cb..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs +++ /dev/null @@ -1,57 +0,0 @@ -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/DiscordFileComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordFileComponent.cs deleted file mode 100755 index 0281d68566..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordFileComponent.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a component that will display a single file. -/// -public sealed class DiscordFileComponent : DiscordComponent -{ - /// - /// Gets the file associated with this component. It may be an arbitrary URL or an attachment:// reference. - /// - [JsonProperty("file", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUnfurledMediaItem File { get; internal set; } - - /// - /// Gets whether this file is spoilered. - /// - [JsonProperty("spoiler")] - public bool IsSpoilered { get; internal set; } - - public DiscordFileComponent(string url, bool isSpoilered) - : this() - { - this.File = new(url); - this.IsSpoilered = isSpoilered; - } - - internal DiscordFileComponent() => this.Type = DiscordComponentType.File; - -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordFileUploadComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordFileUploadComponent.cs deleted file mode 100644 index bf989d351f..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordFileUploadComponent.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A component wherethrough users can upload files. Only used in modals. The maximum cumulative size of the files depends on the server's boost level and nitro. -/// -public sealed class DiscordFileUploadComponent : DiscordComponent -{ - /// - /// The minimum amount of files to upload, between 0 and 10. - /// - [JsonProperty("min_values", DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? MinValues { get; internal set; } - - /// - /// The maximum of files to upload, between 1 and 10. - /// - [JsonProperty("max_values", DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? MaxValues { get; internal set; } - - /// - /// Indicates whether a submission is required. This is mutually exclusive with setting to 0. - /// - [JsonProperty("required")] - public bool IsRequired { get; internal set; } - - /// - /// Internally used for handling responses. - /// - [JsonProperty("values")] - internal ulong[] Values { get; set; } - - internal DiscordFileUploadComponent() => this.Type = DiscordComponentType.FileUpload; - - public DiscordFileUploadComponent(string customId, int? minValues = null, int? maxValues = null, bool isRequired = true) - : this() - { - this.CustomId = customId; - this.MinValues = minValues; - this.MaxValues = maxValues; - this.IsRequired = isRequired; - } -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordLabelComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordLabelComponent.cs deleted file mode 100644 index 881e8e5f1c..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordLabelComponent.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a label in a modal. -/// -public class DiscordLabelComponent : DiscordComponent -{ - [JsonProperty("type")] - public DiscordComponentType ComponentType => DiscordComponentType.Label; - - /// - /// Gets or sets the label. - /// - /// This value is not returned by Discord, and will be null in a modal submit event. - [JsonProperty("label")] - public string? Label { get; set; } - - /// - /// Gets or sets the description of the label. - /// - [JsonProperty("description")] - public string? Description { get; set; } - - /// - /// Gets the component contained within the label. - /// - [JsonProperty("component")] - public DiscordComponent Component { get; internal set; } - - public DiscordLabelComponent - ( - DiscordComponent component, - string label = "", - string? description = null - ) - { - this.Component = component; - this.Label = label; - this.Description = description; - } - - internal DiscordLabelComponent() { } -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs deleted file mode 100644 index 15508c1bec..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs +++ /dev/null @@ -1,54 +0,0 @@ -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/DiscordMediaGalleryComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordMediaGalleryComponent.cs deleted file mode 100755 index 697f7511fd..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordMediaGalleryComponent.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a gallery of various media. -/// -public sealed class DiscordMediaGalleryComponent : DiscordComponent -{ - - /// - /// Gets the items in the gallery. - /// - [JsonProperty("items", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Items { get; internal set; } - - /// - public DiscordMediaGalleryComponent(params IEnumerable items) - : this(items, 0) - { - - } - - /// - /// Constructs a new media gallery component. - /// - /// The items of the gallery. - /// The optional ID of the component. - public DiscordMediaGalleryComponent(IEnumerable items, int id = 0) - : this() - { - this.Items = items.ToArray(); - this.Id = id; - } - - internal DiscordMediaGalleryComponent() => this.Type = DiscordComponentType.MediaGallery; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordMediaGalleryItem.cs b/DSharpPlus/Entities/Interaction/Components/DiscordMediaGalleryItem.cs deleted file mode 100755 index 6e6fdb10a6..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordMediaGalleryItem.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an item in a media gallery. -/// -public sealed class DiscordMediaGalleryItem -{ - /// - /// Gets the media item in the gallery. - /// - [JsonProperty("media", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUnfurledMediaItem Media { get; internal set; } - - /// - /// Gets the description (alt text) of the media item. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string? Description { get; internal set; } - - /// - /// Gets whether the media item is spoilered. - /// - [JsonProperty("spoiler")] - public bool? IsSpoilered { get; internal set; } - - /// - /// Constructs a new media gallery item. - /// - /// The URL of the media item. This must be a direct link to media, or an attachment:// reference. - /// The description (alt text) of the media item. - /// Whether the attachment is spoilered. - public DiscordMediaGalleryItem(string url, string? description = null, bool? isSpoilered = null) - { - this.Media = new(url); - this.Description = description; - this.IsSpoilered = isSpoilered; - } -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordRadioGroupComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordRadioGroupComponent.cs deleted file mode 100644 index f202a9ae13..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordRadioGroupComponent.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a group of two to ten options to choose from. Available in modals. -/// -public class DiscordRadioGroupComponent : DiscordComponent -{ - /// - /// The options within this group that can be selected from. - /// - [JsonProperty("options")] - public IReadOnlyList Options { get; internal set; } - - /// - /// Indicates whether checking at least one option is required to submit the modal. - /// - [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsRequired { get; internal set; } - - // for the modal submit event - [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] - internal string? Value { get; private set; } - - /// - /// Creates a new radio group. - /// - /// The custom ID of this component. - /// The options to include within this group, 2-10. - /// Indicates whether responding to this component is required. - public DiscordRadioGroupComponent - ( - string customId, - IReadOnlyList options, - - bool? required = null - ) - : this() - { - ArgumentOutOfRangeException.ThrowIfLessThan(options.Count, 2, "options.Count"); - ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Count, 10, "options.Count"); - ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Count(x => x.SelectedByDefault == true), 1, "options selected by default"); - - this.CustomId = customId; - this.Options = options; - this.IsRequired = required; - } - - internal DiscordRadioGroupComponent() - => this.Type = DiscordComponentType.RadioGroup; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordRadioGroupOption.cs b/DSharpPlus/Entities/Interaction/Components/DiscordRadioGroupOption.cs deleted file mode 100644 index b5532e4f38..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordRadioGroupOption.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a single option within a . -/// -public class DiscordRadioGroupOption -{ - /// - /// The developer-defined value that will be returned to the bot when the modal is submitted. - /// - [JsonProperty("value")] - public string Value { get; internal set; } - - /// - /// The user-facing label of the option. Maximum 100 characters. - /// - [JsonProperty("label")] - public string Label { get; internal set; } - - /// - /// An optional user-facing description of this option. Maximum 100 characters. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string? Description { get; internal set; } - - /// - /// Indicates whether this option is selected by default. - /// - [JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)] - public bool? SelectedByDefault { get; internal set; } - - /// - /// Creates a new option for a radio group. - /// - /// The developer-defined value that will be returned to the bot when the modal is submitted. - /// The user-facing label of the option, max 100 characters. - /// The user-facing description of the option, max 100 characters. - /// Indicates whether this option is selected by default. - public DiscordRadioGroupOption(string value, string label, string? description = null, bool? selectedByDefault = null) - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, 100, "value.Length"); - ArgumentOutOfRangeException.ThrowIfGreaterThan(label.Length, 100, "label.Length"); - ArgumentOutOfRangeException.ThrowIfGreaterThan(description?.Length ?? 0, 100, "description.Length"); - - this.Value = value; - this.Label = label; - this.Description = description; - this.SelectedByDefault = selectedByDefault; - } -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordSectionComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordSectionComponent.cs deleted file mode 100644 index b7c299d34b..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordSectionComponent.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; - -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A section for components (as of now, just text) and an accessory on the side. -/// -public class DiscordSectionComponent : DiscordComponent -{ - - /// - /// Gets the accessory for this section. - /// - /// - /// Accessories take the place of a thumbnail (that is, are positioned as a thumbnail would be) regardless of component. - /// At this time, only and are supported. - /// - [JsonProperty("accessory", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponent? Accessory { get; internal set; } - - /// - /// Gets the components for this section. - /// As of now, this is only text components, but may allow for more components in the future. - /// - /// This is a Discord limitation. - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Components { get; internal set; } - - /// - public DiscordSectionComponent(DiscordComponent textDisplayComponent, DiscordComponent accessory) - : this([textDisplayComponent], accessory) - { - } - - /// - /// Constructs a new - /// - /// The text for this section. - /// The accessory for this section. - public DiscordSectionComponent - ( - string text, - DiscordComponent accessory - ) : this(new DiscordTextDisplayComponent(text), accessory) - { - } - - /// - /// Constructs a new section component. - /// - /// The sections (generally text) that this section contains. - /// The accessory to this section. - /// At this time, this must either be a or a . - public DiscordSectionComponent(IReadOnlyList sections, DiscordComponent accessory) - : this() - { - this.Accessory = accessory; - this.Components = sections; - } - - internal DiscordSectionComponent() => this.Type = DiscordComponentType.Section; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs b/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs deleted file mode 100644 index ed09237ea7..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DSharpPlus; - - -/// -/// Type of a default value for a select component. -/// -public enum DiscordSelectDefaultValueType -{ - User, - Role, - Channel -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordSeparatorComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordSeparatorComponent.cs deleted file mode 100644 index ffa14d3f35..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordSeparatorComponent.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a division between components. Can optionally be rendered as a dividing line. -/// -public class DiscordSeparatorComponent : DiscordComponent -{ - /// - /// Whether the separator renders as a dividing line. - /// - [JsonProperty("divider", NullValueHandling = NullValueHandling.Ignore)] - public bool Divider { get; internal set; } - - /// - /// The spacing for the separator. Defaults to - /// - [JsonProperty("spacing", NullValueHandling = NullValueHandling.Ignore)] - public DiscordSeparatorSpacing Spacing { get; internal set; } - - public DiscordSeparatorComponent(bool divider = false, DiscordSeparatorSpacing spacing = DiscordSeparatorSpacing.Small) - : this() - { - this.Divider = divider; - this.Spacing = spacing; - } - - internal DiscordSeparatorComponent() => this.Type = DiscordComponentType.Separator; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordSeparatorSpacing.cs b/DSharpPlus/Entities/Interaction/Components/DiscordSeparatorSpacing.cs deleted file mode 100644 index 33891290d6..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordSeparatorSpacing.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DSharpPlus.Entities; - -/// -/// Represents the spacing for a separator. -/// -public enum DiscordSeparatorSpacing -{ - /// - /// A small spacing, equivalent to 17px, or ~1 line of text. - /// - Small = 1, - - /// - /// A large spacing, equivalent to 33px or ~2 lines of text. - /// - Large = 2, -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordTextDisplayComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordTextDisplayComponent.cs deleted file mode 100755 index 77640c32ad..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordTextDisplayComponent.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a block of text. -/// -public sealed class DiscordTextDisplayComponent : DiscordComponent -{ - /// - /// Gets the content for this text display. This can be up to 4000 characters, summed by all text displays in a message. - ///
- /// One text display could contain 4000 characters, or 10 displays of 400 characters each for example. - ///
- [JsonProperty("content")] - public string Content { get; internal set; } - - public DiscordTextDisplayComponent(string content) : this() => this.Content = content; - - internal DiscordTextDisplayComponent() => this.Type = DiscordComponentType.TextDisplay; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs deleted file mode 100644 index d964ca66f8..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs +++ /dev/null @@ -1,78 +0,0 @@ -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; } - - /// - /// 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.TextInput; - - /// - /// Constructs a new text input field. - /// - /// 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 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.TextInput; - 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 deleted file mode 100644 index 5de6629421..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputStyle.cs +++ /dev/null @@ -1,17 +0,0 @@ -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/DiscordThumbnailComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordThumbnailComponent.cs deleted file mode 100644 index 98dd84f5dc..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordThumbnailComponent.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a thumbnail. -/// -public class DiscordThumbnailComponent : DiscordComponent -{ - /// - /// The image for this thumbnail. - /// - [JsonProperty("media", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUnfurledMediaItem Media { get; internal set; } - - /// - /// Gets the description (alt-text) for this thumbnail. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string? Description { get; internal set; } - - /// - /// Gets whether this thumbnail is spoilered. - /// - [JsonProperty("spoiler", NullValueHandling = NullValueHandling.Ignore)] - public bool Spoiler { get; internal set; } - - public DiscordThumbnailComponent(string url, string? description = null, bool spoiler = false) - : this(new DiscordUnfurledMediaItem(url), description, spoiler) - { - - } - - public DiscordThumbnailComponent(DiscordUnfurledMediaItem media, string? description = null, bool spoiler = false) - : this() - { - this.Media = media; - this.Description = description; - this.Spoiler = spoiler; - } - - internal DiscordThumbnailComponent() => this.Type = DiscordComponentType.Thumbnail; -} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordUnfurledMediaItem.cs b/DSharpPlus/Entities/Interaction/Components/DiscordUnfurledMediaItem.cs deleted file mode 100755 index 479b37104c..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/DiscordUnfurledMediaItem.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an unfurled url; can be arbitrary URL or attachment:// schema. -/// -public sealed class DiscordUnfurledMediaItem -{ - /// - /// Gets the URL of the media item. - /// - [JsonProperty("url")] - public string Url { get; internal set; } - - public DiscordUnfurledMediaItem(string url) - => this.Url = url; -} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs deleted file mode 100644 index 0ae6f08452..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs +++ /dev/null @@ -1,81 +0,0 @@ -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; } - - /// - /// Whether this component is required. Only affects usage in modals. Defaults to true. - /// - [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] - public bool Required { 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; } - - /// - /// Internally used for parsing responses to modals, since those send submitted values in the component response object. - /// - [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)] - internal string[]? SubmittedValues { get; set; } - - // used by Newtonsoft.Json - public BaseDiscordSelectComponent() - { - } - - internal BaseDiscordSelectComponent - ( - DiscordComponentType type, - string customId, - string placeholder, - bool disabled = false, - int minOptions = 1, - int maxOptions = 1, - bool? required = null - ) - { - ArgumentOutOfRangeException.ThrowIfLessThan(minOptions, 0); - ArgumentOutOfRangeException.ThrowIfLessThan(maxOptions, 1); - ArgumentOutOfRangeException.ThrowIfGreaterThan(minOptions, maxOptions); - - if (required ?? false) - { - ArgumentOutOfRangeException.ThrowIfEqual(minOptions, 0); - } - - this.Type = type; - this.CustomId = customId; - this.Placeholder = placeholder; - this.Disabled = disabled; - this.MinimumSelectedValues = minOptions; - this.MaximumSelectedValues = maxOptions; - this.Required = required ?? minOptions != 0; - } -} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs deleted file mode 100644 index d3c3b16558..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs +++ /dev/null @@ -1,117 +0,0 @@ -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. - /// Indicates whether this component, in a modal, requires user input. - public DiscordChannelSelectComponent - ( - string customId, - string placeholder, - IEnumerable? channelTypes = null, - bool disabled = false, - int minOptions = 1, - int maxOptions = 1, - bool? required = null - ) - : base(DiscordComponentType.ChannelSelect, customId, placeholder, disabled, minOptions, maxOptions, required) - => this.ChannelTypes = channelTypes?.ToList(); -} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs deleted file mode 100644 index 65147cbca7..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs +++ /dev/null @@ -1,88 +0,0 @@ -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. - /// Indicates whether this component, in a modal, requires user input. - public DiscordMentionableSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1, bool? required = null) - : base(DiscordComponentType.MentionableSelect, customId, placeholder, disabled, minOptions, maxOptions, required) { } -} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs deleted file mode 100644 index 64d953cdeb..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs +++ /dev/null @@ -1,101 +0,0 @@ -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. - /// Indicates whether this component, in a modal, requires user input. - public DiscordRoleSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1, bool? required = null) - : base(DiscordComponentType.RoleSelect, customId, placeholder, disabled, minOptions, maxOptions, required) { } -} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs deleted file mode 100644 index 47c80aeca8..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs +++ /dev/null @@ -1,63 +0,0 @@ -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; - - /// - /// Creates a new string select component. - /// - /// The ID of this component. - /// Placeholder text that's shown when no options are selected. - /// The selectable options for this component. - /// Whether this component is disabled. - /// The minimum amount of options to be selected. - /// The maximum amount of options to be selected, up to 25. - /// Indicates whether this component, in a modal, requires user input. - public DiscordSelectComponent - ( - string customId, - string placeholder, - IEnumerable options, - bool disabled = false, - int minOptions = 1, - int maxOptions = 1, - bool? required = null - ) - : base(DiscordComponentType.StringSelect, customId, placeholder, disabled, minOptions, maxOptions, required) - => this.Options = options.ToArray(); -} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs deleted file mode 100644 index d190da9c0a..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index bc512059a8..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectDefaultValue.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 89bb523738..0000000000 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordUserSelectComponent.cs +++ /dev/null @@ -1,105 +0,0 @@ -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. - /// Indicates whether this component, in a modal, requires user input. - public DiscordUserSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1, bool? required = null) - : base(DiscordComponentType.UserSelect, customId, placeholder, disabled, minOptions, maxOptions, required) { } -} diff --git a/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs b/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs deleted file mode 100644 index 0a49c9b5a2..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs +++ /dev/null @@ -1,71 +0,0 @@ -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/DiscordHttpInteraction.cs b/DSharpPlus/Entities/Interaction/DiscordHttpInteraction.cs deleted file mode 100644 index db3d8fbb82..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordHttpInteraction.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class DiscordHttpInteraction : DiscordInteraction -{ - [JsonIgnore] - internal readonly TaskCompletionSource taskCompletionSource = new(); - - [JsonIgnore] - internal byte[] response; - - internal bool Cancel() => this.taskCompletionSource.TrySetCanceled(); - - internal async Task GetResponseAsync() - { - await this.taskCompletionSource.Task; - - return this.response; - } - - /// - public override Task CreateResponseAsync(DiscordInteractionResponseType type, DiscordInteractionResponseBuilder? builder = null) - { - if (this.taskCompletionSource.Task.IsCanceled) - { - throw new InvalidOperationException( - "Discord closed the connection. This is likely due to exeeding the limit of 3 seconds to the response."); - } - - 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; - - DiscordInteractionResponsePayload payload = new() - { - Type = type, - Data = builder is not null - ? new DiscordInteractionApplicationCommandCallbackData - { - Content = builder.Content, - 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 - }; - - this.response = Encoding.UTF8.GetBytes(DiscordJson.SerializeObject(payload)); - this.taskCompletionSource.SetResult(); - - return Task.CompletedTask; - } - - public override Task CreateResponseAsync - ( - DiscordInteractionResponseType type, - DiscordModalBuilder builder - ) - { - if (this.taskCompletionSource.Task.IsCanceled) - { - throw new InvalidOperationException( - "Discord closed the connection. This is likely due to exeeding the limit of 3 seconds to the response."); - } - - if (this.ResponseState is not DiscordInteractionResponseState.Unacknowledged) - { - throw new InvalidOperationException("A response has already been made to this interaction."); - } - - DiscordInteractionResponsePayload payload = new() - { - Type = type, - Data = new DiscordInteractionApplicationCommandCallbackData - { - Title = builder.Title, - CustomId = builder.CustomId, - Components = builder.Components, - } - }; - - this.response = Encoding.UTF8.GetBytes(DiscordJson.SerializeObject(payload)); - this.taskCompletionSource.SetResult(); - - return Task.CompletedTask; - } -} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs deleted file mode 100644 index 5deac64cc1..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs +++ /dev/null @@ -1,319 +0,0 @@ -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 -{ - /// - /// The channel context this interaction was provided during the event dispatch, if any. - /// - [JsonIgnore] - internal DiscordChannel? ContextChannel { get; set; } - - /// - /// 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; - } - - if (this.ContextChannel is not null) - { - return this.ContextChannel; - } - - 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; } - - /// - /// For monetized apps, any entitlements for the invoking user, representing access to premium SKUs - /// - [JsonIgnore] - public IReadOnlyList Entitlements => this.entitlements; - - [JsonProperty("entitlements")] - private List entitlements = []; - - /// - /// 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); - } - - public virtual async Task CreateResponseAsync - ( - DiscordInteractionResponseType type, - DiscordModalBuilder builder - ) - { - if (this.ResponseState is not DiscordInteractionResponseState.Unacknowledged) - { - throw new InvalidOperationException("A response has already been made to this interaction."); - } - - await this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, customID: builder.CustomId, components: builder.Components, title: builder.Title); - } - - /// - /// 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 deleted file mode 100644 index 5f4ed4dd7d..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 2abd634e5b..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs +++ /dev/null @@ -1,102 +0,0 @@ -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 deleted file mode 100644 index 82cf8917ec..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionDataOption.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index 1568cb02f6..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResolvedCollection.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 1cd6f17d9d..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseBuilder.cs +++ /dev/null @@ -1,108 +0,0 @@ -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 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); - } - - /// - /// 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 deleted file mode 100644 index bffc4ad077..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseState.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 0ff3317f61..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseType.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index ca04ba1575..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionType.cs +++ /dev/null @@ -1,33 +0,0 @@ -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/DiscordModalBuilder.cs b/DSharpPlus/Entities/Interaction/DiscordModalBuilder.cs deleted file mode 100644 index c66aa1e7c2..0000000000 --- a/DSharpPlus/Entities/Interaction/DiscordModalBuilder.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System; -using System.Collections.Generic; -namespace DSharpPlus.Entities; - -/// -/// Represents a builder class to construct modals with. -/// -public class DiscordModalBuilder -{ - private readonly List components = []; - - /// - /// Gets the components to be displayed in this modal. - /// - /// - /// Generally, this will be either a or - /// . This restriction is subject to change as Discord continues to release new Modal APIs. - /// - public IReadOnlyList Components => this.components; - - /// - /// Gets or sets the title of the modal. - /// - public string Title { get; set; } - - /// - /// Gets the Custom ID of this modal. - /// - public string CustomId { get; set; } - - /// - /// Sets the title for the modal. - /// - /// The title of the modal. (Maximum of 256 characters.) - /// The updated builder to chain calls with. - public DiscordModalBuilder 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; - } - - /// - /// Sets the custom ID of the modal. - /// - /// The custom ID. - /// The updated builder to chain calls with. - public DiscordModalBuilder WithCustomId - ( - string customId - ) - { - if (string.IsNullOrEmpty(customId) || customId.Length > 100) - { - throw new ArgumentException("Custom ID must be between 1 and 100 characters."); - } - - this.CustomId = customId; - return this; - } - - /// - /// Adds a block of text to the modal. All markdown supported. - /// - /// The text to display. - /// The updated builder to chain calls with. - public DiscordModalBuilder AddTextDisplay(string text) - { - if (this.Components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - this.components.Add(new DiscordTextDisplayComponent(text)); - return this; - } - - /// - /// Adds a new text input to the modal. - /// - /// The text input to add to this modal - /// A label text shown above the text input. - /// An optional description for the text input. - /// The updated builder to chain calls with. - public DiscordModalBuilder AddTextInput - ( - DiscordTextInputComponent input, - string label, - string? description = null - ) - { - if (this.components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - DiscordLabelComponent component = new(input, label, description); - - this.components.Add(component); - - return this; - } - - /// - /// Adds a select input to this modal. - /// - /// The select menu to add to this modal - /// A label text shown above the select menu. - /// An optional description for the menu. - /// The builder instance for chaining. - public DiscordModalBuilder AddSelectMenu - ( - BaseDiscordSelectComponent select, - string label, - string? description = null - ) - { - if (this.components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - DiscordLabelComponent component = new(select, label, description); - - this.components.Add(component); - - return this; - } - - /// - /// Adds a file upload field to this modal. - /// - /// The upload field to add to this modal - /// A label text shown above the upload field. - /// An optional description for the upload field. - /// The builder instance for chaining. - public DiscordModalBuilder AddFileUpload - ( - DiscordFileUploadComponent upload, - string label, - string? description = null - ) - { - if (this.components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - DiscordLabelComponent component = new(upload, label, description); - - this.components.Add(component); - - return this; - } - - /// - /// Adds a single-binary-choice checkbox to this modal. - /// - /// The checkbox to add to this modal. - /// The user-facing label for this checkbox. - /// An optional description for the checkbox. - /// The builder instance for chaining. - public DiscordModalBuilder AddCheckbox - ( - DiscordCheckboxComponent checkbox, - string label, - string? description = null - ) - { - if (this.components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - DiscordLabelComponent component = new(checkbox, label, description); - - this.components.Add(component); - - return this; - } - - /// - /// Adds a group of checkboxes to this modal. - /// - /// The group of checkboxes to add to this modal. - /// A label text shown above the checkbox group. Each individual checkbox has its own label. - /// An optional description for the checkbox group. Each individual checkbox may have its own description. - /// The builder instance for chaining. - public DiscordModalBuilder AddCheckboxGroup - ( - DiscordCheckboxGroupComponent checkboxGroup, - string label, - string? description = null - ) - { - if (this.components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - DiscordLabelComponent component = new(checkboxGroup, label, description); - - this.components.Add(component); - - return this; - } - - /// - /// Adds a radio group to this modal. - /// - /// The radio group to add to this modal. - /// A label text shown above the radio group. Each individual option has its own label. - /// An optional description for the radio group. Each individual option may have its own description. - /// The builder instance for chaining. - public DiscordModalBuilder AddRadioGroup - ( - DiscordRadioGroupComponent radioGroup, - string label, - string? description = null - ) - { - if (this.components.Count >= 5) - { - throw new InvalidOperationException("Modals can only have 5 components at this time."); - } - - DiscordLabelComponent component = new(radioGroup, label, description); - - this.components.Add(component); - - return this; - } - - public void Clear() - { - this.components.Clear(); - this.Title = string.Empty; - this.CustomId = string.Empty; - } -} diff --git a/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs b/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs deleted file mode 100644 index a643d376d7..0000000000 --- a/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 43cd34f8c5..0000000000 --- a/DSharpPlus/Entities/Interaction/Permissions/DiscordApplicationCommandPermission.cs +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 228fc6bf52..0000000000 --- a/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 4ce2bd3030..0000000000 --- a/DSharpPlus/Entities/Invite/DiscordInvite.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Collections.Generic; -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; } - - /// - /// The roles given to the user when accepting the invite. - /// - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? Roles { 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 deleted file mode 100644 index 17855596ca..0000000000 --- a/DSharpPlus/Entities/Invite/DiscordInviteChannel.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 386c138b9e..0000000000 --- a/DSharpPlus/Entities/Invite/DiscordInviteGuild.cs +++ /dev/null @@ -1,88 +0,0 @@ -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 deleted file mode 100644 index 268f2f2093..0000000000 --- a/DSharpPlus/Entities/Invite/DiscordInviteTargetType.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 16df5898e1..0000000000 --- a/DSharpPlus/Entities/Invite/DiscordStageInvite.cs +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 142012bd98..0000000000 --- a/DSharpPlus/Entities/Optional.cs +++ /dev/null @@ -1,251 +0,0 @@ -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 deleted file mode 100644 index 51045c0715..0000000000 --- a/DSharpPlus/Entities/SnowflakeObject.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 30d06b5f18..0000000000 --- a/DSharpPlus/Entities/User/DiscordActivity.cs +++ /dev/null @@ -1,447 +0,0 @@ -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 deleted file mode 100644 index bb228697ca..0000000000 --- a/DSharpPlus/Entities/User/DiscordPremiumType.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 5e109b66ae..0000000000 --- a/DSharpPlus/Entities/User/DiscordPresence.cs +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 655315aeee..0000000000 --- a/DSharpPlus/Entities/User/DiscordUser.cs +++ /dev/null @@ -1,430 +0,0 @@ -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; - this.PrimaryGuild = transport.PrimaryGuild; - } - - /// - /// 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; } - - /// - /// The user's primary guild also known as the "guild tag". - /// - [JsonProperty("primary_guild", NullValueHandling = NullValueHandling.Ignore)] - public virtual DiscordUserPrimaryGuild? PrimaryGuild { 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 deleted file mode 100644 index 1a2d371a79..0000000000 --- a/DSharpPlus/Entities/User/DiscordUserFlags.cs +++ /dev/null @@ -1,95 +0,0 @@ -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/User/DiscordUserPrimaryGuild.cs b/DSharpPlus/Entities/User/DiscordUserPrimaryGuild.cs deleted file mode 100644 index 9bc2b045f5..0000000000 --- a/DSharpPlus/Entities/User/DiscordUserPrimaryGuild.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a user's primary guild, which is the guild that the user has chosen to display as their "guild tag" on Discord. -/// -public class DiscordUserPrimaryGuild -{ - internal DiscordUserPrimaryGuild() { } - - /// - /// The id of the user's primary guild - /// - [JsonProperty("identity_guild_id")] - public ulong? IdentityGuildId { get; init; } - - /// - /// Whether the user is displaying the primary guild's server tag. This can be null if the system clears the identity, e.g. because the server no longer supports tags. - /// - [JsonProperty("identity_enabled")] - public bool? IdentityEnabled { get; init; } - - /// - /// The text of the user's server tag. Limited to 4 characters - /// - [JsonProperty("tag")] - public string? Tag { get; init; } - - /// - /// The server tag badge hash - /// - [JsonProperty("badge")] - public string? BadgeHash { get; init; } - - /// - /// The URL of the user's server tag badge, if available. This will be null if the user does not have a server tag badge or if the is empty. - /// - [JsonIgnore] - public string? BadgeUrl => string.IsNullOrWhiteSpace(this.BadgeHash) - ? null - : $"https://cdn.discordapp.com/guild-tag-badges/{this.IdentityGuildId}/{this.BadgeHash}.png"; -} diff --git a/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs b/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs deleted file mode 100644 index 6dc5247e8f..0000000000 --- a/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs +++ /dev/null @@ -1,86 +0,0 @@ -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 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. - /// The region to compare against. - /// - public static bool Equals(DiscordVoiceRegion? left, DiscordVoiceRegion? right) - { - if (ReferenceEquals(left, right)) - { - return true; - } - - if (left is null || right is null) - { - return false; - } - - return left.Id == right.Id; - } - - /// - public override bool Equals(object? obj) => Equals(this, 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) - => Equals(left, right); - - /// - /// 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 deleted file mode 100644 index fda40518aa..0000000000 --- a/DSharpPlus/Entities/Voice/DiscordVoiceState.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Collections.Generic; -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; internal set; } - - /// - /// 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)] - public 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) - { - return null; - } - - if (skipCache) - { - return await this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value); - } - - DiscordChannel? channel = null; - DiscordGuild? guild = null; - if (this.Discord is DiscordClient discordClient) - { - channel = discordClient.InternalGetCachedChannel(this.ChannelId.Value, this.GuildId); - } - else if (this.GuildId is not null) - { - guild = this.Discord.Guilds.GetValueOrDefault(this.GuildId.Value); - guild?.Channels.TryGetValue(this.ChannelId.Value, out channel); - } - - if (channel is not null) - { - return channel; - } - - channel = await this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value); - - if (this.GuildId is null) - { - return channel; - } - - guild ??= this.Discord.Guilds.GetValueOrDefault(this.GuildId.Value); - - if (guild is not null && this.Discord is DiscordClient dc) - { - dc.guilds.TryAdd(this.GuildId.Value, guild); - } - - return channel; - } - - internal DiscordVoiceState() { } - - 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 deleted file mode 100644 index 8bd75ab02e..0000000000 --- a/DSharpPlus/Entities/Webhook/DiscordWebhook.cs +++ /dev/null @@ -1,231 +0,0 @@ -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 DiscordRestApiClient 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 deleted file mode 100644 index 7bec86cc82..0000000000 --- a/DSharpPlus/Entities/Webhook/DiscordWebhookBuilder.cs +++ /dev/null @@ -1,161 +0,0 @@ -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/ApplicationAuthorizedEventArgs.cs b/DSharpPlus/EventArgs/ApplicationAuthorizedEventArgs.cs deleted file mode 100644 index 780de3a493..0000000000 --- a/DSharpPlus/EventArgs/ApplicationAuthorizedEventArgs.cs +++ /dev/null @@ -1,39 +0,0 @@ -using DSharpPlus.Entities; -using System; -using System.Collections.Generic; - -namespace DSharpPlus.EventArgs; - -/// -/// Invoked when the current application is added to a server or user account. This is not available via the -/// standard gateway, and requires webhook events to be enabled. -/// -public sealed class ApplicationAuthorizedEventArgs : DiscordEventArgs -{ - /// - /// The context this authorization occurred in. - /// - public DiscordApplicationIntegrationType IntegrationType { get; internal set; } - - /// - /// The user who authorized the application. This may be a member object if the application was authorized - /// into a guild. - /// - public DiscordUser User { get; internal set; } - - /// - /// The scopes the app was authorized for. - /// - public IReadOnlyList Scopes { get; internal set; } - - /// - /// The guild the application was authorized for. Only applicable if is - /// . - /// - public DiscordGuild? Guild { get; internal set; } - - /// - /// The timestamp at which this event was fired. - /// - public DateTimeOffset Timestamp { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/ApplicationCommandEventArgs.cs b/DSharpPlus/EventArgs/ApplicationCommandEventArgs.cs deleted file mode 100644 index 7e2caedd7c..0000000000 --- a/DSharpPlus/EventArgs/ApplicationCommandEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for application command events. -/// -public sealed class ApplicationCommandEventArgs : DiscordEventArgs -{ - /// - /// Gets the command that was modified. - /// - public DiscordApplicationCommand Command { get; internal set; } - - /// - /// Gets the optional guild of the command. - /// - public DiscordGuild Guild { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/ApplicationCommandPermissionsUpdatedEventArgs.cs b/DSharpPlus/EventArgs/ApplicationCommandPermissionsUpdatedEventArgs.cs deleted file mode 100644 index bffd3946f6..0000000000 --- a/DSharpPlus/EventArgs/ApplicationCommandPermissionsUpdatedEventArgs.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.EventArgs; - -public class ApplicationCommandPermissionsUpdatedEventArgs : DiscordEventArgs -{ - /// - /// The Id of the guild the command was updated for. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; internal set; } - - /// - /// The Id of the command that was updated. - /// - [JsonProperty("id")] - public ulong CommandId { get; internal set; } - - /// - /// The Id of the application the command was updated for. - /// - [JsonProperty("application_id")] - public ulong ApplicationId { get; internal set; } - - /// - /// The new permissions for the command. - /// - [JsonProperty("permissions")] - public IReadOnlyList NewPermissions { get; internal set; } -} - -public class ApplicationCommandPermissionUpdate -{ - /// - /// The Id of the entity this permission is for. - /// - [JsonProperty("id")] - public ulong Id { get; internal set; } - - /// - /// Whether the role/user/channel [or anyone in the channel/with the role] is allowed to use the command. - /// - [JsonProperty("permission")] - public bool Allow { get; internal set; } - - /// - /// - /// - [JsonProperty("type")] - public DiscordApplicationCommandPermissionType Type { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleCreatedEventArgs.cs b/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleCreatedEventArgs.cs deleted file mode 100644 index 64c1024ab4..0000000000 --- a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleCreatedEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents argument for the AutoModerationRuleCreated event. -/// -public class AutoModerationRuleCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the created rule. - /// - public DiscordAutoModerationRule? Rule { get; internal set; } - - internal AutoModerationRuleCreatedEventArgs() : base() { } - - internal AutoModerationRuleCreatedEventArgs(DiscordAutoModerationRule rule) : base() => this.Rule = rule; -} diff --git a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleDeletedEventArgs.cs b/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleDeletedEventArgs.cs deleted file mode 100644 index bf565d6222..0000000000 --- a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleDeletedEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the AutoModerationRuleDeleted event. -/// -public class AutoModerationRuleDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the deleted rule. - /// - public DiscordAutoModerationRule Rule { get; internal set; } - - internal AutoModerationRuleDeletedEventArgs() : base() { } - - internal AutoModerationRuleDeletedEventArgs(DiscordAutoModerationRule rule) : base() => this.Rule = rule; -} diff --git a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleExecutedEventArgs.cs b/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleExecutedEventArgs.cs deleted file mode 100644 index e51a1ba3be..0000000000 --- a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleExecutedEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the AutoModerationRuleExecuted event. -/// -public class AutoModerationRuleExecutedEventArgs : DiscordEventArgs -{ - /// - /// Gets the executed rule. - /// - public DiscordAutoModerationActionExecution Rule { get; internal set; } - - internal AutoModerationRuleExecutedEventArgs() : base() { } - - internal AutoModerationRuleExecutedEventArgs(DiscordAutoModerationActionExecution rule) : base() => this.Rule = rule; -} diff --git a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleUpdatedEventArgs.cs b/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleUpdatedEventArgs.cs deleted file mode 100644 index 86453c1e8a..0000000000 --- a/DSharpPlus/EventArgs/AutoModeration/AutoModerationRuleUpdatedEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the AutoModerationRuleUpdated event. -/// -public class AutoModerationRuleUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the updated rule. - /// - public DiscordAutoModerationRule? Rule { get; internal set; } - - internal AutoModerationRuleUpdatedEventArgs() : base() { } - - internal AutoModerationRuleUpdatedEventArgs(DiscordAutoModerationRule rule) : base() => this.Rule = rule; -} diff --git a/DSharpPlus/EventArgs/Channel/ChannelCreatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelCreatedEventArgs.cs deleted file mode 100644 index 67b6dfeba0..0000000000 --- a/DSharpPlus/EventArgs/Channel/ChannelCreatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the ChannelCreated event. -/// -public class ChannelCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the channel that was created. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the guild in which the channel was created. - /// - public DiscordGuild Guild { get; internal set; } - - internal ChannelCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/ChannelDeletedEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelDeletedEventArgs.cs deleted file mode 100644 index fb462421be..0000000000 --- a/DSharpPlus/EventArgs/Channel/ChannelDeletedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the ChannelDeleted event. -/// -public class ChannelDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the channel that was deleted. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the guild this channel belonged to. - /// - public DiscordGuild Guild { get; internal set; } - - internal ChannelDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/ChannelPinsUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelPinsUpdatedEventArgs.cs deleted file mode 100644 index 8268b2e6f1..0000000000 --- a/DSharpPlus/EventArgs/Channel/ChannelPinsUpdatedEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the ChannelPinsUpdated event. -/// -public class ChannelPinsUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild in which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the channel in which the update occurred. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the timestamp of the latest pin. - /// - public DateTimeOffset? LastPinTimestamp { get; internal set; } - - internal ChannelPinsUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/ChannelUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/ChannelUpdatedEventArgs.cs deleted file mode 100644 index dc34180ea4..0000000000 --- a/DSharpPlus/EventArgs/Channel/ChannelUpdatedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the ChannelUpdated event. -/// -public class ChannelUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the post-update channel. - /// - public DiscordChannel ChannelAfter { get; internal set; } - - /// - /// Gets the pre-update channel. - /// - public DiscordChannel ChannelBefore { get; internal set; } - - /// - /// Gets the guild in which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - internal ChannelUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/DmChannelDeletedEventArgs.cs b/DSharpPlus/EventArgs/Channel/DmChannelDeletedEventArgs.cs deleted file mode 100644 index 0a787fe206..0000000000 --- a/DSharpPlus/EventArgs/Channel/DmChannelDeletedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the DmChannelDeleted event. -/// -public class DmChannelDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the direct message channel that was deleted. - /// - public DiscordDmChannel Channel { get; internal set; } - - internal DmChannelDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/Thread/ThreadCreatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/Thread/ThreadCreatedEventArgs.cs deleted file mode 100644 index 6cf979ab2d..0000000000 --- a/DSharpPlus/EventArgs/Channel/Thread/ThreadCreatedEventArgs.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for ThreadCreated event. -/// -public class ThreadCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets whether this thread has been newly created. - /// - public bool NewlyCreated { get; internal set; } - - /// - /// Gets the thread that was created. - /// - public DiscordThreadChannel Thread { get; internal set; } - - /// - /// Gets the threads parent channel. - /// - public DiscordChannel Parent { get; internal set; } - - /// - /// Gets the guild in which the thread was created. - /// - public DiscordGuild Guild { get; internal set; } - - internal ThreadCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/Thread/ThreadDeletedEventArgs.cs b/DSharpPlus/EventArgs/Channel/Thread/ThreadDeletedEventArgs.cs deleted file mode 100644 index a74dc33ab7..0000000000 --- a/DSharpPlus/EventArgs/Channel/Thread/ThreadDeletedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for ThreadDeleted event. -/// -public class ThreadDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the thread that was deleted. - /// - public DiscordThreadChannel Thread { get; internal set; } - - /// - /// Gets the threads parent channel. - /// - public DiscordChannel Parent { get; internal set; } - - /// - /// Gets the guild this thread belonged to. - /// - public DiscordGuild Guild { get; internal set; } - - internal ThreadDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/Thread/ThreadListSyncedEventArgs.cs b/DSharpPlus/EventArgs/Channel/Thread/ThreadListSyncedEventArgs.cs deleted file mode 100644 index db890d227f..0000000000 --- a/DSharpPlus/EventArgs/Channel/Thread/ThreadListSyncedEventArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for ThreadListSynced event. -/// -public class ThreadListSyncedEventArgs : DiscordEventArgs -{ - /// - /// Gets all thread member objects, indicating which threads the current user has been added to. - /// - public IReadOnlyList CurrentMembers { get; internal set; } - - /// - /// Gets all active threads in the given channels that the current user can access. - /// - public IReadOnlyList Threads { get; internal set; } - - /// - /// Gets the parent channels whose threads are being synced. May contain channels that have no active threads as well. - /// - public IReadOnlyList Channels { get; internal set; } - - /// - /// Gets the guild being synced. - /// - public DiscordGuild Guild { get; internal set; } - - internal ThreadListSyncedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/Thread/ThreadMemberUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/Thread/ThreadMemberUpdatedEventArgs.cs deleted file mode 100644 index b32c4779cd..0000000000 --- a/DSharpPlus/EventArgs/Channel/Thread/ThreadMemberUpdatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for ThreadMemberUpdated event. -/// -public class ThreadMemberUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the thread member that was updated. - /// - public DiscordThreadChannelMember ThreadMember { get; internal set; } - - /// - /// Gets the thread the current member was updated for. - /// - public DiscordThreadChannel Thread { get; internal set; } - - internal ThreadMemberUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/Thread/ThreadMembersUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/Thread/ThreadMembersUpdatedEventArgs.cs deleted file mode 100644 index 9a9fcbf6a2..0000000000 --- a/DSharpPlus/EventArgs/Channel/Thread/ThreadMembersUpdatedEventArgs.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for ThreadMembersUpdated event. -/// -public class ThreadMembersUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the approximate number of members in the thread, capped at 50. - /// - public int MemberCount { get; internal set; } - - /// - /// Gets the members who were removed from the thread. These could be skeleton objects depending on cache state. - /// - public IReadOnlyList RemovedMembers { get; internal set; } - - /// - /// Gets the members who were added to the thread. - /// - public IReadOnlyList AddedMembers { get; internal set; } - - /// - /// Gets the thread associated with the member changes. - /// - public DiscordThreadChannel Thread { get; internal set; } - - /// - /// Gets the guild. - /// - public DiscordGuild Guild { get; internal set; } - - internal ThreadMembersUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Channel/Thread/ThreadUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Channel/Thread/ThreadUpdatedEventArgs.cs deleted file mode 100644 index 2b3fde16fb..0000000000 --- a/DSharpPlus/EventArgs/Channel/Thread/ThreadUpdatedEventArgs.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for ThreadUpdated event. -/// -public class ThreadUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the post-update thread. - /// - public DiscordThreadChannel ThreadAfter { get; internal set; } - - /// - /// Gets the pre-update thread. - /// - public DiscordThreadChannel ThreadBefore { get; internal set; } - - /// - /// Gets the threads parent channel. - /// - public DiscordChannel Parent { get; internal set; } - - /// - /// Gets the guild in which the thread was updated. - /// - public DiscordGuild Guild { get; internal set; } - - internal ThreadUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/ClientErrorEventArgs.cs b/DSharpPlus/EventArgs/ClientErrorEventArgs.cs deleted file mode 100644 index 64f06ac22f..0000000000 --- a/DSharpPlus/EventArgs/ClientErrorEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the ClientErrored event. -/// -public class ClientErrorEventArgs : DiscordEventArgs -{ - /// - /// Gets the exception thrown by the client. - /// - public Exception Exception { get; internal set; } - - /// - /// Gets the name of the event that threw the exception. - /// - public string EventName { get; internal set; } - - internal ClientErrorEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/ClientStartedEventArgs.cs b/DSharpPlus/EventArgs/ClientStartedEventArgs.cs deleted file mode 100644 index bb243f1a2f..0000000000 --- a/DSharpPlus/EventArgs/ClientStartedEventArgs.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Represents an event invoked when the client starts. -/// -public sealed class ClientStartedEventArgs : DiscordEventArgs; diff --git a/DSharpPlus/EventArgs/ClientStoppedEventArgs.cs b/DSharpPlus/EventArgs/ClientStoppedEventArgs.cs deleted file mode 100644 index 85fbdf0e09..0000000000 --- a/DSharpPlus/EventArgs/ClientStoppedEventArgs.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Represents an event fired when the client stops. -/// -public sealed class ClientStoppedEventArgs : DiscordEventArgs; diff --git a/DSharpPlus/EventArgs/DiscordEventArgs.cs b/DSharpPlus/EventArgs/DiscordEventArgs.cs deleted file mode 100644 index d68d8f0837..0000000000 --- a/DSharpPlus/EventArgs/DiscordEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.EventArgs; - -// Note: this might seem useless, but should we ever need to add a common property or method to all event arg -// classes, it would be useful to already have a base for all of it. - -/// -/// Common base for all other -related event argument classes. -/// -public abstract class DiscordEventArgs : AsyncEventArgs -{ - protected DiscordEventArgs() - { } -} diff --git a/DSharpPlus/EventArgs/Entitlement/EntitlementCreatedEventArgs.cs b/DSharpPlus/EventArgs/Entitlement/EntitlementCreatedEventArgs.cs deleted file mode 100644 index 9148804960..0000000000 --- a/DSharpPlus/EventArgs/Entitlement/EntitlementCreatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for EntitlementCreated event. -/// -public class EntitlementCreatedEventArgs : DiscordEventArgs -{ - /// - /// Entitlement which was created - /// - public DiscordEntitlement Entitlement { get; internal set; } - - /// - /// The timestamp at which this event was invoked. Unset for gateway events. - /// - public DateTimeOffset? Timestamp { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Entitlement/EntitlementDeletedEventArgs.cs b/DSharpPlus/EventArgs/Entitlement/EntitlementDeletedEventArgs.cs deleted file mode 100644 index 6213b4f24a..0000000000 --- a/DSharpPlus/EventArgs/Entitlement/EntitlementDeletedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for EntitlementDeleted event. -/// -public class EntitlementDeletedEventArgs : DiscordEventArgs -{ - /// - /// Entitlement which was deleted - /// - public DiscordEntitlement Entitlement { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Entitlement/EntitlementUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Entitlement/EntitlementUpdatedEventArgs.cs deleted file mode 100644 index ed70ebab30..0000000000 --- a/DSharpPlus/EventArgs/Entitlement/EntitlementUpdatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for EntitlementUpdated event. -/// -public class EntitlementUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Entitlement which was updated - /// - public DiscordEntitlement Entitlement { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddedEventArgs.cs deleted file mode 100644 index 828c378943..0000000000 --- a/DSharpPlus/EventArgs/Guild/Ban/GuildBanAddedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the GuildBanAdded event. -/// -public class GuildBanAddedEventArgs : DiscordEventArgs -{ - /// - /// Gets the member that was banned. - /// - public DiscordMember Member { get; internal set; } - - /// - /// Gets the guild this member was banned in. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildBanAddedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemovedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemovedEventArgs.cs deleted file mode 100644 index 4dc6abc674..0000000000 --- a/DSharpPlus/EventArgs/Guild/Ban/GuildBanRemovedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the GuildBanRemoved event. -/// -public class GuildBanRemovedEventArgs : DiscordEventArgs -{ - /// - /// Gets the member that just got unbanned. - /// - public DiscordMember Member { get; internal set; } - - /// - /// Gets the guild this member was unbanned in. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildBanRemovedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs deleted file mode 100644 index 136f9f4389..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -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/GuildAvailableEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildAvailableEventArgs.cs deleted file mode 100644 index 356e8542e7..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildAvailableEventArgs.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Contains information pertaining to a guild becoming available. -/// -public class GuildAvailableEventArgs : GuildCreatedEventArgs; diff --git a/DSharpPlus/EventArgs/Guild/GuildCreatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildCreatedEventArgs.cs deleted file mode 100644 index e8787c6df2..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildCreatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the GuildCreated event. -/// -public class GuildCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that was created. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/GuildDeletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildDeletedEventArgs.cs deleted file mode 100644 index 077d7ff5b8..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildDeletedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the GuildDeleted event. -/// -public class GuildDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that was deleted. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets whether the guild is unavailable or not. - /// - public bool Unavailable { get; internal set; } - - internal GuildDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs deleted file mode 100644 index e9b392ac8a..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -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/GuildEmojisUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildEmojisUpdatedEventArgs.cs deleted file mode 100644 index e18019d49e..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildEmojisUpdatedEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildEmojisUpdated event. -/// -public class GuildEmojisUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the list of emojis after the change. - /// - public IReadOnlyDictionary EmojisAfter { get; internal set; } - - /// - /// Gets the list of emojis before the change. - /// - public IReadOnlyDictionary EmojisBefore { get; internal set; } - - /// - /// Gets the guild in which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildEmojisUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdatedEventArgs.cs deleted file mode 100644 index 99fffd3958..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildIntegrationsUpdatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildIntegrationsUpdated event. -/// -public class GuildIntegrationsUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that had its integrations updated. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildIntegrationsUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/GuildStickersUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildStickersUpdatedEventArgs.cs deleted file mode 100644 index 8b314b4e28..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildStickersUpdatedEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents event args for the GuildStickersUpdated event. -/// -public class GuildStickersUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the list of stickers after the change. - /// - public IReadOnlyDictionary StickersAfter { get; internal set; } - - /// - /// Gets the list of stickers before the change. - /// - public IReadOnlyDictionary StickersBefore { get; internal set; } - - /// - /// Gets the guild in which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildStickersUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/GuildUnavailableEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildUnavailableEventArgs.cs deleted file mode 100644 index 058f5c628b..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildUnavailableEventArgs.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Contains information pertaining to a guild becoming unavailable. -/// -public class GuildUnavailableEventArgs : GuildDeletedEventArgs; diff --git a/DSharpPlus/EventArgs/Guild/GuildUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildUpdatedEventArgs.cs deleted file mode 100644 index e9c8f8a0e3..0000000000 --- a/DSharpPlus/EventArgs/Guild/GuildUpdatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildUpdated event. -/// -public class GuildUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild before it was updated. - /// - public DiscordGuild GuildBefore { get; internal set; } - - /// - /// Gets the guild after it was updated. - /// - public DiscordGuild GuildAfter { get; internal set; } - - internal GuildUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddedEventArgs.cs deleted file mode 100644 index e54433df7e..0000000000 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMemberAddedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildMemberAdded event. -/// -public class GuildMemberAddedEventArgs : DiscordEventArgs -{ - /// - /// Gets the member that was added. - /// - public DiscordMember Member { get; internal set; } - - /// - /// Gets the guild the member was added to. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildMemberAddedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemovedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemovedEventArgs.cs deleted file mode 100644 index d3135d9abb..0000000000 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMemberRemovedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildMemberRemoved event. -/// -public class GuildMemberRemovedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild the member was removed from. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the member that was removed. - /// - public DiscordMember Member { get; internal set; } - - internal GuildMemberRemovedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdatedEventArgs.cs deleted file mode 100644 index d35f36a333..0000000000 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMemberUpdatedEventArgs.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildMemberUpdated event. -/// -public class GuildMemberUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild in which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Get the member with post-update info - /// - public DiscordMember MemberAfter { get; internal set; } - - /// - /// Get the member with pre-update info - /// - public DiscordMember MemberBefore { get; internal set; } - - /// - /// Gets a collection containing post-update roles. - /// - public IReadOnlyList RolesAfter => this.MemberAfter.Roles.ToList(); - - /// - /// Gets a collection containing pre-update roles. - /// - public IReadOnlyList RolesBefore => this.MemberBefore.Roles.ToList(); - - /// - /// Gets the member's new nickname. - /// - public string NicknameAfter => this.MemberAfter.Nickname; - - /// - /// Gets the member's old nickname. - /// - public string NicknameBefore => this.MemberBefore.Nickname; - - /// - /// Gets the member's old guild avatar hash. - /// - public string GuildAvatarHashBefore => this.MemberBefore.GuildAvatarHash; - - /// - /// Gets the member's new guild avatar hash. - /// - public string GuildAvatarHashAfter => this.MemberAfter.GuildAvatarHash; - - /// - /// Gets the member's old username. - /// - public string UsernameBefore => this.MemberBefore.Username; - - /// - /// Gets the member's new username. - /// - public string UsernameAfter => this.MemberAfter.Username; - - /// - /// Gets the member's old avatar hash. - /// - public string AvatarHashBefore => this.MemberBefore.AvatarHash; - - /// - /// Gets the member's new avatar hash. - /// - public string AvatarHashAfter => this.MemberAfter.AvatarHash; - - /// - /// Gets whether the member had passed membership screening before the update - /// - public bool? PendingBefore => this.MemberBefore.IsPending; - - /// - /// Gets whether the member had passed membership screening after the update - /// - public bool? PendingAfter => this.MemberAfter.IsPending; - - /// - /// Gets the member's communication restriction before the update - /// - public DateTimeOffset? CommunicationDisabledUntilBefore => this.MemberBefore.CommunicationDisabledUntil; - - /// - /// Gets the member's communication restriction after the update - /// - public DateTimeOffset? CommunicationDisabledUntilAfter => this.MemberAfter.CommunicationDisabledUntil; - - /// - /// Gets the member that was updated. - /// - public DiscordMember Member => this.MemberAfter; - - internal GuildMemberUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkedEventArgs.cs deleted file mode 100644 index 78e58f268a..0000000000 --- a/DSharpPlus/EventArgs/Guild/Member/GuildMembersChunkedEventArgs.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildMembersChunked event. -/// -public class GuildMembersChunkedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that requested this chunk. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the collection of members returned from this chunk. - /// - public IReadOnlyList Members { get; internal set; } - - /// - /// Gets the current chunk index from the response. - /// - public int ChunkIndex { get; internal set; } - - /// - /// Gets the total amount of chunks for the request. - /// - public int ChunkCount { get; internal set; } - - /// - /// Gets the collection of presences returned from this chunk, if specified. - /// - public IReadOnlyList Presences { get; internal set; } - - /// - /// Gets the returned Ids that were not found in the chunk, if specified. - /// - public IReadOnlyList NotFound { get; internal set; } - - /// - /// Gets the unique string used to identify the request, if specified. - /// - public string Nonce { get; set; } - - internal GuildMembersChunkedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreatedEventArgs.cs deleted file mode 100644 index 4f1d053d21..0000000000 --- a/DSharpPlus/EventArgs/Guild/Role/GuildRoleCreatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildRoleCreated event. -/// -public class GuildRoleCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild in which the role was created. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the role that was created. - /// - public DiscordRole Role { get; internal set; } - - internal GuildRoleCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeletedEventArgs.cs deleted file mode 100644 index 28cf5027bb..0000000000 --- a/DSharpPlus/EventArgs/Guild/Role/GuildRoleDeletedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildRoleDeleted event. -/// -public class GuildRoleDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild in which the role was deleted. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the role that was deleted. - /// - public DiscordRole Role { get; internal set; } - - internal GuildRoleDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdatedEventArgs.cs deleted file mode 100644 index e4b13752fc..0000000000 --- a/DSharpPlus/EventArgs/Guild/Role/GuildRoleUpdatedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildRoleUpdated event. -/// -public class GuildRoleUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild in which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the post-update role. - /// - public DiscordRole RoleAfter { get; internal set; } - - /// - /// Gets the pre-update role. - /// - public DiscordRole RoleBefore { get; internal set; } - - internal GuildRoleUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs deleted file mode 100644 index 19563e48f5..0000000000 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/Guild/ScheduledEvents/ScheduledGuildEventCreatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCreatedEventArgs.cs deleted file mode 100644 index 06c0c2d29b..0000000000 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCreatedEventArgs.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when a new scheduled guild event is created. -/// -public class ScheduledGuildEventCreatedEventArgs : DiscordEventArgs -{ - /// - /// The guild this event is scheduled for. - /// - public DiscordGuild Guild => this.Event.Guild; - - /// - /// The channel this event is scheduled for, if applicable. - /// - public DiscordChannel Channel => this.Event.Channel; - - /// - /// The user that created the event. - /// - public DiscordUser Creator => this.Event.Creator; - - /// - /// The event that was created. - /// - public DiscordScheduledGuildEvent Event { get; internal set; } - - internal ScheduledGuildEventCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventDeletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventDeletedEventArgs.cs deleted file mode 100644 index e0dd9ac361..0000000000 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventDeletedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when an event is deleted. -/// -public class ScheduledGuildEventDeletedEventArgs : DiscordEventArgs -{ - /// - /// The event that was deleted. - /// - public DiscordScheduledGuildEvent Event { get; internal set; } - - internal ScheduledGuildEventDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdatedEventArgs.cs deleted file mode 100644 index af733a2414..0000000000 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUpdatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when an event is updated. -/// -public class ScheduledGuildEventUpdatedEventArgs : DiscordEventArgs -{ - /// - /// The event before the update, or null if it wasn't cached. - /// - public DiscordScheduledGuildEvent EventBefore { get; internal set; } - - /// - /// The event after the update. - /// - public DiscordScheduledGuildEvent EventAfter { get; internal set; } - - internal ScheduledGuildEventUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddedEventArgs.cs deleted file mode 100644 index 0310eb5e1f..0000000000 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserAddedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when someone subscribes to the scheduled event. -/// -public class ScheduledGuildEventUserAddedEventArgs : DiscordEventArgs -{ - /// - /// The guild the event is scheduled for. - /// - public DiscordGuild Guild => this.Event.Guild; - - /// - /// The event that was subscribed to. - /// - public DiscordScheduledGuildEvent Event { get; internal set; } - - /// - /// The user that subscribed to the event. - /// - public DiscordUser User { get; internal set; } - - internal ScheduledGuildEventUserAddedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserRemovedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserRemovedEventArgs.cs deleted file mode 100644 index 2ace4ee6b2..0000000000 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventUserRemovedEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when someone unsubcribes from an event. -/// -public class ScheduledGuildEventUserRemovedEventArgs : ScheduledGuildEventUserAddedEventArgs -{ - /// - /// The guild the event is scheduled for. - /// - public new DiscordGuild Guild => this.Event.Guild; - - /// - /// The event that was unsubscribed from. - /// - public new DiscordScheduledGuildEvent Event { get; internal set; } - - /// - /// The user that unsubscribed from the event. - /// - public new DiscordUser User { get; internal set; } - internal ScheduledGuildEventUserRemovedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/HeartbeatedEventArgs.cs b/DSharpPlus/EventArgs/HeartbeatedEventArgs.cs deleted file mode 100644 index 071b2cc792..0000000000 --- a/DSharpPlus/EventArgs/HeartbeatedEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for Heartbeated event. -/// -[Obsolete("This event is obsolete and wont be invoked. Use IGatewayController.HeartbeatedAsync instead")] -public class HeartbeatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the round-trip time of the heartbeat. - /// - public int Ping { get; internal set; } - - /// - /// Gets the timestamp of the heartbeat. - /// - public DateTimeOffset Timestamp { get; internal set; } - - internal HeartbeatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Integration/IntegrationCreatedEventArgs.cs b/DSharpPlus/EventArgs/Integration/IntegrationCreatedEventArgs.cs deleted file mode 100644 index 9515d96900..0000000000 --- a/DSharpPlus/EventArgs/Integration/IntegrationCreatedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for IntegrationCreated -/// -public sealed class IntegrationCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the integration. - /// - public DiscordIntegration Integration { get; internal set; } - - /// - /// Gets the guild the integration was added to. - /// - public DiscordGuild Guild { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Integration/IntegrationDeletedEventArgs.cs b/DSharpPlus/EventArgs/Integration/IntegrationDeletedEventArgs.cs deleted file mode 100644 index 9e5abc1b85..0000000000 --- a/DSharpPlus/EventArgs/Integration/IntegrationDeletedEventArgs.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for IntegrationDeleted -/// -public sealed class IntegrationDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the id of the integration. - /// - public ulong IntegrationId { get; internal set; } - - /// - /// Gets the guild the integration was removed from. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the id of the bot or OAuth2 application for the integration. - /// - public ulong? Applicationid { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Integration/IntegrationUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Integration/IntegrationUpdatedEventArgs.cs deleted file mode 100644 index 1f4204ee8a..0000000000 --- a/DSharpPlus/EventArgs/Integration/IntegrationUpdatedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for IntegrationUpdated -/// -public sealed class IntegrationUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the integration. - /// - public DiscordIntegration Integration { get; internal set; } - - /// - /// Gets the guild the integration was updated in. - /// - public DiscordGuild Guild { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Interaction/ChannelSelectMenuModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/ChannelSelectMenuModalSubmission.cs deleted file mode 100644 index 5118eb2b77..0000000000 --- a/DSharpPlus/EventArgs/Interaction/ChannelSelectMenuModalSubmission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a channel select menu submitted through a modal. -/// -public sealed class ChannelSelectMenuModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.ChannelSelect; - - /// - public string CustomId { get; internal set; } - - /// - /// The snowflake identifiers of the channels submitted. - /// - public IReadOnlyList Ids { get; internal set; } - - internal ChannelSelectMenuModalSubmission(string customId, IReadOnlyList ids) - { - this.CustomId = customId; - this.Ids = ids; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/CheckboxGroupModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/CheckboxGroupModalSubmission.cs deleted file mode 100644 index 7fcbe144d7..0000000000 --- a/DSharpPlus/EventArgs/Interaction/CheckboxGroupModalSubmission.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a checkbox group submitted through a modal. -/// -public class CheckboxGroupModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.CheckboxGroup; - - /// - public string CustomId { get; } - - /// - /// The developer-defined values of the checkboxes that were checked within this group. - /// - public IReadOnlyList Values { get; } - - internal CheckboxGroupModalSubmission(string customId, IReadOnlyList values) - { - this.CustomId = customId; - this.Values = values; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/CheckboxModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/CheckboxModalSubmission.cs deleted file mode 100644 index f176569bf5..0000000000 --- a/DSharpPlus/EventArgs/Interaction/CheckboxModalSubmission.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a checkbox submitted through a modal. -/// -public class CheckboxModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.Checkbox; - - /// - public string CustomId { get; } - - /// - /// Indicates whether the checkbox was checked by the user. - /// - public bool Value { get; } - - internal CheckboxModalSubmission(string customId, bool value) - { - this.CustomId = customId; - this.Value = value; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/ComponentInteractionCreatedEventArgs.cs b/DSharpPlus/EventArgs/Interaction/ComponentInteractionCreatedEventArgs.cs deleted file mode 100644 index 678658bd0f..0000000000 --- a/DSharpPlus/EventArgs/Interaction/ComponentInteractionCreatedEventArgs.cs +++ /dev/null @@ -1,51 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for the ComponentInteractionCreated event. -/// -public class ComponentInteractionCreatedEventArgs : InteractionCreatedEventArgs -{ - /// - /// The Id of the component that was interacted with. - /// - public string Id => this.Interaction.Data.CustomId; - - /// - /// The user that invoked this interaction. - /// - public DiscordUser User => this.Interaction.User; - - /// - /// The guild this interaction was invoked on, if any. - /// - public DiscordGuild Guild => this.Channel.Guild; - - /// - /// The channel this interaction was invoked in. - /// - public DiscordChannel Channel => this.Interaction.Channel; - - /// - /// The value(s) selected. Only applicable to SelectMenu components. - /// - public string[] Values => this.Interaction.Data.Values; - - /// - /// The message this interaction is attached to. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// The locale of the user that invoked this interaction. - /// - public string Locale => this.Interaction.Locale; - - /// - /// The guild's locale that the user invoked in. - /// - public string GuildLocale => this.Interaction.GuildLocale; - - internal ComponentInteractionCreatedEventArgs() { } -} diff --git a/DSharpPlus/EventArgs/Interaction/ContextMenuInteractionCreatedEventArgs.cs b/DSharpPlus/EventArgs/Interaction/ContextMenuInteractionCreatedEventArgs.cs deleted file mode 100644 index ae8f120058..0000000000 --- a/DSharpPlus/EventArgs/Interaction/ContextMenuInteractionCreatedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -public sealed class ContextMenuInteractionCreatedEventArgs : InteractionCreatedEventArgs -{ - /// - /// The type of context menu that was used. This is never . - /// - public DiscordApplicationCommandType Type { get; internal set; } //TODO: Set this - - /// - /// The user that invoked this interaction. Can be casted to a member if this was on a guild. - /// - public DiscordUser User => this.Interaction.User; - - /// - /// The user this interaction targets, if applicable. - /// - public DiscordUser TargetUser { get; internal set; } - - /// - /// The message this interaction targets, if applicable. - /// - public DiscordMessage TargetMessage { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Interaction/FileUploadModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/FileUploadModalSubmission.cs deleted file mode 100644 index ff72258227..0000000000 --- a/DSharpPlus/EventArgs/Interaction/FileUploadModalSubmission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about files uploaded through a modal submission. -/// -public sealed class FileUploadModalSubmission : IModalSubmission -{ - internal FileUploadModalSubmission(string customId, IReadOnlyList uploadedFiles) - { - this.CustomId = customId; - this.UploadedFiles = uploadedFiles; - } - - /// - public DiscordComponentType ComponentType => DiscordComponentType.FileUpload; - - /// - public string CustomId { get; internal set; } - - /// - /// The files uploaded to the modal. - /// - public IReadOnlyList UploadedFiles { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Interaction/IModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/IModalSubmission.cs deleted file mode 100644 index d485dcfb72..0000000000 --- a/DSharpPlus/EventArgs/Interaction/IModalSubmission.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents the values submitted to a component from a modal. -/// -public interface IModalSubmission -{ - /// - /// The type of component this submission represents. - /// - public DiscordComponentType ComponentType { get; } - - /// - /// The custom ID of this component. - /// - public string CustomId { get; } -} diff --git a/DSharpPlus/EventArgs/Interaction/InteractionCreatedEventArgs.cs b/DSharpPlus/EventArgs/Interaction/InteractionCreatedEventArgs.cs deleted file mode 100644 index d6416d75d3..0000000000 --- a/DSharpPlus/EventArgs/Interaction/InteractionCreatedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for InteractionCreated -/// -public class InteractionCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the interaction data that was invoked. - /// - public DiscordInteraction Interaction { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Interaction/MentionableSelectMenuModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/MentionableSelectMenuModalSubmission.cs deleted file mode 100644 index 83ca1a665a..0000000000 --- a/DSharpPlus/EventArgs/Interaction/MentionableSelectMenuModalSubmission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a select menu for roles and users submitted through a modal. -/// -public sealed class MentionableSelectMenuModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.MentionableSelect; - - /// - public string CustomId { get; internal set; } - - /// - /// The snowflake identifiers of the roles and users submitted. - /// - public IReadOnlyList Ids { get; internal set; } - - internal MentionableSelectMenuModalSubmission(string customId, IReadOnlyList ids) - { - this.CustomId = customId; - this.Ids = ids; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/ModalSubmittedEventArgs.cs b/DSharpPlus/EventArgs/Interaction/ModalSubmittedEventArgs.cs deleted file mode 100644 index 15023d4b67..0000000000 --- a/DSharpPlus/EventArgs/Interaction/ModalSubmittedEventArgs.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using DSharpPlus.Entities; - -using Newtonsoft.Json; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when a modal is submitted. Note that this event is fired only if the modal is submitted by the user, and not if the modal is closed. -/// -public class ModalSubmittedEventArgs : InteractionCreatedEventArgs -{ - /// - /// A dictionary of submitted fields, keyed on the custom id of the input component. - /// - [JsonIgnore] - public IReadOnlyDictionary Values { get; } - - /// - /// The custom ID this modal was sent with. - /// - [JsonIgnore] - public string Id => this.Interaction.Data.CustomId; - - internal ModalSubmittedEventArgs(DiscordInteraction interaction) - { - this.Interaction = interaction; - - Dictionary dict = []; - - foreach (DiscordComponent component in interaction.Data.components) - { - if (component is not DiscordLabelComponent label) - { - continue; - } - - dict.Add - ( - label.Component.CustomId, label.Component switch - { - DiscordTextInputComponent input => new TextInputModalSubmission(input.CustomId, input.Value), - - DiscordSelectComponent select => new SelectMenuModalSubmission(select.CustomId, select.SubmittedValues ?? []), - - DiscordChannelSelectComponent channel - => new ChannelSelectMenuModalSubmission(channel.CustomId, (channel.SubmittedValues ?? []).Select(ulong.Parse).ToArray()), - - DiscordUserSelectComponent user - => new UserSelectMenuModalSubmission(user.CustomId, (user.SubmittedValues ?? []).Select(ulong.Parse).ToArray()), - - DiscordRoleSelectComponent role - => new RoleSelectMenuModalSubmission(role.CustomId, (role.SubmittedValues ?? []).Select(ulong.Parse).ToArray()), - - DiscordMentionableSelectComponent mentionable - => new MentionableSelectMenuModalSubmission(mentionable.CustomId, (mentionable.SubmittedValues ?? []).Select(ulong.Parse).ToArray()), - - DiscordFileUploadComponent fileUpload - => new FileUploadModalSubmission(fileUpload.CustomId, (fileUpload.Values ?? []) - .Select(x => interaction.Data.Resolved.Attachments[x]) - .ToArray()), - - DiscordRadioGroupComponent radioGroup => new RadioGroupModalSubmission(radioGroup.CustomId, radioGroup.Value!), - - DiscordCheckboxGroupComponent checkboxGroup => new CheckboxGroupModalSubmission(checkboxGroup.CustomId, checkboxGroup.Values!), - - DiscordCheckboxComponent checkbox => new CheckboxModalSubmission(checkbox.CustomId, checkbox.Value!.Value), - - _ => new UnknownComponentModalSubmission(label.Component.Type, label.Component.CustomId, label.Component) - } - ); - } - - this.Values = dict; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/RadioGroupModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/RadioGroupModalSubmission.cs deleted file mode 100644 index 7cb07db8b0..0000000000 --- a/DSharpPlus/EventArgs/Interaction/RadioGroupModalSubmission.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a radio group submitted through a modal. -/// -public class RadioGroupModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.RadioGroup; - - /// - public string CustomId { get; } - - /// - /// The developer-defined value of the option that was chosen from this group. - /// - public string Value { get; } - - internal RadioGroupModalSubmission(string customId, string value) - { - this.CustomId = customId; - this.Value = value; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/RoleSelectMenuModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/RoleSelectMenuModalSubmission.cs deleted file mode 100644 index 12d755e0f6..0000000000 --- a/DSharpPlus/EventArgs/Interaction/RoleSelectMenuModalSubmission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a role select menu submitted through a modal. -/// -public sealed class RoleSelectMenuModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.RoleSelect; - - /// - public string CustomId { get; internal set; } - - /// - /// The snowflake identifiers of the roles submitted. - /// - public IReadOnlyList Ids { get; internal set; } - - internal RoleSelectMenuModalSubmission(string customId, IReadOnlyList ids) - { - this.CustomId = customId; - this.Ids = ids; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/SelectMenuModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/SelectMenuModalSubmission.cs deleted file mode 100644 index b2f5fae9c5..0000000000 --- a/DSharpPlus/EventArgs/Interaction/SelectMenuModalSubmission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a string select menu submitted through a modal. -/// -public sealed class SelectMenuModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.StringSelect; - - /// - public string CustomId { get; internal set; } - - /// - /// The values selected from the menu. - /// - public IReadOnlyList Values { get; internal set; } - - internal SelectMenuModalSubmission(string customId, IReadOnlyList values) - { - this.CustomId = customId; - this.Values = values; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/TextInputModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/TextInputModalSubmission.cs deleted file mode 100644 index a7b2c23608..0000000000 --- a/DSharpPlus/EventArgs/Interaction/TextInputModalSubmission.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about the data submitted through a text input component in a modal. -/// -public sealed class TextInputModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.TextInput; - - /// - public string CustomId { get; internal set; } - - /// - public string Value { get; internal set; } - - internal TextInputModalSubmission(string customId, string value) - { - this.CustomId = customId; - this.Value = value; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/UnknownComponentModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/UnknownComponentModalSubmission.cs deleted file mode 100644 index fe5473fdd3..0000000000 --- a/DSharpPlus/EventArgs/Interaction/UnknownComponentModalSubmission.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -public sealed class UnknownComponentModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType { get; internal set; } - - /// - public string CustomId { get; internal set; } - - /// - /// The received component. Please note that this component will be partial and may not contain the desired data. - /// - public DiscordComponent Component { get; internal set; } - - internal UnknownComponentModalSubmission(DiscordComponentType componentType, string customId, DiscordComponent component) - { - this.ComponentType = componentType; - this.CustomId = customId; - this.Component = component; - } -} diff --git a/DSharpPlus/EventArgs/Interaction/UserSelectMenuModalSubmission.cs b/DSharpPlus/EventArgs/Interaction/UserSelectMenuModalSubmission.cs deleted file mode 100644 index e900fa98ec..0000000000 --- a/DSharpPlus/EventArgs/Interaction/UserSelectMenuModalSubmission.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Provides information about a user select menu submitted through a modal. -/// -public sealed class UserSelectMenuModalSubmission : IModalSubmission -{ - /// - public DiscordComponentType ComponentType => DiscordComponentType.UserSelect; - - /// - public string CustomId { get; internal set; } - - /// - /// The snowflake identifiers of the users submitted. - /// - public IReadOnlyList Ids { get; internal set; } - - internal UserSelectMenuModalSubmission(string customId, IReadOnlyList ids) - { - this.CustomId = customId; - this.Ids = ids; - } -} diff --git a/DSharpPlus/EventArgs/Invite/InviteCreatedEventArgs.cs b/DSharpPlus/EventArgs/Invite/InviteCreatedEventArgs.cs deleted file mode 100644 index daf32c90c6..0000000000 --- a/DSharpPlus/EventArgs/Invite/InviteCreatedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for InviteCreated. -/// -public sealed class InviteCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that created the invite. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the channel that the invite is for. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the created invite. - /// - public DiscordInvite Invite { get; internal set; } - - internal InviteCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Invite/InviteDeletedEventArgs.cs b/DSharpPlus/EventArgs/Invite/InviteDeletedEventArgs.cs deleted file mode 100644 index 80a2c6911d..0000000000 --- a/DSharpPlus/EventArgs/Invite/InviteDeletedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for InviteDeleted -/// -public sealed class InviteDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that deleted the invite. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the channel that the invite was for. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the deleted invite. - /// - public DiscordInvite Invite { get; internal set; } - - internal InviteDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/MessageCreatedEventArgs.cs b/DSharpPlus/EventArgs/Message/MessageCreatedEventArgs.cs deleted file mode 100644 index 21bb113758..0000000000 --- a/DSharpPlus/EventArgs/Message/MessageCreatedEventArgs.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageCreated event. -/// -public class MessageCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the message that was created. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the channel this message belongs to. - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the guild this message belongs to. - /// - public DiscordGuild Guild - => this.Channel.Guild; - - /// - /// Gets the author of the message. - /// - public DiscordUser Author - => this.Message.Author; - - /// - /// Gets the collection of mentioned users. - /// - public IReadOnlyList MentionedUsers { get; internal set; } - - /// - /// Gets the collection of mentioned roles. - /// - public IReadOnlyList MentionedRoles { get; internal set; } - - /// - /// Gets the collection of mentioned channels. - /// - public IReadOnlyList MentionedChannels { get; internal set; } - - internal MessageCreatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/MessageDeletedEventArgs.cs b/DSharpPlus/EventArgs/Message/MessageDeletedEventArgs.cs deleted file mode 100644 index 1f999828c9..0000000000 --- a/DSharpPlus/EventArgs/Message/MessageDeletedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageDeleted event. -/// -public class MessageDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the message that was deleted. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the channel this message belonged to. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the guild this message belonged to. - /// - public DiscordGuild Guild { get; internal set; } - - internal MessageDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/MessagePollCompletedEventArgs.cs b/DSharpPlus/EventArgs/Message/MessagePollCompletedEventArgs.cs deleted file mode 100644 index ad23727edb..0000000000 --- a/DSharpPlus/EventArgs/Message/MessagePollCompletedEventArgs.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when a poll completes and results are available. -/// -public sealed class MessagePollCompletedEventArgs : DiscordEventArgs -{ - /// - /// The message containing the poll results. This will be null if the Message Content intent is not enabled. - /// - public DiscordPollCompletionMessage? PollCompletion { get; internal set; } - - /// - /// Gets the message created by the poll completion. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the channel this message belongs to. - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the guild this message belongs to. - /// - public DiscordGuild Guild - => this.Channel.Guild; -} diff --git a/DSharpPlus/EventArgs/Message/MessagePollVotedEventArgs.cs b/DSharpPlus/EventArgs/Message/MessagePollVotedEventArgs.cs deleted file mode 100644 index 217948963e..0000000000 --- a/DSharpPlus/EventArgs/Message/MessagePollVotedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents an update for a poll vote. -/// -public sealed class MessagePollVotedEventArgs : DiscordEventArgs -{ - /// - /// Gets the vote update. - /// - public DiscordPollVoteUpdate PollVoteUpdate { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Message/MessageUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Message/MessageUpdatedEventArgs.cs deleted file mode 100644 index f66ae72f9f..0000000000 --- a/DSharpPlus/EventArgs/Message/MessageUpdatedEventArgs.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageUpdated event. -/// -public class MessageUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the message that was updated. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the message before it got updated. This property will be null if the message was not cached. - /// - public DiscordMessage? MessageBefore { get; internal set; } - - /// - /// Gets the channel this message belongs to. - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the guild this message belongs to. - /// - public DiscordGuild Guild - => this.Channel.Guild; - - /// - /// Gets the author of the message. - /// - public DiscordUser Author - => this.Message.Author; - - /// - /// Gets the collection of mentioned users. - /// - public IReadOnlyList MentionedUsers { get; internal set; } - - /// - /// Gets the collection of mentioned roles. - /// - /// - /// Only shows the mentioned roles from MessageCreated. EDITS ARE NOT INCLUDED. - /// - public IReadOnlyList MentionedRoles { get; internal set; } - - /// - /// Gets the collection of mentioned channels. - /// - public IReadOnlyList MentionedChannels { get; internal set; } - - internal MessageUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/MessagesBulkDeletedEventArgs.cs b/DSharpPlus/EventArgs/Message/MessagesBulkDeletedEventArgs.cs deleted file mode 100644 index d6d8141f7d..0000000000 --- a/DSharpPlus/EventArgs/Message/MessagesBulkDeletedEventArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessagesBulkDeleted event. -/// -public class MessagesBulkDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets a collection of the deleted messages. - /// - public IReadOnlyList Messages { get; internal set; } - - /// - /// Gets the channel in which the deletion occurred. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the guild in which the deletion occurred. - /// - public DiscordGuild Guild { get; internal set; } - - internal MessagesBulkDeletedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddedEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddedEventArgs.cs deleted file mode 100644 index b9d19228fd..0000000000 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionAddedEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageReactionAdded event. -/// -public class MessageReactionAddedEventArgs : DiscordEventArgs -{ - /// - /// Gets the message for which the update occurred. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the channel to which this message belongs. - /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the guild in which the reaction was added. - /// - public DiscordGuild? Guild { get; internal set; } - - /// - /// Gets the user who created the reaction. - /// This can be cast to a if the reaction was in a guild. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the emoji used for this reaction. - /// - public DiscordEmoji Emoji { get; internal set; } - - internal MessageReactionAddedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemovedEmojiEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemovedEmojiEventArgs.cs deleted file mode 100644 index 967f1cd6fe..0000000000 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemovedEmojiEventArgs.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageReactionRemovedEmoji -/// -public sealed class MessageReactionRemovedEmojiEventArgs : DiscordEventArgs -{ - /// - /// Gets the channel the removed reactions were in. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the guild the removed reactions were in. - /// - public DiscordGuild? Guild { get; internal set; } - - /// - /// Gets the message that had the removed reactions. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the emoji of the reaction that was removed. - /// - public DiscordEmoji Emoji { get; internal set; } - - internal MessageReactionRemovedEmojiEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemovedEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemovedEventArgs.cs deleted file mode 100644 index 6801902bc9..0000000000 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionRemovedEventArgs.cs +++ /dev/null @@ -1,41 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageReactionRemoved event. -/// -public class MessageReactionRemovedEventArgs : DiscordEventArgs -{ - /// - /// Gets the message for which the update occurred. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the channel to which this message belongs. - /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the users whose reaction was removed. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the guild in which the reaction was deleted. - /// - public DiscordGuild? Guild { get; internal set; } - - /// - /// Gets the emoji used for this reaction. - /// - public DiscordEmoji Emoji { get; internal set; } - - internal MessageReactionRemovedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearedEventArgs.cs b/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearedEventArgs.cs deleted file mode 100644 index 8ca3e7a0f1..0000000000 --- a/DSharpPlus/EventArgs/Message/Reaction/MessageReactionsClearedEventArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for MessageReactionsCleared event. -/// -public class MessageReactionsClearedEventArgs : DiscordEventArgs -{ - /// - /// Gets the message for which the update occurred. - /// - public DiscordMessage Message { get; internal set; } - - /// - /// Gets the channel to which this message belongs. - /// - /// - /// This will be null for an uncached channel, which will usually happen for when this event triggers on - /// DM channels in which no prior messages were received or sent. - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the guild in which the reactions were cleared. - /// - public DiscordGuild? Guild - => this.Channel?.Guild; - - internal MessageReactionsClearedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Ratelimit/RatelimitMetadata.cs b/DSharpPlus/EventArgs/Ratelimit/RatelimitMetadata.cs deleted file mode 100644 index 2575bcccf0..0000000000 --- a/DSharpPlus/EventArgs/Ratelimit/RatelimitMetadata.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Represents metadata sent with . The exact type depends on . -/// -public abstract class RatelimitMetadata; diff --git a/DSharpPlus/EventArgs/Ratelimit/RatelimitedEventArgs.cs b/DSharpPlus/EventArgs/Ratelimit/RatelimitedEventArgs.cs deleted file mode 100644 index f87d65b41e..0000000000 --- a/DSharpPlus/EventArgs/Ratelimit/RatelimitedEventArgs.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -using DSharpPlus.Net.Abstractions; - -using Newtonsoft.Json; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired if a request made over the gateway got ratelimited. -/// -public sealed class RatelimitedEventArgs : DiscordEventArgs -{ - /// - /// The opcode of the request that got ratelimited. - /// - [JsonProperty("opcode")] - public GatewayOpCode Opcode { get; private set; } - - /// - /// Indicates how long we need to wait to retry. - /// - [JsonIgnore] - public TimeSpan RetryAfter => TimeSpan.FromSeconds(this.retryAfter); - - [JsonProperty("retry_after")] -#pragma warning disable IDE0044 // Add readonly modifier - set by newtonsoft.json - private float retryAfter = 30.0f; -#pragma warning restore IDE0044 - - /// - /// Additional information about the request that got ratelimited. The exact type depends on :
- /// - corresponds to . - ///
- [JsonIgnore] - public RatelimitMetadata Metadata { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Ratelimit/RequestGuildMembersRatelimitMetadata.cs b/DSharpPlus/EventArgs/Ratelimit/RequestGuildMembersRatelimitMetadata.cs deleted file mode 100644 index 5f165cfac1..0000000000 --- a/DSharpPlus/EventArgs/Ratelimit/RequestGuildMembersRatelimitMetadata.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents metadata for if it was a Request Guild Member operation that got ratelimited. -/// -public sealed class RequestGuildMembersRatelimitMetadata : RatelimitMetadata -{ - /// - /// The ID of the guild the bot attempted to get members for. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; private set; } - - /// - /// The nonce of the original request, to identify which request got ratelimited. - /// - [JsonProperty("nonce")] - public string? Nonce { get; private set; } -} diff --git a/DSharpPlus/EventArgs/Ratelimit/UnknownRatelimitMetadata.cs b/DSharpPlus/EventArgs/Ratelimit/UnknownRatelimitMetadata.cs deleted file mode 100644 index 353f4616ca..0000000000 --- a/DSharpPlus/EventArgs/Ratelimit/UnknownRatelimitMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Represents metadata for opcodes DSharpPlus doesn't know metadata for. -/// -public sealed class UnknownRatelimitMetadata : RatelimitMetadata -{ - /// - /// The JSON payload sent as the metadata object. - /// - public string Json { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/SessionCreatedEventArgs.cs b/DSharpPlus/EventArgs/SessionCreatedEventArgs.cs deleted file mode 100644 index 3be586a968..0000000000 --- a/DSharpPlus/EventArgs/SessionCreatedEventArgs.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for SessionCreated event. -/// -public sealed class SessionCreatedEventArgs : DiscordEventArgs -{ - internal SessionCreatedEventArgs() : base() { } - - /// - /// The ID of the shard this event occurred on. - /// - public int ShardId { get; internal set; } - - /// - /// Gets the IDs of guilds connected to the shard that created this session. Note that DiscordGuild objects may - /// not yet be available by the time this event is fired, and if you require access to any objects, you should wait - /// for GuildDownloadCompleted. - /// - public IReadOnlyList GuildIds { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/SessionResumedEventArgs.cs b/DSharpPlus/EventArgs/SessionResumedEventArgs.cs deleted file mode 100644 index 7f73ed7983..0000000000 --- a/DSharpPlus/EventArgs/SessionResumedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Contains information sent with "resumed" events. -/// -public sealed class SessionResumedEventArgs : DiscordEventArgs -{ - internal SessionResumedEventArgs() : base() { } - - /// - /// The ID of the shard this event occurred on. - /// - public int ShardId { get; internal set; } -} diff --git a/DSharpPlus/EventArgs/Socket/SocketClosedEventArgs.cs b/DSharpPlus/EventArgs/Socket/SocketClosedEventArgs.cs deleted file mode 100644 index 0942bf3ff5..0000000000 --- a/DSharpPlus/EventArgs/Socket/SocketClosedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for SocketClosed event. -/// -public class SocketClosedEventArgs : DiscordEventArgs -{ - /// - /// Gets the close code sent by remote host. - /// - public int CloseCode { get; internal set; } - - /// - /// Gets the close message sent by remote host. - /// - public string CloseMessage { get; internal set; } - - internal SocketClosedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Socket/SocketErrorEventArgs.cs b/DSharpPlus/EventArgs/Socket/SocketErrorEventArgs.cs deleted file mode 100644 index c202ab635f..0000000000 --- a/DSharpPlus/EventArgs/Socket/SocketErrorEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for SocketErrored event. -/// -public class SocketErrorEventArgs : DiscordEventArgs -{ - /// - /// Gets the exception thrown by websocket client. - /// - public Exception Exception { get; internal set; } - - public SocketErrorEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs b/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs deleted file mode 100644 index 06c73ac6b2..0000000000 --- a/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -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/SocketOpenedEventArgs.cs b/DSharpPlus/EventArgs/Socket/SocketOpenedEventArgs.cs deleted file mode 100644 index 40776c240e..0000000000 --- a/DSharpPlus/EventArgs/Socket/SocketOpenedEventArgs.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Contains information pertaining to the underlying socket opening. -/// -public class SocketOpenedEventArgs : SocketEventArgs; diff --git a/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs b/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs deleted file mode 100644 index 343e60f4e8..0000000000 --- a/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs +++ /dev/null @@ -1,43 +0,0 @@ -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/Stage Instance/StageInstanceCreatedEventArgs.cs b/DSharpPlus/EventArgs/Stage Instance/StageInstanceCreatedEventArgs.cs deleted file mode 100644 index 4c8395a78d..0000000000 --- a/DSharpPlus/EventArgs/Stage Instance/StageInstanceCreatedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for StageInstanceCreated. -/// -public class StageInstanceCreatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the stage instance that was created. - /// - public DiscordStageInstance StageInstance { get; internal set; } - - /// - /// Gets the guild the stage instance was created in. - /// - public DiscordGuild Guild - => this.StageInstance.Guild; - - /// - /// Gets the channel the stage instance was created in. - /// - public DiscordChannel Channel - => this.StageInstance.Channel; -} diff --git a/DSharpPlus/EventArgs/Stage Instance/StageInstanceDeletedEventArgs.cs b/DSharpPlus/EventArgs/Stage Instance/StageInstanceDeletedEventArgs.cs deleted file mode 100644 index e949f0cf6d..0000000000 --- a/DSharpPlus/EventArgs/Stage Instance/StageInstanceDeletedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for StageInstanceDeleted. -/// -public class StageInstanceDeletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the stage instance that was deleted. - /// - public DiscordStageInstance StageInstance { get; internal set; } - - /// - /// Gets the guild the stage instance was in. - /// - public DiscordGuild Guild - => this.StageInstance.Guild; - - /// - /// Gets the channel the stage instance was in. - /// - public DiscordChannel Channel - => this.StageInstance.Channel; -} diff --git a/DSharpPlus/EventArgs/Stage Instance/StageInstanceUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Stage Instance/StageInstanceUpdatedEventArgs.cs deleted file mode 100644 index 9638c82986..0000000000 --- a/DSharpPlus/EventArgs/Stage Instance/StageInstanceUpdatedEventArgs.cs +++ /dev/null @@ -1,31 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for StageInstanceUpdated. -/// -public class StageInstanceUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the stage instance before the update. - /// - public DiscordStageInstance StageInstanceBefore { get; internal set; } - - /// - /// Gets the stage instance after the update. - /// - public DiscordStageInstance StageInstanceAfter { get; internal set; } - - /// - /// Gets the guild the stage instance is in. - /// - public DiscordGuild Guild - => this.StageInstanceAfter.Guild; - - /// - /// Gets the channel the stage instance is in. - /// - public DiscordChannel Channel - => this.StageInstanceAfter.Channel; -} diff --git a/DSharpPlus/EventArgs/TypingStartedEventArgs.cs b/DSharpPlus/EventArgs/TypingStartedEventArgs.cs deleted file mode 100644 index 32c9185b21..0000000000 --- a/DSharpPlus/EventArgs/TypingStartedEventArgs.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for TypingStarted event. -/// -public class TypingStartedEventArgs : DiscordEventArgs -{ - /// - /// Gets the channel in which the indicator was triggered. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the user that started typing. - /// This can be cast to a if the typing occurred in a guild. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the guild in which the indicator was triggered. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the date and time at which the user started typing. - /// - public DateTimeOffset StartedAt { get; internal set; } - - internal TypingStartedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/UnknownEventArgs.cs b/DSharpPlus/EventArgs/UnknownEventArgs.cs deleted file mode 100644 index e07ab0b726..0000000000 --- a/DSharpPlus/EventArgs/UnknownEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DSharpPlus.EventArgs; - - -/// -/// Represents arguments for UnknownEvent. -/// -public class UnknownEventArgs : DiscordEventArgs -{ - /// - /// Gets the event's name. - /// - public string EventName { get; internal set; } - - /// - /// Gets the event's data. - /// - public string Json { get; internal set; } - - internal UnknownEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/User/PresenceUpdatedEventArgs.cs b/DSharpPlus/EventArgs/User/PresenceUpdatedEventArgs.cs deleted file mode 100644 index 9fceaf9ba1..0000000000 --- a/DSharpPlus/EventArgs/User/PresenceUpdatedEventArgs.cs +++ /dev/null @@ -1,46 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for PresenceUpdated event. -/// -public class PresenceUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the user whose presence was updated. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the user's new game. - /// - public DiscordActivity Activity { get; internal set; } - - /// - /// Gets the user's status. - /// - public DiscordUserStatus Status { get; internal set; } - - /// - /// Gets the user's old presence. - /// - public DiscordPresence PresenceBefore { get; internal set; } - - /// - /// Gets the user's new presence. - /// - public DiscordPresence PresenceAfter { get; internal set; } - - /// - /// Gets the user prior to presence update. - /// - public DiscordUser UserBefore { get; internal set; } - - /// - /// Gets the user after the presence update. - /// - public DiscordUser UserAfter { get; internal set; } - - internal PresenceUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/User/UserSettingsUpdatedEventArgs.cs b/DSharpPlus/EventArgs/User/UserSettingsUpdatedEventArgs.cs deleted file mode 100644 index decb3ae59b..0000000000 --- a/DSharpPlus/EventArgs/User/UserSettingsUpdatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for UserSettingsUpdated event. -/// -public class UserSettingsUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the user whose settings were updated. - /// - public DiscordUser User { get; internal set; } - - internal UserSettingsUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs b/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs deleted file mode 100644 index d6e5eeb2e7..0000000000 --- a/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -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/User/UserUpdatedEventArgs.cs b/DSharpPlus/EventArgs/User/UserUpdatedEventArgs.cs deleted file mode 100644 index b2d920d524..0000000000 --- a/DSharpPlus/EventArgs/User/UserUpdatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for UserUpdated event. -/// -public class UserUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the post-update user. - /// - public DiscordUser UserAfter { get; internal set; } - - /// - /// Gets the pre-update user. - /// - public DiscordUser UserBefore { get; internal set; } - - internal UserUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Voice/VoiceServerUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Voice/VoiceServerUpdatedEventArgs.cs deleted file mode 100644 index f6a78fafd5..0000000000 --- a/DSharpPlus/EventArgs/Voice/VoiceServerUpdatedEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for VoiceServerUpdated event. -/// -public class VoiceServerUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild for which the update occurred. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the new voice endpoint. - /// - public string Endpoint { get; internal set; } - - /// - /// Gets the voice connection token. - /// - public string VoiceToken { get; internal set; } - - internal VoiceServerUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs deleted file mode 100644 index f082b40cd9..0000000000 --- a/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for VoiceStateUpdated event. -/// -public class VoiceStateUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the ID of the user whose voice state was updated. - /// - public ulong UserId => this.After.UserId; - - /// - /// Gets the ID of the channel this user is now connected to. - /// - public ulong? ChannelId => this.After.ChannelId; - - /// - /// Gets the ID of the guild this voice state update is associated with. - /// - public ulong? GuildId => this.After.GuildId; - - /// - /// 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. - /// - public DiscordVoiceState Before { get; internal set; } - - /// - /// Gets the voice state post-update. - /// - public DiscordVoiceState After { get; internal set; } - - /// - /// Gets the ID of voice session. - /// - public string SessionId { get; internal set; } - - internal VoiceStateUpdatedEventArgs() { } -} diff --git a/DSharpPlus/EventArgs/WebhooksUpdatedEventArgs.cs b/DSharpPlus/EventArgs/WebhooksUpdatedEventArgs.cs deleted file mode 100644 index 5e121ed39f..0000000000 --- a/DSharpPlus/EventArgs/WebhooksUpdatedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments to the WebhooksUpdated event. -/// -public class WebhooksUpdatedEventArgs : DiscordEventArgs -{ - /// - /// Gets the guild that had its webhooks updated. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the channel to which the webhook belongs to. - /// - public DiscordChannel Channel { get; internal set; } - - internal WebhooksUpdatedEventArgs() : base() { } -} diff --git a/DSharpPlus/EventArgs/ZombiedEventArgs.cs b/DSharpPlus/EventArgs/ZombiedEventArgs.cs deleted file mode 100644 index 44fefef015..0000000000 --- a/DSharpPlus/EventArgs/ZombiedEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DSharpPlus.EventArgs; - - -/// -/// Represents arguments for the Zombied event. -/// -public class ZombiedEventArgs : DiscordEventArgs -{ - /// - /// Gets how many heartbeat failures have occured. - /// - public int Failures { get; internal set; } - - /// - /// Gets whether the zombie event occured whilst guilds are downloading. - /// - public bool GuildDownloadCompleted { get; internal set; } - - internal ZombiedEventArgs() : base() { } -} diff --git a/DSharpPlus/Exceptions/BadRequestException.cs b/DSharpPlus/Exceptions/BadRequestException.cs deleted file mode 100644 index dd5d189286..0000000000 --- a/DSharpPlus/Exceptions/BadRequestException.cs +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index f1cca60da2..0000000000 --- a/DSharpPlus/Exceptions/BulkDeleteFailedException.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 792081d097..0000000000 --- a/DSharpPlus/Exceptions/DiscordException.cs +++ /dev/null @@ -1,26 +0,0 @@ -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/GatewayRatelimitedException.cs b/DSharpPlus/Exceptions/GatewayRatelimitedException.cs deleted file mode 100644 index 1dbfd2f26e..0000000000 --- a/DSharpPlus/Exceptions/GatewayRatelimitedException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace DSharpPlus.Exceptions; - -/// -/// Thrown if a request made to the gateway was ratelimited. -/// -public sealed class GatewayRatelimitedException : Exception -{ - /// - /// Indicates how long to wait until retrying the request. - /// - public TimeSpan RetryAfter { get; private set; } - - public GatewayRatelimitedException(TimeSpan retryAfter) - : base("The attempted operation was ratelimited.") - => this.RetryAfter = retryAfter; -} diff --git a/DSharpPlus/Exceptions/NotFoundException.cs b/DSharpPlus/Exceptions/NotFoundException.cs deleted file mode 100644 index 4552a690f1..0000000000 --- a/DSharpPlus/Exceptions/NotFoundException.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 901a8a6934..0000000000 --- a/DSharpPlus/Exceptions/RateLimitException.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 3b39ec3986..0000000000 --- a/DSharpPlus/Exceptions/RequestSizeException.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index a5a41f6391..0000000000 --- a/DSharpPlus/Exceptions/ServerErrorException.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index f4158d6a13..0000000000 --- a/DSharpPlus/Exceptions/UnauthorizedException.cs +++ /dev/null @@ -1,33 +0,0 @@ -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/ModalSubmissionExtensions.cs b/DSharpPlus/Extensions/ModalSubmissionExtensions.cs deleted file mode 100644 index 7991280801..0000000000 --- a/DSharpPlus/Extensions/ModalSubmissionExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Extensions; - -/// -/// Provides convenience extensions for extracting information from modal submissions. -/// -public static class ModalSubmissionExtensions -{ - /// - /// Gets the component submission associated with a particular custom ID, returns null if either the ID didn't exist or pointed to a different type. - /// - /// The type of the component submission to extract. - /// The modal submission dictionary to extract from. - /// The custom ID of the component. - public static T? GetComponentSubmission(this IReadOnlyDictionary submissions, string customId) - where T : IModalSubmission - { - _ = TryGetComponentSubmission(submissions, customId, out T? candidate); - return candidate; - } - - /// - /// Gets the component submission associated with a particular custom ID, returns false if either the ID didn't exist or pointed to a different type. - /// - /// The type of the component submission to extract. - /// The modal submission dictionary to extract from. - /// The custom ID of the component. - /// The value of the submission, or null if it couldn't be found - public static bool TryGetComponentSubmission - ( - this IReadOnlyDictionary submissions, - string customId, - - [NotNullWhen(true)] - out T? value - ) - where T : IModalSubmission - { - if (submissions.TryGetValue(customId, out IModalSubmission? candidate) && candidate is T submission) - { - value = submission; - return true; - } - - value = default; - return false; - } -} diff --git a/DSharpPlus/Extensions/ServiceCollectionExtensions.Events.cs b/DSharpPlus/Extensions/ServiceCollectionExtensions.Events.cs deleted file mode 100644 index ecdddc4841..0000000000 --- a/DSharpPlus/Extensions/ServiceCollectionExtensions.Events.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Extensions; - -public static partial class ServiceCollectionExtensions -{ - /// - /// Configures event handlers on the present service collection. - /// - /// The service collection to add event handlers to. - /// A configuration delegate enabling specific configuration. - /// The service collection for chaining. - public static IServiceCollection ConfigureEventHandlers - ( - this IServiceCollection services, - Action configure - ) - { - EventHandlingBuilder builder = new(services); - - configure(builder); - - return services; - } -} diff --git a/DSharpPlus/Extensions/ServiceCollectionExtensions.InternalSetup.cs b/DSharpPlus/Extensions/ServiceCollectionExtensions.InternalSetup.cs deleted file mode 100644 index 03ea09c119..0000000000 --- a/DSharpPlus/Extensions/ServiceCollectionExtensions.InternalSetup.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Channels; - -using DSharpPlus.Clients; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Gateway; -using DSharpPlus.Net.InboundWebhooks; -using DSharpPlus.Net.InboundWebhooks.Transport; -using DSharpPlus.Net.Gateway.Compression; -using DSharpPlus.Net.Gateway.Compression.Zlib; -using DSharpPlus.Net.Gateway.Compression.Zstd; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Extensions; - -public static partial class ServiceCollectionExtensions -{ - internal static IServiceCollection AddDSharpPlusDefaultsSingleShard - ( - this IServiceCollection serviceCollection, - DiscordIntents intents - ) - { - serviceCollection.AddDSharpPlusServices(intents) - .AddSingleton(); - - return serviceCollection; - } - - internal static IServiceCollection AddDSharpPlusDefaultsMultiShard - ( - this IServiceCollection serviceCollection, - DiscordIntents intents - ) - { - serviceCollection.AddDSharpPlusServices(intents) - .AddSingleton(); - - return serviceCollection; - } - - internal static IServiceCollection AddDSharpPlusServices - ( - this IServiceCollection serviceCollection, - DiscordIntents intents - ) - { - // peripheral setup - serviceCollection.AddMemoryCache() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - // rest setup - serviceCollection.AddHttpClient("DSharpPlus.Rest.HttpClient") - .UseSocketsHttpHandler((handler, _) => handler.PooledConnectionLifetime = TimeSpan.FromMinutes(30)) - .SetHandlerLifetime(Timeout.InfiniteTimeSpan) - .ConfigureHttpClient(client => - { - client.BaseAddress = new Uri(Utilities.GetApiBaseUri()); - client.Timeout = Timeout.InfiniteTimeSpan; - client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); - client.BaseAddress = new(Endpoints.BASE_URI); - }); - - serviceCollection.AddTransient() - .AddSingleton() - .AddTransient(); - - // gateway setup - serviceCollection.Configure(c => c.Intents = intents) - .AddKeyedSingleton("DSharpPlus.Gateway.EventChannel", Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true })) - .AddTransient() - .AddTransient() - .RegisterBestDecompressor() - .AddSingleton() - .AddSingleton(); - - // http events/interactions, if we're using those - doesn't actually cause any overhead if we aren't - serviceCollection.AddKeyedSingleton("DSharpPlus.Webhooks.EventChannel", Channel.CreateUnbounded - ( - new UnboundedChannelOptions - { - SingleReader = true - } - )) - .AddKeyedSingleton("DSharpPlus.Interactions.EventChannel", Channel.CreateUnbounded - ( - new UnboundedChannelOptions - { - SingleReader = true - } - )) - .AddSingleton() - .AddSingleton(); - - return serviceCollection; - } - - private static IServiceCollection RegisterBestDecompressor(this IServiceCollection services) - { - if (NativeLibrary.TryLoad("libzstd", Assembly.GetEntryAssembly(), default, out _)) - { - services.AddTransient(); - } - else - { - services.AddTransient(); - } - - return services; - } -} diff --git a/DSharpPlus/Extensions/ServiceCollectionExtensions.cs b/DSharpPlus/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 4803edb6f9..0000000000 --- a/DSharpPlus/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,235 +0,0 @@ -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.AddDSharpPlusDefaultsMultiShard(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 deleted file mode 100644 index 44a7865f26..0000000000 --- a/DSharpPlus/Formatter.cs +++ /dev/null @@ -1,225 +0,0 @@ -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/IClientErrorHandler.cs b/DSharpPlus/IClientErrorHandler.cs deleted file mode 100644 index 56d0f89937..0000000000 --- a/DSharpPlus/IClientErrorHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus; - -/// -/// Represents a contract for handling errors the DSharpPlus core library may raise. -/// -public interface IClientErrorHandler -{ - /// - /// Handles an error that occurred in an event handler. - /// - /// The name of the event. - /// The exception thrown. - /// The delegate that was invoked. - /// The object that dispatched this event. - /// The arguments passed to this event. - public ValueTask HandleEventHandlerError - ( - string name, - Exception exception, - Delegate invokedDelegate, - object sender, - object args - ); - - /// - /// Handles a gateway error of any kind. - /// - /// The exception that occurred. - public ValueTask HandleGatewayError(Exception exception); -} diff --git a/DSharpPlus/IEventHandler.cs b/DSharpPlus/IEventHandler.cs deleted file mode 100644 index 7ca264e2c8..0000000000 --- a/DSharpPlus/IEventHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.ComponentModel; -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus; - -/// -/// Don't touch this. -/// -// an internal marker interface for event handlers. -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IEventHandler; - -/// -/// Represents a base interface for an event handler. -/// -/// The type of event this handler is supposed to handle. -public interface IEventHandler : IEventHandler - where TEventArgs : DiscordEventArgs -{ - /// - /// Handles the provided event asynchronously. - /// - /// The DiscordClient this event originates from. - /// Any additional information pertaining to this event. - public Task HandleEventAsync(DiscordClient sender, TEventArgs eventArgs); -} diff --git a/DSharpPlus/IMessageCacheProvider.cs b/DSharpPlus/IMessageCacheProvider.cs deleted file mode 100644 index c470655fe0..0000000000 --- a/DSharpPlus/IMessageCacheProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -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/InlineMediaTool.cs b/DSharpPlus/InlineMediaTool.cs deleted file mode 100644 index 86dc136883..0000000000 --- a/DSharpPlus/InlineMediaTool.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Buffers; -using System.Buffers.Text; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace DSharpPlus; - -/// -/// Tool to detect image formats and convert from binary data to base64 strings. -/// -public sealed class InlineMediaTool : IDisposable -{ - private const int MAX_SIGNATURE_LENGTH = 16; - private const byte WILDCARD_BYTE = 0x11; - - private record FileSignature(MediaFormat Format, byte[] MagicBytes); - - private static readonly IReadOnlyList knownSignatures = - [ - new(MediaFormat.Png, [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), - new(MediaFormat.Gif, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]), - new(MediaFormat.Gif, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]), - new(MediaFormat.Jpeg, [0xFF, 0xD8, 0xFF, 0xDB]), - new(MediaFormat.Jpeg, [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01]), - new(MediaFormat.Jpeg, [0xFF, 0xD8, 0xFF, 0xEE]), - new(MediaFormat.Jpeg, [0xFF, 0xD8, 0xFF, 0xE1, WILDCARD_BYTE, WILDCARD_BYTE, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00]), - new(MediaFormat.Jpeg, [0xFF, 0xD8, 0xFF, 0xE0]), - new(MediaFormat.WebP, [0x52, 0x49, 0x46, 0x46, WILDCARD_BYTE, WILDCARD_BYTE, WILDCARD_BYTE, WILDCARD_BYTE, 0x57, 0x45, 0x42, 0x50]), - new(MediaFormat.Avif, [0x00, 0x00, 0x00, WILDCARD_BYTE, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66, 0x00, 0x00, 0x00, 0x00]), - new(MediaFormat.Ogg, [0x4F, 0x67, 0x67, 0x53]), - new(MediaFormat.Mp3, [0xFF, 0xFB]), - new(MediaFormat.Mp3, [0xFF, 0xF3]), - new(MediaFormat.Mp3, [0xFF, 0xF2]), - new(MediaFormat.Mp3, [0x49, 0x44, 0x33]) - ]; - - /// - /// Gets the stream this tool is operating on. - /// - public Stream SourceStream { get; } - - private MediaFormat format; - - /// - /// Creates a new media tool from given stream. - /// - /// Stream to work with. - public InlineMediaTool(Stream stream) - { - ArgumentNullException.ThrowIfNull(stream); - - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("The stream needs to be both readable and seekable.", nameof(stream)); - } - - this.SourceStream = stream; - this.SourceStream.Seek(0, SeekOrigin.Begin); - - if (stream.Length < MAX_SIGNATURE_LENGTH) - { - throw new InvalidDataException("The stream is too short to be valid media data."); - } - - this.format = MediaFormat.Unknown; - } - - /// - /// Detects the format of this media item. - /// - /// Detected format. - public MediaFormat GetFormat() - { - if (this.format != MediaFormat.Unknown) - { - return this.format; - } - - long originalPosition = this.SourceStream.Position; - this.SourceStream.Seek(0, SeekOrigin.Begin); - - Span first16 = stackalloc byte[MAX_SIGNATURE_LENGTH]; - this.SourceStream.ReadExactly(first16); - - try - { - foreach (FileSignature sig in knownSignatures) - { - bool match = true; - - for (int i = 0; i < sig.MagicBytes.Length; i++) - { - if (sig.MagicBytes[i] == WILDCARD_BYTE) - { - continue; - } - - if (first16[i] != sig.MagicBytes[i]) - { - match = false; - break; - } - } - - if (match) - { - return this.format = sig.Format; - } - } - - throw new InvalidDataException("The data within the stream was not valid media data."); - } - finally - { - this.SourceStream.Seek(originalPosition, SeekOrigin.Begin); - } - } - - /// - /// Converts this image into base64 data format string. - /// - /// Data-scheme base64 string. - public string GetBase64() - { - const int readLength = 12288; - const int writeLength = 16384; - - MediaFormat fmt = GetFormat(); - - int contentLength = Base64.GetMaxEncodedToUtf8Length((int)this.SourceStream.Length); - - int formatLength = fmt.ToString().Length; - - byte[] b64Buffer = ArrayPool.Shared.Rent(formatLength + contentLength + 19); - byte[] readBufferBacking = ArrayPool.Shared.Rent(readLength); - - Span readBuffer = readBufferBacking.AsSpan()[..readLength]; - - int processed = 0; - int totalWritten = 0; - - (fmt switch - { - MediaFormat.Png => "data:image/png;base64,"u8, - MediaFormat.Jpeg => "data:image/jpeg;base64,"u8, - MediaFormat.Gif => "data:image/gif;base64,"u8, - MediaFormat.WebP => "data:image/webp;base64,"u8, - MediaFormat.Avif => "data:image/avif;base64,"u8, - MediaFormat.Ogg => "data:audio/ogg;base64,"u8, - MediaFormat.Mp3 => "data:audio/mp3;base64,"u8, - MediaFormat.Auto => "data:image/auto;base64,"u8, - _ => "data:image/unknown;base64"u8 - }).CopyTo(b64Buffer); - - totalWritten += 19; - totalWritten += formatLength; - - while (processed < this.SourceStream.Length - readLength) - { - this.SourceStream.ReadExactly(readBuffer); - - Base64.EncodeToUtf8(readBuffer, b64Buffer.AsSpan().Slice(totalWritten, writeLength), out int _, out int written, false); - - processed += readLength; - totalWritten += written; - } - - int remainingLength = (int)this.SourceStream.Length - processed; - - this.SourceStream.ReadExactly(readBufferBacking, 0, remainingLength); - - Base64.EncodeToUtf8(readBufferBacking.AsSpan()[..remainingLength], b64Buffer.AsSpan()[totalWritten..], out int _, out int lastWritten); - - string value = Encoding.UTF8.GetString(b64Buffer.AsSpan()[..(totalWritten + lastWritten)]); - - ArrayPool.Shared.Return(b64Buffer); - ArrayPool.Shared.Return(readBufferBacking); - - return value; - } - - /// - /// Disposes this media tool. - /// - public void Dispose() => this.SourceStream?.Dispose(); -} - -/// -/// Represents format of an inline media item. -/// -public enum MediaFormat : int -{ - Png, - Gif, - Jpeg, - WebP, - Avif, - Ogg, - Mp3, - Auto, - Unknown, -} diff --git a/DSharpPlus/Logging/AnonymizationUtilities.cs b/DSharpPlus/Logging/AnonymizationUtilities.cs deleted file mode 100644 index 65c0af209d..0000000000 --- a/DSharpPlus/Logging/AnonymizationUtilities.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Text.RegularExpressions; - -namespace DSharpPlus.Logging; - -internal static partial class AnonymizationUtilities -{ - [GeneratedRegex(@"""token"":""[a-zA-Z0-9_\-\. ]+""")] - private static partial Regex GetJsonEncodedTokenRegex(); - - [GeneratedRegex(@"\/webhooks\/[0-9]+\/[a-zA-Z0-9\-\. ]+\/")] - private static partial Regex GetWebhookPathRegex(); - - public static string AnonymizeTokens(string input) - { - string intermediate = GetJsonEncodedTokenRegex().Replace(input, "\"token\":\"\""); - intermediate = GetWebhookPathRegex().Replace(intermediate, "/webhooks//"); - return intermediate; - } - - // -------------------------------------------------------------------------------------------------- - - [GeneratedRegex(@"""[0-9]{17,22}""")] - private static partial Regex GetSnowflakeRegex(); - - [GeneratedRegex(@"""content"":""[^""]+""")] - private static partial Regex GetMessageContentRegex(); - - [GeneratedRegex(@"""username"":""[a-zA-Z0-9\.\-_]{3,32}""")] - private static partial Regex GetUsernameRegex(); - - public static string AnonymizeContents(string input) - { - string intermediate = GetSnowflakeRegex().Replace(input, "\"\""); - intermediate = GetMessageContentRegex().Replace(intermediate, "\"content\":\"\""); - intermediate = GetUsernameRegex().Replace(intermediate, "\"username\":\"\""); - return intermediate; - } - - // -------------------------------------------------------------------------------------------------- - - public static string Anonymize(string input) - { - string anonymized = input; - - if (RuntimeFeatures.AnonymizeTokens) - { - anonymized = AnonymizeTokens(anonymized); - } - - if (RuntimeFeatures.AnonymizeContents) - { - anonymized = AnonymizeContents(anonymized); - } - - return anonymized; - } -} diff --git a/DSharpPlus/Logging/CompositeDefaultLogger.cs b/DSharpPlus/Logging/CompositeDefaultLogger.cs deleted file mode 100644 index be2bd97e57..0000000000 --- a/DSharpPlus/Logging/CompositeDefaultLogger.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100755 index d6372e687c..0000000000 --- a/DSharpPlus/Logging/DefaultLogger.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Threading; -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 Lock @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; - } - - if (this.name.StartsWith("System.Net.Http")) - { - 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 deleted file mode 100644 index 99669f180a..0000000000 --- a/DSharpPlus/Logging/DefaultLoggerFactory.cs +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index d5c55a8e51..0000000000 --- a/DSharpPlus/Logging/DefaultLoggerProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index fed4845b7b..0000000000 --- a/DSharpPlus/Logging/LoggerEvents.cs +++ /dev/null @@ -1,128 +0,0 @@ -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/Logging/RuntimeFeatures.cs b/DSharpPlus/Logging/RuntimeFeatures.cs deleted file mode 100644 index 8ebf541111..0000000000 --- a/DSharpPlus/Logging/RuntimeFeatures.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace DSharpPlus.Logging; - -/// -/// Contains runtime feature switches for trace logging. -/// -public static class RuntimeFeatures -{ - /// - /// Specifies whether bot and webhook tokens are being anonymized. Defaults to true. - /// - public static bool AnonymizeTokens - => !AppContext.TryGetSwitch("DSharpPlus.Trace.AnonymizeTokens", out bool value) || value; - - /// - /// Specifies whether snowflake IDs and message contents are being anonymized. Defaults to false. - /// - /// - /// Note that enabling this switch may significantly reduce the quality of debugging data. - /// - public static bool AnonymizeContents - => AppContext.TryGetSwitch("DSharpPlus.Trace.AnonymizeContents", out bool value) && value; - - /// - /// Specifies whether rest requests should be logged. Defaults to true. - /// - public static bool EnableRestRequestLogging - => !AppContext.TryGetSwitch("DSharpPlus.Trace.EnableRestRequestLogging", out bool value) || value; - - /// - /// Specifies whether inbound gateway payloads should be logged. Defaults to true. - /// - public static bool EnableInboundGatewayLogging - => !AppContext.TryGetSwitch("DSharpPlus.Trace.EnableInboundGatewayLogging", out bool value) || value; - - /// - /// Specifies whether outbound gateway payloads should be logged. Defaults to true. - /// - public static bool EnableOutboundGatewayLogging - => !AppContext.TryGetSwitch("DSharpPlus.Trace.EnableOutboundGatewayLogging", out bool value) || value; -} diff --git a/DSharpPlus/MessageCache.cs b/DSharpPlus/MessageCache.cs deleted file mode 100644 index e326449f2f..0000000000 --- a/DSharpPlus/MessageCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 5da071f004..0000000000 --- a/DSharpPlus/Metrics/LiveRequestMetrics.cs +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index fe1c5173ad..0000000000 --- a/DSharpPlus/Metrics/RequestMetricsCollection.cs +++ /dev/null @@ -1,178 +0,0 @@ -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 deleted file mode 100644 index 102ce55cea..0000000000 --- a/DSharpPlus/Metrics/RequestMetricsContainer.cs +++ /dev/null @@ -1,132 +0,0 @@ -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 deleted file mode 100644 index 94405e1639..0000000000 --- a/DSharpPlus/Net/Abstractions/AuditLogAbstractions.cs +++ /dev/null @@ -1,150 +0,0 @@ -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 deleted file mode 100644 index f855715cbf..0000000000 --- a/DSharpPlus/Net/Abstractions/ClientProperties.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 3c7cc2f8cb..0000000000 --- a/DSharpPlus/Net/Abstractions/FollowedChannelAddPayload.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index f331bbcb22..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayHello.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index db0a8b852a..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayIdentifyResume.cs +++ /dev/null @@ -1,75 +0,0 @@ -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 deleted file mode 100644 index 30fd9c6740..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index d8bfdfaf64..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayOpCode.cs +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index 59105d83ef..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayPayload.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 68d12eb8d5..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs +++ /dev/null @@ -1,28 +0,0 @@ -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/Gateway/ShardIdContainingGatewayPayload.cs b/DSharpPlus/Net/Abstractions/Gateway/ShardIdContainingGatewayPayload.cs deleted file mode 100644 index 42ad7624f0..0000000000 --- a/DSharpPlus/Net/Abstractions/Gateway/ShardIdContainingGatewayPayload.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Internal implementation detail to communicate shard IDs between IGatewayClient and dispatch. -/// -internal sealed class ShardIdContainingGatewayPayload : GatewayPayload -{ - /// - /// Gets the shard ID that invoked this event. - /// - [JsonIgnore] - public int ShardId { get; internal set; } -} diff --git a/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs b/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs deleted file mode 100644 index f542433fed..0000000000 --- a/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index e712e2a809..0000000000 --- a/DSharpPlus/Net/Abstractions/PollCreatePayload.cs +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 819d9a4ca9..0000000000 --- a/DSharpPlus/Net/Abstractions/ReadyPayload.cs +++ /dev/null @@ -1,59 +0,0 @@ -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/DiscordChannelPosition.cs b/DSharpPlus/Net/Abstractions/Rest/DiscordChannelPosition.cs deleted file mode 100644 index 2edd11b614..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/DiscordChannelPosition.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions.Rest; - -public sealed class DiscordChannelPosition -{ - [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; } -} diff --git a/DSharpPlus/Net/Abstractions/Rest/DiscordRolePosition.cs b/DSharpPlus/Net/Abstractions/Rest/DiscordRolePosition.cs deleted file mode 100644 index f2506494b2..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/DiscordRolePosition.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions.Rest; - -public sealed class DiscordRolePosition -{ - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong RoleId { get; set; } - - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; set; } -} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs deleted file mode 100644 index 86ea6d612b..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 17cfaa1f04..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs +++ /dev/null @@ -1,333 +0,0 @@ -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; } - [JsonProperty("role_ids", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? RoleIds { 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/RestCreateTestEntitlementPayload.cs b/DSharpPlus/Net/Abstractions/Rest/RestCreateTestEntitlementPayload.cs deleted file mode 100644 index aa2c36cb17..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestCreateTestEntitlementPayload.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal class RestCreateTestEntitlementPayload -{ - [JsonProperty("sku_id")] - public ulong SkuId { get; set; } - - [JsonProperty("owner_id")] - public ulong OwnerId { get; set; } - - [JsonProperty("owner_type")] - public DiscordTestEntitlementOwnerType OwnerType { get; set; } -} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs deleted file mode 100644 index 6ac85d29bb..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs +++ /dev/null @@ -1,379 +0,0 @@ -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 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; } - - [JsonProperty("banner")] - public Optional Banner { get; set; } - - [JsonProperty("avatar")] - public Optional Avatar { get; set; } - - [JsonProperty("bio")] - public Optional Bio { 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 deleted file mode 100644 index 98080a6d3a..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestStickerPayloads.cs +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 2398d0bbaf..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 802253c079..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 5a7625bc5b..0000000000 --- a/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 91626b4bef..0000000000 --- a/DSharpPlus/Net/Abstractions/ShardInfo.cs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 915fa0eb88..0000000000 --- a/DSharpPlus/Net/Abstractions/StatusUpdate.cs +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 273e5b2d9b..0000000000 --- a/DSharpPlus/Net/Abstractions/Transport/TransportActivity.cs +++ /dev/null @@ -1,275 +0,0 @@ -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 deleted file mode 100644 index cdd71d80f5..0000000000 --- a/DSharpPlus/Net/Abstractions/Transport/TransportApplication.cs +++ /dev/null @@ -1,94 +0,0 @@ -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 deleted file mode 100644 index ad390f023a..0000000000 --- a/DSharpPlus/Net/Abstractions/Transport/TransportMember.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 532c98f489..0000000000 --- a/DSharpPlus/Net/Abstractions/Transport/TransportTeam.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index cb032d669d..0000000000 --- a/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs +++ /dev/null @@ -1,77 +0,0 @@ -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; } - - [JsonProperty("primary_guild", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUserPrimaryGuild? PrimaryGuild { 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; - this.PrimaryGuild = other.PrimaryGuild; - } -} diff --git a/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs b/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs deleted file mode 100644 index 12dbc49184..0000000000 --- a/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index a4f88d3276..0000000000 --- a/DSharpPlus/Net/ConnectionEndpoint.cs +++ /dev/null @@ -1,60 +0,0 @@ -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/Gateway/Compression/IPayloadDecompressor.cs b/DSharpPlus/Net/Gateway/Compression/IPayloadDecompressor.cs deleted file mode 100644 index 8212e309a8..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/IPayloadDecompressor.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression; - -/// -/// Contains functionality for decompressing inbound gateway payloads. -/// -public interface IPayloadDecompressor : IDisposable -{ - /// - /// Gets the name of the decompressor. - /// - public string? Name { get; } - - /// - /// Indicates whether the present compression format is connection-wide. - /// - public bool IsTransportCompression { get; } - - /// - /// Attempts to decompress the provided payload. - /// - /// The raw, compressed data. - /// A buffer writer for writing decompressed data. - /// A value indicating whether the operation was successful. - public bool TryDecompress(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed); - - /// - /// Initializes the decompressor for a new connection. - /// - public void Initialize(); - - /// - /// Frees and destroys all internal state when a connection has terminated. - /// - public void Reset(); -} diff --git a/DSharpPlus/Net/Gateway/Compression/NullDecompressor.cs b/DSharpPlus/Net/Gateway/Compression/NullDecompressor.cs deleted file mode 100644 index e9ff3cc71d..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/NullDecompressor.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -using CommunityToolkit.HighPerformance; -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression; - -/// -/// Represents a decompressor that doesn't decompress at all. -/// -public sealed class NullDecompressor : IPayloadDecompressor -{ - /// - public string? Name => null; - - // this decompressor *technically* applies transport-wide, and this simplifies composing the IDENTIFY payload. - /// - public bool IsTransportCompression => true; - - /// - public bool TryDecompress(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed) - { - decompressed.Write(compressed); - return true; - } - - /// - public void Dispose() - { - - } - - /// - public void Reset() - { - - } - - /// - public void Initialize() - { - - } -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibPayloadDecompressor.cs b/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibPayloadDecompressor.cs deleted file mode 100644 index 10a2a5e6a9..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibPayloadDecompressor.cs +++ /dev/null @@ -1,52 +0,0 @@ -#pragma warning disable IDE0046 - -using System; - -using CommunityToolkit.HighPerformance; -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression.Zlib; - -/// -/// A payload decompressor using zlib on the payload level. -/// -public sealed class ZlibPayloadDecompressor : IPayloadDecompressor -{ - /// - public string? Name => null; - - /// - public bool IsTransportCompression => false; - - /// - public bool TryDecompress(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed) - { - using ZlibWrapper wrapper = new(); - - if (!wrapper.TryInflate(compressed, decompressed)) - { - decompressed.Clear(); - decompressed.Write(compressed); - } - - return true; - } - - /// - public void Dispose() - { - - } - - /// - public void Reset() - { - - } - - /// - public void Initialize() - { - - } -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibStreamDecompressor.cs b/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibStreamDecompressor.cs deleted file mode 100644 index cdfbd18732..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibStreamDecompressor.cs +++ /dev/null @@ -1,61 +0,0 @@ -#pragma warning disable IDE0046 - -using System; - -using CommunityToolkit.HighPerformance; -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression.Zlib; - -/// -/// A payload decompressor using zlib on the transport level. -/// -public sealed class ZlibStreamDecompressor : IPayloadDecompressor -{ - private ZlibWrapper wrapper = new(); - - /// - public string Name => "zlib-stream"; - - /// - public bool IsTransportCompression => true; - - /// - public bool TryDecompress(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed) - { - if (!this.wrapper.TryInflate(compressed, decompressed)) - { - decompressed.Clear(); - decompressed.Write(compressed); - } - - return true; - } - - /// - public void Dispose() - { - if (this.wrapper != default) - { - this.wrapper.Dispose(); - } - } - - /// - public void Reset() - { - this.wrapper.Dispose(); - this.wrapper = default; - } - - /// - public void Initialize() - { - if (this.wrapper != default) - { - Reset(); - } - - this.wrapper = new(); - } -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibWrapper.cs b/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibWrapper.cs deleted file mode 100644 index 6035120ff7..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zlib/ZlibWrapper.cs +++ /dev/null @@ -1,74 +0,0 @@ -#pragma warning disable CS0659, CS0661 - -using System; -using System.IO; -using System.IO.Compression; - -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression.Zlib; - -/// -/// A thin wrapper around zlib natives to provide decompression. -/// -internal readonly struct ZlibWrapper : IDisposable, IEquatable -{ - private readonly ZLibStream stream; - private readonly MemoryStream underlying; - - public ZlibWrapper() - { - this.underlying = new(); - this.stream = new(this.underlying, CompressionMode.Decompress, true); - } - - /// - /// Inflates the provided data. - /// - /// The compressed input data. - /// A span for decompressed data. - public bool TryInflate(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed) - { - try - { - this.underlying.Write(compressed); - - this.underlying.Flush(); - this.underlying.Position = 0; - - while (true) - { - int read = this.stream.Read(decompressed.GetSpan()); - - if (read == 0) - { - break; - } - - decompressed.Advance(read); - } - - return true; - } - catch // not valid zlib, treat as uncompressed - { - return false; - } - finally - { - this.underlying.SetLength(0); - } - } - - public void Dispose() - { - this.stream.Dispose(); - this.underlying.Dispose(); - } - - public override bool Equals(object? obj) => obj is ZlibWrapper wrapper && Equals(wrapper); - public bool Equals(ZlibWrapper other) => this.stream == other.stream && this.underlying == other.underlying; - - public static bool operator ==(ZlibWrapper left, ZlibWrapper right) => left.Equals(right); - public static bool operator !=(ZlibWrapper left, ZlibWrapper right) => !(left == right); -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdDecompressor.cs b/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdDecompressor.cs deleted file mode 100644 index 99c20441d4..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdDecompressor.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; - -using CommunityToolkit.HighPerformance; -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression.Zstd; - -/// -/// A payload decompressor using zstd. -/// -public sealed class ZstdDecompressor : IPayloadDecompressor -{ - private ZstdInterop wrapper; - private bool isInitialized = false; - - /// - public string Name => "zstd-stream"; - - /// - public bool IsTransportCompression => true; - - /// - public bool TryDecompress(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed) - { - if (!this.isInitialized) - { - return false; - } - - // the magic header goes missing, we have to try it anyway - all explodes if we fix the magic header up :ioa: - if (!this.wrapper.TryDecompress(compressed, decompressed)) - { - decompressed.Clear(); - decompressed.Write(compressed); - } - - return true; - } - - /// - public void Dispose() - { - if (this.isInitialized) - { - this.wrapper.Dispose(); - } - - GC.SuppressFinalize(this); - } - - /// - public void Reset() - { - this.wrapper.Dispose(); - this.isInitialized = false; - } - - /// - public void Initialize() - { - if (this.isInitialized) - { - Reset(); - } - - this.wrapper = new(); - this.isInitialized = true; - } - - /// - /// Frees the unmanaged zstd stream. - /// - ~ZstdDecompressor() - { - if (this.isInitialized) - { - this.wrapper.Dispose(); - } - } -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdErrorCode.cs b/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdErrorCode.cs deleted file mode 100644 index 7fced5d04d..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdErrorCode.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DSharpPlus.Net.Gateway.Compression.Zstd; - -/// -/// Enumerates zstd error codes we care about handling in code. -/// -internal enum ZstdErrorCode -{ - NoError = 0, - Generic = 1, - VersionUnsupported = 12, - InitMissing = 62, - DestinationSizeTooSmall = 70, - SourceSizeWrong = 72, - DestinationBufferNull = 74, - DestinationFull = 80, - SourceEmpty = 82 -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInputBuffer.cs b/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInputBuffer.cs deleted file mode 100644 index 8e2bc9b6b7..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInputBuffer.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DSharpPlus.Net.Gateway.Compression.Zstd; - -/// -/// Contains information about a buffer passed as input to zstd. -/// -internal unsafe struct ZstdInputBuffer -{ - internal byte* source; // const void* src - internal nuint size; // size_t size - internal nuint position; // size_t pos -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInterop.Bindings.cs b/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInterop.Bindings.cs deleted file mode 100644 index 1e7f537a27..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInterop.Bindings.cs +++ /dev/null @@ -1,49 +0,0 @@ -#pragma warning disable CS0659, CS0661, IDE0040 - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using ZstdErrorCodeConvertible = nuint; - -namespace DSharpPlus.Net.Gateway.Compression.Zstd; - -internal partial struct ZstdInterop -{ - // ZstdInterop.Bindings is a nested type to lazily load zstd. the native load is done by the static constructor, - // which will not be executed unless this code actually gets used. since we cannot rely on zstd being present at all - // times, it is imperative this remains a nested type. - private static partial class Bindings - { - [LibraryImport("libzstd")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static unsafe partial ZstdStream* ZSTD_createDStream(); - - [LibraryImport("libzstd")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static unsafe partial ZstdErrorCodeConvertible ZSTD_freeDStream(ZstdStream* stream); - - [LibraryImport("libzstd")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static unsafe partial ZstdErrorCodeConvertible ZSTD_initDStream(ZstdStream* stream); - - [LibraryImport("libzstd")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static unsafe partial ZstdErrorCodeConvertible ZSTD_decompressStream - ( - ZstdStream* stream, - ZstdOutputBuffer* output, - ZstdInputBuffer* input - ); - - [LibraryImport("libzstd")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static unsafe partial nuint ZSTD_DStreamOutSize(); - - [LibraryImport("libzstd")] - [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static unsafe partial ZstdErrorCode ZSTD_getErrorCode(ZstdErrorCodeConvertible returnCode); - } - - // exists purely to put a name on the relevant parameters - internal struct ZstdStream; -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInterop.cs b/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInterop.cs deleted file mode 100644 index 398783d84b..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdInterop.cs +++ /dev/null @@ -1,112 +0,0 @@ -#pragma warning disable CS0659, CS0661 - -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; - -using CommunityToolkit.HighPerformance.Buffers; - -namespace DSharpPlus.Net.Gateway.Compression.Zstd; - -internal readonly unsafe partial struct ZstdInterop : IDisposable, IEquatable -{ - // must be stored as a pointer. we don't enlighten managed code about the exact layout of this, and we don't - // really want to. - private readonly ZstdStream* stream; - - internal static int RecommendedBufferSize { get; } = (int)Bindings.ZSTD_DStreamOutSize(); - - public ZstdInterop() - { - this.stream = Bindings.ZSTD_createDStream(); - - nuint code = Bindings.ZSTD_initDStream(this.stream); - Debug.Assert(Bindings.ZSTD_getErrorCode(code) == ZstdErrorCode.NoError); - } - - public bool TryDecompress(ReadOnlySpan compressed, ArrayPoolBufferWriter decompressed) - { - bool isCompleted = false; - - while (true) - { - fixed (byte* pCompressed = compressed) - { - ZstdInputBuffer inputBuffer = new() - { - source = pCompressed, - position = 0, - size = (nuint)compressed.Length - }; - - Span buffer = decompressed.GetSpan(compressed.Length); - - fixed (byte* pDecompressed = buffer) - { - ZstdOutputBuffer outputBuffer = new() - { - destination = pDecompressed, - position = 0, - size = (nuint)buffer.Length - }; - - nuint code = Bindings.ZSTD_decompressStream(this.stream, &outputBuffer, &inputBuffer); - ZstdErrorCode errorCode = Bindings.ZSTD_getErrorCode(code); - - decompressed.Advance((int)outputBuffer.position); - compressed = compressed[(int)inputBuffer.position..]; - - if (errorCode == ZstdErrorCode.NoError) - { - if (outputBuffer.position < outputBuffer.size) - { - isCompleted = true; - break; - } - - continue; - } - else if (errorCode is ZstdErrorCode.DestinationSizeTooSmall or ZstdErrorCode.DestinationFull) - { - continue; - } - else - { - Debug.Assert(true, $"Hit zstd error code {errorCode}"); - return false; - } - } - } - } - - return isCompleted; - } - - public void Dispose() - { - nuint code = Bindings.ZSTD_freeDStream(this.stream); - Debug.Assert(Bindings.ZSTD_getErrorCode(code) == ZstdErrorCode.NoError); - } - - public override readonly bool Equals(object? obj) => obj is ZstdInterop interop && Equals(interop); - public readonly bool Equals(ZstdInterop other) => this.stream == other.stream; - - public static bool operator ==(ZstdInterop left, ZstdInterop right) => left.Equals(right); - public static bool operator !=(ZstdInterop left, ZstdInterop right) => !(left == right); -} - -static file class ThrowHelper -{ - [DoesNotReturn] - [DebuggerHidden] - [StackTraceHidden] - public static void ThrowZstdError(ZstdErrorCode error) - => throw new InvalidDataException($"Encountered an error in deserializing a ZSTD payload: {error}"); - - [DoesNotReturn] - [DebuggerHidden] - [StackTraceHidden] - public static int ThrowZstdInvalidHeader() - => throw new InvalidCastException($"Encountered an invalid ZSTD frame header."); -} diff --git a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdOutputBuffer.cs b/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdOutputBuffer.cs deleted file mode 100644 index 0833aa83ae..0000000000 --- a/DSharpPlus/Net/Gateway/Compression/Zstd/ZstdOutputBuffer.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DSharpPlus.Net.Gateway.Compression.Zstd; - -/// -/// Contains information about a buffer containing output from zstd. -/// -internal unsafe struct ZstdOutputBuffer -{ - internal byte* destination; // const void* dst - internal nuint size; // size_t size - internal nuint position; // size_t pos -} diff --git a/DSharpPlus/Net/Gateway/DefaultGatewayController.cs b/DSharpPlus/Net/Gateway/DefaultGatewayController.cs deleted file mode 100644 index 26e1b8d21a..0000000000 --- a/DSharpPlus/Net/Gateway/DefaultGatewayController.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.Net.Gateway; - -namespace DSharpPlus.Clients; - -// intentionally doesn't do anything. if users want to customize this, their logic is likely to have so little in common -// with anything we could provide. -internal class DefaultGatewayController : IGatewayController -{ - /// - public Task HeartbeatedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ReconnectFailedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ReconnectRequestedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ResumeAttemptedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task SessionInvalidatedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ZombiedAsync(IGatewayClient client) => Task.CompletedTask; -} diff --git a/DSharpPlus/Net/Gateway/GatewayClient.cs b/DSharpPlus/Net/Gateway/GatewayClient.cs deleted file mode 100644 index 083df4c80c..0000000000 --- a/DSharpPlus/Net/Gateway/GatewayClient.cs +++ /dev/null @@ -1,742 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Gateway.Compression; -using DSharpPlus.Net.Serialization; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Gateway; - -/// -public sealed class GatewayClient : IGatewayClient -{ - private ILogger logger; - private readonly IGatewayController controller; - private readonly ITransportService transportService; - private readonly ChannelWriter eventWriter; - private readonly GatewayClientOptions options; - private readonly ILoggerFactory factory; - private readonly EventHandlerCollection handlers; - private readonly string token; - private readonly bool compress; - - private DateTimeOffset lastSentHeartbeat = DateTimeOffset.UtcNow; - private int pendingHeartbeats; - - private int remainingOutboundPayloads = 120; - private DateTimeOffset lastOutboundPayloadReset = DateTimeOffset.UtcNow; - private SpinLock resetLock = new(); - - private int lastReceivedSequence = 0; - private string? resumeUrl; - private string? sessionId; - private string? reconnectUrl; - private ShardInfo? shardInfo; - - private GatewayPayload? identify; - private CancellationTokenSource gatewayTokenSource; - private bool closureRequested = false; - - /// - public bool IsConnected { get; private set; } - - /// - public TimeSpan Ping { get; private set; } - - /// - public int ShardId => this.shardInfo?.ShardId ?? 0; - - public GatewayClient - ( - [FromKeyedServices("DSharpPlus.Gateway.EventChannel")] - Channel eventChannel, - - ITransportService transportService, - IOptions tokenContainer, - IPayloadDecompressor decompressor, - IOptions options, - IGatewayController controller, - ILoggerFactory factory, - IOptions handlers - ) - { - this.transportService = transportService; - this.eventWriter = eventChannel.Writer; - this.factory = factory; - this.token = tokenContainer.Value.GetToken(); - this.gatewayTokenSource = null!; - this.compress = !decompressor.IsTransportCompression; - this.options = options.Value; - this.controller = controller; - this.handlers = handlers.Value; - - this.logger = factory.CreateLogger("DSharpPlus.Net.Gateway.IGatewayClient - invalid shard"); - } - - /// - public async ValueTask ConnectAsync - ( - string url, - DiscordActivity? activity = null, - DiscordUserStatus? status = null, - DateTimeOffset? idleSince = null, - ShardInfo? shardInfo = null - ) - { - this.closureRequested = false; - - this.logger = shardInfo is null - ? this.factory.CreateLogger("DSharpPlus.Net.Gateway.IGatewayClient") - : this.factory.CreateLogger($"DSharpPlus.Net.Gateway.IGatewayClient - Shard {shardInfo.ShardId}"); - - this.shardInfo = shardInfo; - this.reconnectUrl = url; - - for (uint i = 0; i < this.options.MaxReconnects; i++) - { - try - { - this.gatewayTokenSource = new(); - await this.transportService.ConnectAsync(url, shardInfo?.ShardId); - - TransportFrame initialFrame = await this.transportService.ReadAsync(); - GatewayPayload? helloEvent = await ProcessAndDeserializeTransportFrameAsync(initialFrame); - - if (helloEvent is not { OpCode: GatewayOpCode.Hello }) - { - this.logger.LogWarning("Expected HELLO payload from Discord"); - continue; - } - - GatewayHello helloPayload = ((JObject)helloEvent.Data).ToDiscordObject(); - - this.logger.LogDebug - ( - "Received hello event, starting heartbeating with an interval of {interval} and identifying.", - TimeSpan.FromMilliseconds(helloPayload.HeartbeatInterval) - ); - - _ = HeartbeatAsync(helloPayload.HeartbeatInterval, this.gatewayTokenSource.Token); - _ = HandleEventsAsync(this.gatewayTokenSource.Token); - - StatusUpdate? statusUpdate; - - if (activity is null && status is null && idleSince is null) - { - statusUpdate = null; - } - else - { - statusUpdate = new(); - - if (activity is not null) - { - statusUpdate.Activity = new TransportActivity(activity); - } - - if (status is not null) - { - statusUpdate.Status = status.Value; - } - - if (idleSince is not null) - { - statusUpdate.IdleSince = idleSince.Value.ToUnixTimeMilliseconds(); - } - } - - GatewayIdentify inner = new() - { - Token = this.token, - Compress = this.compress, - LargeThreshold = this.options.LargeThreshold, - ShardInfo = shardInfo, - Presence = statusUpdate, - Intents = this.options.Intents - }; - - GatewayPayload identify = new() - { - OpCode = GatewayOpCode.Identify, - Data = inner - }; - - this.identify = identify; - - await WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(identify))); - - this.logger.LogDebug("Identified with the Discord gateway"); - break; - } - catch (Exception e) - { - this.logger.LogError(exception: e, "Encountered an error while connecting."); - await Task.Delay(this.options.GetReconnectionDelay(i)); - continue; - } - } - } - - /// - public async ValueTask DisconnectAsync() - { - this.closureRequested = true; - this.IsConnected = false; - this.gatewayTokenSource.Cancel(); - await this.transportService.DisconnectAsync(WebSocketCloseStatus.NormalClosure); - } - - /// - public async ValueTask WriteAsync(byte[] payload) - { - if (DateTimeOffset.UtcNow >= this.lastOutboundPayloadReset.AddMinutes(1)) - { - this.lastOutboundPayloadReset = DateTimeOffset.UtcNow; - this.remainingOutboundPayloads = 120; - } - - int remaining = Interlocked.Decrement(ref this.remainingOutboundPayloads); - - while (remaining < 0) - { - await Task.Delay(this.lastOutboundPayloadReset.AddMinutes(1) - DateTimeOffset.UtcNow); - - bool taken = false; - this.resetLock.TryEnter(ref taken); - - // assume that another thread is taking care of this. wait until the lock is free to continue (this is horrible) - if (!taken) - { - this.resetLock.Enter(ref taken); - this.resetLock.Exit(); - } - else - { - this.lastOutboundPayloadReset = DateTimeOffset.UtcNow; - this.remainingOutboundPayloads = 120; - } - - remaining = Interlocked.Decrement(ref this.remainingOutboundPayloads); - } - - await this.transportService.WriteAsync(payload); - } - - /// - /// Handles dispatching heartbeats to Discord. - /// - private async Task HeartbeatAsync(int heartbeatInterval, CancellationToken ct) - { - double jitter = Random.Shared.NextDouble() * 0.95; - - await Task.Delay((int)(heartbeatInterval * jitter), ct); - using PeriodicTimer timer = new(TimeSpan.FromMilliseconds(heartbeatInterval)); - - do - { - try - { - await WriteAsync(Encoding.UTF8.GetBytes($"{{\"op\":1,\"d\":{this.lastReceivedSequence}}}")); - this.logger.LogTrace("Heartbeat sent with sequence number {Sequence}.", this.lastReceivedSequence); - - this.lastSentHeartbeat = DateTimeOffset.UtcNow; - this.pendingHeartbeats++; - - if (this.pendingHeartbeats > 5) - { - _ = this.controller.ZombiedAsync(this); - } - } - catch (WebSocketException e) - { - this.logger.LogWarning("The connection died or entered an invalid state, reconnecting. Exception: {ExceptionMessage}", e.Message); - - await ReconnectAsync(); - return; - } - catch (Exception e) - { - this.logger.LogError(e, "An error occurred while sending a heartbeat."); - } - - } while (await timer.WaitForNextTickAsync(ct)); - } - - /// - /// Handles events incoming from Discord. - /// - private async Task HandleEventsAsync(CancellationToken ct) - { - try - { - while (!ct.IsCancellationRequested) - { - TransportFrame frame = await this.transportService.ReadAsync(); - GatewayPayload? payload = await ProcessAndDeserializeTransportFrameAsync(frame); - - if (payload is null) - { - continue; - } - - this.lastReceivedSequence = payload.Sequence ?? this.lastReceivedSequence; - - switch (payload.OpCode) - { - case GatewayOpCode.Dispatch when payload.EventName is "READY": - - ReadyPayload readyPayload = ((JObject)payload.Data).ToDiscordObject(); - - this.resumeUrl = readyPayload.ResumeGatewayUrl; - this.sessionId = readyPayload.SessionId; - - payload = new ShardIdContainingGatewayPayload - { - Data = payload.Data, - EventName = payload.EventName, - OpCode = payload.OpCode, - Sequence = payload.Sequence, - ShardId = this.ShardId - }; - - this.IsConnected = true; - - this.logger.LogDebug("Received READY, the gateway is now operational."); - - break; - - case GatewayOpCode.Dispatch when payload.EventName is "RESUMED": - - payload = new ShardIdContainingGatewayPayload - { - Data = payload.Data, - EventName = payload.EventName, - OpCode = payload.OpCode, - Sequence = payload.Sequence, - ShardId = this.ShardId - }; - - this.IsConnected = true; - - this.logger.LogDebug("A session was resumed successfully."); - - break; - - case GatewayOpCode.HeartbeatAck: - - this.Ping = DateTimeOffset.UtcNow - this.lastSentHeartbeat; - this.pendingHeartbeats = 0; - - // Task is not awaited to dont block gw recieve loop - _ = this.controller.HeartbeatedAsync(this); - - continue; - - case GatewayOpCode.InvalidSession: - - this.logger.LogDebug("Received INVALID_SESSION, resumable: {Resumable}", (bool)payload.Data); - bool success = (bool)payload.Data ? await TryResumeAsync() : await TryReconnectAsync(); - - if (!success) - { - this.logger.LogError("The session was invalidated and resuming/reconnecting failed."); - _ = this.controller.SessionInvalidatedAsync(this); - } - - continue; - - case GatewayOpCode.Reconnect: - - this.logger.LogDebug("Received RECONNECT"); - _ = this.controller.ReconnectRequestedAsync(this); - - if (!(this.options.AutoReconnect && await TryReconnectAsync())) - { - this.logger.LogError("A reconnection attempt requested by Discord failed."); - _ = this.controller.ReconnectFailedAsync(this); - } - - continue; - } - - if (CheckShouldBeEnqueued(payload)) - { - await this.eventWriter.WriteAsync(payload, CancellationToken.None); - } - } - } - catch (Exception e) - { - this.logger.LogError(e, "An exception occurred in event handling."); - } - -#pragma warning disable IDE0046 - bool CheckShouldBeEnqueued(GatewayPayload payload) - { - if (!this.options.EnableEventQueuePruning) - { - return true; - } - - // similarly, if the user has an unconditional handler enabled, don't bother checking - if (this.handlers[typeof(DiscordEventArgs)] is not []) - { - return true; - } - - if (payload.OpCode != GatewayOpCode.Dispatch) - { - return true; - } - - // these events are always enqueued - if (payload.EventName is "GUILD_CREATE" or "GUILD_DELETE" or "CHANNEL_CREATE" or "CHANNEL_DELETE" or "INTERACTION_CREATE" - or "GUILD_MEMBERS_CHUNK" or "READY" or "RESUMED") - { - return true; - } - - return this.handlers[GetEventArgsType(payload.EventName)] is not []; - } -#pragma warning restore IDE0046 - } - - /// - /// Attempts to resume a connection, returning whether this was successful. - /// - private async Task TryResumeAsync() - { - if (this.resumeUrl is null || this.sessionId is null) - { - return this.options.AutoReconnect && await TryReconnectAsync(); - } - - _ = this.controller.ResumeAttemptedAsync(this); - - try - { - for (uint i = 0; i < this.options.MaxReconnects; i++) - { - this.logger.LogTrace("Attempting resume, attempt {Attempt}", i + 1); - - try - { - this.IsConnected = false; - - await this.transportService.DisconnectAsync(WebSocketCloseStatus.NormalClosure); - await this.transportService.ConnectAsync(this.resumeUrl, this.shardInfo?.ShardId); - - await WriteAsync - ( - Encoding.UTF8.GetBytes - ( - JsonConvert.SerializeObject - ( - new GatewayPayload - { - OpCode = GatewayOpCode.Resume, - Data = new GatewayResume - { - SequenceNumber = this.lastReceivedSequence, - Token = this.token, - SessionId = this.sessionId - } - } - ) - ) - ); - - this.logger.LogDebug("Attempted to resume an existing gateway session."); - break; - } - catch (WebSocketException e) when (e.InnerException is HttpRequestException) - { - // no internet connection, but we can still try to resume later - TimeSpan delay = this.options.GetReconnectionDelay(i); - - this.logger.LogWarning("Internet connection interrupted, waiting for {Delay}", delay); - - await Task.Delay(delay); - continue; - } - catch - { - throw; - } - } - } - catch (Exception e) - { - this.logger.LogError(e, "Failed to resume an existing gateway session."); - return this.options.AutoReconnect && await TryReconnectAsync(); - } - - return true; - } - - /// - /// Attempts to reconnect to the gateway, returning whether this was successful. - /// - private async Task TryReconnectAsync() - { - for (uint i = 0; i < this.options.MaxReconnects; i++) - { - this.logger.LogTrace("Attempting reconnect, attempt {Attempt}", i + 1); - - try - { - this.IsConnected = false; - - try - { - this.gatewayTokenSource.Cancel(); - } - catch (ObjectDisposedException) - { - // thrown if gatewayTokenSource was disposed when cancelling, ignore since we create a new one anyway - } - - this.gatewayTokenSource = new(); - - // ensure we're disconnected no matter what the previous state was - await this.transportService.DisconnectAsync(WebSocketCloseStatus.NormalClosure); - - await this.transportService.ConnectAsync(this.reconnectUrl!, this.shardInfo?.ShardId); - - TransportFrame initialFrame = await this.transportService.ReadAsync(); - GatewayPayload? helloEvent = await ProcessAndDeserializeTransportFrameAsync(initialFrame); - - if (helloEvent is not { OpCode: GatewayOpCode.Hello }) - { - throw new InvalidDataException($"Expected HELLO payload from Discord"); - } - - GatewayHello helloPayload = ((JObject)helloEvent.Data).ToDiscordObject(); - - this.logger.LogDebug - ( - "Received hello event, starting heartbeating with an interval of {interval} and identifying.", - TimeSpan.FromMilliseconds(helloPayload.HeartbeatInterval) - ); - - this.IsConnected = true; - _ = HeartbeatAsync(helloPayload.HeartbeatInterval, this.gatewayTokenSource.Token); - _ = HandleEventsAsync(this.gatewayTokenSource.Token); - - await WriteAsync(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this.identify))); - - this.logger.LogDebug("Identified with the Discord gateway"); - return true; - } - catch (Exception e) - { - TimeSpan delay = this.options.GetReconnectionDelay(i); - - this.logger.LogError(e, "Reconnecting failed, waiting for {Delay}", delay); - - await Task.Delay(delay); - continue; - } - } - - return false; - } - - private async Task HandleErrorAndAttemptToReconnectAsync(TransportFrame frame) - { - if(this.closureRequested) - { - this.logger.LogDebug("Connection was requested to be closed, ignoring any errors."); - return; - } - - if (frame.TryGetException(out _)) - { - await TryResumeAsync(); - } - else if (frame.TryGetException(out _) && this.options.AutoReconnect) - { - await TryReconnectAsync(); - } - else if (frame.TryGetErrorCode(out int errorCode)) - { - this.logger.LogInformation("Received error code {Code} from gateway websocket.", errorCode); - - bool success = errorCode switch - { - < 4000 => await HandleSystemErrorAsync(errorCode), - (>= 4000 and <= 4002) or 4005 or 4008 => await TryResumeAsync(), - 4003 or 4007 or 4009 => this.options.AutoReconnect && await TryReconnectAsync(), - 4004 or (>= 4010 and <= 4014) => false, - _ => this.options.AutoReconnect && await TryReconnectAsync() - }; - - if (!success) - { - this.logger.LogError("An attempt to reconnect upon error code {Code} failed.", errorCode); - _ = this.controller.ReconnectFailedAsync(this); - } - } - } - - private async ValueTask ProcessAndDeserializeTransportFrameAsync(TransportFrame frame) - { - GatewayPayload? payload; - - if (!frame.IsSuccess) - { - await HandleErrorAndAttemptToReconnectAsync(frame); - return null; - } - - if (frame.TryGetMessage(out string? stringMessage)) - { - payload = JsonConvert.DeserializeObject(stringMessage); - - if (payload is null) - { - this.logger.LogError("Received invalid inbound event: {Data}", stringMessage); - return null; - } - } - else if (frame.TryGetStreamMessage(out MemoryStream? streamMessage)) - { - using StreamReader reader = new(streamMessage); - using JsonReader jsonReader = new JsonTextReader(reader); - - JsonSerializer serializer = new(); - payload = serializer.Deserialize(jsonReader); - - if (payload is null) - { - this.logger.LogError - ( - "Received invalid inbound event: {Data}", - Encoding.UTF8.GetString(streamMessage.ToArray()) - ); - - return null; - } - } - else - { - this.logger.LogCritical("Unrecognized transport frame encountered: {Frame}", frame); - return null; - } - - return payload; - } - - private async Task HandleSystemErrorAsync(int errorCode) - { - // 3000 Unauthorized and 3003 Forbidden are a problem we can't fix, error and return to the user - if (errorCode is 3000 or 3003) - { - this.logger.LogError("Encountered irrecoverable gateway close code {CloseCode}", errorCode); - } - - // else, try to reconnect if so requested - if (this.options.AutoReconnect && !this.closureRequested) - { - return await TryReconnectAsync(); - } - - this.logger.LogDebug("Gateway shutdown in progress, not reconnecting on recoverable close code {CloseCode}", errorCode); - return false; - } - - /// - public async ValueTask ReconnectAsync() - { - this.closureRequested = false; // Manual reconnect, so we're not closing - _ = await TryReconnectAsync(); - } - - private Type GetEventArgsType(string eventName) - { - // since we have nothing to sync Dispatch.cs and this, the event queue pruning option may... not work optimally - return eventName switch - { - "APPLICATION_COMMAND_PERMISSIONS_UPDATE" => typeof(ApplicationCommandPermissionsUpdatedEventArgs), - "AUTO_MODERATION_RULE_CREATE" => typeof(AutoModerationRuleCreatedEventArgs), - "AUTO_MODERATION_RULE_UPDATE" => typeof(AutoModerationRuleUpdatedEventArgs), - "AUTO_MODERATION_RULE_DELETE" => typeof(AutoModerationRuleDeletedEventArgs), - "AUTO_MODERATION_ACTION_EXECUTION" => typeof(AutoModerationRuleExecutedEventArgs), - "CHANNEL_CREATE" => typeof(ChannelCreatedEventArgs), - "CHANNEL_UPDATE" => typeof(ChannelUpdatedEventArgs), - "CHANNEL_DELETE" => typeof(ChannelDeletedEventArgs), - "CHANNEL_PINS_UPDATE" => typeof(ChannelPinsUpdatedEventArgs), - "THREAD_CREATE" => typeof(ThreadCreatedEventArgs), - "THREAD_UPDATE" => typeof(ThreadUpdatedEventArgs), - "THREAD_DELETE" => typeof(ThreadDeletedEventArgs), - "THREAD_LIST_SYNC" => typeof(ThreadListSyncedEventArgs), - "THREAD_MEMBER_UPDATE" => typeof(ThreadMemberUpdatedEventArgs), - "THREAD_MEMBERS_UPDATE" => typeof(ThreadMembersUpdatedEventArgs), - "ENTITLEMENT_CREATE" => typeof(EntitlementCreatedEventArgs), - "ENTITLEMENT_UPDATE" => typeof(EntitlementUpdatedEventArgs), - "ENTITLEMENT_DELETE" => typeof(EntitlementDeletedEventArgs), - "GUILD_CREATE" => typeof(GuildCreatedEventArgs), - "GUILD_UPDATE" => typeof(GuildUpdatedEventArgs), - "GUILD_DELETE" => typeof(GuildDeletedEventArgs), - "GUILD_AUDIT_LOG_ENTRY_CREATE" => typeof(GuildAuditLogCreatedEventArgs), - "GUILD_BAN_ADD" => typeof(GuildBanAddedEventArgs), - "GUILD_BAN_REMOVE" => typeof(GuildBanRemovedEventArgs), - "GUILD_EMOJIS_UPDATE" => typeof(GuildEmojisUpdatedEventArgs), - "GUILD_STICKERS_UPDATE" => typeof(GuildStickersUpdatedEventArgs), - "GUILD_INTEGRATIONS_UPDATE" => typeof(GuildIntegrationsUpdatedEventArgs), - "GUILD_MEMBER_ADD" => typeof(GuildMemberAddedEventArgs), - "GUILD_MEMBER_REMOVE" => typeof(GuildMemberRemovedEventArgs), - "GUILD_MEMBER_UPDATE" => typeof(GuildMemberUpdatedEventArgs), - "GUILD_MEMBERS_CHUNK" => typeof(GuildMembersChunkedEventArgs), - "GUILD_ROLE_CREATE" => typeof(GuildRoleCreatedEventArgs), - "GUILD_ROLE_UPDATE" => typeof(GuildRoleUpdatedEventArgs), - "GUILD_ROLE_DELETE" => typeof(GuildRoleDeletedEventArgs), - "GUILD_SCHEDULED_EVENT_CREATE" => typeof(ScheduledGuildEventCreatedEventArgs), - "GUILD_SCHEDULED_EVENT_UPDATE" => typeof(ScheduledGuildEventUpdatedEventArgs), - "GUILD_SCHEDULED_EVENT_DELETE" => typeof(ScheduledGuildEventDeletedEventArgs), - "GUILD_SCHEDULED_EVENT_USER_ADD" => typeof(ScheduledGuildEventUserAddedEventArgs), - "GUILD_SCHEDULED_EVENT_USER_REMOVE" => typeof(ScheduledGuildEventUserRemovedEventArgs), - "INTEGRATION_CREATE" => typeof(IntegrationCreatedEventArgs), - "INTEGRATION_UPDATE" => typeof(IntegrationUpdatedEventArgs), - "INTEGRATION_DELETE" => typeof(IntegrationDeletedEventArgs), - "INTERACTION_CREATE" => typeof(InteractionCreatedEventArgs), - "INVITE_CREATE" => typeof(InviteCreatedEventArgs), - "INVITE_DELETE" => typeof(InviteDeletedEventArgs), - "MESSAGE_CREATE" => typeof(MessageCreatedEventArgs), - "MESSAGE_UPDATE" => typeof(MessageUpdatedEventArgs), - "MESSAGE_DELETE" => typeof(MessageDeletedEventArgs), - "MESSAGE_DELETE_BULK" => typeof(MessagesBulkDeletedEventArgs), - "MESSAGE_REACTION_ADD" => typeof(MessageReactionAddedEventArgs), - "MESSAGE_REACTION_REMOVE" => typeof(MessageReactionRemovedEventArgs), - "MESSAGE_REACTION_REMOVE_ALL" => typeof(MessageReactionsClearedEventArgs), - "MESSAGE_REACTION_REMOVE_EMOJI" => typeof(MessageReactionRemovedEmojiEventArgs), - "PRESENCE_UPDATE" => typeof(PresenceUpdatedEventArgs), - "STAGE_INSTANCE_CREATE" => typeof(StageInstanceCreatedEventArgs), - "STAGE_INSTANCE_UPDATE" => typeof(StageInstanceUpdatedEventArgs), - "STAGE_INSTANCE_DELETE" => typeof(StageInstanceDeletedEventArgs), - "TYPING_START" => typeof(TypingStartedEventArgs), - "USER_UPDATE" => typeof(UserUpdatedEventArgs), - "VOICE_STATE_UPDATE" => typeof(VoiceStateUpdatedEventArgs), - "VOICE_SERVER_UPDATE" => typeof(VoiceServerUpdatedEventArgs), - "WEBHOOKS_UPDATE" => typeof(WebhooksUpdatedEventArgs), - "MESSAGE_POLL_VOTE_ADD" => typeof(MessagePollVotedEventArgs), - "MESSAGE_POLL_VOTE_REMOVE" => typeof(MessagePollVotedEventArgs), - _ => typeof(UnknownEventArgs) - }; - } -} diff --git a/DSharpPlus/Net/Gateway/GatewayClientOptions.cs b/DSharpPlus/Net/Gateway/GatewayClientOptions.cs deleted file mode 100644 index 18eae0a1c1..0000000000 --- a/DSharpPlus/Net/Gateway/GatewayClientOptions.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; - -using DSharpPlus.Logging; - -namespace DSharpPlus.Net.Gateway; - -/// -/// Controls the behaviour of the default . -/// -public sealed class GatewayClientOptions -{ - /// - /// Specifies a function to get the reconnection delay on a given consecutive attempt to reconnect. - /// - /// - /// Defaults to doubling the time spent waiting until 2^10 seconds, or 17:04 minutes are reached, at which point - /// the value becomes constant. - /// - public Func GetReconnectionDelay { get; set; } - = (num) => TimeSpan.FromSeconds(double.Pow(2, uint.Min(num, 10))); - - /// - /// Specifies the maximum amount of reconnects to attempt consecutively. The counter resets if a connection is - /// successfully established. Defaults to . - /// - public uint MaxReconnects { get; set; } = uint.MaxValue; - - /// - /// Specifies whether the gateway should attempt to reconnect automatically, if possible. It will always attempt - /// to resume a session, regardless of this setting. Defaults to true. - /// - public bool AutoReconnect { get; set; } = true; - - /// - /// Specifies the member count at which guilds are considered "large" and the information sent about members is - /// reduced. Defaults to 250. - /// - public int LargeThreshold { get; set; } = 250; - - /// - /// Specifies the gateway intents for this client. The client will only receive events they specified the relevant - /// intents for. Defaults to . - /// - public DiscordIntents Intents { get; set; } = DiscordIntents.AllUnprivileged; - - /// - /// Toggles the use of streams for deserializing inbound gateway events. Enabling this will increase the total - /// allocation volume of the gateway, but decrease the maximum size of allocations and may reduce object - /// longevity. - /// - /// - /// This option will be automatically disabled if trace logs are enabled and - /// is enabled (as it is by default). Please refer to - /// our article - /// on trace logging to learn how to modify this option. - /// - public bool EnableStreamingDeserialization { get; set; } = true; - - /// - /// Toggles pruning the gateway event queue of user-unused events. This may reduce gateway back-pressure and memory - /// costs, but may cause library cache to go stale quicker as only directly consumed events are processed. - /// - /// - /// The following event types are entirely immune to pruning: GUILD_CREATE, GUILD_DELETE, CHANNEL_CREATE, - /// CHANNEL_DELETE, INTERACTION_CREATE, GUILD_MEMBERS_CHUNK READY, RESUMED. All other events - /// may be pruned if there is no user code handling them. Exercise caution with this option. - /// - public bool EnableEventQueuePruning { get; set; } = false; -} diff --git a/DSharpPlus/Net/Gateway/IGatewayClient.cs b/DSharpPlus/Net/Gateway/IGatewayClient.cs deleted file mode 100644 index 6bfe650917..0000000000 --- a/DSharpPlus/Net/Gateway/IGatewayClient.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Net.Abstractions; - -namespace DSharpPlus.Net.Gateway; - -/// -/// Represents a gateway client handling all system events. -/// -public interface IGatewayClient -{ - /// - /// Connects this client to the gateway. - /// - /// The gateway URL to use for connecting, including any potential reconnects. - /// An optional activity to send to the gateway when connecting. - /// An optional status to send to the gateway when connecting. - /// An optional idle timer to send to the gateway when connecting. - /// If this isn't the only shard, additional information about this shard. - public ValueTask ConnectAsync - ( - string url, - DiscordActivity? activity = null, - DiscordUserStatus? status = null, - DateTimeOffset? idleSince = null, - ShardInfo? shardInfo = null - ); - - /// - /// Disconnects from the gateway. - /// - public ValueTask DisconnectAsync(); - - /// - /// Reconnects to the gateway. - /// - public ValueTask ReconnectAsync(); - - /// - /// Sends the provided payload to the gateway. - /// - public ValueTask WriteAsync(byte[] payload); - - /// - /// Indicates whether this client is connected. - /// - public bool IsConnected { get; } - - /// - /// Indicates the latency between this client and Discord. - /// - public TimeSpan Ping { get; } - - /// - /// Gets the shard ID of this client. Defaults to zero if not sharding. - /// - public int ShardId { get; } -} diff --git a/DSharpPlus/Net/Gateway/IGatewayController.cs b/DSharpPlus/Net/Gateway/IGatewayController.cs deleted file mode 100644 index ad5569fcb7..0000000000 --- a/DSharpPlus/Net/Gateway/IGatewayController.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Net.Gateway; - -/// -/// Provides a low-level interface for controlling individual gateway clients and their connections. -/// -public interface IGatewayController -{ - /// - /// Called when the gateway connection zombies. - /// - /// The gateway client whose connection zombied. - public Task ZombiedAsync(IGatewayClient client); - - /// - /// Called when the gateway heartbeated correctly and got an ACK from Discord - /// - /// The gateway client who recieved the heartbeat ACK. - public Task HeartbeatedAsync(IGatewayClient client); - - /// - /// Called when DSharpPlus attempts to resume a gateway session. - /// - /// The gateway client attempting to resume a session. - public Task ResumeAttemptedAsync(IGatewayClient client); - - /// - /// Called when Discord requests a reconnect. This does not imply that DSharpPlus' reconnection attempt failed. - /// - /// The gateway client reconnection was requested from. - public Task ReconnectRequestedAsync(IGatewayClient client); - - /// - /// Called when a reconnecting attempt definitively failed and DSharpPlus can no longer reconnect on its own. - /// - /// The gateway client reconnection was requested from. - public Task ReconnectFailedAsync(IGatewayClient client); - - /// - /// Called when a session was invalidated and DSharpPlus failed to resume or reconnect. - /// - /// The gateway client reconnection was requested from. - public Task SessionInvalidatedAsync(IGatewayClient client); -} diff --git a/DSharpPlus/Net/Gateway/ITransportService.cs b/DSharpPlus/Net/Gateway/ITransportService.cs deleted file mode 100644 index 3183a0f13c..0000000000 --- a/DSharpPlus/Net/Gateway/ITransportService.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Threading.Tasks; - -namespace DSharpPlus.Net.Gateway; - -/// -/// Represents the lowest-level transport layer over the gateway. This type is not thread-safe. -/// -public interface ITransportService : IDisposable -{ - /// - /// Opens a connection to the gateway. - /// - public ValueTask ConnectAsync(string url, int? shardId); - - /// - /// Reads the next message from the gateway asynchronously. - /// - public ValueTask ReadAsync(); - - /// - /// Writes the specified message to the gateway. - /// - public ValueTask WriteAsync(byte[] payload); - - /// - /// Disconnects from the gateway. - /// - /// The status message to send to the Discord gateway. - public ValueTask DisconnectAsync(WebSocketCloseStatus closeStatus); -} diff --git a/DSharpPlus/Net/Gateway/ReconnectingGatewayController.cs b/DSharpPlus/Net/Gateway/ReconnectingGatewayController.cs deleted file mode 100644 index 28f42961a3..0000000000 --- a/DSharpPlus/Net/Gateway/ReconnectingGatewayController.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Net.Gateway; - -/// -/// A gateway controller implementation that automatically attempts to reconnect. -/// -public sealed class ReconnectingGatewayController : IGatewayController -{ - /// - public Task HeartbeatedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ReconnectFailedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ReconnectRequestedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public Task ResumeAttemptedAsync(IGatewayClient client) => Task.CompletedTask; - - /// - public async Task SessionInvalidatedAsync(IGatewayClient client) => await client.ReconnectAsync(); - - /// - public async Task ZombiedAsync(IGatewayClient client) => await client.ReconnectAsync(); -} diff --git a/DSharpPlus/Net/Gateway/TransportFrame.cs b/DSharpPlus/Net/Gateway/TransportFrame.cs deleted file mode 100644 index 1224fd10ba..0000000000 --- a/DSharpPlus/Net/Gateway/TransportFrame.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; - -namespace DSharpPlus.Net.Gateway; - -/// -/// Represents an union between a string message read from the gateway, an exception or a close code. -/// -public readonly record struct TransportFrame -{ - private readonly object value; - - /// - /// Indicates whether reading this gateway frame was successful. - /// - public readonly bool IsSuccess => this.value is string or MemoryStream; - - /// - /// Attempts to retrieve the string message received. - /// - public readonly bool TryGetMessage([NotNullWhen(true)] out string? message) - { - message = null; - - if (this.value is string str) - { - message = str; - return true; - } - - return false; - } - - /// - /// Attempts to retrieve a message wrapped in a MemoryStream. This is only permissible if the log level - /// is higher than Trace or if inbound gateway logging is disabled. - /// - public readonly bool TryGetStreamMessage([NotNullWhen(true)] out MemoryStream? message) - { - message = null; - - if (this.value is MemoryStream str) - { - message = str; - return true; - } - - return false; - } - - /// - /// Attempts to retrieve the exception thrown attempting to read this frame. - /// - public readonly bool TryGetException([NotNullWhen(true)] out Exception? exception) - { - exception = null; - - if (this.value is Exception ex) - { - exception = ex; - return true; - } - - return false; - } - - /// - /// Attempts to retrieve the exception thrown attempting to read this frame. - /// - public readonly bool TryGetException([NotNullWhen(true)] out T? exception) - where T : Exception - { - exception = null; - - if (this.value is T ex) - { - exception = ex; - return true; - } - - return false; - } - - /// - /// Attempts to retrieve the error code returned attempting to read this frame. - /// - public readonly bool TryGetErrorCode(out int errorCode) - { - errorCode = default; - - if (this.value is int code) - { - errorCode = code; - return true; - } - - return false; - } - - /// - /// Creates a new transport frame from the specified data. - /// - public TransportFrame(object value) - => this.value = value; -} diff --git a/DSharpPlus/Net/Gateway/TransportService.cs b/DSharpPlus/Net/Gateway/TransportService.cs deleted file mode 100644 index 989fe8c5a8..0000000000 --- a/DSharpPlus/Net/Gateway/TransportService.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.IO; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -using CommunityToolkit.HighPerformance.Buffers; - -using DSharpPlus.Logging; -using DSharpPlus.Net.Gateway.Compression; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace DSharpPlus.Net.Gateway; - -/// -internal sealed class TransportService : ITransportService -{ - private ILogger logger; - private ClientWebSocket socket; - private readonly ArrayPoolBufferWriter writer; - private readonly ArrayPoolBufferWriter decompressedWriter; - private readonly IPayloadDecompressor decompressor; - private readonly ILoggerFactory factory; - - private readonly bool streamingDeserialization; - - private bool isConnected = false; - private bool isDisposed = false; - - public TransportService - ( - ILoggerFactory factory, - IPayloadDecompressor decompressor, - IOptions options - ) - { - this.factory = factory; - this.writer = new(); - this.decompressedWriter = new(); - this.decompressor = decompressor; - - this.streamingDeserialization = options.Value.EnableStreamingDeserialization; - - this.logger = factory.CreateLogger("DSharpPlus.Net.Gateway.ITransportService - invalid shard"); - } - - /// - public async ValueTask ConnectAsync(string url, int? shardId) - { - this.logger = shardId is null - ? this.factory.CreateLogger("DSharpPlus.Net.Gateway.ITransportService") - : this.factory.CreateLogger($"DSharpPlus.Net.Gateway.ITransportService - Shard {shardId}"); - - this.socket = new(); - this.decompressor.Initialize(); - - ObjectDisposedException.ThrowIf(this.isDisposed, this); - - if (this.isConnected) - { - this.logger.LogWarning("Attempted to connect, but there already is a connection opened. Ignoring."); - return; - } - - this.logger.LogTrace("Connecting to the Discord gateway."); - - await this.socket.ConnectAsync(new(url), CancellationToken.None); - this.isConnected = true; - - this.logger.LogDebug("Connected to the Discord websocket, using {compression} compression.", this.decompressor.Name); - } - - /// - public async ValueTask DisconnectAsync(WebSocketCloseStatus closeStatus) - { - this.logger.LogTrace("Disconnect requested: {CloseStatus}", closeStatus.ToString()); - - if (!this.isConnected) - { - this.logger.LogTrace - ( - "Attempting to disconnect from the Discord gateway, but there was no open connection. Ignoring." - ); - - return; - } - - this.isConnected = false; - - switch (this.socket.State) - { - case WebSocketState.CloseSent: - case WebSocketState.CloseReceived: - case WebSocketState.Closed: - case WebSocketState.Aborted: - - this.logger.LogTrace - ( - "Attempting to disconnect from the Discord gateway, but there is a disconnect in progress or complete. " + - "Current websocket state: {state}", - this.socket.State.ToString() - ); - - return; - - case WebSocketState.Open: - case WebSocketState.Connecting: - - this.logger.LogTrace("Disconnecting. Current websocket state: {state}", this.socket.State.ToString()); - - try - { - await this.socket.CloseAsync - ( - closeStatus, - "Disconnecting.", - CancellationToken.None - ); - } - catch (WebSocketException) { } - catch (OperationCanceledException) { } - - break; - } - - this.decompressor.Reset(); - this.socket.Dispose(); - } - - /// - public async ValueTask ReadAsync() - { - ObjectDisposedException.ThrowIf(this.isDisposed, this); - - if (!this.isConnected) - { - throw new InvalidOperationException("The transport service was not connected to the gateway."); - } - - ValueWebSocketReceiveResult receiveResult; - - this.writer.Clear(); - this.decompressedWriter.Clear(); - - try - { - do - { - receiveResult = await this.socket.ReceiveAsync(this.writer.GetMemory(), CancellationToken.None); - - this.writer.Advance(receiveResult.Count); - - } while (!receiveResult.EndOfMessage); - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - return new(ex); - } - - if (!this.decompressor.TryDecompress(this.writer.WrittenSpan, this.decompressedWriter)) - { - throw new InvalidDataException("Failed to decompress a gateway payload."); - } - - if (this.logger.IsEnabled(LogLevel.Trace) && RuntimeFeatures.EnableInboundGatewayLogging) - { - string result = Encoding.UTF8.GetString(this.decompressedWriter.WrittenSpan); - - this.logger.LogTrace - ( - "Length for the last inbound gateway event: {length}", - this.writer.WrittenCount != 0 ? this.writer.WrittenCount : $"closed: {(int)this.socket.CloseStatus!}" - ); - - string anonymized = result; - - if (RuntimeFeatures.AnonymizeTokens) - { - anonymized = AnonymizationUtilities.AnonymizeTokens(anonymized); - } - - if (RuntimeFeatures.AnonymizeContents) - { - anonymized = AnonymizationUtilities.AnonymizeContents(anonymized); - } - - this.logger.LogTrace("Payload for the last inbound gateway event: {event}", anonymized); - - return this.writer.WrittenCount == 0 ? new((int)this.socket.CloseStatus!) : new(result); - } - else if (this.streamingDeserialization) - { - MemoryStream result = new(this.decompressedWriter.WrittenSpan.ToArray()); - return this.writer.WrittenCount == 0 ? new((int)this.socket.CloseStatus!) : new(result); - } - else - { - string result = Encoding.UTF8.GetString(this.decompressedWriter.WrittenSpan); - return this.writer.WrittenCount == 0 ? new((int)this.socket.CloseStatus!) : new(result); - } - } - - /// - public async ValueTask WriteAsync(byte[] payload) - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(payload.Length, 4096, nameof(payload)); - - if (this.logger.IsEnabled(LogLevel.Trace) && RuntimeFeatures.EnableOutboundGatewayLogging) - { - - this.logger.LogTrace("Length for the last outbound outbound event: {length}", payload.Length); - - string anonymized = Encoding.UTF8.GetString(payload); - - if (RuntimeFeatures.AnonymizeTokens) - { - anonymized = AnonymizationUtilities.AnonymizeTokens(anonymized); - } - - if (RuntimeFeatures.AnonymizeContents) - { - anonymized = AnonymizationUtilities.AnonymizeContents(anonymized); - } - - this.logger.LogTrace("Payload for the last outbound gateway event: {event}", anonymized); - } - - if (!this.isDisposed) - { - await this.socket.SendAsync - ( - buffer: payload, - messageType: WebSocketMessageType.Text, - endOfMessage: true, - cancellationToken: CancellationToken.None - ); - } - } - - /// - public void Dispose() - { - if (!this.isDisposed) - { - this.socket.Dispose(); - } - - this.isDisposed = true; - GC.SuppressFinalize(this); - } -} diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordApplicationAuthorizedEvent.cs b/DSharpPlus/Net/InboundWebhooks/DiscordApplicationAuthorizedEvent.cs deleted file mode 100644 index 0aa59968be..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordApplicationAuthorizedEvent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -using Newtonsoft.Json; - -namespace DSharpPlus.Net.InboundWebhooks; - -/// -/// Contains data regarding an application being authorized to a guild or user. -/// -public sealed class DiscordApplicationAuthorizedEvent -{ - /// - /// Gets the type of integration for the install. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordApplicationIntegrationType Type { get; internal set; } - - /// - /// Gets the user that authorized the application. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the scopes the application was authorized for. - /// - public IReadOnlyList Scopes { get; internal set; } - - /// - /// Gets the guild the application was authorized for (if applicable). - /// - public DiscordGuild Guild { get; internal set; } -} diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordHeaders.cs b/DSharpPlus/Net/InboundWebhooks/DiscordHeaders.cs deleted file mode 100644 index 6f9be12bca..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordHeaders.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Buffers; -using System.Text; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.InboundWebhooks; - -public class DiscordHeaders -{ - /// - /// Name of the HTTP header which contains the timestamp of the signature - /// - public const string TimestampHeaderName = "x-signature-timestamp"; - - /// - /// Name of the HTTP header which contains the signature - /// - public const string SignatureHeaderName = "x-signature-ed25519"; - - /// - /// Verifies the signature of a http interaction. - /// - /// Raw http body - /// Timestamp header sent by discord. - /// Signing key sent by discord. - /// - /// Public key of the application this interaction was sent. - /// This key can be accessed at DiscordApplication. - /// - /// - /// Indicates if this signature is valid. - public static bool VerifySignature(ReadOnlySpan body, string timestamp, string signingKey, string publicKey) - { - byte[] timestampBytes = Encoding.UTF8.GetBytes(timestamp); - byte[] publicKeyBytes = Convert.FromHexString(publicKey); - byte[] signatureBytes = Convert.FromHexString(signingKey); - - int messageLength = body.Length + timestampBytes.Length; - byte[] message = ArrayPool.Shared.Rent(messageLength); - - timestampBytes.CopyTo(message, 0); - body.CopyTo(message.AsSpan(timestampBytes.Length)); - - bool result = Ed25519.TryVerifySignature(message.AsSpan(..messageLength), publicKeyBytes.AsSpan(), signatureBytes.AsSpan()); - - ArrayPool.Shared.Return(message); - - return result; - } -} diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordHttpInteractionPayload.cs b/DSharpPlus/Net/InboundWebhooks/DiscordHttpInteractionPayload.cs deleted file mode 100644 index 1c8cd894c8..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordHttpInteractionPayload.cs +++ /dev/null @@ -1,11 +0,0 @@ -using DSharpPlus.Entities; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.InboundWebhooks; - -/// -/// Represents a transport-time wrapper for a pre-deserialized interaction and the raw data it was spawned from. -/// -/// A partly deserialized interaction, as far as is possible to deserialize. -/// The data it was spawned from. -public readonly record struct DiscordHttpInteractionPayload(DiscordHttpInteraction ProtoInteraction, JObject Data); diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEvent.cs b/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEvent.cs deleted file mode 100644 index f5351e0b4f..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEvent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.InboundWebhooks; - -/// -/// A wrapper type for an incoming webhook event. -/// -public sealed class DiscordWebhookEvent -{ - /// - /// Gets the version. - /// - [JsonProperty("version")] - public int Version { get; internal set; } - - /// - /// Gets the ID of the application that triggered this event. - /// - [JsonProperty("application_id")] - public ulong ApplicationID { get; internal set; } - - /// - /// Gets the type of the event. - /// - [JsonProperty("type")] - public DiscordWebhookEventType Type { get; internal set; } - - /// - /// The event data payload. - /// - [JsonProperty("event")] - public DiscordWebhookEventBody? Event { get; internal set; } -} diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventBody.cs b/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventBody.cs deleted file mode 100644 index 1ecd366854..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventBody.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.InboundWebhooks; - -/// -/// Represents the outer data for a . -/// -/// This contains metadata about the actual event. -/// -// Hello source-code viewer! I hate this too -V -public class DiscordWebhookEventBody -{ - /// - /// The type of the event. - /// - [JsonProperty("type")] - [JsonConverter(typeof(StringEnumConverter))] - public DiscordWebhookEventBodyType Type { get; internal set; } - - /// - /// The timestamp at which this event was invoked. - /// - [JsonProperty("timestamp")] - public DateTimeOffset Timestamp { get; internal set; } - - /// - /// The data of the event. The data within depends on the value of . - /// - [JsonProperty("data")] - public JObject? Data { get; internal set; } -} diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventBodyType.cs b/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventBodyType.cs deleted file mode 100644 index 8964f40b77..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventBodyType.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DSharpPlus.Net.InboundWebhooks; - -/// -/// Represents the type of the webhook event body. -/// -public enum DiscordWebhookEventBodyType -{ - /// - /// Represents that the application was authorized to a user or guild. - /// - ApplicationAuthorized, - - /// - /// Represents that an entitlement was created. - /// - EntitlementCreate, - - /// - /// Represents that a user was enrolled in a quest. - /// - /// - /// The details of this are currently undocumented, and thus this value only exists for parity with Discord's documentation. - /// - QuestUserEnrollment, -} diff --git a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventType.cs b/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventType.cs deleted file mode 100644 index 580a27b515..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/DiscordWebhookEventType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DSharpPlus.Net.InboundWebhooks; - -/// -/// Represents the type of the webhook event. -/// -public enum DiscordWebhookEventType -{ - /// - /// A ping event. - /// - Ping = 0, - - /// - /// An event. - /// - Event = 1, -} diff --git a/DSharpPlus/Net/InboundWebhooks/ED25519.cs b/DSharpPlus/Net/InboundWebhooks/ED25519.cs deleted file mode 100644 index af9ddacc23..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/ED25519.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace DSharpPlus.Net.InboundWebhooks; - -internal static partial class Ed25519 -{ - internal const int SignatureBytes = 64; - internal const int PublicKeyBytes = 32; - - internal static unsafe bool TryVerifySignature(ReadOnlySpan body, ReadOnlySpan publicKey, ReadOnlySpan signature) - { - ArgumentOutOfRangeException.ThrowIfNotEqual(signature.Length, SignatureBytes); - ArgumentOutOfRangeException.ThrowIfNotEqual(publicKey.Length, PublicKeyBytes); - - fixed (byte* signaturePtr = signature) - fixed (byte* messagePtr = body) - fixed (byte* publicKeyPtr = publicKey) - { - return Bindings.crypto_sign_ed25519_verify_detached(signaturePtr, messagePtr, (ulong)body.Length, publicKeyPtr) == 0; - } - } - - // Ed25519.Bindings is a nested type to lazily load sodium. the native load is done by the static constructor, - // which will not be executed unless this code actually gets used. since we cannot rely on sodium being present at all - // times, it is imperative this remains a nested type. - private static partial class Bindings - { - static Bindings() - { - if (sodium_init() == -1) - { - throw new InvalidOperationException("Failed to initialize libsodium."); - } - } - - [LibraryImport("sodium")] - private static unsafe partial int sodium_init(); - - [LibraryImport("sodium")] - internal static unsafe partial int crypto_sign_ed25519_verify_detached(byte* signature, byte* message, ulong messageLength, byte* publicKey); - } -} diff --git a/DSharpPlus/Net/InboundWebhooks/Payloads/ApplicationAuthorizedPayload.cs b/DSharpPlus/Net/InboundWebhooks/Payloads/ApplicationAuthorizedPayload.cs deleted file mode 100644 index e02ec38706..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/Payloads/ApplicationAuthorizedPayload.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Collections.Generic; - -using DSharpPlus.Entities; - -using Newtonsoft.Json; - -namespace DSharpPlus.Net.InboundWebhooks.Payloads; - -/// -/// Payload for . -/// -internal sealed class ApplicationAuthorizedPayload -{ - /// - /// The context this authorization occurred in. - /// - [JsonProperty("integration_type")] - public DiscordApplicationIntegrationType IntegrationType { get; set; } - - /// - /// The user who authorized the application. - /// - [JsonProperty("user")] - public DiscordUser User { get; set; } - - /// - /// A list of scopes the user authorized to. - /// - [JsonProperty("scopes")] - public IReadOnlyList Scopes { get; set; } - - /// - /// The guild the application was authorized for. Only applicable if is - /// . - /// - [JsonProperty("guild")] - public DiscordGuild? Guild { get; set; } -} diff --git a/DSharpPlus/Net/InboundWebhooks/Transport/IInteractionTransportService.cs b/DSharpPlus/Net/InboundWebhooks/Transport/IInteractionTransportService.cs deleted file mode 100644 index 3bf4853639..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/Transport/IInteractionTransportService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.Net.InboundWebhooks.Transport; - -/// -/// Handles communication with the bot through HTTP interactions. -/// -public interface IInteractionTransportService -{ - /// - /// Handles an interaction coming from the registered HTTP webhook. - /// - /// The payload of the http request. This must be UTF-8 encoded. - /// A token to cancel the interaction when the http request was canceled - /// Returns the body which should be returned to the http request - /// Thrown when the passed cancellation token was canceled - public Task HandleHttpInteractionAsync(ArraySegment payload, CancellationToken token); -} diff --git a/DSharpPlus/Net/InboundWebhooks/Transport/IWebhookTransportService.cs b/DSharpPlus/Net/InboundWebhooks/Transport/IWebhookTransportService.cs deleted file mode 100644 index b2a94a92f9..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/Transport/IWebhookTransportService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.Net.InboundWebhooks.Transport; - -public interface IWebhookTransportService -{ - public Task HandleWebhookEventAsync(ArraySegment payload); -} diff --git a/DSharpPlus/Net/InboundWebhooks/Transport/InteractionTransportService.cs b/DSharpPlus/Net/InboundWebhooks/Transport/InteractionTransportService.cs deleted file mode 100644 index 51534deb96..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/Transport/InteractionTransportService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Text; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Logging; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Serialization; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.InboundWebhooks.Transport; - -/// -public sealed class InteractionTransportService : IInteractionTransportService -{ - private readonly ILogger logger; - private readonly ChannelWriter writer; - - public InteractionTransportService - ( - ILogger logger, - - [FromKeyedServices("DSharpPlus.Interactions.EventChannel")] - Channel channel - ) - { - this.logger = logger; - this.writer = channel.Writer; - } - - /// - public async Task HandleHttpInteractionAsync(ArraySegment payload, CancellationToken token) - { - string bodyString = Encoding.UTF8.GetString(payload); - - // we lump this in with trace logs - if (RuntimeFeatures.EnableInboundGatewayLogging && this.logger.IsEnabled(LogLevel.Trace)) - { - this.logger.LogTrace("Received HTTP interaction payload: {Payload}", AnonymizationUtilities.Anonymize(bodyString)); - } - - JObject data = JObject.Parse(bodyString); - - DiscordHttpInteraction? interaction = data.ToDiscordObject() - ?? throw new ArgumentException("Unable to parse provided request body to DiscordHttpInteraction"); - - if (interaction.Type is DiscordInteractionType.Ping) - { - DiscordInteractionResponsePayload responsePayload = new() - { - Type = DiscordInteractionResponseType.Pong - }; - - string responseString = DiscordJson.SerializeObject(responsePayload); - byte[] responseBytes = Encoding.UTF8.GetBytes(responseString); - - return responseBytes; - } - - token.Register(() => interaction.Cancel()); - - await this.writer.WriteAsync(new(interaction, data), token); - - return await interaction.GetResponseAsync(); - } -} diff --git a/DSharpPlus/Net/InboundWebhooks/Transport/WebhookEventTransportService.cs b/DSharpPlus/Net/InboundWebhooks/Transport/WebhookEventTransportService.cs deleted file mode 100644 index 0deae97ffe..0000000000 --- a/DSharpPlus/Net/InboundWebhooks/Transport/WebhookEventTransportService.cs +++ /dev/null @@ -1,53 +0,0 @@ -using DSharpPlus.Logging; -using DSharpPlus.Net.Serialization; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; -using System.Text; -using System.Threading.Channels; -using System.Threading.Tasks; -using System; - -namespace DSharpPlus.Net.InboundWebhooks.Transport; - -public sealed class WebhookEventTransportService : IWebhookTransportService -{ - private readonly ILogger logger; - private readonly ChannelWriter writer; - - public WebhookEventTransportService - ( - ILogger logger, - - [FromKeyedServices("DSharpPlus.Webhooks.EventChannel")] - Channel channel - ) - { - this.logger = logger; - this.writer = channel.Writer; - } - - /// - public async Task HandleWebhookEventAsync(ArraySegment payload) - { - string payloadString = Encoding.UTF8.GetString(payload); - - // we lump this in with trace logs - if (RuntimeFeatures.EnableInboundGatewayLogging && this.logger.IsEnabled(LogLevel.Trace)) - { - this.logger.LogTrace("Received HTTP gateway payload: {Payload}", AnonymizationUtilities.Anonymize(payloadString)); - } - - JObject data = JObject.Parse(payloadString); - - DiscordWebhookEvent? @event = data.ToDiscordObject(); - - if (@event is null) - { - this.logger.LogError("Failed to deserialize HTTP gateway payload: {Payload}", AnonymizationUtilities.Anonymize(payloadString)); - return; - } - - await this.writer.WriteAsync(@event); - } -} diff --git a/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs b/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs deleted file mode 100644 index a4929c7259..0000000000 --- a/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs +++ /dev/null @@ -1,91 +0,0 @@ -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 deleted file mode 100644 index 65741040b4..0000000000 --- a/DSharpPlus/Net/Models/AutoModerationRuleEditModel.cs +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 1877330476..0000000000 --- a/DSharpPlus/Net/Models/BaseEditModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index d5a2a389e9..0000000000 --- a/DSharpPlus/Net/Models/ChannelEditModel.cs +++ /dev/null @@ -1,110 +0,0 @@ -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 deleted file mode 100644 index 4f99f27a94..0000000000 --- a/DSharpPlus/Net/Models/GuildEditModel.cs +++ /dev/null @@ -1,113 +0,0 @@ -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 deleted file mode 100644 index 8075be4b4c..0000000000 --- a/DSharpPlus/Net/Models/MemberEditModel.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -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; } - - /// - /// New banner - /// - public Optional Banner { internal get; set; } - - /// - /// New avatar - /// - public Optional Avatar { internal get; set; } - - /// - /// New bio - /// - public Optional Bio { internal get; set; } - - internal MemberEditModel() - { - - } -} diff --git a/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs b/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs deleted file mode 100644 index 35d0bcb55e..0000000000 --- a/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 0198616d25..0000000000 --- a/DSharpPlus/Net/Models/RoleEditModel.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 5672fc0e03..0000000000 --- a/DSharpPlus/Net/Models/ScheduledGuildEventEditModel.cs +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 5be99b5618..0000000000 --- a/DSharpPlus/Net/Models/StageInstanceEditModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index c5e3fb0ede..0000000000 --- a/DSharpPlus/Net/Models/StickerEditModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index a01168ecc8..0000000000 --- a/DSharpPlus/Net/Models/ThreadChannelEditModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 8455be09ef..0000000000 --- a/DSharpPlus/Net/Models/WelcomeScreenEditModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -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/DiscordRestApiClient.cs b/DSharpPlus/Net/Rest/DiscordRestApiClient.cs deleted file mode 100644 index 430277a833..0000000000 --- a/DSharpPlus/Net/Rest/DiscordRestApiClient.cs +++ /dev/null @@ -1,7120 +0,0 @@ -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; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Entities.AuditLogs; -using DSharpPlus.Exceptions; -using DSharpPlus.Metrics; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Abstractions.Rest; -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 DiscordRestApiClient -{ - private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; - - internal BaseDiscordClient? discord; - internal RestClient rest; - - [ActivatorUtilitiesConstructor] - public DiscordRestApiClient(RestClient rest) => this.rest = rest; - - // This is for meta-clients, such as the webhook client - internal DiscordRestApiClient(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; - - internal void SetToken(TokenType type, string token) - => this.rest.SetToken(type, token); - - 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 - - public 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; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public async ValueTask DeleteGuildAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}", - Url = $"{Endpoints.GUILDS}/{guildId}", - Method = HttpMethod.Delete - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - public 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; - } - - public 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); - - List bans = 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; - }) - .ToList(); - - return bans; - } - - public 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); - } - - public 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); - } - - public 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; - } - - public 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); - } - - public 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 }; - } - - public 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!)!; - List members = new(rawMembers.Count); - - foreach (TransportMember tm in rawMembers) - { - this.discord.UpdateUserCache(new(tm.User) - { - Discord = this.discord - }); - - DiscordMember member = new(tm) - { - Discord = this.discord, - guild_id = guildId - }; - - members.Add(member); - } - - return members; - } - - public 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); - } - - public 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); - } - - public 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 - public 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; - } - - public async Task> GetAuditLogsAsync - ( - DiscordGuild guild, - int limit, - ulong? after = null, - ulong? before = null, - ulong? userId = null, - DiscordAuditLogActionType? actionType = null, - CancellationToken ct = default - ) - { - AuditLog auditLog = await GetAuditLogsAsync(guild.Id, limit, after, before, userId, actionType); - return AuditLogParser.ParseAuditLogToEntriesAsync(guild, auditLog, ct); - } - - 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!)!; - } - - public 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!)!; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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 templates.ToList(); - } - - public 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!)!; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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; - } - - public 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); - } - - public 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 - - public 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; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public async ValueTask CreateGuildStickerAsync - ( - ulong guildId, - string name, - string description, - string tags, - DiscordFile 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 DiscordFile[] - { - 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; - } - - public 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; - } - - public 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 - public 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; - } - - public 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); - } - - public 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); - } - - public 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(); - } - - public 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; - } - - public 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); - } - - public 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(); - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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); - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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 = await this.rest.ExecuteRequestAsync(request); - - return PrepareMessage(JObject.Parse(res.Response!)); - } - } - - public 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); - - List channels = JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xc => - { - xc.Discord = this.discord!; - return xc; - } - ).ToList(); - - foreach (DiscordChannel? ret in channels) - { - foreach (DiscordOverwrite xo in ret.permissionOverwrites) - { - xo.Discord = this.discord!; - xo.channelId = ret.Id; - } - } - - return channels; - } - - public 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 = []; - msgs.AddRange(msgsRaw.Select(PrepareMessage)); - - return msgs; - } - - public 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; - } - - public 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!)); - - return ret; - } - - public 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); - } - - public 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); - } - - public 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); - - List invites = JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xi => - { - xi.Discord = this.discord!; - return xi; - } - ) - .ToList(); - - return invites; - } - - public async ValueTask CreateChannelInviteAsync - ( - ulong channelId, - int maxAge, - int maxUses, - bool temporary, - bool unique, - string reason, - DiscordInviteTargetType? targetType = null, - ulong? targetUserId = null, - ulong? targetApplicationId = null, - IEnumerable? roleIds = null - ) - { - RestChannelInviteCreatePayload pld = new() - { - MaxAge = maxAge, - MaxUses = maxUses, - Temporary = temporary, - Unique = unique, - TargetType = targetType, - TargetUserId = targetUserId, - TargetApplicationId = targetApplicationId, - RoleIds = roleIds - }; - - 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; - } - - public 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); - } - - public 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); - } - - public 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); - } - - public 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 msgs; - } - - public 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); - } - - public 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); - } - - public 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); - } - - public 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); - } - - public 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; - } - - public 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; - } - - public 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!)!; - } - - public 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!)!; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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); - } - - public 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 - - public 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; - } - - public 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; - } - - public 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); - } - - public 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); - } - - public 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; - } - - public 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); - } - - public 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); - } - - public 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 threadMembers; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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)); - - public 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; - } - - public 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 - }; - } - - public 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); - } - - public 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 new DiscordUser(userRaw) - { - Discord = this.discord - }; - } - - public 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 guilds.ToList(); - } - else - { - List guilds = [.. JsonConvert.DeserializeObject>(res.Response!)!]; - foreach (DiscordGuild guild in guilds) - { - guild.Discord = this.discord!; - - } - return guilds; - } - } - - public 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 - }; - } - - public 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, - MemberFlags = memberFlags - }; - - 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); - } - - public async ValueTask ModifyCurrentMemberAsync - ( - ulong guildId, - Optional nick = default, - Optional banner = default, - Optional avatar = default, - Optional bio = default, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestGuildMemberModifyPayload pld = new() - { - Nickname = nick, - Banner = banner, - Avatar = avatar, - Bio = bio - }; - - 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 - public 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; - } - - public 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); - - List roles = JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xr => - { - xr.Discord = this.discord!; - xr.guild_id = guildId; - return xr; - } - ) - .ToList(); - - return roles; - } - public async ValueTask> GetGuildRoleMemberCountsAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{Endpoints.MEMBER_COUNTS}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{Endpoints.MEMBER_COUNTS}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject>(res.Response!)!; - } - - public 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; - } - } - - public 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; - } - - public 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); - } - - public 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 - public 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; - } - - public 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 - public 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; - } - - public 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); - - List integrations = - JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xi => - { - xi.Discord = this.discord!; - return xi; - } - ) - .ToList(); - - return integrations; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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); - } - - public 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); - } - - public 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); - - List regions - = JsonConvert.DeserializeObject>(res.Response!)!.ToList(); - - return regions; - } - - public 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); - - List invites = - JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xi => - { - xi.Discord = this.discord!; - return xi; - } - ) - .ToList(); - - return invites; - } - #endregion - - #region Invite - public async ValueTask GetInviteAsync - ( - string inviteCode, - bool? withCounts = null - ) - { - QueryUriBuilder uriBuilder = new($"{Endpoints.INVITES}/{inviteCode}"); - - if (withCounts is true) - { - uriBuilder.AddParameter("with_counts", "true"); - } - - const string route = $"{Endpoints.INVITES}/:invite_code"; - string url = uriBuilder.Build(); - - 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; - } - - public 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 - public 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); - - List connections = - JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xc => - { - xc.Discord = this.discord!; - return xc; - } - ) - .ToList(); - - return connections; - } - #endregion - - #region Voice - public 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); - - List regions = - JsonConvert.DeserializeObject>(res.Response!)! - .ToList(); - - return regions; - } - #endregion - - #region Webhooks - public 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; - } - - public 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); - - List webhooks = - JsonConvert - .DeserializeObject>(res.Response!)! - .Select - ( - xw => - { - xw.Discord = this.discord!; - xw.ApiClient = this; - return xw; - } - ) - .ToList(); - - return webhooks; - } - - public 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); - - List webhooks = - JsonConvert - .DeserializeObject>(res.Response!)! - .Select - ( - xw => - { - xw.Discord = this.discord!; - xw.ApiClient = this; - return xw; - } - ) - .ToList(); - - return webhooks; - } - - public 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 - public 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; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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); - } - - public 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); - } - - public 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 = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - return ret; - } - - public 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; - } - - public 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; - } - - public 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"; - QueryUriBuilder uriBuilder = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"); - - uriBuilder.AddParameter("wait", "true"); - uriBuilder.AddParameter("with_components", "true"); - - if (builder.ThreadId.HasValue) - { - uriBuilder.AddParameter("thread_id", builder.ThreadId.Value.ToString()); - } - - Dictionary values = new() - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; - - MultipartRestRequest request = new() - { - Route = route, - Url = uriBuilder.Build(), - Method = HttpMethod.Patch, - Values = values, - Files = builder.Files, - IsExemptFromGlobalLimit = true - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return PrepareMessage(JObject.Parse(res.Response!)); - } - - public 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 - public 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); - } - - public 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); - } - - public 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); - } - - public 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 users; - } - - public 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); - } - - public 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 - - public 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(); - } - - public 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 - public 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 emojis; - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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 - public 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(); - } - - public 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(); - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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); - } - - public 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(); - } - - public 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(); - } - - public 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; - } - - public 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; - } - - public 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; - } - - public 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, - string? content = null, - IReadOnlyList? embeds = null, - bool isTTS = false, - string? customID = null, - string? title = null, - IReadOnlyList? mentions = null, - IReadOnlyList? components = null, - DiscordMessageFlags? flags = null, - IReadOnlyList? choices = null, - DiscordPollBuilder? pollBuilder = null, - IReadOnlyList? files = null - ) - { - bool hasContent = content is not null || embeds is not null || components is not null || choices is not null || pollBuilder is not null; - - if (embeds is not null) - { - foreach (DiscordEmbed embed in embeds) - { - embed.Timestamp = embed.Timestamp?.ToUniversalTime(); - } - } - - DiscordInteractionResponsePayload payload = new() - { - Type = type, - Data = !hasContent - ? null - : new DiscordInteractionApplicationCommandCallbackData - { - Content = content, - IsTTS = isTTS, - Title = title, - CustomId = customID, - Embeds = embeds, - Mentions = new DiscordMentions(mentions ?? Mentions.All, mentions is not (null or [])), - Components = components, - Choices = choices, - Poll = pollBuilder?.BuildInternal(), - Flags = flags, - } - }; - - Dictionary values = []; - - if (hasContent) - { - if (!string.IsNullOrEmpty(content) || embeds?.Count > 0 || isTTS || 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 (files is not (null or [])) - { - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Values = values, - Files = files, - IsExemptFromAllLimits = true - }; - - await this.rest.ExecuteRequestAsync(request); - } - else - { - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(payload), - IsExemptFromGlobalLimit = true - }; - - await this.rest.ExecuteRequestAsync(request); - } - } - - public ValueTask CreateInteractionResponseAsync - ( - ulong interactionId, - string interactionToken, - DiscordInteractionResponseType type, - DiscordInteractionResponseBuilder? builder - ) - { - return CreateInteractionResponseAsync - ( - interactionId, - interactionToken, - type, - builder?.Content, - builder?.Embeds, - builder?.IsTTS ?? false, - null, - null, - builder?.Mentions, - builder?.Components, - builder?.Flags, - builder?.Choices, - builder?.Poll, - builder?.Files - ); - } - - public 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; - } - - public 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; - - if (builder.Files.Any()) - { - attachments ??= []; - } - - 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!; - - return ret; - } - } - - public 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); - } - - public 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 = await this.rest.ExecuteRequestAsync(request); - - 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); - - public 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(); - } - - public 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; - } - - public 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; - } - - public 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!)!; - } - - public 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); - - List assets - = JsonConvert.DeserializeObject>(res.Response!)!.ToList(); - - foreach (DiscordApplicationAsset asset in assets) - { - asset.Discord = application.Discord; - asset.Application = application; - } - - return assets; - } - - public 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 - - public 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; - } - - public 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; - } - - public 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); - } - - public 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; - } - - public 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(); - } - - public 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. - public 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. - public 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. - public 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. - public 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. - public 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 - public 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) - public 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 - public 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 - public 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 - public 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 deleted file mode 100644 index 0393f34b1d..0000000000 --- a/DSharpPlus/Net/Rest/Endpoints.cs +++ /dev/null @@ -1,81 +0,0 @@ -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_COUNTS = "member-counts"; - 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 deleted file mode 100644 index c38bf1b2a3..0000000000 --- a/DSharpPlus/Net/Rest/IRestRequest.cs +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 76c9e2feb4..0000000000 --- a/DSharpPlus/Net/Rest/IpEndpoint.cs +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 11c3f0a164..0000000000 --- a/DSharpPlus/Net/Rest/MultipartRestRequest.cs +++ /dev/null @@ -1,176 +0,0 @@ -// 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.IO; -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++) - { - DiscordFile 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(); - - PostprocessAddedFiles(current); - - 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; - } - - private static void PostprocessAddedFiles(DiscordFile file) - { - if (file.FileOptions.HasFlag(AddFileOptions.CloseStream)) - { - if (file.Stream is RequestStreamWrapper wrapper) - { - wrapper.UnderlyingStream.Dispose(); - } - else - { - file.Stream.Dispose(); - } - } - else if (file.ResetPositionTo.HasValue) - { - file.Stream.Seek(file.ResetPositionTo!.Value, SeekOrigin.Begin); - } - } -} diff --git a/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs b/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs deleted file mode 100644 index 7f347f440e..0000000000 --- a/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index b042ccde75..0000000000 --- a/DSharpPlus/Net/Rest/RateLimitBucket.cs +++ /dev/null @@ -1,186 +0,0 @@ -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 deleted file mode 100644 index 6b49dd55df..0000000000 --- a/DSharpPlus/Net/Rest/RateLimitCandidateBucket.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index c9c34d5125..0000000000 --- a/DSharpPlus/Net/Rest/RateLimitOptions.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Polly; - -namespace DSharpPlus.Net; - -internal class RateLimitOptions : ResilienceStrategyOptions; diff --git a/DSharpPlus/Net/Rest/RateLimitStrategy.cs b/DSharpPlus/Net/Rest/RateLimitStrategy.cs deleted file mode 100644 index 424b611f8a..0000000000 --- a/DSharpPlus/Net/Rest/RateLimitStrategy.cs +++ /dev/null @@ -1,332 +0,0 @@ -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 deleted file mode 100644 index 3c71a47a75..0000000000 --- a/DSharpPlus/Net/Rest/RequestStreamWrapper.cs +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 073fd86c0b..0000000000 --- a/DSharpPlus/Net/Rest/RestClient.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -using DSharpPlus.Exceptions; -using DSharpPlus.Logging; -using DSharpPlus.Metrics; - -using Microsoft.Extensions.DependencyInjection; -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 -{ - 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 readonly TimeSpan timeout; - - private string token; - - private volatile bool disposed; - - [ActivatorUtilitiesConstructor] - public RestClient - ( - ILogger logger, - IHttpClientFactory clientFactory, - IOptions options - ) - : this - ( - clientFactory.CreateClient("DSharpPlus.Rest.HttpClient"), - options.Value.Timeout, - logger, - options.Value.MaximumRatelimitRetries, - (int)options.Value.RatelimitRetryDelayFallback.TotalMilliseconds, - (int)options.Value.InitialRequestTimeout.TotalMilliseconds, - options.Value.MaximumConcurrentRestRequests - ) - { - - } - - // 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.timeout = timeout; - - 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 void SetToken(TokenType type, string token) - => this.token = type == TokenType.Bot ? $"Bot {token}" : $"Bearer {token}"; - - 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); - - CancellationTokenSource cts = new(this.timeout); - - using HttpResponseMessage response = await this.pipeline.ExecuteAsync - ( - async (_) => - { - using HttpRequestMessage req = request.Build(); - req.Headers.TryAddWithoutValidation("Authorization", this.token); - - return await this.httpClient.SendAsync - ( - req, - HttpCompletionOption.ResponseContentRead, - cts.Token - ); - }, - 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/RestClientOptions.cs b/DSharpPlus/Net/Rest/RestClientOptions.cs deleted file mode 100644 index 7dae71fc03..0000000000 --- a/DSharpPlus/Net/Rest/RestClientOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace DSharpPlus.Net; - -/// -/// Represents configuration options passed to DSharpPlus' Rest client. -/// -public sealed class RestClientOptions -{ - /// - /// Sets the timeout for RestClient operations. Set this to - /// to never time out. Defaults to 100 seconds. - /// - /// - /// Setting this value does not affect the of the supplied by Dependency Injection. - /// - public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100); - - /// - /// Specifies the maximum amount of retries to attempt when ratelimited. Retries will still try to respect the ratelimit. - /// - /// - /// Setting this value to 0 disables retrying, including on pre-emptive ratelimits. Defaults to . - /// - public int MaximumRatelimitRetries { get; set; } = int.MaxValue; - - /// - /// Specifies the delay to use when there was no delay information passed to the rest client. Defaults to 2.5 seconds. - /// - public TimeSpan RatelimitRetryDelayFallback { get; set; } = TimeSpan.FromMilliseconds(2500); - - /// - /// Specifies the time we should be waiting for a ratelimit bucket hash to initialize. - /// - public TimeSpan InitialRequestTimeout { get; set; } = TimeSpan.FromMilliseconds(200); - - /// - /// Specifies the maximum rest requests to attempt concurrently. Defaults to 50. Only increase this if Discord has approved you to do so. - /// - public int MaximumConcurrentRestRequests { get; set; } = 50; -} diff --git a/DSharpPlus/Net/Rest/RestRequest.cs b/DSharpPlus/Net/Rest/RestRequest.cs deleted file mode 100644 index 24b9f5c3c8..0000000000 --- a/DSharpPlus/Net/Rest/RestRequest.cs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 672b7df895..0000000000 --- a/DSharpPlus/Net/Rest/RestResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -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/RetryableRatelimitException.cs b/DSharpPlus/Net/Rest/RetryableRatelimitException.cs deleted file mode 100644 index 4f24f14666..0000000000 --- a/DSharpPlus/Net/Rest/RetryableRatelimitException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace DSharpPlus.Net; - -internal sealed class RetryableRatelimitException : Exception -{ - public required TimeSpan ResetAfter { get; set; } - - [SetsRequiredMembers] - public RetryableRatelimitException(TimeSpan resetAfter) - => this.ResetAfter = resetAfter; -} diff --git a/DSharpPlus/Net/Rest/SessionBucket.cs b/DSharpPlus/Net/Rest/SessionBucket.cs deleted file mode 100644 index ad94544371..0000000000 --- a/DSharpPlus/Net/Rest/SessionBucket.cs +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 0f11f37400..0000000000 --- a/DSharpPlus/Net/Serialization/DiscordComponentJsonConverter.cs +++ /dev/null @@ -1,61 +0,0 @@ -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); - - // this is type whenever we deserialize a proper component, or if we receive a modal. in message based interactions, - // the docs specify this as component_type, for reasons beyond anybody's comprehension. - DiscordComponentType? type = (job["type"] ?? job["component_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.TextInput => 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.File => new DiscordFileComponent(), - DiscordComponentType.Container => new DiscordContainerComponent(), - DiscordComponentType.Label => new DiscordLabelComponent(), - DiscordComponentType.FileUpload => new DiscordFileUploadComponent(), - DiscordComponentType.RadioGroup => new DiscordRadioGroupComponent(), - DiscordComponentType.CheckboxGroup => new DiscordCheckboxGroupComponent(), - DiscordComponentType.Checkbox => new DiscordCheckboxComponent(), - _ => 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 deleted file mode 100644 index b55c13a51d..0000000000 --- a/DSharpPlus/Net/Serialization/DiscordForumChannelJsonConverter.cs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index b89ea09534..0000000000 --- a/DSharpPlus/Net/Serialization/DiscordJson.cs +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index f0d1a9ff48..0000000000 --- a/DSharpPlus/Net/Serialization/DiscordPermissionsAsStringJsonConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 6b118d7664..0000000000 --- a/DSharpPlus/Net/Serialization/ISO8601DateTimeOffsetJsonConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 3468f8335a..0000000000 --- a/DSharpPlus/Net/Serialization/SnowflakeArrayAsDictionaryJsonConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 09b42fc5d7..0000000000 --- a/DSharpPlus/Net/Udp/BaseUdpClient.cs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index d125c8f09b..0000000000 --- a/DSharpPlus/Net/Udp/DspUdpClient.cs +++ /dev/null @@ -1,91 +0,0 @@ -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/IWebSocketClient.cs b/DSharpPlus/Net/WebSocket/IWebSocketClient.cs deleted file mode 100644 index 329606c1ec..0000000000 --- a/DSharpPlus/Net/WebSocket/IWebSocketClient.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; - -using DSharpPlus.AsyncEvents; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Net.WebSocket; - -/// -/// Creates an instance of a WebSocket client implementation. -/// -/// Proxy settings to use for the new WebSocket client instance. -/// Constructed WebSocket client implementation. -public delegate IWebSocketClient WebSocketClientFactoryDelegate(IWebProxy proxy); - -/// -/// Represents a base abstraction for all WebSocket client implementations. -/// -public interface IWebSocketClient : IDisposable -{ - /// - /// Gets the proxy settings for this client. - /// - public IWebProxy Proxy { get; } - - /// - /// Gets the collection of default headers to send when connecting to the remote endpoint. - /// - public IReadOnlyDictionary DefaultHeaders { get; } - - /// - /// Gets the current state of the WebSocket connection. - /// - public bool IsConnected { get; } - - /// - /// Connects to a specified remote WebSocket endpoint. - /// - /// The URI of the WebSocket endpoint. - /// - public Task ConnectAsync(Uri uri); - - /// - /// Disconnects the WebSocket connection. - /// - /// - public Task DisconnectAsync(int code = 1000, string message = ""); - - /// - /// Send a message to the WebSocket server. - /// - /// The message to send. - public Task SendMessageAsync(string message); - - /// - /// Adds a header to the default header collection. - /// - /// Name of the header to add. - /// Value of the header to add. - /// Whether the operation succeeded. - public bool AddDefaultHeader(string name, string value); - - /// - /// Removes a header from the default header collection. - /// - /// Name of the header to remove. - /// Whether the operation succeeded. - public bool RemoveDefaultHeader(string name); - - /// - /// Triggered when the client connects successfully. - /// - public event AsyncEventHandler Connected; - - /// - /// Triggered when the client is disconnected. - /// - public event AsyncEventHandler Disconnected; - - /// - /// Triggered when the client receives a message from the remote party. - /// - public event AsyncEventHandler MessageReceived; - - /// - /// Triggered when an error occurs in the client. - /// - public event AsyncEventHandler ExceptionThrown; -} diff --git a/DSharpPlus/Net/WebSocket/SocketLock.cs b/DSharpPlus/Net/WebSocket/SocketLock.cs deleted file mode 100644 index 56e1b89e4a..0000000000 --- a/DSharpPlus/Net/WebSocket/SocketLock.cs +++ /dev/null @@ -1,70 +0,0 @@ -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/Net/WebSocket/WebSocketClient.cs b/DSharpPlus/Net/WebSocket/WebSocketClient.cs deleted file mode 100644 index 3fda530687..0000000000 --- a/DSharpPlus/Net/WebSocket/WebSocketClient.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Net; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -using DSharpPlus.AsyncEvents; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Net.WebSocket; - -// weebsocket -// not even sure whether emzi or I posted this. much love, naam. -/// -/// The default, native-based WebSocket client implementation. -/// -public class WebSocketClient : IWebSocketClient -{ - private const int OutgoingChunkSize = 8192; // 8 KiB - private const int IncomingChunkSize = 32768; // 32 KiB - - /// - public IWebProxy Proxy { get; } - - /// - public IReadOnlyDictionary DefaultHeaders { get; } - - /// - public bool IsConnected - => this.isConnected; - - private readonly Dictionary defaultHeaders; - - private Task receiverTask; - private CancellationTokenSource receiverTokenSource; - private CancellationToken receiverToken; - private readonly SemaphoreSlim senderLock; - - private CancellationTokenSource socketTokenSource; - private CancellationToken socketToken; - private ClientWebSocket ws; - - private volatile bool isClientClose = false; - private volatile bool isConnected = false; - private bool isDisposed = false; - - /// - /// Instantiates a new WebSocket client. - /// - public WebSocketClient(IClientErrorHandler handler) - { - this.connected = new(handler); - this.disconnected = new(handler); - this.messageReceived = new(handler); - this.exceptionThrown = new(handler); - - this.defaultHeaders = []; - this.DefaultHeaders = new ReadOnlyDictionary(this.defaultHeaders); - - this.receiverTokenSource = null; - this.receiverToken = CancellationToken.None; - this.senderLock = new SemaphoreSlim(1); - - this.socketTokenSource = null; - this.socketToken = CancellationToken.None; - } - - /// - public async Task ConnectAsync(Uri uri) - { - // Disconnect first - try - { - await DisconnectAsync(); - } - catch { } - - // Disallow sending messages - await this.senderLock.WaitAsync(); - - try - { - // This can be null at this point - this.receiverTokenSource?.Dispose(); - this.socketTokenSource?.Dispose(); - - this.ws?.Dispose(); - this.ws = new ClientWebSocket(); - this.ws.Options.Proxy = this.Proxy; - this.ws.Options.KeepAliveInterval = TimeSpan.Zero; - if (this.defaultHeaders != null) - { - foreach ((string k, string v) in this.defaultHeaders) - { - this.ws.Options.SetRequestHeader(k, v); - } - } - - this.receiverTokenSource = new CancellationTokenSource(); - this.receiverToken = this.receiverTokenSource.Token; - - this.socketTokenSource = new CancellationTokenSource(); - this.socketToken = this.socketTokenSource.Token; - - this.isClientClose = false; - this.isDisposed = false; - await this.ws.ConnectAsync(uri, this.socketToken); - this.receiverTask = Task.Run(ReceiverLoopAsync, this.receiverToken); - } - finally - { - this.senderLock.Release(); - } - } - - /// - public async Task DisconnectAsync(int code = 1000, string message = "") - { - // Ensure that messages cannot be sent - await this.senderLock.WaitAsync(); - - try - { - this.isClientClose = true; - if (this.ws != null && (this.ws.State == WebSocketState.Open || this.ws.State == WebSocketState.CloseReceived)) - { - await this.ws.CloseOutputAsync((WebSocketCloseStatus)code, message, CancellationToken.None); - } - - if (this.receiverTask != null) - { - await this.receiverTask; // Ensure that receiving completed - } - - if (this.isConnected) - { - this.isConnected = false; - } - - if (!this.isDisposed) - { - // Cancel all running tasks - if (this.socketToken.CanBeCanceled) - { - this.socketTokenSource?.Cancel(); - } - - this.socketTokenSource?.Dispose(); - - if (this.receiverToken.CanBeCanceled) - { - this.receiverTokenSource?.Cancel(); - } - - this.receiverTokenSource?.Dispose(); - - this.isDisposed = true; - } - } - catch { } - finally - { - this.senderLock.Release(); - } - } - - /// - public async Task SendMessageAsync(string message) - { - if (this.ws == null) - { - return; - } - - if (this.ws.State is not WebSocketState.Open and not WebSocketState.CloseReceived) - { - return; - } - - byte[] bytes = Utilities.UTF8.GetBytes(message); - await this.senderLock.WaitAsync(); - try - { - int len = bytes.Length; - int segCount = len / OutgoingChunkSize; - if (len % OutgoingChunkSize != 0) - { - segCount++; - } - - for (int i = 0; i < segCount; i++) - { - int segStart = OutgoingChunkSize * i; - int segLen = Math.Min(OutgoingChunkSize, len - segStart); - - await this.ws.SendAsync(new ArraySegment(bytes, segStart, segLen), WebSocketMessageType.Text, i == segCount - 1, CancellationToken.None); - } - } - finally - { - this.senderLock.Release(); - } - } - - /// - public bool AddDefaultHeader(string name, string value) - { - this.defaultHeaders[name] = value; - return true; - } - - /// - public bool RemoveDefaultHeader(string name) - => this.defaultHeaders.Remove(name); - - /// - /// Disposes of resources used by this WebSocket client instance. - /// - //public void Dispose() - //{ - // - //} - - internal async Task ReceiverLoopAsync() - { - await Task.Yield(); - - CancellationToken token = this.receiverToken; - ArraySegment buffer = new(new byte[IncomingChunkSize]); - - try - { - using MemoryStream bs = new(); - while (!token.IsCancellationRequested) - { - // See https://github.com/RogueException/Discord.Net/commit/ac389f5f6823e3a720aedd81b7805adbdd78b66d - // for explanation on the cancellation token - - WebSocketReceiveResult result; - byte[] resultBytes; - do - { - result = await this.ws.ReceiveAsync(buffer, CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Close) - { - break; - } - - bs.Write(buffer.Array, 0, result.Count); - } - while (!result.EndOfMessage); - - resultBytes = new byte[bs.Length]; - bs.Position = 0; - bs.Read(resultBytes, 0, resultBytes.Length); - bs.Position = 0; - bs.SetLength(0); - - if (!this.isConnected && result.MessageType != WebSocketMessageType.Close) - { - this.isConnected = true; - await this.connected.InvokeAsync(this, new SocketOpenedEventArgs()); - } - - if (result.MessageType == WebSocketMessageType.Binary) - { - await this.messageReceived.InvokeAsync(this, new SocketBinaryMessageEventArgs(resultBytes)); - } - else if (result.MessageType == WebSocketMessageType.Text) - { - await this.messageReceived.InvokeAsync(this, new SocketTextMessageEventArgs(Utilities.UTF8.GetString(resultBytes))); - } - else // close - { - if (!this.isClientClose) - { - WebSocketCloseStatus code = result.CloseStatus.Value; - code = code is WebSocketCloseStatus.NormalClosure or WebSocketCloseStatus.EndpointUnavailable - ? (WebSocketCloseStatus)4000 - : code; - - await this.ws.CloseOutputAsync(code, result.CloseStatusDescription, CancellationToken.None); - } - - await this.disconnected.InvokeAsync(this, new SocketClosedEventArgs() { CloseCode = (int)result.CloseStatus, CloseMessage = result.CloseStatusDescription }); - break; - } - } - } - catch (Exception ex) - { - await this.exceptionThrown.InvokeAsync(this, new SocketErrorEventArgs() { Exception = ex }); - await this.disconnected.InvokeAsync(this, new SocketClosedEventArgs() { CloseCode = -1, CloseMessage = "" }); - } - - // Don't await or you deadlock - // DisconnectAsync waits for this method - _ = DisconnectAsync(); - } - - #region Events - /// - /// Triggered when the client connects successfully. - /// - public event AsyncEventHandler Connected - { - add => this.connected.Register(value); - remove => this.connected.Unregister(value); - } - private readonly AsyncEvent connected; - - /// - /// Triggered when the client is disconnected. - /// - public event AsyncEventHandler Disconnected - { - add => this.disconnected.Register(value); - remove => this.disconnected.Unregister(value); - } - private readonly AsyncEvent disconnected; - - /// - /// Triggered when the client receives a message from the remote party. - /// - public event AsyncEventHandler MessageReceived - { - add => this.messageReceived.Register(value); - remove => this.messageReceived.Unregister(value); - } - private readonly AsyncEvent messageReceived; - - /// - /// Triggered when an error occurs in the client. - /// - public event AsyncEventHandler ExceptionThrown - { - add => this.exceptionThrown.Register(value); - remove => this.exceptionThrown.Unregister(value); - } - private readonly AsyncEvent exceptionThrown; - - private void EventErrorHandler(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, WebSocketClient sender, TArgs eventArgs) - where TArgs : AsyncEventArgs - => this.exceptionThrown.InvokeAsync(this, new SocketErrorEventArgs() { Exception = ex }).GetAwaiter().GetResult(); - - protected virtual void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - DisconnectAsync().GetAwaiter().GetResult(); - this.receiverTokenSource?.Dispose(); - this.socketTokenSource?.Dispose(); - } - - this.isDisposed = true; - } - } - - public 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/Properties/AssemblyProperties.cs b/DSharpPlus/Properties/AssemblyProperties.cs deleted file mode 100644 index 5c3db4c306..0000000000 --- a/DSharpPlus/Properties/AssemblyProperties.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index f4fd49e5fd..0000000000 --- a/DSharpPlus/QueryUriBuilder.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index cd242c85c2..0000000000 --- a/DSharpPlus/ReadOnlyConcurrentDictionary.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index aeec8a90e5..0000000000 --- a/DSharpPlus/ReadOnlySet.cs +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index 839b9ea6f6..0000000000 --- a/DSharpPlus/RingBuffer.cs +++ /dev/null @@ -1,230 +0,0 @@ -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 deleted file mode 100644 index 271f7b2ff1..0000000000 --- a/DSharpPlus/TimestampFormat.cs +++ /dev/null @@ -1,43 +0,0 @@ -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/TokenContainer.cs b/DSharpPlus/TokenContainer.cs deleted file mode 100644 index 5819979ff5..0000000000 --- a/DSharpPlus/TokenContainer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace DSharpPlus; - -/// -/// Holds a delegate to obtain a token to initialize the application with. -/// -public sealed class TokenContainer -{ - /// - /// Gets the token for this application. - /// - public required Func GetToken { get; set; } -} diff --git a/DSharpPlus/TokenType.cs b/DSharpPlus/TokenType.cs deleted file mode 100644 index 5087a7b210..0000000000 --- a/DSharpPlus/TokenType.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 4cd23f1c0b..0000000000 --- a/DSharpPlus/Utilities.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -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, - }; - - /// - /// Converts a stream to a base64-encoded data URL string. - /// - /// The optional stream to convert. - /// - /// An optional base64-encoded data URL string. If the stream has no value, returns an optional with no value. - /// If the stream has a value but it is null, returns an optional containing null. - /// Otherwise, returns the base64-encoded data URL string. - /// - internal static Optional ConvertStreamToBase64(Optional stream) - { - if (stream.HasValue && stream.Value is not null) - { - using InlineMediaTool imgtool = new(stream.Value); - return imgtool.GetBase64(); - } - - return stream.HasValue - ? Optional.FromValue(null) - : Optional.FromNoValue(); - } - - // 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 b0c906b777..3865007bd9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,54 +1,61 @@ - + + - - $(CoreCompileDependsOn);_DisableAnalyzers - true - false - Latest - true + net10.0 enable - Library - $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), "DSharpPlus.slnx")) - 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) + disable + preview + True + True + All + $(NoWarn);CS1591;CA1028;CA1062;CA1308;CA1711;CA1716;CA1720;CA2007;CA2225;IDE0130 + + + True + true + true - + DSharpPlus - DSharpPlus contributors - dsharpplus.png - LICENSE + A C# API for Discord based off DiscordSharp. + DSharpPlus Contributors 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 + Git + MPL + dsharpplus.png + https://raw.githubusercontent.com/DSharpPlus/DSharpPlus/master/logo/dsharpplus.png + + + + + + + + <_DSharpPlusReleaseVersion>6.0.0 + + + <_DSharpPlusInternalAbstractionsModelsVersion>0.1.0 + + + <_DSharpPlusInternalModelsVersion>0.1.0 + + + <_DSharpPlusInternalAbstractionsRestVersion>0.1.0 + + + <_DSharpPlusInternalRestVersion>0.1.0 + + + + + <_DSharpPlusExtensionsInternalToolboxVersion>0.1.0 + + + <_DSharpPlusExtensionsInternalBadRequestHelperVersion>0.1.0 - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000000..5f837e8d23 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,54 @@ + + + + + + + <_ProjectReferenceWithExplicitPackageVersion + Include="@(ProjectReference->'%(FullPath)')" + Condition="'%(ProjectReference.PackageVersion)' != ''" /> + + <_ProjectReferenceWithExactPackageVersion + Include="@(ProjectReference->'%(FullPath)')" + Condition="'%(ProjectReference.ExactVersion)' == 'true'" /> + + <_ProjectReferenceWithReassignedVersion + Include="@(_ProjectReferencesWithVersions)" + Condition="'%(Identity)' != '' And '@(_ProjectReferencesWithVersions)' == '@(_ProjectReferenceWithExplicitPackageVersion)'"> + + @(_ProjectReferenceWithExplicitPackageVersion->'%(PackageVersion)') + + + <_ProjectReferenceWithReassignedVersion + Include="@(_ProjectReferencesWithVersions)" + Condition="'%(Identity)' != '' And '@(_ProjectReferencesWithVersions)' == '@(_ProjectReferenceWithExactPackageVersion)'"> + + [@(_ProjectReferencesWithVersions->'%(ProjectVersion)')] + + + <_ProjectReferencesWithVersions Remove="@(_ProjectReferenceWithReassignedVersion)" /> + <_ProjectReferencesWithVersions Include="@(_ProjectReferenceWithReassignedVersion)" /> + + + + + + + + + + + $(CoreCompileDependsOn);_DisableAnalyzers + + + + + <_RemoveAnalyzer>%(RemoveAnalyzer.Identity) + + + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index c7dc4b8875..933f18ec48 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,35 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DiscordSharp.LICENSE b/DiscordSharp.Old.License similarity index 100% rename from DiscordSharp.LICENSE rename to DiscordSharp.Old.License diff --git a/LICENSE b/LICENSE index 0b955741df..dbdb0fa8e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,373 @@ -The MIT License (MIT) - -Copyright (c) 2015 Mike Santiago -Copyright (c) 2016-2025 DSharpPlus Development Team - -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. +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000000..2f0a224ca8 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2015 Mike Santiago +Copyright (c) 2016-2025 DSharpPlus Development Team (v1-v5) +Copyright (c) 2023-2025 DSharpPlus Development Team (v6+, selected contributors: ./readme.md#licensing) + +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. diff --git a/README.md b/README.md deleted file mode 100644 index 057710750f..0000000000 --- a/README.md +++ /dev/null @@ -1,73 +0,0 @@ -![Logo of DSharpPlus](https://github.com/DSharpPlus/DSharpPlus/raw/master/logo/dsharp+_smaller.png) - -# DSharpPlus - -An unofficial .NET wrapper for the Discord API, based off [DiscordSharp](https://github.com/suicvne/DiscordSharp), but rewritten to fit the API standards. - -[![Nightly Build Status](https://github.com/DSharpPlus/DSharpPlus/actions/workflows/publish_nightly_master.yml/badge.svg?branch=master)](https://github.com/DSharpPlus/DSharpPlus/actions/workflows/publish_nightly_master.yml) -[![Discord Server](https://img.shields.io/discord/379378609942560770.svg?label=Discord&color=506de2)](https://discord.gg/dsharpplus) -[![NuGet](https://img.shields.io/nuget/v/DSharpPlus.svg?label=NuGet)](https://nuget.org/packages/DSharpPlus) -[![NuGet Latest Nightly/Prerelease](https://img.shields.io/nuget/vpre/DSharpPlus?color=505050&label=NuGet%20Latest%20Nightly%2FPrerelease)](https://nuget.org/packages/DSharpPlus) - -# Installing - -You can install the library from following sources: - -1. All Nightly versions are available on [Nuget](https://www.nuget.org/packages/DSharpPlus/) as a pre-release. These are cutting-edge versions automatically built from the latest commit in the `master` branch in this repository, and as such always contains the latest changes. If you want to use the latest features on Discord, you should use the nightlies. - - Despite the nature of pre-release software, all changes to the library are held under a level of scrutiny; for this library, unstable does not mean bad quality, rather it means that the API can be subject to change without prior notice (to ease rapid iteration) and that consumers of the library should always remain on the latest version available (to immediately get the latest fixes and improvements). You will usually want to use this version. - -2. The latest stable release is always available on [NuGet](https://nuget.org/packages/DSharpPlus). Stable versions are released less often, but are guaranteed to not receive any breaking API changes without a major version bump. - - Critical bugfixes in the nightly releases will usually be backported to the latest major stable release, but only after they have passed our soak tests. Additionally, some smaller fixes may be infrastructurally impossible or very difficult to backport without "breaking everything", and as such they will remain only in the nightly release until the next major release. You should evaluate whether or not this version suits your specific needs. - -3. The library can be directly referenced from your csproj file. Cloning the repository and referencing the library is as easy as: - - ``` - git clone https://github.com/DSharpPlus/DSharpPlus.git DSharpPlus-Repo - ``` - - Edit MyProject.csproj and add the following line: - - ```xml - - ``` - - This belongs in the ItemGroup tag with the rest of your dependencies. The library should not be in the same directory or subdirectory as your project. This method should only be used if you're making local changes to the library. - -# Documentation - -The documentation for the latest nightly version is available at [dsharpplus.github.io](https://dsharpplus.github.io/DSharpPlus). - -## Resources - -The following resources apply only for the latest stable version of the library. - -### Tutorials - -* [Making your first bot in C#](https://dsharpplus.github.io/DSharpPlus/articles/basics/bot_account.html). - -### Example bots - -* [Example by OoLunar](https://github.com/DSharpPlus/Example-Bots) - -# I want to throw my money at you - -If you want to give us some money as a thank you gesture, you can do so using one of these links: - -* Naamloos - * [Ko-Fi](https://ko-fi.com/naamloos) -* Emzi0767 - * [Ko-Fi](https://ko-fi.com/emzi0767) - * [PayPal](https://paypal.me/Emzi0767/5USD) - * [Patreon](https://patreon.com/emzi0767) - -# Questions? - -Come talk to us here: - -[![DSharpPlus Chat](https://discord.com/api/guilds/379378609942560770/embed.png?style=banner1)](https://discord.gg/dsharpplus) - -Alternatively, you could also join us in the [Discord API chat](https://discord.gg/discord-api) at **#dotnet_dsharpplus**. - -[![Discord API Chat](https://discord.com/api/guilds/81384788765712384/embed.png?style=banner1)](https://discord.gg/discord-api) diff --git a/Solution.Build.Targets b/Solution.Build.Targets new file mode 100644 index 0000000000..5f319aa2f5 --- /dev/null +++ b/Solution.Build.Targets @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 909b12f601..0000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -############### -# folder # -############### -/**/DROP/ -/**/TEMP/ -/**/packages/ -/**/bin/ -/**/obj/ -_site - -# .manifest files -api/.manifest \ No newline at end of file diff --git a/docs/api/.gitignore b/docs/api/.gitignore deleted file mode 100644 index da7c71b83a..0000000000 --- a/docs/api/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -############### -# temp file # -############### -*.yml diff --git a/docs/api/index.md b/docs/api/index.md deleted file mode 100644 index a2db05c743..0000000000 --- a/docs/api/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -uid: apidocs -title: API Reference ---- - -# API Reference - -Welcome to DSharpPlus API reference. To begin, select a namespace, then a class, from the table of contents on the left. diff --git a/docs/articles/advanced_topics/buttons.md b/docs/articles/advanced_topics/buttons.md deleted file mode 100644 index 178e9c7506..0000000000 --- a/docs/articles/advanced_topics/buttons.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -uid: articles.advanced_topics.buttons -title: Buttons ---- - -# Introduction - -Buttons are a feature in Discord based on the interaction framework appended to the bottom of a message which come in -several colors. You will want to familarize yourself with the [message builder][0] as it and similar builder objects -will be used throughout this article. - -With buttons, you can have up to five buttons in a row, and up to five (5) rows of buttons, for a maximum for 25 buttons -per message. Furthermore, buttons come in two types: regular, and link. Link buttons contain a Url field, and are always -grey. - -# Buttons Continued - -> [!WARNING] -> Component (Button) Ids on buttons should be unique, as this is what's sent back when a user presses a button. -> -> Link buttons do **not** have a custom id and do **not** send interactions when pressed. - -Buttons consist of five parts: - -- Id -- Style -- Label -- Emoji -- Disabled - -The id of the button is a settable string on buttons, and is specified by the developer. Discord sends this id back in -the [interaction object][1]. - -Non-link buttons come in four colors, which are known as styles: Blurple, Grey, Green, and Red. Or as their styles are -named: Primary, Secondary, Success, and Danger respectively. - -How does one construct a button? It's simple, buttons support constructor and object initialization like so: - -```cs -var myButton = new DiscordButtonComponent( - DiscordButtonStyle.Primary, - "my_very_cool_button", - "Very cool button!", - false, - new DiscordComponentEmoji("😀")); -``` - -This will create a blurple button with the text that reads "Very cool button!". When a user pushes it, -`"my_very_cool_button"` will be sent back as the @DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs.Id property -on the event. This is expanded on in the [how to respond to buttons][2]. - -The label of a button is optional *if* an emoji is specified. The label can be up to 80 characters in length. The emoji -of a button is a [partial emoji object][3], which means that **any valid emoji is usable**, even if your bot does not -have access to it's origin server. - -The disabled field of a button is rather self explanatory. If this is set to true, the user will see a greyed out button -which they cannot interact with. - -# Adding buttons -> -> [!NOTE] -> This article will use underscores in button ids for consistency and styling, but spaces are also usable. - -Adding buttons to a message is relatively simple. Simply make a builder, and sprinkle some content and the buttons you'd -like. - -```cs -var builder = new DiscordMessageBuilder(); -builder.WithContent("This message has buttons! Pretty neat innit?"); -``` - -Well, there's a builder, but no buttons. What now? Simply make a new @DSharpPlus.Entities.DiscordButtonComponent object -and call AddComponents on the -message builder. - -```cs -var myButton = new DiscordButtonComponent(DiscordButtonStyle.Primary, "my_custom_id", "This is a button!"); - -var builder = new DiscordMessageBuilder() - .WithContent("This message has buttons! Pretty neat innit?") - .AddComponents(myButton); -``` - -Now you have a message with a button. Congratulations! It's important to note that -AddComponents will create a new row with each call, so **add everything you -want on one row in one call!** - -Buttons can be added in any order you fancy. Lets add 5 to demonstrate each color, and a link button for good measure. - -```cs -var builder = new DiscordMessageBuilder() - .WithContent("This message has buttons! Pretty neat innit?") - .AddComponents(new DiscordComponent[] - { - new DiscordButtonComponent(DiscordButtonStyle.Primary, "1_top", "Blurple!"), - new DiscordButtonComponent(DiscordButtonStyle.Secondary, "2_top", "Grey!"), - new DiscordButtonComponent(DiscordButtonStyle.Success, "3_top", "Green!"), - new DiscordButtonComponent(DiscordButtonStyle.Danger, "4_top", "Red!"), - new DiscordLinkButtonComponent("https://some-super-cool.site", "Link!") - }); -``` - -As promised, not too complicated. Links however are @DSharpPlus.Entities.DiscordLinkButtonComponent, which takes a URL -as its first parameter, and the label. Link buttons can also have an emoji, like regular buttons. - -Lets also add a second row of buttons, but disable them, so the user can't push them all willy-nilly. - -```cs -builder.AddComponents(new DiscordComponent[] -{ - new DiscordButtonComponent(DiscordButtonStyle.Primary, "1_top_d", "Blurple!", true), - new DiscordButtonComponent(DiscordButtonStyle.Secondary, "2_top_d", "Grey!", true), - new DiscordButtonComponent(DiscordButtonStyle.Success, "3_top_d", "Green!", true), - new DiscordButtonComponent(DiscordButtonStyle.Danger, "4_top_d", "Red!", true), - new DiscordLinkButtonComponent("https://some-super-cool.site", "Link!", true) -}); -``` - -Practically identical, but now with `true` as an extra paremeter. This is the -@DSharpPlus.Entities.DiscordButtonComponent.Disabled property. - -Produces a message like such: - -![Buttons][4] - -Well, that's all neat, but lets say you want to add an emoji. Being able to use any emoji is pretty neat, afterall. -That's also very simple! - -```cs -var myButton = new DiscordButtonComponent( - DiscordButtonStyle.Primary, - "emoji_button", - null, - false, - new DiscordComponentEmoji(595381687026843651)); -``` - -And you're done! Simply add that to a builder, and when you send, you'll get a message that has a button with a little -Pikachu enjoying a lolipop. Adorable. - -![PikaLolipop][5] - -# Responding to button presses - -When any button is pressed, it will fire the @DSharpPlus.DiscordClient.ComponentInteractionCreated. - -In the event args, @DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs.Id will be the id of the button you -specified. There's also an @DSharpPlus.EventArgs.InteractionCreateEventArgs.Interaction property, which contains the -interaction the event created. It's important to respond to an interaction within 3 seconds, or it will time out. -Responding after this period will throw a @DSharpPlus.Exceptions.NotFoundException. - -With buttons, there are two new response types: @DSharpPlus.InteractionResponseType.DeferredMessageUpdate and -@DSharpPlus.InteractionResponseType.UpdateMessage. - -Using @DSharpPlus.InteractionResponseType.DeferredMessageUpdate lets you create followup messages via the -[followup message builder][6]. The button will return to being in it's 'dormant' state, or it's 'unpushed' state, if you -will. - -You have 15 minutes from that point to make followup messages. Responding to that interaction looks like this: - -```cs -client.ComponentInteractionCreated += async (s, e) => -{ - await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate); - // Do things.. // -} -``` - -If you would like to update the message when a button is pressed, however, you'd use -@DSharpPlus.InteractionResponseType.UpdateMessage instead, and pass a -@DSharpPlus.Entities.DiscordInteractionResponseBuilder with the new content you'd like. - -```cs -client.ComponentInteractionCreated += async (s, e) => -{ - await e.Interaction.CreateResponseAsync( - InteractionResponseType.UpdateMessage, - new DiscordInteractionResponseBuilder() - .WithContent("No more buttons for you >:)")); -} -``` - -This will update the message, and remove all the buttons. Nice. - -# Interactivity - -Along with the typical @DSharpPlus.Interactivity.InteractivityExtension.WaitForMessageAsync* and -@DSharpPlus.Interactivity.InteractivityExtension.WaitForReactionAsync* methods provided by interactivity, there are also -button implementations as well. - -More information about how interactivity works can be found in [the interactivity article][7]. - -Since buttons create interactions, there are also two additional properties in the configuration: - -- @DSharpPlus.Interactivity.InteractivityConfiguration.ResponseBehavior -- @DSharpPlus.Interactivity.InteractivityConfiguration.ResponseMessage - -@DSharpPlus.Interactivity.InteractivityConfiguration.ResponseBehavior is what interactivity will do when handling -something that isn't a valid valid button, in the context of waiting for a specific button. It defaults to -@DSharpPlus.Interactivity.Enums.InteractionResponseBehavior.Ignore, which will cause the interaction fail. - -Alternatively, setting it to @DSharpPlus.Interactivity.Enums.InteractionResponseBehavior.Ack will acknowledge the -button, and continue waiting. - -Respond will reply with an ephemeral message with the aforementioned response message. - -@DSharpPlus.Interactivity.InteractivityConfiguration.ResponseBehavior only applies to the overload accepting a string id -of the button to wait for. - - -[0]: xref:articles.beyond_basics.messagebuilder -[1]: https://discord.dev/interactions/slash-commands#interaction -[2]: #responding-to-button-presses -[3]: https://discord.dev/interactions/message-components#component-object -[4]: ../../images/advanced_topics_buttons_01.png -[5]: ../../images/advanced_topics_buttons_02.png -[6]: xref:DSharpPlus.Entities.DiscordFollowupMessageBuilder -[7]: xref:articles.interactivity diff --git a/docs/articles/advanced_topics/compression.md b/docs/articles/advanced_topics/compression.md deleted file mode 100644 index b389ba2691..0000000000 --- a/docs/articles/advanced_topics/compression.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -uid: articles.advanced_topics.compression -title: Gateway Compression ---- - -Discord bots can use a surprising amount of bandwidth receiving events from Discord. To help reduce the impact of this, Discord supports two approaches to lower gateway load: first, intents, which are discussed in , and second, compressing the events that end up sent across the gateway. - -More specifically, Discord supports two compression methods: zlib and zstd. By default, DSharpPlus will automatically select the "best" compression method currently available. It is possible to forcibly set the used compression method or to disable it entirely; and it is also possible to use zlib for payload-wise compression rather than connection-wise compression. This is generally not advisable. - -## A note on better-ness - -The arguments on which compression method is best could fill entire books, and often the answer is very situational, but in the circumstances at play in DSharpPlus there generally is a very clear answer. Zstd is implemented very efficiently and without unnecessary memory allocations and copies, but it is not by default available on all systems in .NET. While DSharpPlus does provide a package to make it available - `DSharpPlus.Natives.Zstd`, the package is quite large and doesn't support 32-bit systems, mobile or RISC-V-based systems (it supports windows, linux and osx running on x86_64 and arm64). On the other hand, the zlib-based implementation is considerably less efficient due to constraints placed by the standard library design, but does not come with any compatibility caveats. Since their effectiveness is very similar, DSharpPlus by default prefers zstd if it is available, falling back to zlib otherwise. - -## Changing the compression method - -If the default choice DSharpPlus makes is not satisfactory for you, there are methods provided on both `DiscordClientBuilder` and `IServiceCollection` to change the compression method used. These methods are - symmetrically - called `UseZstdCompression`, `UseZlibCompression` and `DisableGatewayCompression`. Note that they must be called after `AddDiscordClient` if you're using `IServiceCollection`-based setup. - -Both the default zlib and zstd compression methods are applied "transport"-wide, meaning that they use one compression context for the entire lifetime of a connection. This is generally advisable as it performs better and results in better compression ratios. It is, however, also alternatively possible to apply zlib compression "payload"-wide, meaning that for each payload a new compression context will be created. This feature is provided only for completeness and can be enabled like so: `serviceCollection.Replace`. It is available in `DiscordClientBuilder`-based setup through `DiscordClientBuilder.ConfigureServices`. - -## Installing Zstd - -Most Linux systems will already have zstd installed one way or another, but Windows systems generally do not. To make zstd easily available regardless, install `DSharpPlus.Natives.Zstd` into your project. `dotnet publish -c Release --use-current-runtime` will pick the correct native library file based on the target architecture and place it so that DSharpPlus can detect zstd and enable it by default. - -When using `dotnet build`, dotnet makes no assumptions about the target architecture and instead places all native library files in a subdirectory of the build directory. DSharpPlus will still make a best-effort attempt to find zstd in the predicted output from `dotnet build`, but DSharpPlus may not succeed automatically. You may use `UseZstdCompression` to enforce it, but you should weigh the advantages of zstd compression against the potential disadvantage of having to provide your own zstd build on unsupported targets. \ No newline at end of file diff --git a/docs/articles/advanced_topics/generic_host.md b/docs/articles/advanced_topics/generic_host.md deleted file mode 100644 index 477bd6f7c6..0000000000 --- a/docs/articles/advanced_topics/generic_host.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -uid: articles.advanced_topics.generic_host -title: Generic Host ---- - -# Introduction - -The .NET Generic Host is a reusable, lightweight, and extensible hosting framework that provides a consistent way to -host different types of.NET applications. It is designed to simplify the startup process and provide a common -infrastructure for configuring, logging, dependency injection, and other common tasks required by modern applications. - -It allows developers to build console applications, background services, and other types of .NET applications that can -run as standalone processes, Windows services, or Docker containers, among other deployment options. By using a generic -host, developers can focus on implementing the core business logic of their application rather than dealing with the -infrastructure and plumbing required to host and manage it. - -You can read more about Generic hosts [here](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host). - -# Making a Bot using the Generic Host - -## Setting up the Builder - -To get started, you'll need to write some code that configures and runs your application. Here's a simple example that -shows how to use the Generic Host to run a bot service: - -```cs -private static async Task Main() -{ - await Host.CreateDefaultBuilder() - .UseConsoleLifetime() - .ConfigureServices((hostContext, services) => - { - services.AddHostedService() - .AddDiscordClient(token, intents); - }) - .RunConsoleAsync(); -} -``` - -This code does a few things. First, it calls the `Host.CreateDefaultBuilder()` method, which creates a new -`IHostBuilder` instance with some default settings. Then, it calls the `UseConsoleLifetime()` method to configure the -lifetime of the host. This tells the host to keep running until it receives a [SIGINT or SIGTERM -Signal](https://en.wikipedia.org/wiki/Signal_(IPC)#SIGINT) i.e. when the user presses Ctrl+C in the console, or when -another program tells it to stop. - -Next, it configures the services that the host will use by calling the `ConfigureServices()` method. In this case, it -adds a new `BotService` service, which is a class that you'll need to define next, as well as setting up DSharpPlus' DiscordClient. - -Finally, it calls the `RunConsoleAsync()` method to start the host and begin running your service. That's it! With just -a few lines of code, you can use the.NET Generic Host to run your application as a service. - -## Setting up your Service - -You'll need to create a class which implements the `IHostedService` interface, which defines two methods: -`StartAsync()` and `StopAsync()`. These methods are called by the host when your application starts and stops -respectively. - -```cs -public sealed class BotService : IHostedService -{ - private readonly ILogger logger; - private readonly IHostApplicationLifetime applicationLifetime; - private readonly DiscordClient discordClient; - - public BotService(ILogger logger, IHostApplicationLifetime applicationLifetime, DiscordClient client) - { - this.logger = logger; - this.applicationLifetime = applicationLifetime; - this.discordClient = client; - } - - public async Task StartAsync(CancellationToken token) - { - await discordClient.ConnectAsync(); - // Other startup things here - } - - public async Task StopAsync(CancellationToken token) - { - await discordClient.DisconnectAsync(); - // More cleanup possibly here - } -} -``` - ->[!WARNING] -> Hard-coding your bot token into your source code is not a good idea, especially if you plan to distribute your code -publicly. This is because anyone with access to your code can easily extract your bot token, which can be used to -perform unauthorized actions on your bot account. ->Instead, it's recommended that you store your bot token in a secure location, such as a configuration file, -environment variable, or secret storage service. You can then retrieve the token at runtime and pass it to the -initializer. See for example [How to consume configuration with the Generic -Host](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-7.0) or [how to use an -environment -variable](https://learn.microsoft.com/en-us/dotnet/api/system.environment.getenvironmentvariable?view=net-7.0). - -The `StartAsync()` method contains the code that runs when your application starts. In this case, the `DiscordClient` -connects to the Discord API. - -The `StopAsync()` method contains the code that runs when your application is shut down. In the case of a bot, this -might involve closing connections to external APIs, releasing resources, or performing other cleanup tasks. - -By implementing these methods, you can ensure that your application starts and stops cleanly, and that any necessary -resources are properly managed. This makes it easier to build robust, reliable applications that can be run as services. - -With this class, you can easily create and run your own bot services using the .NET Generic Host. Just replace the -Token property with your own Discord bot token, and you're ready to go! - -## Using Serilog with the Generic Host - -Logging is an important part of any application, especially one that runs as a service. Fortunately, the .NET Generic -Host makes it easy to integrate with popular logging libraries, like Serilog. - -### Dependencies - -You will need the [`Serilog.Extensions.Hosting`](https://www.nuget.org/packages/Serilog.Extensions.Hosting) package -(along with [`Serilog`](https://www.nuget.org/packages/Serilog) itself with whichever sinks you prefer), which are -available from NuGet. - -### In your Host section - -When configuring the .NET Generic Host to use Serilog, you will need to add the logger service to your host builder and -call the `UseSerilog()` method to configure Serilog as your logging provider. Conveniently, DSharpPlus will use the logger you specify here. - -To do this, you can add the logger service to the `ConfigureServices()` method of your host builder, like this: - -```cs -await Host.CreateDefaultBuilder() - .UseSerilog() - .UseConsoleLifetime() - .ConfigureServices((hostContext, services) => - { - services.AddLogging(logging => logging.ClearProviders().AddSerilog()); - services.AddHostedService() - .AddDiscordClient(token, intents); - }) - .RunConsoleAsync(); -``` - -In this example, we call the `UseSerilog()` method to configure Serilog as our logging provider, and then add the -logger service to the `ConfigureServices()` method using the `AddLogging()` method on the Services collection. We then -call the `ClearProviders()` method to remove any default logging providers that may be present, and add the Serilog -provider using the `AddSerilog()` method. - -Don't forget that before you can use Serilog to log messages in your bot, you will need to initialize Serilog and -configure the sinks you want to use. For example, you can initialize Serilog like this: - -```cs -Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .WriteTo.File("logs/.log", rollingInterval: RollingInterval.Day) - .CreateLogger(); -``` - -When shutting down your bot, it's a good idea to call `Log.CloseAndFlushAsync()` to make sure that any pending log -messages are written to the sinks before the process exits. You can add this call to your `Main` method, which could -look like this once you're done: - -```cs -private static async Task Main() -{ - Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .WriteTo.File("logs/.log", rollingInterval: RollingInterval.Day) - .CreateLogger(); - - await Host.CreateDefaultBuilder() - .UseSerilog() - .UseConsoleLifetime() - .ConfigureServices((hostContext, services) => - { - services.AddLogging(logging => logging.ClearProviders().AddSerilog()); - services.AddHostedService() - .AddDiscordClient(token, intents); - }) - .RunConsoleAsync(); - - await Log.CloseAndFlushAsync(); -} -``` diff --git a/docs/articles/advanced_topics/metrics_profiling.md b/docs/articles/advanced_topics/metrics_profiling.md deleted file mode 100644 index dea723924d..0000000000 --- a/docs/articles/advanced_topics/metrics_profiling.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -uid: articles.advanced_topics.metrics_profiling -title: Metrics and Profiling ---- - -# Introduction - -Understanding the characteristics of your application is important, particularly for large applications. -Unfortunately, DSharpPlus does not lend itself very well to conventional profiling, and while we intend -to improve the general usability of the library with respect to performance and profiling, we also intend -to provide our own means to gather insight into what's happening. Currently, we track REST requests and -their outcomes, and we intend to add more insights - feel free to let us know via a -[feature request](https://github.com/DSharpPlus/DSharpPlus/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml)! - -## Rest Metrics - -Metrics on REST requests can be obtained through `GetRequestMetrics` methods on the respective client. -This is supported on the webhook client, @DSharpPlus.DiscordRestClient and @DSharpPlus.DiscordClient. To -obtain metrics from a @DSharpPlus.DiscordShardedClient , fetch metrics from the first shard. - -You may optionally specify to reset the tracking metrics, for example when polling regularly to calculate -statistics: - -~~~cs -using System; -using System.Threading; - -using DSharpPlus; - -PeriodicTimer timer = new(TimeSpan.FromHours(1)); - -while (await timer.WaitForNextTickAsync(ct)) -{ - Console.WriteLine(client.GetRequestMetrics(sinceLastCall: true)); -} -~~~ - -This will not reset the lifetime metrics, and you can both poll regularly and access lifetime metrics, however, -different parts of your application can cause the metrics to reset without the other knowing. diff --git a/docs/articles/advanced_topics/selects.md b/docs/articles/advanced_topics/selects.md deleted file mode 100644 index ef59b65327..0000000000 --- a/docs/articles/advanced_topics/selects.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -uid: articles.advanced_topics.selects -title: Select Menus ---- -# Introduction - -**They're here!** What's here? Select menus (aka dropdowns) of course. - -Dropdowns are another [message component][0] added to the Discord API. Additionally, just like buttons, dropdowns are -supported in all builders that take @DSharpPlus.Entities.DiscordComponent. However, dropdowns occupy an entire action -row, so you can only have up to 5! Furthermore, buttons cannot occupy the same row as a dropdown. - -In this article, we will go over what dropdowns are, how to use them, and the limitations of dropdowns. - -# Dropdowns overview -> -> [!NOTE] -> This article is under the presumption that you are familiar with buttons. -> In addition to this, just like buttons, select menu ids should be unique. - -Dropdowns consist of several parts, and share some in common with buttons. They have a: - -- Custom id -- Placeholder -- Disabled -- Options -- Min Values -- Max Values - -So lets go over these one by one, starting with the id. The id of a dropdown should of course be unique, just like -buttons, and Discord will send this id back in the [interaction object][1]. - -Placeholder is also relatively relatively simple! It's hopefully self-explanatory, too. Placeholder text is the text the -user will see when no options are selected. - -If you do not wish to have placeholder text, simply pass `null` as that parameter in the constructor for the dropdown. -Placeholder only supports plain-text, and up to 100 characters. - -Disabled: Applies to the entire dropdown, and will grey it out if set to `true`. - -Min and Max values determine how many or how few options are valid. There are few requirements, though: Min < Max, Min -\>= 0, Max > 0, Max <= 25. Simple enough, right? - -"But you skipped options!", you may say, and that we have. Options are a bit more complicated, and have their own -section right below. - -# Dropdown options - -Dropdown options are somewhat more involved than handling buttons, but they're still relatively simple. They can have up -to **25** options, but must have at least 1. These consist of several parts: - -- Label -- Value -- Default -- Description -- Emoji - -Label is the label of the option. This is always required, and can be up to **100** characters long. - -Value is like the custom id of the dropdown; for the most part it should be unique. This will be accessible on the -@DSharpPlus.Entities.DiscordInteractionData.Values property the interaction, and will contain all the selected options. - -Individual values unfortunately cannot be disabled. - -Description is text that is placed under the label on each option, and can also be up to 100 characters. This text is -also plain-text, and does not support markdown. - -Default determines whether or not the option will be the default option (which overrides placeholder). If you set -multiple to default (and allow multiple to be selected), the user will see the options as pre-selected. - -Emoji is the same as a button. You can pass an emoji id, a unicode emote or a DiscordEmoji object, which will -automatically use either. - -> [!WARNING] -> When using DiscordComponentEmoji's string overload, you **MUST** use the unicode representation of the emoji you want. -> ex: 👋 and not \:wave\: - -# Putting it all together -> -> [!NOTE] -> Spaces are valid in custom ids as well, but underscores will be used in this article for consistency. - -Well now you know how dropdowns work, and how dropdown options work, but how do you make the darn thing??? - -It would look something along the lines of this: - -```cs -// Create the options for the user to pick -var options = new List() -{ - new DiscordSelectComponentOption( - "Label, no description", - "label_no_desc"), - - new DiscordSelectComponentOption( - "Label, Description", - "label_with_desc", - "This is a description!"), - - new DiscordSelectComponentOption( - "Label, Description, Emoji", - "label_with_desc_emoji", - "This is a description!", - emoji: new DiscordComponentEmoji(854260064906117121)), - - new DiscordSelectComponentOption( - "Label, Description, Emoji (Default)", - "label_with_desc_emoji_default", - "This is a description!", - isDefault: true, - new DiscordComponentEmoji(854260064906117121)) -}; - -// Make the dropdown -var dropdown = new DiscordSelectComponent("dropdown", null, options, false, 1, 2); -``` - -Okay, so we have a dropdown...now what? Simply pass it to any builder that constructs a response, be it a -@DSharpPlus.Entities.DiscordMessageBuilder, @DSharpPlus.Entities.DiscordInteractionResponseBuilder, or -@DSharpPlus.Entities.DiscordWebhookBuilder. - -It'll look something like this, using the code above: - -```cs -// [...] Code trunctated for brevity - -var builder = new DiscordMessageBuilder() - .WithContent("Look, it's a dropdown!") - .AddComponents(dropdown); - -await builder.SendAsync(channel); // Replace with any method of getting a channel. // -``` - -# Final result - -Congrats! You've made a dropdown. It should look like this - -![SelectImageOne][2] - -When you click the dropdown, the bottom option should be pre-selected, and it will look like this. You can choose one or -two options. - -![SelectImageTwo][3] - -# Interactivity/Footnotes - -"**Oh no, I'm getting 'This interaction failed' when selecting! What do I do?**" - -Dropdowns are like buttons; when a user interacts with them, you need to respond to that interaction. -@DSharpPlus.DiscordClient.ComponentInteractionCreated is fired from the client, just like buttons. - -This applies to interactivity, too! Simply swap -@DSharpPlus.Interactivity.Extensions.MessageExtensions.WaitForButtonAsync* for -@DSharpPlus.Interactivity.Extensions.MessageExtensions.WaitForSelectAsync*, and pass a dropdown. How to go about -component-based interactivity is described [in the buttons article][4]. - -And that's it! Go forth and create amazing things. - - -[0]: https://discord.dev/interactions/message-components -[1]: https://discord.dev/interactions/slash-commands#interaction -[2]: ../../images/advanced_topics_selects_01.png -[3]: ../../images/advanced_topics_selects_02.png -[4]: xref:articles.advanced_topics.buttons diff --git a/docs/articles/advanced_topics/trace_logs.md b/docs/articles/advanced_topics/trace_logs.md deleted file mode 100644 index 8a25a8549a..0000000000 --- a/docs/articles/advanced_topics/trace_logs.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -uid: articles.advanced_topics.trace -title: Trace Logging ---- - -# Introduction - -Trace logs are a very useful debugging feature. They will reveal all information your bot sends to Discord and Discord sends to your bot, both through rest and the real-time gateway, as well as information about the library's internal state and what the library is attempting to do at any given moment. - -We recommend you do not enable trace logging with the built-in logger provider, but to register a custom logger and, importantly, to enable logging to files: trace logs are very lengthy and will very quickly hit console length limits. For enabling trace logging, please refer to the documentation of your chosen logger - Serilog, for example, calls it "Verbose" logging. - -## Controlling Trace Contents - -DSharpPlus offers several options to configure what specific data should be logged to traces. These options live in `runtimeconfig.json`, unlike most other configuration the library offers, due to their nature as hosting-specific rather than development/functionality-specific switches. They may be specified as follows: - -1. In `runtimeconfig.json`, switches are specified in the `"configProperties"` section: - -```json -{ - "runtimeOptions": { - "tfm": "net8.0", - // ..., - "configProperties": { - "DSharpPlus.Trace.EnableInboundGatewayLogging": true - } - } -} -``` - -2. In your `.csproj` file: - -```xml - - - -``` - -DSharpPlus supports the following switches for controlling trace log contents: -- `DSharpPlus.Trace.EnableRestRequestLogging`, controlling whether payloads from REST should be logged; -- `DSharpPlus.Trace.EnableInboundGatewayLogging`, controlling whether incoming gateway events should be logged; -- `DSharpPlus.Trace.EnableOutboundGatewayLogging`, controlling whether outgoing gateway events should be logged. - -Each of these options defaults to `true` - by default, DSharpPlus logs as much information as possible. - -### Anonymizing Trace Contents - -Trace logs contain huge amounts of potentially sensitive data, such as user IDs, message contents and tokens - everything Discord sends us, and everything we send to Discord. DSharpPlus offers additional feature switches to restrict sensitive information ending up in trace logs: - -First, `DSharpPlus.Trace.AnonymizeTokens`. This switch is enabled by default and will hide your bot and webhook tokens in trace logs. As a library consumer, you should typically not turn this off. - -Second, `DSharpPlus.Trace.AnonymizeContents`. This switch is disabled by default and will hide snowflake IDs, message contents and usernames in your logs. Since this significantly reduces the quality of debug information in your trace logs, you should evaluate whether you should use this switch on a case-by-case basis. diff --git a/docs/articles/analyzers/commands.md b/docs/articles/analyzers/commands.md deleted file mode 100644 index ffd691f230..0000000000 --- a/docs/articles/analyzers/commands.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -uid: articles.analyzers.commands -title: DSharpPlus.Commands Analyzer Rules ---- - -# DSharpPlus.Commands Rules - -This page documents the analyzer rules defined for APIs defined in DSharpPlus.Commands and their associated usage -patterns: - -- [DSP1001](#usage-error-dsp1001) -- [DSP1002](#usage-warning-dsp1002) -- [DSP1003](#usage-error-dsp1003) - -## Usage Error DSP1001 - -A slash command explicitly registered to a guild should not be installable to users. - -Slash commands registered to a guild are restricted to that guild and cannot be referenced outside of it, but a -registered installation type requires it to be usable outside the guild. Either remove the specified installation type -or remove the `RegisterToGuilds` attribute. - -The following sample will generate DSP1001: - -```csharp -public class PingCommand -{ - [Command("ping")] - [RegisterToGuilds(379378609942560770)] - [InteractionInstallType(DiscordApplicationIntegrationType.UserInstall)] - public static async ValueTask ExecuteAsync(CommandContext ctx) - { - await ctx.RespondAsync("Pong!"); - } -} -``` - -## Usage Warning DSP1002 - -Do not explicitly register nested classes of elsewhere-registered classes to DSharpPlus.Commands. - -Do not register nested classes. If their containing class gets registered as well, the commands inside the nested class -get registered twice. - -The following sample will generate DSP1002: - -```csharp -public class Registerator -{ - public static void Register(CommandsExtension extension) - { - extension.AddCommands([typeof(ACommands.BCommands), typeof(ACommands)]); - } -} - -[Command("a")] -public class ACommands -{ - [Command("b")] - public class BCommands - { - [Command("c")] - public static async ValueTask CAsync(CommandContext context) - { - await context.RespondAsync("C"); - } - } -} -``` - -## Usage Error DSP1003 - -A command taking a specific context type should not restrict itself to other processors. - -Specifying a command context type acts as a form of filtering where the command will only be executable by processors -capable of creating the demanded context. In a similar vein, `AllowedProcessorAttribute` acts as a form of filtering -where the command will only be executable by one of the listed processors. Filtering to two sets of processors that are -not compatible with one another will render your command partially or wholly unusable. - -The following sample will generate DSP1003: - -```csharp -using System.Threading.Tasks; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Processors.SlashCommands; - -public class Test -{ - [AllowedProcessors] - public async Task Tester(TextCommandContext context) - { - await context.RespondAsync("Tester!"); - } -} -``` \ No newline at end of file diff --git a/docs/articles/analyzers/core.md b/docs/articles/analyzers/core.md deleted file mode 100644 index b2663bea69..0000000000 --- a/docs/articles/analyzers/core.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -uid: articles.analyzers.core -title: DSharpPlus Core Library Analyzer Rules ---- - -# DSharpPlus Core Rules - -This page documents the analyzer rules defined for APIs defined in the DSharpPlus core library, `DSharpPlus.dll`, and -their associated usage patterns: - -- [DSP0006](#usage-warning-dsp0006) -- [DSP0007](#design-warning-dsp0007) -- [DSP0008](#design-info-dsp0008) -- [DSP0009](#usage-error-dsp0009) -- [DSP0010](#usage-info-dsp0010) - -### Usage Warning DSP0006 - -`DiscordPermissions.HasPermission` should always be preferred over bitwise operations. - -Bitwise operations risk missing Administrator permissions, making a direct bitwise check unreliable. Use -`HasPermission`, `HasAnyPermission` or `HasAllPermissions` instead as appropriate. - -The following sample will generate DSP0006: - -```csharp -public class PermissionExample -{ - public static bool HasManageGuild(DiscordPermissions permissions) - { - return (permissions & DiscordPermissions.ManageGuild) != 0; - } -} -``` - -### Design Warning DSP0007 - -Use `ModifyAsync` instead of multiple calls to `AddOverwriteAsync`. - -Multiple calls to `AddOverwriteAsync` on the same channel can cause multiple requests to happen on the same channel. -Instead, prefer using `ModifyAsync` with the aggregated overwrites to minimize this to a single request. - -The following sample will generate DSP0007: - -```csharp -public class PermissionOverwriting -{ - public static async Task UpdatePermissionsAsync(DiscordChannel channel, List members, DiscordPermissions newAllowed) - { - foreach (DiscordUser member in members) - { - await channel.AddOverwriteAsync(member, newAllowed); - } - } -} -``` - -### Design Info DSP0008 - -Use a bulk-fetching method instead of fetching single entities inside of a loop. - -Fetching single entities individually incurs one request each. If there is only one entity being requested, put it -outside the loop. If there are multiple entities, prefer bulk-fetching methods. - -The following sample will generate DSP0008: - -```csharp -public class GetSpecificGuilds -{ - public static async Task> GetTheseGuilds(DiscordClient client, List ids) - { - List guilds = new(ids.Count); - foreach (ulong id in ids) - { - DiscordGuild guild = await client.GetGuildAsync(id); - guilds.Add(guild); - } - - return guilds; - } -} -``` - -### Usage Error DSP0009 - -Use `DiscordPermissions` and its operators instead of doing operations on `DiscordPermission`. - -@DSharpPlus.Entities.DiscordPermission is only an enum containing each permission flag. It does not take care of multiple permissions. -@DSharpPlus.Entities.DiscordPermissions represents multiple permissions and has utilities for managing this, use it over @DSharpPlus.Entities.DiscordPermission when representing multiple permissions. -Performing any math on an instance of `DiscordPermission` is incorrect behaviour. - -The following sample will generate DSP0009: - -```csharp -public class PermissionUtility -{ - public static DiscordPermission AddAdmin(DiscordPermission permission) - { - return permission | DiscordPermission.Administrator; - } -} -``` - -### Usage Info DSP0010 - -Use explicit methods on `DiscordPermissions` rather than bitwise operators. - -DiscordPermissions provides explicit ways to accomplish common operations that should be preferred for clarity and performance: - -| Bitwise Operation | Explicit Operation | -| - | - | -| permissions | other | `permissions + other` | -| `permissions & (~other)` | `permissions - other` | -| `permissions ^ other` | `permissions.Toggle(other)` | - -The following sample will generate DSP0010: - -```csharp -public class PermissionUtility -{ - public static DiscordPermissions Problem(DiscordPermissions input, DiscordPermissions mask) - { - return input & (~mask); - } -} -``` diff --git a/docs/articles/audio/lavalink/configuration.md b/docs/articles/audio/lavalink/configuration.md deleted file mode 100644 index 58c5146a9b..0000000000 --- a/docs/articles/audio/lavalink/configuration.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -uid: articles.audio.lavalink.configuration -title: Lavalink Configuration ---- - ->[!WARNING] -> `DSharpPlus.Lavalink` has been deprecated, and this article may contain outdated information. Both the extension and this article will be removed -> in the future. - -# Setting up DSharpPlus.Lavalink - -## Configuring Your Client - -To begin using DSharpPlus's Lavalink client, you will need to add the `DSharpPlus.Lavalink` nuget package. Once -installed, simply add these namespaces at the top of your bot file: - -```csharp -using DSharpPlus.Net; -using DSharpPlus.Lavalink; -``` - -After that, we will need to create a configuration for our extension to use. This is where the special values from the -server configuration are used. - -```csharp -var endpoint = new ConnectionEndpoint -{ - Hostname = "127.0.0.1", // From your server configuration. - Port = 2333 // From your server configuration -}; - -var lavalinkConfig = new LavalinkConfiguration -{ - Password = "youshallnotpass", // From your server configuration. - RestEndpoint = endpoint, - SocketEndpoint = endpoint -}; -``` - -Finally, initialize the extension. - -```csharp -var lavalink = Discord.UseLavalink(); -``` - -## Connecting with Lavalink - -We are now ready to connect to the server. Call the Lavalink extension's connect method and pass the configuration. Make -sure to call this **after** your Discord client connects. This can be called either directly after your client's connect -method or in your client's ready event. - -```csharp -LavalinkNode = await Lavalink.ConnectAsync(lavalinkConfig); -``` - -Your main bot file should now look like this: - -```csharp -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using DSharpPlus; -using DSharpPlus.Net; -using DSharpPlus.Lavalink; - -namespace MyFirstMusicBot -{ - class Program - { - public static DiscordClient Discord; - - public static async Task Main(string[] args) - { - Discord = new DiscordClient(new DiscordConfiguration - { - Token = "", - TokenType = TokenType.Bot, - MinimumLogLevel = LogLevel.Debug - }); - - var endpoint = new ConnectionEndpoint - { - Hostname = "127.0.0.1", // From your server configuration. - Port = 2333 // From your server configuration - }; - - var lavalinkConfig = new LavalinkConfiguration - { - Password = "youshallnotpass", // From your server configuration. - RestEndpoint = endpoint, - SocketEndpoint = endpoint - }; - - var lavalink = Discord.UseLavalink(); - - await Discord.ConnectAsync(); - await lavalink.ConnectAsync(lavalinkConfig); // Make sure this is after Discord.ConnectAsync(). - - await Task.Delay(-1); - } - } -} -``` - -We are now ready to start the bot. If everything is configured properly, you should see a Lavalink connection appear in -your DSharpPlus console: - -``` -[2020-10-10 17:56:07 -04:00] [403 /LavalinkConn] [Debug] Connection to Lavalink node established -``` - -And a client connection appear in your Lavalink console: - -``` -INFO 5180 --- [ XNIO-1 task-1] io.undertow.servlet : Initializing Spring DispatcherServlet 'dispatcherServlet' -INFO 5180 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' -INFO 5180 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms -INFO 5180 --- [ XNIO-1 task-1] l.server.io.HandshakeInterceptorImpl : Incoming connection from /0:0:0:0:0:0:0:1:58238 -INFO 5180 --- [ XNIO-1 task-1] lavalink.server.io.SocketServer : Connection successfully established from /0:0:0:0:0:0:0:1:58238 -``` - -We are now ready to set up some music commands! diff --git a/docs/articles/audio/lavalink/music_commands.md b/docs/articles/audio/lavalink/music_commands.md deleted file mode 100644 index 71851ab52d..0000000000 --- a/docs/articles/audio/lavalink/music_commands.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -uid: articles.audio.lavalink.music_commands -title: Lavalink Music Commands ---- - ->[!WARNING] -> `DSharpPlus.Lavalink` has been deprecated, and this article may contain outdated information. Both the extension and this article will be removed -> in the future. - -# Adding Music Commands - -This article assumes that you know how to use CommandsNext. If you do not, you should learn [here][0] -before continuing with this guide. - -## Prerequisites - -Before we start we will need to make sure CommandsNext is configured. For this we can make a simple configuration and -command class: - -```csharp -using DSharpPlus.CommandsNext; - -namespace MyFirstMusicBot -{ - public class MyLavalinkCommands : BaseCommandModule - { - - } -} -``` - -And be sure to register it in your program file: - -```csharp -CommandsNext = Discord.UseCommandsNext(new CommandsNextConfiguration -{ - StringPrefixes = new string[] { ";;" } -}); - -CommandsNext.RegisterCommands(); -``` - -## Adding join and leave commands - -Your bot, and Lavalink, will need to connect to a voice channel to play music. Let's create the base for these commands: - -```csharp -[Command] -public async Task Join(CommandContext ctx, DiscordChannel channel) -{ - -} - -[Command] -public async Task Leave(CommandContext ctx, DiscordChannel channel) -{ - -} -``` - -In order to connect to a voice channel, we'll need to do a few things. - -1. Get our node connection. You can either use linq or @DSharpPlus.Lavalink.LavalinkExtension.GetIdealNodeConnection* -2. Check if the channel is a voice channel, and tell the user if not. -3. Connect the node to the channel. - -And for the leave command: - -1. Get the node connection, using the same process. -2. Check if the channel is a voice channel, and tell the user if not. -3. Get our existing connection. -4. Check if the connection exists, and tell the user if not. -5. Disconnect from the channel. - -@DSharpPlus.Lavalink.LavalinkExtension.GetIdealNodeConnection* will return the least affected node through load -balancing, which is useful for larger bots. It can also filter nodes based on an optional voice region to use the -closest nodes available. Since we only have one connection we can use linq's `.First()` method on the extensions -connected nodes to get what we need. - -So far, your command class should look something like this: - -```csharp -using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.Entities; -using DSharpPlus.CommandsNext; -using DSharpPlus.CommandsNext.Attributes; - -namespace MyFirstMusicBot -{ - public class MyLavalinkCommands : BaseCommandModule - { - [Command] - public async Task Join(CommandContext ctx, DiscordChannel channel) - { - var lava = ctx.Client.GetLavalink(); - if (!lava.ConnectedNodes.Any()) - { - await ctx.RespondAsync("The Lavalink connection is not established"); - return; - } - - var node = lava.ConnectedNodes.Values.First(); - - if (channel.Type != ChannelType.Voice) - { - await ctx.RespondAsync("Not a valid voice channel."); - return; - } - - await node.ConnectAsync(channel); - await ctx.RespondAsync($"Joined {channel.Name}!"); - } - - [Command] - public async Task Leave(CommandContext ctx, DiscordChannel channel) - { - var lava = ctx.Client.GetLavalink(); - if (!lava.ConnectedNodes.Any()) - { - await ctx.RespondAsync("The Lavalink connection is not established"); - return; - } - - var node = lava.ConnectedNodes.Values.First(); - - if (channel.Type != ChannelType.Voice) - { - await ctx.RespondAsync("Not a valid voice channel."); - return; - } - - var conn = node.GetGuildConnection(channel.Guild); - - if (conn == null) - { - await ctx.RespondAsync("Lavalink is not connected."); - return; - } - - await conn.DisconnectAsync(); - await ctx.RespondAsync($"Left {channel.Name}!"); - } - } -} -``` - -## Adding player commands - -Now that we can join a voice channel, we can make our bot play music! Let's now create the base for a play command: - -```csharp -[Command] -public async Task Play(CommandContext ctx, [RemainingText] string search) -{ - -} -``` - -One of Lavalink's best features is its ability to search for tracks from a variety of media sources, such as YouTube, -SoundCloud, Twitch, and more. This is what makes bots like Rythm, Fredboat, and Groovy popular. The search is used in a -REST request to get the track data, which is then sent through the WebSocket connection to play the track in the voice -channel. That is what we will be doing in this command. - -Lavalink can also play tracks directly from a media url, in which case the play command can look like this: - -```csharp -[Command] -public async Task Play(CommandContext ctx, Uri url) -{ - -} -``` - -Like before, we will need to get our node and guild connection and have the appropriate checks. Since it wouldn't make -sense to have the channel as a parameter, we will instead get it from the member's voice state: - -```csharp -//Important to check the voice state itself first, -//as it may throw a NullReferenceException if they don't have a voice state. -if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) -{ - await ctx.RespondAsync("You are not in a voice channel."); - return; -} - -var lava = ctx.Client.GetLavalink(); -var node = lava.ConnectedNodes.Values.First(); -var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); - -if (conn == null) -{ - await ctx.RespondAsync("Lavalink is not connected."); - return; -} -``` - -Next, we will get the track details by calling @DSharpPlus.Lavalink.LavalinkGuildConnection.GetTracksAsync*. There is a -variety of overloads for this: - -1. @DSharpPlus.Lavalink.LavalinkGuildConnection.GetTracksAsync(System.String,DSharpPlus.Lavalink.LavalinkSearchType) - will search various services for the specified query: - - @DSharpPlus.Lavalink.LavalinkSearchType.Youtube will search YouTube - - @DSharpPlus.Lavalink.LavalinkSearchType.SoundCloud will search SoundCloud -2. @DSharpPlus.Lavalink.LavalinkGuildConnection.GetTracksAsync(System.Uri) will use the direct url to obtain the track. This is - mainly used for the other media sources. - -For this guide we will be searching YouTube. Let's pass in our search string and store the result in a variable: - -```csharp -//We don't need to specify the search type here -//since it is YouTube by default. -var loadResult = await node.Rest.GetTracksAsync(search); -``` - -The load result will contain an enum called @DSharpPlus.Lavalink.LavalinkLoadResult.LoadResultType, which will inform us -if Lavalink was able to retrieve the track data. We can use this as a check: - -```csharp -//If something went wrong on Lavalink's end -if (loadResult.LoadResultType == LavalinkLoadResultType.LoadFailed - - //or it just couldn't find anything. - || loadResult.LoadResultType == LavalinkLoadResultType.NoMatches) -{ - await ctx.RespondAsync($"Track search failed for {search}."); - return; -} -``` - -Lavalink will return the track data from your search in a collection called -@DSharpPlus.Lavalink.LavalinkLoadResult.Tracks, similar to using the search bar in YouTube or SoundCloud directly. The -first track is typically the most accurate one, so that is what we will use: - -```csharp -var track = loadResult.Tracks.First(); -``` - -And finally, we can play the track: - -```csharp -await conn.PlayAsync(track); - -await ctx.RespondAsync($"Now playing {track.Title}!"); -``` - -Your play command should look like this: - -```csharp -[Command] -public async Task Play(CommandContext ctx, [RemainingText] string search) -{ - if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) - { - await ctx.RespondAsync("You are not in a voice channel."); - return; - } - - var lava = ctx.Client.GetLavalink(); - var node = lava.ConnectedNodes.Values.First(); - var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); - - if (conn == null) - { - await ctx.RespondAsync("Lavalink is not connected."); - return; - } - - var loadResult = await node.Rest.GetTracksAsync(search); - - if (loadResult.LoadResultType == LavalinkLoadResultType.LoadFailed - || loadResult.LoadResultType == LavalinkLoadResultType.NoMatches) - { - await ctx.RespondAsync($"Track search failed for {search}."); - return; - } - - var track = loadResult.Tracks.First(); - - await conn.PlayAsync(track); - - await ctx.RespondAsync($"Now playing {track.Title}!"); -} -``` - -Being able to pause the player is also useful. For this we can use most of the base from the play command: - -```csharp -[Command] -public async Task Pause(CommandContext ctx) -{ - if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) - { - await ctx.RespondAsync("You are not in a voice channel."); - return; - } - - var lava = ctx.Client.GetLavalink(); - var node = lava.ConnectedNodes.Values.First(); - var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); - - if (conn == null) - { - await ctx.RespondAsync("Lavalink is not connected."); - return; - } -} -``` - -For this command we will also want to check the player state to determine if we should send a pause command. We can do -so by checking @DSharpPlus.Lavalink.Entities.LavalinkPlayerState.CurrentTrack: - -```csharp -if (conn.CurrentState.CurrentTrack == null) -{ - await ctx.RespondAsync("There are no tracks loaded."); - return; -} -``` - -And finally, we can call pause: - -```csharp -await conn.PauseAsync(); -``` - -The finished command should look like so: - -```csharp -[Command] -public async Task Pause(CommandContext ctx) -{ - if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null) - { - await ctx.RespondAsync("You are not in a voice channel."); - return; - } - - var lava = ctx.Client.GetLavalink(); - var node = lava.ConnectedNodes.Values.First(); - var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild); - - if (conn == null) - { - await ctx.RespondAsync("Lavalink is not connected."); - return; - } - - if (conn.CurrentState.CurrentTrack == null) - { - await ctx.RespondAsync("There are no tracks loaded."); - return; - } - - await conn.PauseAsync(); -} -``` - -Of course, there are other commands Lavalink has to offer. Check out [the docs][1] to view the commands you can use -while playing tracks. - -There are also open source examples such as Emzi0767's [Companion Cube Bot][2] and [Turret Bot][3]. - - -[0]: xref:articles.commands_next.intro -[1]: xref:DSharpPlus.Lavalink.LavalinkGuildConnection -[2]: https://github.com/Emzi0767/Discord-Companion-Cube-Bot -[3]: https://github.com/Emzi0767/Discord-Music-Turret-Bot diff --git a/docs/articles/audio/lavalink/setup.md b/docs/articles/audio/lavalink/setup.md deleted file mode 100644 index f3be371d0b..0000000000 --- a/docs/articles/audio/lavalink/setup.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -uid: articles.audio.lavalink.setup -title: Lavalink Setup ---- - ->[!WARNING] -> `DSharpPlus.Lavalink` has been deprecated, and this article may contain outdated information. Both the extension and this article will be removed -> in the future. - -# Lavalink - the newer, better way to do music - -[Lavalink][0] is a standalone program, written in Java. It's a lightweight solution for playing music from sources such -as YouTube or Soundcloud. Unlike raw voice solutions, such as VoiceNext, Lavalink can handle hundreds of concurrent -streams, and supports sharding. - -## Configuring Java - -In order to run Lavalink, you must have Java 13 or greater installed. Certain Java versions may not be functional with -Lavalink, so it is best to check the [requirements][1] before downloading. The latest releases can be found [here][2]. - -Make sure the location of the newest JRE's bin folder is added to your system variable's path. This will make the `java` -command run from the latest runtime. You can verify that you have the right version by entering `java -version` in your -command prompt or terminal. - -## Downloading Lavalink - -Next, head over to the [releases][3] tab on the Lavalink GitHub page and download the Jar file from the latest version. -Alternatively, stable builds with the latest changes can be found on their [CI Server][4]. - -The program will not be ready to run yet, as you will need to create a configuration file first. To do so, create a new -YAML file called `application.yml`, and use the [example file][5], or copy this text: - -```yaml -server: # REST and WS server - port: 2333 - address: 127.0.0.1 -spring: - main: - banner-mode: log -lavalink: - server: - password: "youshallnotpass" - sources: - youtube: true - bandcamp: true - soundcloud: true - twitch: true - vimeo: true - mixer: true - http: true - local: false - bufferDurationMs: 400 - youtubePlaylistLoadLimit: 6 # Number of pages at 100 each - youtubeSearchEnabled: true - soundcloudSearchEnabled: true - gc-warnings: true - -metrics: - prometheus: - enabled: false - endpoint: /metrics - -sentry: - dsn: "" -# tags: -# some_key: some_value -# another_key: another_value - -logging: - file: - max-history: 30 - max-size: 1GB - path: ./logs/ - - level: - root: INFO - lavalink: INFO -``` - -YAML is whitespace-sensitive. Make sure you are using a text editor which properly handles this. - -There are a few values to keep in mind. - -`host` is the IP of the Lavalink host. This will be `0.0.0.0` by default, but it should be changed as it is a security -risk. For this guide, set this to `127.0.0.1` as we will be running Lavalink locally. - -`port` is the allowed port for the Lavalink connection. `2333` is the default port, and is what will be used for this -guide. - -`password` is the password that you will need to specify when connecting. This can be anything as long as it is a valid -YAML string. Keep it as `youshallnotpass` for this guide. - -When you are finished configuring this, save the file in the same directory as your Lavalink executable. - -Keep note of your `port`, `address`, and `password` values, as you will need them later for connecting. - -## Starting Lavalink - -Open your command prompt or terminal and navigate to the directory containing Lavalink. Once there, type -`java -jar Lavalink.jar`. You should start seeing log output from Lavalink. - -If everything is configured properly, you should see this appear somewhere in the log output without any errors: - -``` -[ main] lavalink.server.Launcher : Started Launcher in 5.769 seconds (JVM running for 6.758) -``` - -If it does, congratulations. We are now ready to interact with it using DSharpPlus. - - -[0]: https://github.com/freyacodes/Lavalink -[1]: https://github.com/freyacodes/Lavalink#requirements -[2]: https://adoptium.net/ -[3]: https://github.com/freyacodes/Lavalink/releases -[4]: https://ci.fredboat.com/viewLog.html?buildId=lastSuccessful&buildTypeId=Lavalink_Build&tab=artifacts&guest=1 -[5]: https://github.com/freyacodes/Lavalink/blob/master/LavalinkServer/application.yml.example diff --git a/docs/articles/audio/voicenext/prerequisites.md b/docs/articles/audio/voicenext/prerequisites.md deleted file mode 100644 index 694ee8e093..0000000000 --- a/docs/articles/audio/voicenext/prerequisites.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -uid: articles.audio.voicenext.prerequisites -title: VoiceNext Prerequisites ---- - -## Required Libraries - -VoiceNext depends on the [libsodium][0] and [Opus][1] libraries to decrypt and process audio packets. Both *must* be -available on your development and host machines otherwise VoiceNext will *not* work. - -### Windows - -When installing VoiceNext though NuGet, an additional package containing the native Windows binaries will automatically -be included with **no additional steps required**. - -However, if you are using DSharpPlus from source or without a NuGet package manager, you must manually [download][2] the -binaries and place them at the root of your working directory where your application is located. - -### MacOS - -Native libraries for Apple's macOS can be installed using the [Homebrew][3] package manager: - -```console -brew install opus libsodium -``` - -### Linux - -#### Debian and Derivatives - -Opus package naming is consistent across Debian, Ubuntu, and most derivatives. - -```bash -sudo apt-get install libopus0 libopus-dev -``` - -Package naming for *libsodium* will vary depending on your distro and version: - -Distributions | Terminal Command -:-----------------------------------------------:|:----------------- -Ubuntu 20.04, Ubuntu 18.04, Debian 10, Debian 11 | `sudo apt-get install libsodium23 libsodium-dev` -Linux Mint, Ubuntu 16.04, Debian 9 | `sudo apt-get install libsodium18 libsodium-dev` - - -[0]: https://github.com/jedisct1/libsodium -[1]: https://opus-codec.org/ -[2]: xref:natives -[3]: https://brew.sh diff --git a/docs/articles/audio/voicenext/receive.md b/docs/articles/audio/voicenext/receive.md deleted file mode 100644 index 6ed6ccbf2f..0000000000 --- a/docs/articles/audio/voicenext/receive.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -uid: articles.audio.voicenext.receive -title: Receiving ---- - -## Receiving with VoiceNext - -### Enable Receiver - -Receiving incoming audio is disabled by default to save on bandwidth, as most users will never make use of incoming -data. This can be changed by providing a configuration object to -@DSharpPlus.VoiceNext.DiscordClientExtensions.UseVoiceNext*. - -```cs -var discord = new DiscordClient(); - -discord.UseVoiceNext(new VoiceNextConfiguration() -{ - EnableIncoming = true -}); -``` - -### Establish Connection - -The voice channel join process is the exact same as when transmitting. - -```cs -DiscordChannel channel; -VoiceNextConnection connection = await channel.ConnectAsync(); -``` - -### Write Event Handler - -We'll be able to receive incoming audio from the @DSharpPlus.VoiceNext.VoiceNextConnection.VoiceReceived event fired by -@DSharpPlus.VoiceNext.VoiceNextConnection. - -```cs -connection.VoiceReceived += ReceiveHandler; -``` - -Writing the logic for this event handler will depend on your overall goal. - -The event arguments will contain a PCM audio packet for you to make use of. You can convert each packet to another -format, concatenate them all together, feed them into an external program, or process the packets any way that'll suit -your needs. - -When a user is speaking, @DSharpPlus.VoiceNext.VoiceNextConnection.VoiceReceived should fire once every twenty -milliseconds and its packet will contain around twenty milliseconds worth of audio; this can vary due to differences in -client settings. To help keep track of the torrent of packets for each user, you can use user IDs in combination the -synchronization value (SSRC) sent by Discord to determine the source of each packet. - -This short-and-simple example will use [ffmpeg][0] to convert each packet to a *wav* file. - -```cs -private async Task ReceiveHandler(VoiceNextConnection _, VoiceReceiveEventArgs args) -{ - var name = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - var ffmpeg = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $@"-ac 2 -f s16le -ar 48000 -i pipe:0 -ac 2 -ar 44100 {name}.wav", - RedirectStandardInput = true - }); - - await ffmpeg.StandardInput.BaseStream.WriteAsync(args.PcmData); -} -``` - -That's really all there is to it. Connect to a voice channel, hook an event, process the data as you see fit. - -![Wav Files][1] - -## Example Commands - -```cs -[Command("start")] -public async Task StartCommand(CommandContext ctx, DiscordChannel channel = null) -{ - channel ??= ctx.Member.VoiceState?.Channel; - var connection = await channel.ConnectAsync(); - - Directory.CreateDirectory("Output"); - connection.VoiceReceived += VoiceReceiveHandler; -} - - -[Command("stop")] -public Task StopCommand(CommandContext ctx) -{ - var vnext = ctx.Client.GetVoiceNext(); - - var connection = vnext.GetConnection(ctx.Guild); - connection.VoiceReceived -= VoiceReceiveHandler; - connection.Dispose(); - - return Task.CompletedTask; -} - -private async Task VoiceReceiveHandler(VoiceNextConnection connection, VoiceReceiveEventArgs args) -{ - var fileName = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - var ffmpeg = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $@"-ac 2 -f s16le -ar 48000 -i pipe:0 -ac 2 -ar 44100 Output/{fileName}.wav", - RedirectStandardInput = true - }); - - await ffmpeg.StandardInput.BaseStream.WriteAsync(args.PcmData); - ffmpeg.Dispose(); -} -``` - - -[0]: https://ffmpeg.org/about.html -[1]: ../../../images/voicenext_receive_01.png diff --git a/docs/articles/audio/voicenext/transmit.md b/docs/articles/audio/voicenext/transmit.md deleted file mode 100644 index 3f093ecd50..0000000000 --- a/docs/articles/audio/voicenext/transmit.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -uid: articles.audio.voicenext.transmit -title: Transmitting ---- - -## Transmitting with VoiceNext - -### Enable VoiceNext - -Install the `DSharpPlus.VoiceNext` package from NuGet. - -![NuGet Package Manager][0] - -Then use the @DSharpPlus.VoiceNext.DiscordClientExtensions.UseVoiceNext* extension method on your instance of -@DSharpPlus.DiscordClient. - -```cs -var discord = new DiscordClient(); -discord.UseVoiceNext(); -``` - -### Connect - -Joining a voice channel is *very* easy; simply use the @DSharpPlus.VoiceNext.DiscordClientExtensions.ConnectAsync* -extension method on @DSharpPlus.Entities.DiscordChannel. - -```cs -DiscordChannel channel; -VoiceNextConnection connection = await channel.ConnectAsync(); -``` - -### Transmit - -Discord requires that we send Opus encoded stereo PCM audio data at a sample rate of 48kHz. - -You'll need to convert your audio source to PCM S16LE using your preferred program for media conversion, then read that -data into a `Stream` object or an array of `byte` to be used with VoiceNext. Opus encoding of the PCM data will be done -automatically by VoiceNext before sending it to Discord. - -This example will use [ffmpeg][1] to convert an MP3 file to a PCM stream. - -```cs -var filePath = "funiculi_funicula.mp3"; -var ffmpeg = Process.Start(new ProcessStartInfo -{ - FileName = "ffmpeg", - Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1", - RedirectStandardOutput = true, - UseShellExecute = false -}); - -Stream pcm = ffmpeg.StandardOutput.BaseStream; -``` - -Now that our audio is the correct format, we'll need to get a *transmit sink* for the channel we're connected to. You -can think of the transmit stream as our direct interface with a voice channel; any data written to one will be processed -by VoiceNext, queued, and sent to Discord which will then be output to the connected voice channel. - -```cs -VoiceTransmitSink transmit = connection.GetTransmitSink(); -``` - -Once we have a transmit sink, we can 'play' our audio by copying our PCM data to the transmit sink buffer. - -```cs -await pcm.CopyToAsync(transmit); -``` - -`Stream#CopyToAsync()` will copy PCM data from the input stream to the output sink, up to the sink's configured -capacity, at which point it will wait until it can copy more. This means that the call will hold the task's execution, -until such time that the entire input stream has been consumed, and enqueued in the sink. - -This operation cannot be cancelled. If you'd like to have finer control of the playback, you should instead consider -using `Stream#ReadAsync()` and `VoiceTransmitSink#WriteAsync()` to manually copy small portions of PCM data to the -transmit sink. - -### Disconnect - -Similar to joining, leaving a voice channel is rather straightforward. - -```cs -var vnext = discord.GetVoiceNext(); -var connection = vnext.GetConnection(); - -connection.Disconnect(); -``` - -## Example Commands - -```cs -[Command("join")] -public async Task JoinCommand(CommandContext ctx, DiscordChannel channel = null) -{ - channel ??= ctx.Member.VoiceState?.Channel; - await channel.ConnectAsync(); -} - -[Command("play")] -public async Task PlayCommand(CommandContext ctx, string path) -{ - var vnext = ctx.Client.GetVoiceNext(); - var connection = vnext.GetConnection(ctx.Guild); - - var transmit = connection.GetTransmitSink(); - - var pcm = ConvertAudioToPcm(path); - await pcm.CopyToAsync(transmit); - await pcm.DisposeAsync(); -} - -[Command("leave")] -public async Task LeaveCommand(CommandContext ctx) -{ - var vnext = ctx.Client.GetVoiceNext(); - var connection = vnext.GetConnection(ctx.Guild); - - connection.Disconnect(); -} - -private Stream ConvertAudioToPcm(string filePath) -{ - var ffmpeg = Process.Start(new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1", - RedirectStandardOutput = true, - UseShellExecute = false - }); - - return ffmpeg.StandardOutput.BaseStream; -} -``` - - -[0]: ../../../images/voicenext_transmit_01.png -[1]: https://ffmpeg.org/about.html diff --git a/docs/articles/basics/bot_account.md b/docs/articles/basics/bot_account.md deleted file mode 100644 index 15dd12612f..0000000000 --- a/docs/articles/basics/bot_account.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -uid: articles.basics.bot_account -title: Creating a Bot Account ---- - -# Creating a Bot Account - -## Create an Application - -Before you're able to create a [bot account][0] to interact with the Discord API, you'll need to create a new OAuth2 -application. Go to the [Discord Developer Portal][1] and click `New Application` at the top right of the page. - -![Discord Developer Portal][2] - -You'll then be prompted to enter a name for your application. - -![Naming Application][3] - -The name of your application will be the name displayed to users when they add your bot to their Discord server. With -that in mind, it would be a good idea for your application name to match the desired name of your bot. - -Enter your desired application name into the text box, then hit the `Create` button. - -After you hit `Create`, you'll be taken to the application page for your newly created application. - -![Application Page][4] - -That was easy, wasn't it? - -Before you move on, you may want to upload an icon for your application and provide a short description of what your bot -will do. As with the name of your application, the application icon and description will be displayed to users when -adding your bot. - -If you want your bot to be private so that only you can add it to servers, make sure to set `Install Link` to `None` on the -installation page. Otherwise, you will get an error when turning off `Public Bot` on the bot page. - -![Installation Page][5] - -# Using Your Bot Account - -## Invite Your Bot - -Now that you have a bot account, you'll probably want to invite it to a server! - -A bot account joins a server through a special invite link that'll take users through the OAuth2 flow; you'll probably -be familiar with this if you've ever added a public Discord bot to a server. To get the invite link for your bot, head -on over to the OAuth2 page of your application. - -![OAuth2][6] - -
-We'll be using the *OAuth2 URL Generator* on this page. Simply tick `bot` under the *scopes* panel; your bot invite link -will be generated directly below. - -![OAuth2 Scopes][7] - -
-By default, the generated link will not grant any permissions to your bot when it joins a new server. If your bot -requires specific permissions to function, you'd select them in the *bot permissions* panel. - -![Permissions][8] - -The invite link in the *scopes* panel will update each time you change the permissions. Be sure to copy it again after -any changes! - -## Get Bot Token - -Instead of logging in to Discord with a username and password, bot accounts use a long string called a *token* to -authenticate. You'll want to retrieve the token for your bot account so you can use it with DSharpPlus. - -Head back to the bot page and click on `Reset Token`. - -![Token Reset][9] - -Confirm that you want to reset the token and enter your 2FA code when prompted. - -![Token Confirmation][10] - -Go ahead and copy your bot token and save it somewhere. You'll be using it soon! - -![Token Copy][11] - ->[!IMPORTANT] -> Handle your bot token with care! Anyone who has your token will have access to your bot account. -> Be sure to store it in a secure location and *never* give it to *anybody*. -> -> If you ever believe your token has been compromised, be sure to hit the `Reset Token` button (as seen above) to -> invalidate your old token and get a brand new token. - -## Write Some Code - -You've got a bot account set up and a token ready for use. Sounds like it's time for you to [write your first bot][12]! - - -[0]: https://discord.com/developers/docs/topics/oauth2#bot-users -[1]: https://discord.com/developers/applications -[2]: ../../images/basics_bot_account_01.png "Creating an Application" -[3]: ../../images/basics_bot_account_02.png "Naming our new Application" -[4]: ../../images/basics_bot_account_03.png "Opening the Bot Page" -[5]: ../../images/basics_bot_account_04.png "Making the Bot Private" -[6]: ../../images/basics_bot_account_05.png "The OAuth2 Page" -[7]: ../../images/basics_bot_account_06.png "Scopes Panel" -[8]: ../../images/basics_bot_account_07.png "Permissions Panel" -[9]: ../../images/basics_bot_account_08.png "Resetting the Token" -[10]: ../../images/basics_bot_account_09.png "Confirming the Reset" -[11]: ../../images/basics_bot_account_10.png "Copying the new Token" -[12]: xref:articles.basics.first_bot diff --git a/docs/articles/basics/first_bot.md b/docs/articles/basics/first_bot.md deleted file mode 100644 index 9dbc4835e2..0000000000 --- a/docs/articles/basics/first_bot.md +++ /dev/null @@ -1,252 +0,0 @@ ---- -uid: articles.basics.first_bot -title: Your First Bot ---- - -# Your First Bot -> ->[!NOTE] -> This article assumes the following: -> -> * You have [created a bot account][0] and have a bot token. -> * You have [Visual Studio][1] installed on your computer. - -## Create a Project - -Open up Visual Studio and click on `Create a new project` towards the bottom right. - -![Visual Studio Start Screen][2] - -Select `Console App` then click on the `Next` button. - -![New Project Screen][3] - -Next, you'll give your project a name. For this example, we'll name it `MyFirstBot`. If you'd like, you can also change -the directory that your project will be created in. - -Enter your desired project name, then click on the `Create` button. - -![Name Project Screen][4] - -Voilà! Your project has been created! - -![Visual Studio IDE][5] - -## Install Package - -Now that you have a project created, you'll want to get DSharpPlus installed. -Locate the *solution explorer* on the right side, then right click on `Dependencies` and select `Manage NuGet Packages` -from the context menu. - -![Dependencies Context Menu][6] - -You'll then be greeted by the NuGet package manager. - -Select the `Browse` tab towards the top left, then type `DSharpPlus` into the search text box. - -![NuGet Package Search][7] - -The first results should be the eight DSharpPlus packages. - -![Search Results][8] - -Package | Description -:-------------------------:|:---: -`DSharpPlus` | Main package; Discord API client. -`DSharpPlus.Commands` | Add-on which provides a command framework for both messages and application commands. -`DSharpPlus.CommandsNext` | Add-on which provides a command framework. Scheduled for obsoletion. -`DSharpPlus.SlashCommands` | Add-on which provides an application command framework. Obsolete. -`DSharpPlus.Interactivity` | Add-on which allows for interactive commands. -`DSharpPlus.Lavalink` | Client implementation for [Lavalink][9]. Defunct. -`DSharpPlus.VoiceNext` | Add-on which enables connectivity to Discord voice channels. -`DSharpPlus.Rest` | REST-only Discord client. - -We'll only need the `DSharpPlus` package for the basic bot we'll be writing in this article. Select it from the list -then click the `Install` button to the right (after verifing that you will be installing the **latest version**). - -![Install DSharpPlus][10] - ->[!NOTE] -> Please activate the 'Include prerelease' checkbox and use v5.0 nightlies instead of v4.5.X. v5.0 is a major rewrite of the library and no support will be provided for v4.5.X. - -You're now ready to write some code! - -## First Lines of Code - -DSharpPlus implements the [Task-based Asynchronous Pattern][11]. Because of this, the majority of DSharpPlus methods must be -executed in a method marked as `async` so they can be properly `await`ed. - -We will therefore proceed to mark our `Main` method as `async`, which also means it has to return `Task` instead of `void`. - -Head back to your *Program.cs* tab and edit the method as discussed. - -```cs -static async Task Main(string[] args) -{ - -} -``` - -If you typed this in by hand, Intellisense should have generated the required `using` directive for you. However, if you -copy-pasted the snippet above, VS will complain about being unable to find the `Task` type. - -Hover over `Task` with your mouse and click on `Show potential fixes` from the tooltip. - -![Error Tooltip][12] - -Then apply the recommended solution. - -![Solution Menu][13] - -We'll now create a new `DiscordClient` instance in our brand new asynchronous method. - -Create a new variable in `Main` and assign it a new @DSharpPlus.DiscordClient instance, then pass an instance of -@DSharpPlus.DiscordConfiguration to its constructor. Create an object initializer for @DSharpPlus.DiscordConfiguration -and populate the @DSharpPlus.DiscordConfiguration.Token property with your bot token then set the -@DSharpPlus.DiscordConfiguration.TokenType property to @DSharpPlus.TokenType.Bot. Next add the -@DSharpPlus.DiscordClient.Intents property and populate it with @DSharpPlus.DiscordIntents.AllUnprivileged. -These Intents are required for certain events to be fired. Please visit this [article][14] for more information. - -```cs -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault("My First Token", DiscordIntents.AllUnprivileged); -DiscordClient client = builder.Build(); -``` - ->[!WARNING] -> We hard-code the token in the above snippet to keep things simple and easy to understand. -> -> Hard-coding your token is *not* a smart idea, especially if you plan on distributing your source code. -> Instead you should store your token in an external medium, such as a configuration file or environment variable, and -> read that into your program to be used with DSharpPlus. - -Follow that up with @DSharpPlus.DiscordClient.ConnectAsync* to connect and login to Discord, and `await Task.Delay(-1);` -at the end of the method to prevent the console window from closing prematurely. - -```cs -DiscordClient client = builder.Build(); - -await client.ConnectAsync(); -await Task.Delay(-1); -``` - -As before, Intellisense will have auto generated the needed `using` directive for you if you typed this in by hand. If -you've copied the snippet, be sure to apply the recommended suggestion to insert the required directive. - -If you hit `F5` on your keyboard to compile and run your program, you'll be greeted by a happy little console with a -single log message from DSharpPlus. Woo hoo! - -![Program Console][15] - -## Spicing Up Your Bot - -Right now our bot doesn't do a whole lot. Let's bring it to life by having it respond to a message! - -As of September 1st 2022, Discord started requiring message content intent for bots that want to read message content. This is a privileged intent! - -If your bot has under 100 guilds, all you have to do is flip the switch in the developer dashboard. (over at ) -If your bot has over 100 guilds, you'll need approval from Discord's end. - -After enabling the intent in the developer dashboard, you have to specify your intents to the client: - -```cs -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault("My First Token", DiscordIntents.AllUnprivileged | DiscordIntents.MessageContent); -``` - -Now you can start to listen to messages. - -Register an event handler as follows: - -```cs -builder.ConfigureEventHandlers -( - b => b.HandleMessageCreated(async s, e) => {}) -); -``` - -Then, add an `if` statement into the body of your event lambda that will check if -@DSharpPlus.Entities.DiscordMessage.Content starts with your desired trigger word and respond with a message using -@DSharpPlus.Entities.DiscordMessage.RespondAsync* if it does. For this example, we'll have the bot to respond with -*pong!*for each message that starts with*ping*. - -```cs -builder.ConfigureEventHandlers -( - b => b.HandleMessageCreated(async (s, e) => - { - if (e.Message.Content.ToLower().StartsWith("ping")) - { - await e.Message.RespondAsync("pong!"); - } - }) -); -``` - -## The Finished Product - -Your entire program should now look like this: - -```cs -using System; -using System.Threading.Tasks; -using DSharpPlus; - -namespace MyFirstBot -{ - class Program - { - static async Task Main(string[] args) - { - DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault("My First Token", DiscordIntents.AllUnprivileged | DiscordIntents.MessageContents); - - builder.ConfigureEventHandlers - ( - b => b.HandleMessageCreated(async (s, e) => - { - if (e.Message.Content.ToLower().StartsWith("ping")) - { - await e.Message.RespondAsync("pong!"); - } - }) - ); - - await builder.ConnectAsync(); - await Task.Delay(-1); - } - } -} -``` - -Hit `F5` to run your bot, then send *ping* in any channel your bot account has access to. Your bot should respond with -*pong!* for each *ping* you send. - -Congrats, your bot now does something! - -![Bot Response][17] - -## Further Reading - -Now that you have a basic bot up and running, you should take a look at the following: - -* [Events][18] -* [CommandsNext][19] - - -[0]: xref:articles.basics.bot_account "Creating a Bot Account" -[1]: https://visualstudio.microsoft.com/vs/ -[2]: ../../images/basics_first_bot_01.png -[3]: ../../images/basics_first_bot_02.png -[4]: ../../images/basics_first_bot_03.png -[5]: ../../images/basics_first_bot_04.png -[6]: ../../images/basics_first_bot_05.png -[7]: ../../images/basics_first_bot_06.png -[8]: ../../images/basics_first_bot_07.png -[9]: xref:articles.audio.lavalink.setup -[10]: ../../images/basics_first_bot_08.png -[11]: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern -[12]: ../../images/basics_first_bot_10.png -[13]: ../../images/basics_first_bot_11.png -[14]: xref:articles.beyond_basics.intents -[15]: ../../images/basics_first_bot_12.png -[17]: ../../images/basics_first_bot_13.png -[18]: xref:articles.beyond_basics.events -[19]: xref:articles.commands_next.intro diff --git a/docs/articles/beyond_basics/events.md b/docs/articles/beyond_basics/events.md deleted file mode 100644 index a55d409135..0000000000 --- a/docs/articles/beyond_basics/events.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -uid: articles.beyond_basics.events -title: DSharpPlus Events ---- - -# Consuming Events - -DSharpPlus makes use of *asynchronous events* which will execute each handler asynchronously and in parallel. This -event system will require event handlers have a `Task` return type and take two parameters. - -The first parameter will contain an instance of the object which fired the event. The second parameter will contain an -arguments object for the specific event you're handling. - -Below is a snippet demonstrating this with a lambda expression. - -```cs -private async Task Main(string[] args) -{ - DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault( /* token and intents */ ); - - builder.ConfigureEventHandlers - ( - b => b.HandleMessageCreated(async (s, e) => - { - if (e.Message.Content.ToLower().StartsWith("spiderman")) - { - await e.Message.RespondAsync("I want pictures of Spiderman!"); - } - }) - .HandleGuildMemberAdded((s, e) => - { - // non-asynchronous code here - return Task.CompletedTask; - }) - ); - - DiscordClient client = builder.Build(); -} -``` - -Alternatively, you can create a new method to consume an event. - -```cs -private async Task Main(string[] args) -{ - DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault( /* token and intents */ ); - - builder.ConfigureEventHandlers - ( - b => b.HandleMessageCreated(MessageCreatedHandler) - .HandleGuildMemberAdded(MemberAddedHandler) - ); -} - -private async Task MessageCreatedHandler(DiscordClient s, MessageCreatedEventArgs e) -{ - if (e.Guild?.Id == 379378609942560770 && e.Author.Id == 168548441939509248) - { - await e.Message.DeleteAsync(); - } -} - -private Task MemberAddedHandler(DiscordClient s, GuildMemberAddedEventArgs e) -{ - // Non asynchronous code here. - return Task.CompletedTask; -} -``` - -Furthermore, DSharpPlus supports using types as event handlers. These types can participate in dependency injection and will have a respected service lifetime. All you need to do is implement `IEventHandler` and enlighten the builder about your event handler: - -```cs -public class MyEventHandler : IEventHandler -{ - // ... -} - -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault(token, intents); -builder.ConfigureEventHandlers(b => b.AddEventHandlers(ServiceLifetime.Singleton)); -``` - -One event handler type may handle as many events as you want, simply implement the interface multiple times: - -```cs -public class MyEventHandler : IEventHandler, IEventHandler -``` - -## Usage of the right events - -We advise against the use of the `SessionCreated`, as it does not necessarily mean that the client -is ready for use. If the goal is to obtain `DiscordMember`/`DiscordGuild` information, this event should not be used. Instead, -the `GuildDownloadCompleted` event should be used. The `SessionCreated` event is only meant to signal that the client has -finished the initial handshake with the gateway and is prepared to begin sending payloads. - -## Migrating to parallel events - -In D#+ v4.4.0, events were changed from executing sequentially (each event runs its registered handlers one by one) to -executing in parallel (each event throws all its handlers onto the thread pool). This change has a few benefits, from -mitigating deadlocks previously occurring with certain interactivity-commandsnext interactions to allowing EventArgs -objets to be garbage collected sooner. - -For end users, this change should not cause any problems, **unless:** -- **IF** you previously had an event handler for `ComponentInteractionCreated` that indiscriminately responded to all - interactions while also using button interactivity, your code will break. Make sure you only respond to events you - actually handle. -- **IF** you previously had two different event handlers on the same event relying on one completing before the other, - your code will break. Either register only one event handler dealing with all your logic, or manage state yourself. - -This change also means that there is no longer a timeout on event handlers, and your event handler is free to take however -long it needs to. There is no longer a reason to wrap your events in a `_ = Task.Run(async () => // logic);`. diff --git a/docs/articles/beyond_basics/intents.md b/docs/articles/beyond_basics/intents.md deleted file mode 100644 index 5b9cdf9a2b..0000000000 --- a/docs/articles/beyond_basics/intents.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -uid: articles.beyond_basics.intents -title: Intents ---- - -## Intents - -Intents were added to Discord to help the service not have to push so many events to the bots that were not using them. -If you are going to be needing to subscribe to any type of event, they are going to have to be defined **BOTH** within -the [Discord Application under the Bot Page][0] on Discords Site and also within the @DSharpPlus.DiscordConfiguration. - -### Discord Application - -On the [Discord Application under the Bot Page][0] you will have to specify if your bot requires Privileged Intents. We -recommend having these all enabled at first to ensure the most stability when building your first bot, otherwise you may -run into issues when retrieving entities from the library's cache. - -![Bot Page][1] - ->[!WARNING] -> These privileged intents may not be available for you to toggle on immediately. -> -> Due to their nature of sensitive data, Discord requires you to go through a verification process once your bot is in a -> certain amount of servers. Please read this [blog post][2] for more information and how to apply. - -### Discord Configuration - -Within your setup code you will have to specify all the intents you will need. Here is a list of -all the [Intents][3] DSharpPlus Supports. Like above however, we recommend having all intents -enabled during initial development, so you should specify @DSharpPlus.DiscordIntents.All in your configuration which will include the -privleged intents you enabled in your application: - -```csharp -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault(token, DiscordIntents.All); -``` - -When you become more advanced, you should try experimenting with turning off intents you do not need in order to save -resources. In your DiscordClientBuilder you can specify one or many. - -Here is an example of just specifying one: - -```csharp -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault(token, DiscordIntents.GuildMessages); -``` - -Here is an example of specifying many: - -```csharp -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault -( - token, - DiscordIntents.DirectMessageReactions - | DiscordIntents.DirectMessages - | DiscordIntents.GuildBans - | DiscordIntents.GuildEmojis - | DiscordIntents.GuildInvites - | DiscordIntents.GuildMembers - | DiscordIntents.GuildMessages - | DiscordIntents.Guilds - | DiscordIntents.GuildVoiceStates - | DiscordIntents.GuildWebhooks -}; -``` - -Please Note, if you specify a privileged intent that you have not signed up -for on the Discord Application page, an error will be thrown on the connection. - - -[0]: https://discord.com/developers/applications -[1]: ../../images/Intents.png -[2]: https://support.discord.com/hc/en-us/articles/360040720412-Bot-Verification-and-Data-Whitelisting -[3]: xref:DSharpPlus.DiscordIntents diff --git a/docs/articles/beyond_basics/interactions.md b/docs/articles/beyond_basics/interactions.md deleted file mode 100644 index 77c4f0f873..0000000000 --- a/docs/articles/beyond_basics/interactions.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -uid: articles.beyond_basics.interactions -title: Interactions ---- - -[Interactions](https://discord.com/developers/docs/interactions/receiving-and-responding#interactions) represent a user interacting with your bot. -This can happen in different ways. The most prominent case is "slash commands" but also button presses or context menus. - -## Recieving interactions -Discord offers two ways to receive interactions: Through the gateway, and via an inbound HTTP webhook. -Currently DSharpPlus only supports the first one but there are plans to also integrate webhooks. -To recieve an interaction over the gateway you do not have to configure anything and simply register an EventHandler to the `InteractionCreated` event. - -In addition to that event we have some events that are filtered to provide some convienience: - -- Buttons and Select menus -> `ComponentInteractionCreated` -- Modals -> `ModalSubmitted` -- User or Message context menu -> `ContextMenuInteractionCreated` -- Application Commands and autocompletion for those only in `InteractionCreated` - -## Handling an interaction -The baseline is that every interaction has to be acknowledged in some fashion in the first 3 seconds after recieving it. -Available response types vary depending on the type of interaction. - -> [!Important] -> The initial response has to decide if the repsonse should be ephemeral. You can NOT change this later. - -### Application Commands -When responding to an Application Command ("Slash commands" or context menus) you can defer your response and extend the window for the interaction to 15 minutes. -This deferred response ("XY is thinking..." in the client) can later be edited to show your desired response. - -### Message Components -Responding to a Message Component is pretty much the same as a reaction to a application command. -The biggest difference is that you can use the `UpdateMessage` response type to directly update the message the component is located on. -This response type is also deferrable with `DeferrredMessageUpdate`. - - -### Modals -If you want to respond to any interaction with a modal it has to be the initial response. -When responding to a modal you can not respond with another Modal. - -### Autocompletion -When responding to an Autocomplete request you have to respond with `DiscordInteractionResponseType.AutoCompleteResult` and zero to 25 results within the 3 second window. - - -### Components V2 - -> [!Important] -> The following content cannot be edited on a message with the components V2 flag: `content`, `embeds`, `stickers`. Furthermore, messages cannot be "downgraded" from Components V2, only *upgraded*. - -Components V2 is a relatively new addition to existing components, with 7 new component types, new APIs, and entirely new ways of visualizing content. -Enabling components V2 is as simple as calling `EnableComponentsV2` on a builder, such as `InteractionResponseBuilder`. - -There are some things to take in mind with components V2 however; the biggest one is that once a message is V2, it is *always* V2. -This is a deliberate decision by Discord. Furthermore, messages with the V2 components flag (hereon referred to as Components V2/V2 Messages) only support using components (don't worry, you can still display text!) and setting attachments. However, these attachments *must* be referenced by a component (and for good reason!) - -V2 Messages have some unique advantages however: -- Max top-level components doubled! **5 ➜ 10** -- Max total components increased from **25 ➜ 30** - -Components V2 (specifically, components introduced by Components V2) do not go in action rows! Freedom alas. -Because of this, **we've also introduced a new API** `BaseDiscordMessageBuilder#AddRawComponents`; this method does *not* create an action row for you, so it is important to mind your usage of it. - -> [!Note] -> Components V2 is not limited to interactions. This section may be moved in the future. - -What are these new components? - -- `Section Component` - - Several sections (3) of text with an accessory (either a thumbnail or button 👀). - - May support more components than just text in the future - - -- `Text Display Component` - - A simple display of text, up to 4000 characters (summed across all text in the message) - - Sections also count toward this, and also have the 4000-character limit. - - -- `Thumbnail Component` - - A simple thumbnail, usable in sections - - -- `Media Gallery Component` - - A collection of arbitrary media items (`DiscordMediaGalleryItem`) - - Can be a remote url or a local file referenced via `attachment://my_file.png` - - -- `File Component` - - A singular, arbitrary file - - Also supports urls or `attachment://` attachments - - Does not support native previews for text files - - Can be spoilered - - -- `Separator Component` - - Acts as a vertical spacer between components - - Has two sizes, which are equivalent to 1 and 2 lines of text respectively. - - Invisible by default, but can be set to render as a line (`divider = true`) - -- `Container Component` - - Arguably the coolest component- - - Acts as a "container" for other components; can be colored like an embed - - Can also be spoilered, blurring the entire container and components within - - Holds action rows, and all new V2 components except containers - - Holds up to 10 components diff --git a/docs/articles/beyond_basics/logging/default.md b/docs/articles/beyond_basics/logging/default.md deleted file mode 100644 index 0474a7059a..0000000000 --- a/docs/articles/beyond_basics/logging/default.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -uid: articles.beyond_basics.logging.default -title: The Default Logger ---- - -## The Default Logger - -DSharpPlus ships with a default logging implementation which is **enabled automatically** with **no setup required**. - -![Info Level Logging][0] - -This is a basic implementation that only sends log messages to the console. - -#### Minimum Logging Level - -You're able to adjust the verbosity of log messages via @DSharpPlus.DiscordConfiguration. - -```cs -new DiscordConfiguration() -{ - MinimumLogLevel = LogLevel.Debug -}; -``` - -The example above will display level log messages that are higher than or equal to `Debug`. - -![Debug Level Logging][1] - -#### Timestamp Format - -You're also able to change the format of the log timestamp; this is also set through @DSharpPlus.DiscordConfiguration. - -```cs -new DiscordConfiguration() -{ - LogTimestampFormat = "MMM dd yyyy - hh:mm:ss tt" -}; -``` - -![The Real Timestamp Format][2] - -For a list of all available format specifiers, check out the MSDN page for [custom date and time format strings][3]. - -## Log Levels - -Below is a table of all log levels and the kind of messages you can expect from each. - -Name | Position | Description -:------------:|:--------:|:------------ -`Critical` | 5 | Fatal error which may require a restart. -`Error` | 4 | A failure of an operation or request. -`Warning` | 3 | Non-fatal errors and abnormalities. -`Information` | 2 | Session startup and resume messages. -`Debug` | 1 | Ratelimit buckets and related information. -`Trace` | 0 | Websocket & REST traffic. - ->[!WARNING] -> The `Trace` log level is *not* recommended for use in production. -> -> It is intended for debugging DSharpPlus and may display tokens and other sensitive data. - - -[0]: ../../../images/beyond_basics_logging_default_01.png -[1]: ../../../images/beyond_basics_logging_default_02.png -[2]: ../../../images/beyond_basics_logging_default_03.png -[3]: https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings#day-d-format-specifier diff --git a/docs/articles/beyond_basics/logging/third_party.md b/docs/articles/beyond_basics/logging/third_party.md deleted file mode 100644 index 175eadbedb..0000000000 --- a/docs/articles/beyond_basics/logging/third_party.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -uid: articles.beyond_basics.logging.third_party -title: Third Party Logging ---- - -# Using a Third Party Logger - -While the default logging implementation will meet the needs of most, some may desire to make use of a more robust -implementation which provides more features. Thankfully, DSharpPlus allows you to use any logging library which has an -implementation for the [logging abstractions][0] provided by Microsoft. - -[Serilog][1], one of the more popular logging libraries, will be used to demonstrate. This will simply be a brief demo, -so we won't go into the configuration of Serilog. You'll want to head on over to their [wiki page][2] to learn about -that! - -We'll need to install both the `Serilog` and `Serilog.Extensions.Logging` packages from NuGet, along with at least one -of the many available [sinks][3]. Our example here will only use the `Serilog.Sinks.Console` sink. - -Start off by creating a new `LoggerConfiguration` instance, slap `.WriteTo.Console().CreateLogger()` onto the end of it, -then directly assign that to the static `Logger` property on the `Log` class. - -```cs -Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .CreateLogger(); -``` - -This will make a new Serilog logger instance which will write to the console sink. - -Next, create a new variable and assign it a new `LoggerFactory` instance which calls `AddSerilog()`. - -```cs -var logFactory = new LoggerFactory().AddSerilog(); -``` - -Then assign that variable to the @DSharpPlus.DiscordConfiguration.LoggerFactory property of your of -@DSharpPlus.DiscordConfiguration. - -```cs -new DiscordConfiguration() -{ - LoggerFactory = logFactory -} -``` - -Altogether, you'll have something similar to this: - -```cs -using Microsoft.Extensions.Logging; -using Serilog; - -public async Task MainAsync() -{ - Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .CreateLogger(); - - var logFactory = new LoggerFactory().AddSerilog(); - var discord = new DiscordClient(new DiscordConfiguration() - { - LoggerFactory = logFactory - }); -} -``` - -And that's it! If you now run your bot, you'll see DSharpPlus log messages formatted and displayed by Serilog. - -![Console][4] - - -[0]: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging -[1]: https://serilog.net/ -[2]: https://github.com/serilog/serilog/wiki/Configuration-Basics -[3]: https://github.com/serilog/serilog/wiki/Provided-Sinks -[4]: ../../../images/beyond_basics_logging_third_party_01.png diff --git a/docs/articles/beyond_basics/messagebuilder.md b/docs/articles/beyond_basics/messagebuilder.md deleted file mode 100644 index 318b9f0f0c..0000000000 --- a/docs/articles/beyond_basics/messagebuilder.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -uid: articles.beyond_basics.messagebuilder -title: Message Builder ---- - -## Background - -Before the message builder was put into place, we had one large method for sending messages along with 3 additional -methods for sending files. This was becoming a major code smell and it was hard to maintain and add more parameters onto -it. Now we support just sending a simple message, an embed, a simple message with an embed, or a message builder. - -## Using the Message Builder - -The API Documentation for the message builder can be found at @DSharpPlus.Entities.DiscordMessageBuilder but here we'll -go over some of the concepts of using the message builder: - -### Adding a File - -For sending files, you'll have to use the MessageBuilder to construct your message, see example below: - -```cs -using fs = new FileStream("ADumbFile.txt", FileMode.Open, FileAccess.Read); - -var msg = await new DiscordMessageBuilder() - .WithContent("Here is a really dumb file that I am testing with.") - .WithFiles(new Dictionary() { { "ADumbFile1.txt", fs } }) - .SendAsync(ctx.Channel); -``` - -### Adding Mentions - -For sending mentions, you'll have to use the MessageBuilder to construct your message, see example below: - -```cs -var msg = await new DiscordMessageBuilder() - .WithContent($"✔ UserMention(user): Hey, {user.Mention}! Listen!") - .WithAllowedMentions(new IMention[] { new UserMention(user) }) - .SendAsync(ctx.Channel); -``` - -### Sending TTS Messages - -For sending a TTS message, you'll have to use the MessageBuilder to construct your message, see example below: - -```cs -var msg = await new DiscordMessageBuilder() - .WithContent($"This is a dumb message") - .HasTTS(true) - .SendAsync(ctx.Channel); -``` - -### Sending an Inline Reply - -For sending an inline reply, you'll have to use the MessageBuilder to construct your message, see example below: - -```cs -var msg = await new DiscordMessageBuilder() - .WithContent($"I'm talking to *you*!") - .WithReply(ctx.Message.Id) - .SendAsync(ctx.Channel); -``` - -By default, replies do not mention. To make a reply mention, simply pass true as the second parameter: - -```cs -// ... - .WithReply(ctx.Message.Id, true); -// ... -``` diff --git a/docs/articles/beyond_basics/permissions.md b/docs/articles/beyond_basics/permissions.md deleted file mode 100644 index fe2194ffd7..0000000000 --- a/docs/articles/beyond_basics/permissions.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -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. - -`DiscordPermissions` objects can be created and modified using collection expression syntax and in that capacity works functionally like any collection. - -## 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. - -> [!INFO] -> `Add` and `Remove` edit the current set, while `+` and `-` create a new object. - -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 permission set can be treated as 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/beyond_basics/sharding.md b/docs/articles/beyond_basics/sharding.md deleted file mode 100644 index 26d0b97aa6..0000000000 --- a/docs/articles/beyond_basics/sharding.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -uid: articles.beyond_basics.sharding -title: Sharding ---- - -# Sharding - -As your bot joins more guilds, your poor @DSharpPlus.DiscordClient will be hit with an increasing number of events. -Thankfully, Discord allows you to establish multiple connections to split the event workload; this is called *sharding* -and each individual connection is referred to as a *shard*. Each shard handles a separate set of servers and will *only* -receive events from those servers. However, all direct messages will be handled by your first shard. - -Sharding is recommended once you reach 1,000 servers, and is a *requirement* when you hit 2,500 servers. - -## Automated Sharding - -DSharpPlus provides a built-in sharding solution: `DiscordClient`. `DiscordClientBuilder` and the service collection configuration both offer ways to configure a sharding DiscordClient: - -# [DiscordClientBuilder](#tab/discordclientbuilder) - -```cs -DiscordClientBuilder builder = DiscordClientBuilder.CreateSharded("My First Token", DiscordIntents.All); -DiscordClient shardingClient = builder.Build(); -``` - -# [IServiceCollection](#tab/iservicecollection) - -```cs -serviceCollection.AddShardedDiscordClient("My First Token", DiscordIntents.All); -``` - ---- - -## Further Customization - -For most looking to shard, the built-in sharded client will work well enough. However, those looking for more control over the sharding process may want to handle it manually. The default `MultiShardOrchestrator` provides the ability to only start a certain set of the total shards within the current `DiscordClient` via setting the `Stride` and `TotalShards` properties: - -```cs -serviceCollection.Configure(x => -{ - x.Stride = 16; - x.ShardCount = 16; - x.TotalShards = 32; -}); -``` - -Furthermore, it is possible to override the orchestrator entirely and replace it with your own, which can then do whatever you want: - -```cs -public class MyCustomOrchestrator : IShardOrchestrator; - -serviceCollection.AddSingleton(); -``` - -Your orchestrator will need to be able to connect, reconnect and disconnect, it will need to be able to send payloads to Discord and expose whether a certain shard is connected correctly. By default, each shard is represented as an `IGatewayClient`, but if you are writing your own orchestrator, you are free to implement the individual shards yourself. - -Implementing a custom orchestrator should be done as a last resort, when you have verified the default implementations cannot do anything for you nor can be adapted to work for you and when you are entirely sure you understand sharding to a degree where you would be able to implement your own orchestrator. - -Additionally, if you have a very large bot, the default `MultiShardOrchestrator` may not meet your needs. In that case, please review the documentation and all material Discord has provided you with carefully and consider implementing your own `IShardOrchestrator` or reaching out to us to find a solution. diff --git a/docs/articles/commands/converters/built_in_converters.md b/docs/articles/commands/converters/built_in_converters.md deleted file mode 100644 index e3d5ff1ac6..0000000000 --- a/docs/articles/commands/converters/built_in_converters.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -uid: articles.commands.converters.built_in_converters -title: Built-In Converters ---- - -# Built-In Converters -DSharpPlus provides a number of built-in argument converters for common types. An easy way to remember which converters should be available is ".NET Primitives + Discord Entities." This means that all primitive types in .NET are supported, as well as Discord entities such as `DiscordUser`, `DiscordChannel`, `DiscordRole`, and so on. For an up-to-date list of built-in converters, go to the source and view which files are available: [DSharpPlus.Commands/Converters](https://github.com/DSharpPlus/DSharpPlus/tree/master/DSharpPlus.Commands/Converters). Below is a list of some of the most common built-in converters, and their caveats/quirks: - -## .NET Primitives -All .NET primitive types have built-in support. Each primitive type is parsed through `.TryParse`. - -### DateTime -The `DateTime` converter does not exist and is not planned to be added. Instead, use `DateTimeOffset` for more accurate time handling. - -### Enums -Each enum type that used in a command will have the generic version of `EnumConverter` created at startup. [The generic converter is roughly x2 faster than the non-generic converter](https://github.com/DSharpPlus/DSharpPlus/blob/5d334247afbf3c2895f2fa428d3b9ba0de890849/tools/DSharpPlus.Tools.Benchmarks/benchmark-results.md). Enums with less than 25 members have built-in choice provider support. Enums with more than 25 members will have auto-complete support. - -### `long` and `ulong` -`long` and `ulong` are supported, however they will not have the numeric validation that Discord supports with slash commands. This is because the largest number Discord supports is 9,007,199,254,740,992, also known as 2^53 - JavaScript's maximum safe integer. When using `long` or `ulong`, the parameter will appear as a string in the Discord Client, however the argument converters will still ensure that the value is a valid number. - -### TimeSpan -While `TimeSpan` does use `TimeSpan.TryParse` just as all the other primitives do, there is additional supported syntax ported over from CommandsNext: `1d2h3m4s5ms` will be parsed as `1 day, 2 hours, 3 minutes, 4 seconds, and 5 milliseconds`. The argument converter supports all of the following time units: - -| Unit | Of Measurement | -|------|----------------| -| y | Year | -| mo | Month | -| w | Week | -| d | Day | -| h | Hour | -| m | Minute | -| s | Second | -| ms | Millisecond | -| us | Microsecond | -| µs | Microsecond | -| ns | Nanosecond | - -### Discord Entities - -The most common Discord entities have built-in converters. Almost all of these entities can be used by passing in the entity's ID, mention, or name. The following entities have built-in converters: - -- `DiscordAttachment` -- `DiscordChannel` -- `DiscordEmoji` -- `DiscordGuild` -- `DiscordMember` -- `DiscordMessage`* -- `DiscordRole` -- `DiscordSnowflakeObjectConverter`* -- `DiscordThreadChannel` -- `DiscordUser` - -By default, if the entity is not found, it will attempt to make an API rest request to fetch the entity. - -### DiscordMessage -`DiscordMessage` can be parsed by passing a message link to any message that both the user and the bot can access. Alternatively, you can pass the message id if the message is in the current channel. If the parameter has the `TextMessageReply` attribute applied to it, then the message can be parsed by replying to the message with the command. - -### DiscordSnowflakeObjectConverter -`DiscordSnowflakeObjectConverter` is a converter that supports parsing `DiscordUser`, `DiscordMember`, `DiscordChannel`, `DiscordThreadChannel` and `DiscordRole`. \ No newline at end of file diff --git a/docs/articles/commands/converters/custom_argument_converters.md b/docs/articles/commands/converters/custom_argument_converters.md deleted file mode 100644 index 827dec63a2..0000000000 --- a/docs/articles/commands/converters/custom_argument_converters.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -uid: articles.commands.converters.custom_argument_converters -title: Custom Argument Converters ---- - -# Custom Argument Converters -Creating a custom argument converter isn't difficult, and is accomplished via interfaces. We're going to be creating a custom argument converter for the `Ulid` - a Universally Unique Lexicographically Sortable Identifier - which is a type that's completely compatible with a `Guid` while being much faster and contains more information to work with. - -> [!WARNING] -> Previously in CommandsNext, you would implement the `IArgumentConverter` interface. This is no longer the case! Each command processor will expose it's own version of the argument converter interfaces, such as `ITextArgumentConverter` and `ISlashArgumentConverter`. - -Argument converters are command processor specific. This means that if you want to use a custom argument converter with both text commands and slash commands, you will need to implement two interfaces: `ITextArgumentConverter` and `ISlashArgumentConverter`. `ISlashArgumentConverter` will contain the `DiscordApplicationCommandOptionType ParameterType` property, which is used on command registration. The `ITextArgumentConverter` interface will contain the `bool RequiresText` property, which is used to determine if the next argument should be consumed as text. Since we want to use this argument converter with both text commands and slash commands, we're going to implement the `ISlashArgumentConverter` and `ITextArgumentConverter` interfaces. Both interfaces implement the `IArgumentConverter` interface, which requires the `public Task> ConvertAsync` method: - -```cs -public class UlidArgumentConverter : ITextArgumentConverter, ISlashArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public string ReadableName => "Ulid"; - public bool RequiresText => true; - - public Task> ConvertAsync(ConverterContext context) -} -``` - -Now, a `ConverterContext` is very similar to a `CommandContext` - so much so that they both inherit from the `AbstractContext` type. The main difference is that `ConverterContext` contains more information about the current state of the conversion process. Even though the method receives a `ConverterContext`, the command processor should pass an object that inherits from that abstract class. Now, we need to implement the actual conversion logic. The `Ulid` type has a `TryParse` method that we can use to convert a string to a `Ulid`. We can use this to implement the conversion logic: - -```cs -public class UlidArgumentConverter : ITextArgumentConverter, ISlashArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public string ReadableName => "Ulid"; - public bool RequiresText => true; - - private readonly ILogger logger; - - public UlidArgumentConverter(ILogger logger) => logger = logger; - - public Task> ConvertAsync(ConverterContext context) - { - // This should always be a string since `ISlashArgumentConverter.ParameterType` is - // `DiscordApplicationCommandOptionType.String`, however we type check here as a safety measure - // and to provide a more informative log message. - if (context.Argument is not string value) - { - logger.LogInformation("Argument is not a string."); - return Task.FromResult(Optional.FromNoValue()); - } - - logger.LogInformation("Attempting to convert {Value} to Ulid.", value); - if (Ulid.TryParse(value, out var ulid)) - { - logger.LogInformation("Successfully converted {Value} to Ulid.", value); - return Task.FromResult(Optional.FromValue(ulid)); - } - - logger.LogInformation("Failed to convert {Value} to Ulid.", value); - return Task.FromResult(Optional.FromNoValue()); - } -} -``` - -Since there is no asynchronous operations occuring within the conversion methods, we use `Task.FromResult`. Another thing to note is that argument converters support constructor dependency injection. Lastly, you'll also notice the use of `Optional` here. When executing an argument converter, there are three possible outcomes: - -1. The conversion was successful and the value is returned. -2. The conversion was unsuccessful and the value is not returned. -3. The conversion was unsuccessful due to an unexpected error, causing the converter to throw and exception. - -When implementing your own command processor, you must keep the above cases in mind and handle them accordingly. By default, the `BaseCommandProcessor` will handle the above cases for you, but if you choose to implement `ICommandProcessor` directly, you must handle these cases yourself. - -Next we must register our argument converter with the command processor: -```cs -TextCommandProcessor textCommandProcessor = new TextCommandProcessor(); -textCommandProcessor.AddArgumentConverter(); - -commandsExtension.AddProcessor(textCommandProcessor); -``` - -Alternatively, if you have multiple argument converters to register, you can pass them all in at once via an assembly: -```cs -TextCommandProcessor textCommandProcessor = new TextCommandProcessor(); -textCommandProcessor.AddArgumentConverters(typeof(Program).Assembly); - -commandsExtension.AddProcessor(textCommandProcessor); -``` - -And that's it! We've now created a custom argument converter for the `Ulid` type. You can now use this argument converter in your commands by simply adding a parameter of type `Ulid` to your command methods. The command processor will automatically use the `UlidArgumentConverter` to convert the string to a `Ulid` for you. - -```cs -[Command("ulid")] -public async ValueTask GetUlid(CommandContext commandContext, Ulid ulid) => - await commandContext.RespondAsync($"The Ulid is: {ulid}"); -``` \ No newline at end of file diff --git a/docs/articles/commands/converters/manually_invoking_converters.md b/docs/articles/commands/converters/manually_invoking_converters.md deleted file mode 100644 index be1e070339..0000000000 --- a/docs/articles/commands/converters/manually_invoking_converters.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -uid: articles.commands.converters.manually_invoking_converters -title: Manually Invoking Argument Converters ---- - -# Manually Invoking Argument Converters -Ocassionally, you may need to manually invoke an argument converter. This can be done by calling the `ConvertAsync` method on a `IArgumentConverter` object. This method is asynchronous and will return a `Optional` object. If the conversion was successful, the `Optional` object will contain the converted value. If the conversion was unsuccessful, the `Optional` object will be empty. If there was a truly unexpected error, an exception will be thrown. In order to invoke an argument converter, you must first obtain an object that implements `ConverterContext`. This object contains all the necessary information for the converter to work. Below is an example of how to manually invoke an argument converter: - -```csharp -// Obtain the argument converter -IArgumentConverter converter = context.Extension.GetProcessor().Converters[typeof(T)]; - -// Create a ConverterContext object -TextConverterContext converterContext = new() -{ - Channel = Channel, - Command = CommandsExtension.Commands["day_of_week"], - Extension = CommandsExtension, - Message = context.Message, // This can affect the outcome of the conversion depending on the converter! - RawArguments = "Monday", - ServiceScope = ServiceProvider.CreateScope(), - Splicer = DefaultTextArgumentSplicer.Splice, - User = User -}; - -// Go to the next parameter of the command -converterContext.NextParameter(); - -// Go to the next argument of the message -converterContext.NextArgument(); - -// Invoke the converter -Optional result = await converter.ConvertAsync(converterContext); -``` - -Alternatively, if you aren't able to use generics, you can use the non-generic argument converter delegates that `BaseCommandProcessor` exposes, which all first party processors inherit from. Below is an example of how to manually invoke an argument converter without using generics: - -```csharp -// Select our type -Type type = typeof(ulong); - -// Obtain the argument converter -IArgumentConverter converter = context.Extension.GetProcessor().ConverterDelegates[type]; - -// Create a ConverterContext object -TextConverterContext converterContext = new() -{ - Channel = Channel, - Command = CommandsExtension.Commands["day_of_week"], - Extension = CommandsExtension, - Message = context.Message, // This can affect the outcome of the conversion depending on the converter! - RawArguments = "Monday", - ServiceScope = ServiceProvider.CreateScope(), - Splicer = DefaultTextArgumentSplicer.Splice, - User = User -}; - -IOptional result = await converter.ConvertAsync(converterContext); -``` \ No newline at end of file diff --git a/docs/articles/commands/custom_context_checks.md b/docs/articles/commands/custom_context_checks.md deleted file mode 100644 index 7d89ec632a..0000000000 --- a/docs/articles/commands/custom_context_checks.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -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/custom_error_handler.md b/docs/articles/commands/custom_error_handler.md deleted file mode 100644 index 410add27ec..0000000000 --- a/docs/articles/commands/custom_error_handler.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -uid: articles.commands.custom_error_handler -title: Custom Error Handler ---- - -# Custom Error Handler -Oh no! Someone tried to execute a command, but for whatever reason, it failed! This article will help you understand why commands might fail and how to handle them. - -There are a few reasons why a command might not execute. A few common ones include: -- **Context Checks**: The command might have checks that are not met. For example, the command might require the user to have a certain role, or the command might require the bot to have certain permissions. -- **Argument Parsing**: The command might have arguments that are not valid. For example, the command might require a number, but the user provided a string. -- **Command Execution**: The command might have an unexpected error while executing. For example, the command might try to send a message to a channel, but the bot does not have permission to send messages in that channel, causing an exception to be thrown. - -When any part of the command process fails, the command processor will raise the `CommandErrored` event. This event is executed with a `CommandErrorEventArgs` object, which contains information about the command that errored, the command context provided, the command object itself (when possible), and the exception that caused the error. By default, we have a built in error handler which provides a helpful debug embed to the user, but you can override this behavior by setting `CommandsConfiguration.UseDefaultCommandErrorHandler` to `false` and registering your own delegate to the `CommandErrored` event. Here's an example of how you might handle the `CommandErrored` event: - -```cs -CommandsExtension commandsExtension = discordClient.UseCommands(new CommandsConfiguration -{ - // Disable the default error handler - UseDefaultCommandErrorHandler = false, -}); - -// Add our own error handler -commandsExtension.CommandErrored += async (s, e) => -{ - StringBuilder stringBuilder = new(); - stringBuilder.Append("An error occurred while executing the command: "); - stringBuilder.Append(e.Exception.GetType().Name); - stringBuilder.Append(", "); - stringBuilder.Append(Formatter.InlineCode(Formatter.Sanitize(e.Exception.Message))); - - await eventArgs.Context.RespondAsync(stringBuilder); -}; -``` - -Our default error handler will handle a large variety of common cases and will provide a helpful debug embed (which includes which exception, the exception message and the stack trace) to the user. However, if you want to provide a more custom error handling experience, you can handle the `CommandErrored` event and provide your own error handling logic. \ No newline at end of file diff --git a/docs/articles/commands/introduction.md b/docs/articles/commands/introduction.md deleted file mode 100644 index c08ea4c798..0000000000 --- a/docs/articles/commands/introduction.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -uid: articles.commands.introduction -title: Commands Introduction ---- - -# Commands Introduction -`DSharpPlus.Commands` is a new universal command framework designed with a "write-once, use-anywhere" mindset. Previously when developing bots with DSharpPlus, you'd have to choose between text commands through `DSharpPlus.CommandsNext`, or slash commands through `DSharpPlus.SlashCommands`. With `DSharpPlus.Commands`, you can have both and more. This framework is designed to be easy to use, and easy to extend. - -## Hello World - -First, we're going to setup our code. - -# [Main Method](#tab/main-method) -```cs -public async Task Main(string[] args) -{ - string discordToken = Environment.GetEnvironmentVariable("DISCORD_TOKEN"); - if (string.IsNullOrWhiteSpace(discordToken)) - { - Console.WriteLine("Error: No discord token found. Please provide a token via the DISCORD_TOKEN environment variable."); - Environment.Exit(1); - } - - DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault(discordToken, TextCommandProcessor.RequiredIntents | SlashCommandProcessor.RequiredIntents); - - // Setup the commands extension - builder.UseCommands((IServiceProvider serviceProvider, CommandsExtension extension) => - { - extension.AddCommands([typeof(MyCommand), typeof(MyOtherCommand)]); - TextCommandProcessor textCommandProcessor = new(new() - { - // The default behavior is that the bot reacts to direct - // mentions and to the "!" prefix. If you want to change - // it, you first set if the bot should react to mentions - // and then you can provide as many prefixes as you want. - PrefixResolver = new DefaultPrefixResolver(true, "?", "&").ResolvePrefixAsync, - }); - - // Add text commands with a custom prefix (?ping) - extension.AddProcessor(textCommandProcessor); - }, new CommandsConfiguration() - { - // The default value is true, however it's shown here for clarity - RegisterDefaultCommandProcessors = true, - DebugGuildId = Environment.GetEnvironmentVariable("DEBUG_GUILD_ID") ?? 0, - }); - - DiscordClient client = builder.Build(); - - // We can specify a status for our bot. Let's set it to "playing" and set the activity to "with fire". - DiscordActivity status = new("with fire", DiscordActivityType.Playing); - - // Now we connect and log in. - await client.ConnectAsync(status, DiscordUserStatus.Online); - - // And now we wait infinitely so that our bot actually stays connected. - await Task.Delay(-1); -} -``` - -# [Service Provider](#tab/service-provider) -In the main logic of your program, we're going to register the `DiscordClient` and the extension to your service provider: - -```cs -string discordToken = Environment.GetEnvironmentVariable("DISCORD_TOKEN"); -if (string.IsNullOrWhiteSpace(discordToken)) -{ - Console.WriteLine("Error: No discord token found. Please provide a token via the DISCORD_TOKEN environment variable."); - Environment.Exit(1); -} - -serviceCollection.AddDiscordClient(discordToken, TextCommandProcessor.RequiredIntents | SlashCommandProcessor.RequiredIntents); - -// Setup the commands extension -serviceCollection.AddCommandsExtension((IServiceProvider serviceProvider, CommandsExtension extension) => -{ - extension.AddCommands([typeof(MyCommand), typeof(MyOtherCommand)]); - TextCommandProcessor textCommandProcessor = new(new() - { - // The default behavior is that the bot reacts to direct - // mentions and to the "!" prefix. If you want to change - // it, you first set if the bot should react to mentions - // and then you can provide as many prefixes as you want. - PrefixResolver = new DefaultPrefixResolver(true, "?", "&").ResolvePrefixAsync, - }); - - // Add text commands with a custom prefix (?ping) - extension.AddProcessor(textCommandProcessor); -}, new CommandsConfiguration() -{ - // The default value is true, however it's shown here for clarity - RegisterDefaultCommandProcessors = true, - DebugGuildId = Environment.GetEnvironmentVariable("DEBUG_GUILD_ID") ?? 0, -}); -``` - -And then, once you start your client, everything will work fine: - -```cs -DiscordClient discordClient = serviceProvider.GetRequiredService(); - -// We can specify a status for our bot. Let's set it to "playing" and set the activity to "with fire". -DiscordActivity status = new("with fire", DiscordActivityType.Playing); - -// Now we connect and log in. -await discordClient.ConnectAsync(status, DiscordUserStatus.Online); - -// And now we wait infinitely so that our bot actually stays connected. -await Task.Delay(-1); -``` - ---- - -Let's break this down a bit: -- We use each processor's required intents to ensure that the extension receives the necessary gateway events and data to function properly. -- We register commands explicitly through the collection-based overload. Note that this should be the preferred overload at this time, due to issues in other overloads. -- We register the `TextCommandProcessor` processor with a custom prefix resolver. - -What in the world is a command processor? In order to execute a command, we need to be able to parse the input. Text commands are regular old Discord messages, while slash commands are a special type of event that Discord sends to your bot. The `TextCommandProcessor` and `SlashCommandProcessor` are responsible for parsing these inputs and determining which command to execute. By default, all processors are registered with the command framework. You can add/create your own processors if you need to. - -## Creating A Command - -Now that we have the command framework registered and configured, we can create our first command: - -```cs -public class PingCommand -{ - [Command("ping")] - public static async ValueTask ExecuteAsync(CommandContext context) => - await context.RespondAsync($"Pong! Latency is {context.Client.Ping}ms."); -} -``` - -There's multiple things to note here: -- There is no longer a `BaseCommandModule` class to inherit from. This is because the command framework is now attribute-based. -- Your commands may now be `static`. -- Your command return type may now be `ValueTask` or `Task`, instead of only limiting to `Task`. -- By default, any response made via `CommandContext` will not mention any user or role - those mentions must be specified manually by using a `DiscordMessageBuilder`. - -Now start your Discord client, and type `!ping` in a text channel. Your bot should respond with "Pong! Latency is {Number}ms." - -![Ping command demonstration via text commands and slash commands.](../../images/commands_ping_command_demonstration.png) - -## Creating a Group Command -Creating a group command isn't much different: - -```cs -[Command("math")] -public class MathCommands -{ - [Command("add")] - public static async ValueTask AddAsync(CommandContext context, int a, int b) => - await context.RespondAsync($"{a} + {b} = {a + b}"); - - [Command("subtract")] - public static async ValueTask SubtractAsync(CommandContext context, int a, int b) => - await context.RespondAsync($"{a} - {b} = {a - b}"); -} -``` - -You can invoke these commands by typing `!math add 5 3` or `!math subtract 5 3` in a text channel. diff --git a/docs/articles/commands/processors/introduction.md b/docs/articles/commands/processors/introduction.md deleted file mode 100644 index fb68a657cd..0000000000 --- a/docs/articles/commands/processors/introduction.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -uid: articles.commands.command_processors.introduction -title: Command Processors Introduction ---- - -# Command Processors -Each processor has features specific to it. For example, the `SlashCommand` processor has support for choice providers -and auto-complete, while the `TextCommand` processor has support for command aliases. Each section will be named after -their own processor, explaining which features are available and how to use them. - -## Filter allowed processors -The extension allows you to configure what processors can execute a specific command. This is useful if you want to have commands that are only available for text or slash commands. - -There are two ways to accomplish this filtering: - -### Filter with the `AllowedProcessors` attribute - -Apply the `AllowedProcessors` attribute to your command, specifying the allowed processors: - -```csharp -[Command("debug")] -public class InfoCommand -{ - [Command("textCommand"), AllowedProcessors()] - public static async ValueTask TextOnlyAsync(CommandContext context) => - await context.RespondAsync("This is a text command"); - - [Command("slashCommand"), AllowedProcessors()] - public static async ValueTask SlashOnlyAsync(CommandContext context) => - await context.RespondAsync("This is a slash command"); -} -``` - -The attribute can only be applied to the top-level command, and will be inherited by all subcommands. - -### Filter with concrete `CommandContext` types - -If you use a specific command context instead of the default `CommandContext` the command is only registered -to processors which context is assignable to that specific type - -```csharp -[Command("debug")] -public class InfoCommand -{ - [Command("textCommand")] - public static async ValueTask TextOnlyAsync(TextCommandContext context) => - await context.RespondAsync("This is a text command"); - - [Command("slashCommand")] - public static async ValueTask SlashOnlyAsync(SlashCommandContext context) => - await context.RespondAsync("This is a slash command"); -} -``` \ 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 deleted file mode 100644 index afc933629d..0000000000 --- a/docs/articles/commands/processors/slash_commands/choice_provider_vs_autocomplete.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -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 -{ - // Note: This is just en example data source. It does not exist in DSharpPlus. - private readonly ITagExampleService exampleTagService; - - public TagNameAutoCompleteProvider(ITagExampleService tagService) => exampleTagService = tagService; - - public ValueTask> AutoCompleteAsync(AutoCompleteContext context) - { - var tags = exampleTagService - .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. diff --git a/docs/articles/commands/processors/slash_commands/localizing_interactions.md b/docs/articles/commands/processors/slash_commands/localizing_interactions.md deleted file mode 100644 index 9eb0634ad7..0000000000 --- a/docs/articles/commands/processors/slash_commands/localizing_interactions.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -uid: articles.commands.command_processors.slash_commands.localizing_interactions -title: Localizing Interactions ---- - -# Localizing Interactions - -In the event that you would like to provide translations for your commands, you can use the `IInteractionLocalizer` interface. This interface allows you to provide translations for your commands and parameters. Here is the interface: - -```cs -public interface IInteractionLocalizer -{ - public ValueTask> TranslateAsync(string fullSymbolName); -} -``` - -`fullSymbolName` is a special id you can use to index your translations. Below is the formatting used: - -- `command.name`: The full name of the command (`group.subgroup.command.name`). -- `command.description`: The description of the command. -- `command.parameter.name`: The name of the parameter. -- `command.parameter.description`: The description of the parameter. - -> [!NOTE] -> Localization relies on ICU libraries being installed on the system - if you are using a Docker container, you may need to install the `libicu` package. -> Command names that possess non-ascii characters may fail to execute if invariant globalization is used. -> To avoid this, ensure is set to false in your project file. - -Here is an example of a simple translator provider: - -```cs -public class PingTranslator : IInteractionLocalizer -{ - public ValueTask> TranslateAsync(string fullSymbolName) => fullSymbolName switch - { - "ping.name" => ValueTask.FromResult>(new Dictionary - { - { DiscordLocale.en_US, "ping" }, - { DiscordLocale.ja, "ピン" }, - { DiscordLocale.tr, "ping" } - }), - "ping.description" => ValueTask.FromResult>(new Dictionary - { - { DiscordLocale.en_US, "Pings the bot to check its latency." }, - { DiscordLocale.ja, "ボットにピンを送信して、その遅延を確認します。" }, - { DiscordLocale.tr, "Botun gecikmesini kontrol eder." } - }), - _ => throw new KeyNotFoundException() - }; -} -``` - -You can then use this translator provider in your command registration: - -```cs -public static class PingCommand -{ - [Command("ping"), InteractionLocalizer] - public static async ValueTask ExecuteAsync(CommandContext context) => await context.RespondAsync("Pong!"); -} diff --git a/docs/articles/commands/processors/slash_commands/missing_commands.md b/docs/articles/commands/processors/slash_commands/missing_commands.md deleted file mode 100644 index cec1c915bf..0000000000 --- a/docs/articles/commands/processors/slash_commands/missing_commands.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -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/commands/processors/slash_commands/naming_policies.md b/docs/articles/commands/processors/slash_commands/naming_policies.md deleted file mode 100644 index 8c3afc970d..0000000000 --- a/docs/articles/commands/processors/slash_commands/naming_policies.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -uid: articles.commands.command_processors.slash_commands.naming_policies -title: Interaction Naming Policies ---- - -# Naming Policies - -By default when registering your slash command and parameter names, they will be changed to `snake_case`. This is due to Discord's naming restrictions for interaction-based data. While snake_casing the names is one of the few ways to consistently follow Discord's naming policies, it is not the only way. Some other options include kebab-casing and lowercasing. Maybe there's a third option that you'd like to use, but isn't implemented by default! Let's go over the interface that controls all of this: `IInteractionNamingPolicy`: - -```cs -public interface IInteractionNamingPolicy -{ - string GetCommandName(Command command); - string GetParameterName(CommandParameter parameter, int arrayIndex); - StringBuilder TransformText(ReadOnlySpan text); -} -``` - -There are currently three default implementations of this interface: -- `SnakeCaseInteractionNamingPolicy` -- `KebabCaseInteractionNamingPolicy` -- `LowerCaseInteractionNamingPolicy` - -You can also create your own implementation of this interface if you want to use a different naming policy. Here's an example of a naming policy that converts any multi-argument parameter names to their ordinal numeric form (1 -> first, 2 -> second, etc.): - -```cs -using Humanizer; - -public class OrdinalSnakeCaseInteractionNamingPolicy : IInteractionNamingPolicy -{ - private static readonly SnakeCaseInteractionNamingPolicy _snakeCasePolicy = new SnakeCaseInteractionNamingPolicy(); - - public string GetCommandName(Command command) => _snakeCasePolicy.GetCommandName(command); - - public string GetParameterName(CommandParameter parameter, int arrayIndex) - { - if (string.IsNullOrWhiteSpace(parameter.Name)) - { - throw new InvalidOperationException("Parameter name cannot be null or empty."); - } - - StringBuilder stringBuilder = TransformText(parameter.Name); - if (arrayIndex > -1) - { - // Prepend the ordinal number to the parameter name - // first_parameter_name, second_parameter_name, etc. - stringBuilder.Insert(0, (arrayIndex + 1).ToOrdinalWords() + "_"); - } - - return stringBuilder.ToString(); - } - - public StringBuilder TransformText(ReadOnlySpan text) => _snakeCasePolicy.TransformText(text); -} -``` - -> [!NOTE] -> Humanizer is not a dependency of DSharpPlus and is not affiliated with DSharpPlus. You can find more information about Humanizer [here](https://github.com/Humanizr/Humanizer). - -Now that you have your custom naming policy, you can use it when setting up the Commands extension: - -```cs -serviceCollection.UseCommands((IServiceProvider serviceProvider, CommandsExtension extension) => { - SlashCommandProcessor slashCommandProcessor = new(new SlashCommandConfiguration() - { - NamingPolicy = new OrdinalSnakeCaseInteractionNamingPolicy(), - }); - - extension.AddProcessor(slashCommandProcessor); -}); -``` - -And now your commands should be registered with the naming format you've chosen: - -[!A screenshot of the `/quote` command, listing several messages with the following parameter names: `first_message_link`, `second_message_link`, `third_message_link`, `fourth_message_link` and `fifth_message_link`.](../../../../images/ordinal_snake_case.png) \ No newline at end of file diff --git a/docs/articles/commands/processors/text_commands/command_aliases.md b/docs/articles/commands/processors/text_commands/command_aliases.md deleted file mode 100644 index 99042bf458..0000000000 --- a/docs/articles/commands/processors/text_commands/command_aliases.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -uid: articles.commands.command_processors.text_commands.command_aliases -title: Command Aliases ---- - -# Command Aliases -To add an alias to a command, simply add the `TextAlias` attribute to the method that defines the command. It should be noted that the aliases are applied *only* to text commands. - -```cs -public static class PingCommand -{ - [Command("ping")] - [TextAlias("pong")] - public static async ValueTask ExecuteAsync(CommandContext context) => await context .RespondAsync("Pong!"); -} -``` - -In this example, the `PingCommand` command can be invoked by either `!ping` or `!pong`. \ No newline at end of file diff --git a/docs/articles/commands/processors/text_commands/custom_prefix_handler.md b/docs/articles/commands/processors/text_commands/custom_prefix_handler.md deleted file mode 100644 index 89d8e24360..0000000000 --- a/docs/articles/commands/processors/text_commands/custom_prefix_handler.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -uid: articles.commands.command_processors.text_commands.custom_prefix_handler -title: Custom Prefix Handler ---- - -# Adding dynamic prefixes - -Prefixes are commonplace among Discord bots, and are used to determine if a message is a command or not. By default, DSharpPlus uses the `!` prefix, but you can change this to whatever you want. However, what if you want to have different prefixes for different servers? This is where custom prefix handlers come in. - -There are two manners of going about this; you can either use a prefix resolver delegate, or implement the `IPrefixResolver` interface. The former is simpler, but the latter is more powerful. - -## Prefix resolver delegate -The prefix resolver delegate is very simple to use. Any method that can be converted into a `Func>` (a method that takes a `CommandsExtension` and a `DiscordMessage`, and returns a `ValueTask`) can be used as a prefix resolver delegate. This method will be called for every message, and the return value will be used as the prefix length to slice off the prefix from the message content. If the return value is `-1`, the message will be ignored. - -> [!IMPORTANT] -> Lambdas generated via reflection or compiled expressions should be compiled to `ResolvePrefixDelegateAsync`. Compiling to `Func>` will result in a runtime exception. - -The default prefix resolver uses the `!` prefix. To change this, you can use the following code: - -```cs -DefaultPrefixResolver prefixResolver = new DefaultPrefixResolver(true, "!", "?"); -TextCommandProcessor textCommandProcessor = new(new() -{ - // The default behavior is that the bot reacts to direct - // mentions and to the "!" prefix. If you want to change - // it, you first set if the bot should react to mentions - // and then you can provide as many prefixes as you want. - PrefixResolver = prefixResolver.ResolvePrefixAsync -}); -``` - -In this example, the bot will react to bot mentions, to the `!` prefix, and the `?` prefix. The `true` argument in the `DefaultPrefixResolver` constructor specifies that the bot should treat it's own mention as a prefix: @BotName ping. This should usually be left to `true` as Discord will always pass the message content when the bot is mentioned, preventing the need to request for the message content privileged intent. - -## IPrefixResolver - -The `IPrefixResolver` interface has a bit more setup, but can make dynamic prefixes easier overall. The interface is as follows: - -```cs -public interface IPrefixResolver -{ - ValueTask ResolvePrefixAsync(CommandsExtension extension, DiscordMessage message); -} -``` - -On the surface, this is very similar to setting the delegate property, however implementing the interface allows you to participate in dependency injection. - -A common scenario is using a database to retrieve a per-server prefix. -Here's an example of how you can implement the `IPrefixResolver` interface: - -```cs -public class CustomPrefixResolver(IDatabaseService database) : IPrefixResolver -{ - public async ValueTask ResolvePrefixAsync(CommandsExtension extension, DiscordMessage message) - { - if (string.IsNullOrWhiteSpace(message.Content)) - { - return -1; - } - // Mention check - else if (this.AllowMention && message.Content.StartsWith(extension.Client.CurrentUser.Mention, StringComparison.OrdinalIgnoreCase)) - { - return extension.Client.CurrentUser.Mention.Length; - } - - // Database check - string prefix = await database.GetPrefixAsync(message.Channel.GuildId); - if (message.Content.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return prefix.Length; - } - - return -1; - } -} -``` - -Now, unlike the normal prefix resolver delegate, this isn't set on the TextCommandConfiguration. Instead, you'll register this class with your service provider and the text command processor will use it by default, even if the prefix resolver delegate is already set somewhere else. - -> [!IMPORTANT] -> The prefix resolver is resolved from a scoped service provider. For most scenarios, the only stipulation is that state should be held in an external, more persistent (e.g. singleton) service. Users of Entity Framework Core (EFCore) should ensure that the DbContext is scoped correctly. - -```cs -DiscordClientBuilder builder = DiscordClientBuilder - .CreateDefault(discordToken, TextCommandProcessor.RequiredIntents | SlashCommandProcessor.RequiredIntents) - .ConfigureServices(services => services.AddScoped()); -``` - -And just like that, you're off to the races. \ No newline at end of file diff --git a/docs/articles/commands/processors/text_commands/remaining_text.md b/docs/articles/commands/processors/text_commands/remaining_text.md deleted file mode 100644 index 110792b52c..0000000000 --- a/docs/articles/commands/processors/text_commands/remaining_text.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -uid: articles.commands.command_processors.text_commands.remaining_text -title: Remaining Text ---- - -# Remaining Text -The default behavior for the `string` argument is to take the text between spaces, or the text that's quoted. If you want to take all the text after the previously parsed arguments, you should use the `RemainingText` attribute. - -```cs -public static class PingCommand -{ - [Command("ban"), RequirePermissions(DiscordPermissions.BanMembers)] - public static async ValueTask ExecuteAsync(CommandContext context, DiscordUser user, [RemainingText] string reason = "No reason provided.") - { - await context.Guild.BanMemberAsync(user, 0, reason); - await context.RespondAsync($"Banned {user.Username} for {reason}"); - } -} -``` - -You can use the `RemainingText` attribute on the last argument of a command. This will take all the text after the previously parsed arguments. In the example above, the `reason` argument will take all the text after the `user` argument. If no text is left, then the default value will be used. You can use the command as such: `!ban @user Reason for ban`. \ No newline at end of file diff --git a/docs/articles/commands/var_args_parameters.md b/docs/articles/commands/var_args_parameters.md deleted file mode 100644 index 7fdbbee7b7..0000000000 --- a/docs/articles/commands/var_args_parameters.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -uid: articles.commands.variadic_parameters -title: Variadic Parameters ---- - -# Variadic Parameters - -When creating a command, you may want to have a parameter that can accept multiple arguments. This is useful for commands that require a list of items, such as a list of users or a list of numbers. This was previously supported in the `CommandsNext` extension through the `params` keyword. - -```csharp -[Command("echo")] -[Description("Repeats a message.")] -public async ValueTask ExecuteAsync(CommandContext context, params string[] args) => - await context.RespondAsync(string.Join(' ', args)); -``` - -Which could be used like this: - -``` -!echo hello world 1 2 3 -``` - -This behavior is still supported, but what if you wanted to have multiple lists? For example, a command that takes in a list of users and a list of numbers. This is where variadic parameters come in. - -```csharp -[Command("assign")] -[Description("Assigns multiple roles to multiple users.")] -public async ValueTask ExecuteAsync( - CommandContext context, - [VariadicArgument(1, 5)] IReadOnlyList roles, - [VariadicArgument(1)] IReadOnlyList members -) -{ - // We're making a lot of API calls here, so let the - // user know that we've received the command and we - // are doing work in the background. - await context.DeferResponseAsync(); - foreach (DiscordMember member in members) - { - List memberRoles = new(member.Roles); - memberRoles.AddRange(roles); - memberRoles = memberRoles.Distinct().ToList(); - await member.ModifyAsync(member => member.Roles = memberRoles); - } - - await context.RespondAsync($"Assigned {roles.Count} roles to {members.Count} members."); -} -``` - -In this example, the `roles` parameter will accept between 1 and 5 roles, and the `members` parameter will accept at least 1 member. This allows you to have multiple lists of arguments in a single command. \ No newline at end of file diff --git a/docs/articles/commands_next/argument_converters.md b/docs/articles/commands_next/argument_converters.md deleted file mode 100644 index 43bbd668b4..0000000000 --- a/docs/articles/commands_next/argument_converters.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -uid: articles.commands_next.argument_converters -title: Argument Converter ---- - ->[!WARNING] -> CommandsNext has been replaced by [Commands](xref:articles.commands.introduction). Both this article and CommandsNext itself is no longer maintained and may contain outdated information. CommandsNext will be deprecated in version 5.1.0 of DSharpPlus. - -## Custom Argument Converter - -Writing your own argument converter will enable you to convert custom types and replace the functionality of existing -converters. Like many things in DSharpPlus, doing this is straightforward and simple. - -First, create a new class which implements @DSharpPlus.CommandsNext.Converters.IArgumentConverter`1 and its method -@DSharpPlus.CommandsNext.Converters.IArgumentConverter`1.ConvertAsync(System.String,DSharpPlus.CommandsNext.CommandContext). -Our example will be a boolean converter, so we'll also pass `bool` as the type parameter for -@DSharpPlus.CommandsNext.Converters.IArgumentConverter`1. - -```cs -public class CustomArgumentConverter : IArgumentConverter -{ - public Task> ConvertAsync(string value, CommandContext ctx) - { - if (bool.TryParse(value, out var boolean)) - { - return Task.FromResult(Optional.FromValue(boolean)); - } - - switch (value.ToLower()) - { - case "yes": - case "y": - case "t": - return Task.FromResult(Optional.FromValue(true)); - - case "no": - case "n": - case "f": - return Task.FromResult(Optional.FromValue(false)); - - default: - return Task.FromResult(Optional.FromNoValue()); - } - } -} -``` - -Then register the argument converter with CommandContext. - -```cs -var discord = new DiscordClient(); -var commands = discord.UseCommandsNext(); - -commands.RegisterConverter(new CustomArgumentConverter()); -``` - -Once the argument converter is written and registered, we'll be able to use it: - -```cs -[Command("boolean")] -public async Task BooleanCommand(CommandContext ctx, bool boolean) -{ - await ctx.RespondAsync($"Converted to {boolean}"); -} -``` - -![true][0] - - -[0]: ../../images/commands_next_argument_converters_01.png diff --git a/docs/articles/commands_next/command_attributes.md b/docs/articles/commands_next/command_attributes.md deleted file mode 100644 index 91569064c2..0000000000 --- a/docs/articles/commands_next/command_attributes.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -uid: articles.commands_next.command_attributes -title: Command Attributes ---- - ->[!WARNING] -> CommandsNext has been replaced by [Commands](xref:articles.commands.introduction). Both this article and CommandsNext itself is no longer maintained and may contain outdated information. CommandsNext will be deprecated in version 5.1.0 of DSharpPlus. - -## Built-In Attributes - -CommandsNext has a variety of built-in attributes to enhance your commands and provide some access control. -The majority of these attributes can be applied to your command methods and command groups. - -- @DSharpPlus.CommandsNext.Attributes.AliasesAttribute -- @DSharpPlus.CommandsNext.Attributes.CooldownAttribute -- @DSharpPlus.CommandsNext.Attributes.DescriptionAttribute -- @DSharpPlus.CommandsNext.Attributes.CategoryAttribute -- @DSharpPlus.CommandsNext.Attributes.DontInjectAttribute -- @DSharpPlus.CommandsNext.Attributes.HiddenAttribute -- @DSharpPlus.CommandsNext.Attributes.ModuleLifespanAttribute -- @DSharpPlus.CommandsNext.Attributes.PriorityAttribute -- @DSharpPlus.CommandsNext.Attributes.RemainingTextAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireBotPermissionsAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireDirectMessageAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireGuildAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireNsfwAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireOwnerAttribute -- @DSharpPlus.CommandsNext.Attributes.RequirePermissionsAttribute -- @DSharpPlus.CommandsNext.Attributes.RequirePrefixesAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireRolesAttribute -- @DSharpPlus.CommandsNext.Attributes.RequireUserPermissionsAttribute - -## Custom Attributes - -If the above attributes don't meet your needs, CommandsNext also gives you the option of writing your own! -Simply create a new class which inherits from @DSharpPlus.CommandsNext.Attributes.CheckBaseAttribute and implement the -required method. - -Our example below will only allow a command to be ran during a specified year. - -```cs -public class RequireYearAttribute : CheckBaseAttribute -{ - public int AllowedYear { get; private set; } - - public RequireYearAttribute(int year) - { - AllowedYear = year; - } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - return Task.FromResult(AllowedYear == DateTime.Now.Year); - } -} -``` - -You'll also need to apply the `AttributeUsage` attribute to your attribute. For our example attribute, we'll set it to -only be usable once on methods. - -```cs -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public class RequireYearAttribute : CheckBaseAttribute -{ - // ... -} -``` - -You can provide feedback to the user using the @DSharpPlus.CommandsNext.CommandsNextExtension.CommandErrored event. - -```cs -private async Task Main(string[] args) -{ - var discord = new DiscordClient(); - var commands = discord.UseCommandsNext(); - - commands.CommandErrored += CmdErroredHandler; -} - -private async Task CmdErroredHandler(CommandsNextExtension _, CommandErrorEventArgs e) -{ - var failedChecks = ((ChecksFailedException)e.Exception).FailedChecks; - foreach (var failedCheck in failedChecks) - { - if (failedCheck is RequireYearAttribute) - { - var yearAttribute = (RequireYearAttribute)failedCheck; - await e.Context.RespondAsync($"Only usable during year {yearAttribute.AllowedYear}."); - } - } -} -``` - -Once you've got all of that completed, you'll be able to use it on a command! - -```cs -[Command("generic"), RequireYear(2030)] -public async Task GenericCommand(CommandContext ctx, string generic) -{ - await ctx.RespondAsync("Generic response."); -} -``` - -![Generic Image][0] - - -[0]: ../../images/commands_next_command_attributes_01.png diff --git a/docs/articles/commands_next/command_handler.md b/docs/articles/commands_next/command_handler.md deleted file mode 100644 index d3c73a03cc..0000000000 --- a/docs/articles/commands_next/command_handler.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -uid: articles.commands_next.command_handler -title: Custom Command Handler ---- - ->[!WARNING] -> CommandsNext has been replaced by [Commands](xref:articles.commands.introduction). Both this article and CommandsNext itself is no longer maintained and may contain outdated information. CommandsNext will be deprecated in version 5.1.0 of DSharpPlus. - -## Custom Command Handler -> -> [!IMPORTANT] -> Writing your own handler logic should only be done if *you know what you're doing*. You will be responsible for -> command execution and preventing deadlocks. - -### Disable Default Handler - -To begin, we'll need to disable the default command handler provided by CommandsNext. This is done by setting the -@DSharpPlus.CommandsNext.CommandsNextConfiguration.UseDefaultCommandHandler configuration property to `false`. - -```cs -var discord = new DiscordClient(); -var commands = discord.UseCommandsNext(new CommandsNextConfiguration() -{ - UseDefaultCommandHandler = false -}); -``` - -### Create Event Handler - -We'll then write a new handler for the @DSharpPlus.DiscordClient.MessageCreated event fired from -@DSharpPlus.DiscordClient. - -```cs -discord.MessageCreated += CommandHandler; - -// ... - -private Task CommandHandler(DiscordClient client, MessageCreateEventArgs e) -{ - // See below... -} -``` - -This event handler will be our command handler, and you'll need to write the logic for it. - -### Handle Commands - -Start by parsing the message content for a prefix and command string - -```cs -var cnext = client.GetCommandsNext(); -var msg = e.Message; - -// Check if message has valid prefix. -var cmdStart = msg.GetStringPrefixLength("!"); -if (cmdStart == -1) return; - -// Retrieve prefix. -var prefix = msg.Content.Substring(0, cmdStart); - -// Retrieve full command string. -var cmdString = msg.Content.Substring(cmdStart); -``` - -Then provide the command string to @DSharpPlus.CommandsNext.CommandsNextExtension.FindCommand*. - -```cs -var command = cnext.FindCommand(cmdString, out var args); -``` - -Create a command context using our message and prefix, along with the command and its arguments - -```cs -var ctx = cnext.CreateContext(msg, prefix, command, args); -``` - -And pass the context to @DSharpPlus.CommandsNext.CommandsNextExtension.ExecuteCommandAsync* to execute the command. - -```cs -_ = Task.Run(async () => await cnext.ExecuteCommandAsync(ctx)); -// Wrapped in Task.Run() to prevent deadlocks. -``` - -### Finished Product - -Altogether, your implementation should function similarly to the following: - -```cs -private Task CommandHandler(DiscordClient client, MessageCreateEventArgs e) -{ - var cnext = client.GetCommandsNext(); - var msg = e.Message; - - var cmdStart = msg.GetStringPrefixLength("!"); - if (cmdStart == -1) return Task.CompletedTask; - - var prefix = msg.Content.Substring(0, cmdStart); - var cmdString = msg.Content.Substring(cmdStart); - - var command = cnext.FindCommand(cmdString, out var args); - if (command == null) return Task.CompletedTask; - - var ctx = cnext.CreateContext(msg, prefix, command, args); - Task.Run(async () => await cnext.ExecuteCommandAsync(ctx)); - - return Task.CompletedTask; -} -``` diff --git a/docs/articles/commands_next/dependency_injection.md b/docs/articles/commands_next/dependency_injection.md deleted file mode 100644 index eea4f329ca..0000000000 --- a/docs/articles/commands_next/dependency_injection.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -uid: articles.commands_next.dependency_injection -title: Dependency Injection ---- - ->[!WARNING] -> CommandsNext has been replaced by [Commands](xref:articles.commands.introduction). Both this article and CommandsNext itself is no longer maintained and may contain outdated information. CommandsNext will be deprecated in version 5.1.0 of DSharpPlus. - -## Dependency Injection - -As you begin to write more complex commands, you'll find that you need a way to get data in and out of them. Although -you *could* use `static` fields to accomplish this, the preferred solution would be *dependency injection*. - -This would involve placing all required object instances and types (referred to as *services*) in a container, then -providing that container to CommandsNext. Each time a command module is instantiated, CommandsNext will then attempt to -populate constructor parameters, `public` properties, and `public` fields exposed by the module with instances of -objects from the service container - it is recommended you use constructor parameters for dependency injection. - -We'll go through a simple example of this process to help you understand better. - -### Create a Service Provider - -To begin, we'll need to create a service provider; this will act as the container for the services you need for your -commands. Create a new variable just before you register CommandsNext with your @DSharpPlus.DiscordClient and assign it -a new instance of `ServiceCollection`. - -```cs -var discord = new DiscordClient(); -var services = new ServiceCollection(); // Right here! -var commands = discord.UseCommandsNext(); -``` - -We'll use `.AddSingleton` to add type `Random` to the collection, then chain that call with the -`.BuildServiceProvider()` extension method. The resulting type will be `ServiceProvider`. - -```cs -var services = new ServiceCollection() - .AddSingleton() - .BuildServiceProvider(); -``` - -Then we'll need to provide CommandsNext with our services. - -```cs -var commands = discord.UseCommandsNext(new CommandsNextConfiguration() -{ - Services = services -}); -``` - -### Using Your Services - -Now that we have our services set up, we're able to use them in commands. We'll be tweaking our -[random number command][0] to demonstrate. - -Add a new property to the command module named *Rng*. Make sure it has a `public` setter. - -```cs -public class MyFirstModule : BaseCommandModule -{ - public Random Rng { private get; set; } // Implied public setter. - - // ... -} -``` - -Modify the *random* command to use our property. - -```cs -[Command("random")] -public async Task RandomCommand(CommandContext ctx, int min, int max) -{ - await ctx.RespondAsync($"Your number is: {Rng.Next(min, max)}"); -} -``` - -Then we can give it a try! - -![Command Execution][1] - -CommandsNext has automatically injected our singleton `Random` instance into the `Rng` property when our command module -was instantiated. Now, for any command that needs `Random`, we can simply declare one as a property, field, or in the -module constructor and CommandsNext will take care of the rest. Ain't that neat? - -## Lifespans - -### Modules - -By default, all command modules have a singleton lifespan; this means each command module is instantiated once for the -lifetime of the CommandsNext instance. However, if the reuse of a module instance is undesired, you also have the option -to change the lifespan of a module to *transient* using the @DSharpPlus.CommandsNext.Attributes.ModuleLifespanAttribute. - -```cs -[ModuleLifespan(ModuleLifespan.Transient)] -public class MyFirstModule : BaseCommandModule -{ - // ... -} -``` - -Transient command modules are instantiated each time one of its containing commands is executed. - -### Services - -In addition to the `.AddSingleton()` extension method, you're also able to use the `.AddScoped()` and `.AddTransient()` -extension methods to add services to the collection. The extension method chosen will affect when and how often the -service is instantiated. Scoped and transient services should only be used in transient command modules, as singleton -modules will always have their services injected once. - -Lifespan | Instantiated -:--------:|:------------- -Singleton | One time when added to the collection. -Scoped | Once for each command module. -Transient | Each time its requested. - - -[0]: xref:articles.commands_next.intro#argument-converters -[1]: ../../images/commands_next_dependency_injection_01.png diff --git a/docs/articles/commands_next/help_formatter.md b/docs/articles/commands_next/help_formatter.md deleted file mode 100644 index b72ba334c5..0000000000 --- a/docs/articles/commands_next/help_formatter.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -uid: articles.commands_next.help_formatter -title: Help Formatter ---- - ->[!WARNING] -> CommandsNext has been replaced by [Commands](xref:articles.commands.introduction). Both this article and CommandsNext itself is no longer maintained and may contain outdated information. CommandsNext will be deprecated in version 5.1.0 of DSharpPlus. - -## Custom Help Formatter - -The built-in help command provided by CommandsNext is generated with a *help formatter*. This simple mechanism is given -a command and its subcommands then returns a formatted help message. If you're not happy with the default help -formatter, you're able to write your own and customize the output to your liking. - -Simply inherit from @DSharpPlus.CommandsNext.Converters.BaseHelpFormatter and provide an implementation for each of the -required methods. - -```cs -public class CustomHelpFormatter : BaseHelpFormatter -{ - // protected DiscordEmbedBuilder embed; - // protected StringBuilder strBuilder; - - public CustomHelpFormatter(CommandContext ctx) : base(ctx) - { - // embed = new DiscordEmbedBuilder(); - // strBuilder = new StringBuilder(); - - // Help formatters do support dependency injection. - // Any required services can be specified by declaring constructor parameters. - - // Other required initialization here... - } - - public override BaseHelpFormatter WithCommand(Command command) - { - // embed.AddField(command.Name, command.Description); - // strBuilder.AppendLine($"{command.Name} - {command.Description}"); - - return this; - } - - public override BaseHelpFormatter WithSubcommands(IEnumerable cmds) - { - foreach (var cmd in cmds) - { - // embed.AddField(cmd.Name, cmd.Description); - // strBuilder.AppendLine($"{cmd.Name} - {cmd.Description}"); - } - - return this; - } - - public override CommandHelpMessage Build() - { - // return new CommandHelpMessage(embed: embed); - // return new CommandHelpMessage(content: strBuilder.ToString()); - } -} -``` - -Alternatively, if you're only wanting to make a few small tweaks to the default help, you can write a simple help -formatter which inherits from @DSharpPlus.CommandsNext.Converters.DefaultHelpFormatter and modify the inherited -@DSharpPlus.CommandsNext.Converters.DefaultHelpFormatter.EmbedBuilder property. - -```cs -public class CustomHelpFormatter : DefaultHelpFormatter -{ - public CustomHelpFormatter(CommandContext ctx) : base(ctx) { } - - public override CommandHelpMessage Build() - { - EmbedBuilder.Color = DiscordColor.SpringGreen; - return base.Build(); - } -} -``` - -Your final step is to register your help formatter with CommandsNext. - -```cs -var discord = new DiscordClient(); -var commands = discord.UseCommandsNext(); - -commands.SetHelpFormatter(); -``` - -That's all there is to it. - -![Fresh New Look][0] - - -[0]: ../../images/commands_next_help_formatter_01.png diff --git a/docs/articles/commands_next/intro.md b/docs/articles/commands_next/intro.md deleted file mode 100644 index 0d9cfc06b4..0000000000 --- a/docs/articles/commands_next/intro.md +++ /dev/null @@ -1,376 +0,0 @@ ---- -uid: articles.commands_next.intro -title: CommandsNext Introduction ---- - ->[!WARNING] -> CommandsNext has been replaced by [Commands](xref:articles.commands.introduction). Both this article and CommandsNext itself is no longer maintained and may contain outdated information. CommandsNext will be deprecated in version 5.1.0 of DSharpPlus. - ->[!NOTE] -> This article assumes you've recently read the article on *[writing your first bot][0]*. - -# Introduction to CommandsNext - -This article will introduce you to some basic concepts of our native command framework: *CommandsNext*. Be sure to -install the `DSharpPlus.CommandsNext` package from NuGet before continuing. - -![CommandsNext NuGet Package][1] - -## Writing a Basic Command - -### Create a Command Module - -A command module is simply a class which acts as a container for your command methods. Instead of registering individual -commands, you'd register a single command module which contains multiple commands. There's no limit to the amount of -modules you can have, and no limit to the amount of commands each module can contain. For example: you could have a -module for moderation commands and a separate module for image commands. This will help you keep your commands organized -and reduce the clutter in your project. - -Our first demonstration will be simple, consisting of one command module with a simple command. We'll start by creating -a new folder named `Commands` which contains a new class named `MyFirstModule`. - -![Solution Explorer][2] - -Give this new class `public` access and have it inherit from `BaseCommandModule`. - -```cs -public class MyFirstModule : BaseCommandModule -{ - -} -``` - -### Create a Command Method - -Within our new module, create a method named `GreetCommand` marked as `async` with a `Task` return type. The first -parameter of your method *must* be of type @DSharpPlus.CommandsNext.CommandContext, as required by CommandsNext. - -```cs -public async Task GreetCommand(CommandContext ctx) -{ - -} -``` - -In the body of our new method, we'll use @DSharpPlus.CommandsNext.CommandContext.RespondAsync* to send a simple message. - -```cs -await ctx.RespondAsync("Greetings! Thank you for executing me!"); -``` - -Finally, mark your command method with the @DSharpPlus.CommandsNext.Attributes.CommandAttribute so CommandsNext will -know to treat our method as a command method. This attribute takes a single parameter: the name of the command. - -We'll name our command *greet* to match the name of the method. - -```cs -[Command("greet")] -public async Task GreetCommand(CommandContext ctx) -{ - await ctx.RespondAsync("Greetings! Thank you for executing me!"); -} -``` - -Your command module should now resemble this: - -```cs -using System.Threading.Tasks; -using DSharpPlus.CommandsNext; -using DSharpPlus.CommandsNext.Attributes; - -public class MyFirstModule : BaseCommandModule -{ - [Command("greet")] - public async Task GreetCommand(CommandContext ctx) - { - await ctx.RespondAsync("Greetings! Thank you for executing me!"); - } -} -``` - -### Cleanup and Configuration - -Before we can run our new command, we'll need modify our main method. Start by removing the event handler we created -[previously][3]. - -```cs -var discord = new DiscordClient(); - -discord.MessageCreated += async (s, e) => // REMOVE -{ // ALL - if (e.Message.Content.ToLower().StartsWith("ping")) // OF - await e.Message.RespondAsync("pong!"); // THESE -}; // LINES - -await discord.ConnectAsync(); -``` - -Next, call the @DSharpPlus.CommandsNext.ExtensionMethods.UseCommandsNext* extension method on your -@DSharpPlus.DiscordClient instance and pass it a new @DSharpPlus.CommandsNext.CommandsNextConfiguration instance. Assign -the resulting @DSharpPlus.CommandsNext.CommandsNextExtension instance to a new variable named*commands*. This important -step will enable CommandsNext for your Discord client. - -```cs -var discord = new DiscordClient(); -var commands = discord.UseCommandsNext(new CommandsNextConfiguration()); -``` - -Create an object initializer for @DSharpPlus.CommandsNext.CommandsNextConfiguration and assign the -@DSharpPlus.CommandsNext.CommandsNextConfiguration.StringPrefixes property a new `string` array containing your desired -prefixes. Our example below will only define a single prefix: `!`. - -```cs -new CommandsNextConfiguration() -{ - StringPrefixes = new[] { "!" } -} -``` - -Now we'll register our command module. Call the @DSharpPlus.CommandsNext.CommandsNextExtension.RegisterCommands* method -on our @DSharpPlus.CommandsNext.CommandsNextExtension instance and provide it with your command module. - -```cs -var discord = new DiscordClient(); -var commands = discord.UseCommandsNext(); - -commands.RegisterCommands(); - -await discord.ConnectAsync(); -``` - -Alternatively, you can pass in your assembly to register commands from all modules in your program. - -```cs -commands.RegisterCommands(Assembly.GetExecutingAssembly()); -``` - -Your main method should look similar to the following: - -```cs -static async Task Main(string[] args) -{ - var discord = new DiscordClient(new DiscordConfiguration()); - var commands = discord.UseCommandsNext(new CommandsNextConfiguration() - { - StringPrefixes = new[] { "!" } - }); - - commands.RegisterCommands(); - - await discord.ConnectAsync(); - await Task.Delay(-1); -} -``` - -### Running Your Command - -It's now the moment of truth; all your blood, sweat, and tears have lead to this moment. Hit `F5` on your keyboard to -compile and run your bot, then execute your command in any channel that your bot account has access to. - -![Congratulations, You've Won!][4] - -[That was easy][5]. - -## Taking User Input - -### Command Arguments - -Now that we have a basic command down, let's spice it up a bit by defining *arguments* to accept user input. - -Defining an argument is simple; just add additional parameters to your signature of your command method. CommandsNext -will automatically parse user input and populate the parameters of your command method with those arguments. To -demonstrate, we'll modify our *greet* command to greet a user with a given name. - -Head back to `MyFirstModule` and add a parameter of type `string` to the `GreetCommand` method. - -```cs -[Command("greet")] -public async Task GreetCommand(CommandContext ctx, string name) -``` - -CommandsNext will now interpret this as a command named *greet* that takes one argument. - -Next, replace our original response message with an [interpolated string][6] which uses our new parameter. - -```cs -public async Task GreetCommand(CommandContext ctx, string name) -{ - await ctx.RespondAsync($"Greetings, {name}! You're pretty neat!"); -} -``` - -That's all there is to it. Smack `F5` and test it out in a channel your bot account has access to. - -![Greet Part 2: Electric Boogaloo][7] - -Now, you may have noticed that providing more than one word simply does not work. For example, `!greet Luke Smith` will -result in no response from your bot. - -This fails because a valid [overload][8] could not be found for your command. - -CommandsNext will split arguments by whitespace. This means `Luke Smith` is counted as two separate arguments; `Luke` -and `Smith`. In addition to this, CommandsNext will attempt to find and execute an overload of your command that has the -*same number* of provided arguments. Together, this means that any additional arguments will prevent CommandsNext from -finding a valid overload to execute. - -The simplest way to get around this would be to wrap your input with double quotes. CommandsNext will parse this as one -argument, allowing your command to be executed. - -``` -!greet "Luke Smith" -``` - -If you would prefer not to use quotes, you can use the @DSharpPlus.CommandsNext.Attributes.RemainingTextAttribute -attribute on your parameter. This attribute will instruct CommandsNext to parse all remaining arguments into that -parameter. - -```cs -public async Task GreetCommand(CommandContext ctx, [RemainingText] string name) -``` - -Alternatively, you can use the `params` keyword to have all remaining arguments parsed into an array. - -```cs -public async Task GreetCommand(CommandContext ctx, params string[] names) -``` - -A more obvious solution is to add additional parameters to the method signature of your command method. - -```cs -public async Task GreetCommand(CommandContext ctx, string firstName, string lastName) -``` - -Each of these has their own caveats; it'll be up to you to choose the best solution for your commands. - -### Argument Converters - -CommandsNext can convert arguments, which are natively `string`, to the type specified by a command method parameter. -This functionality is powered by *argument converters*, and it'll help to eliminate the boilerplate code needed to parse -and convert `string` arguments. - -CommandsNext has built-in argument converters for the following types: - -Category | Types -:-------------:|:------ -Discord | `DiscordGuild`, `DiscordChannel`, `DiscordMember`, `DiscordUser`,
`DiscordRole`, `DiscordMessage`, `DiscordEmoji`, `DiscordColor` -Integral | `byte`, `short`, `int`, `long`, `sbyte`, `ushort`, `uint`, `ulong` -Floating-Point | `float`, `double`, `decimal` -Date | `DateTime`, `DateTimeOffset`, `TimeSpan` -Character | `string`, `char` -Boolean | `bool` - -You're also able to create and provide your own [custom argument converters][9], if desired. - -Let's do a quick demonstration of the built-in converters. - -Create a new command method above our `GreetCommand` method named `RandomCommand` and have it take two integer -arguments. As the method name suggests, this command will be named *random*. - -```cs -[Command("random")] -public async Task RandomCommand(CommandContext ctx, int min, int max) -{ - -} -``` - -Make a variable with a new instance of `Random`. - -```cs -var random = new Random(); -``` - -Finally, we'll respond with a random number within the range provided by the user. - -```cs -await ctx.RespondAsync($"Your number is: {random.Next(min, max)}"); -``` - -Run your bot once more with `F5` and give this a try in a text channel. - -![Discord Channel][10] - -CommandsNext converted the two arguments from `string` into `int` and passed them to the parameters of our command, -removing the need to manually parse and convert the arguments yourself. - -We'll do one more to drive the point home. Head back to our old `GreetCommand` method, remove our `name` parameter, and -replace it with a new parameter of type @DSharpPlus.Entities.DiscordMember named `member`. - -```cs -public async Task GreetCommand(CommandContext ctx, DiscordMember member) -``` - -Then modify the response to mention the provided member with the @DSharpPlus.Entities.DiscordUser.Mention property on -@DSharpPlus.Entities.DiscordMember. - -```cs -public async Task GreetCommand(CommandContext ctx, DiscordMember member) -{ - await ctx.RespondAsync($"Greetings, {member.Mention}! Enjoy the mention!"); -} -``` - -Go ahead and give that a test run. - -![According to all known laws of aviation,][11] - -![there is no way a bee should be able to fly.][12] - -![Its wings are too small to get its fat little body off the ground.][13] - -The argument converter for @DSharpPlus.Entities.DiscordMember is able to parse mentions, usernames, nicknames, and user -IDs then look for a matching member within the guild the command was executed from. Ain't that neat? - -## Command Overloads - -Command method overloading allows you to create multiple argument configurations for a single command. - -```cs -[Command("foo")] -public Task FooCommand(CommandContext ctx, string bar, int baz) { } - -[Command("foo")] -public Task FooCommand(CommandContext ctx, DiscordUser bar) { } -``` - -Executing `!foo green 5` will run the first method, and `!foo @Emzi0767` will run the second method. - -Additionally, all check attributes are shared between overloads. - -```cs -[Command("foo"), Aliases("bar", "baz")] -[RequireGuild, RequireBotPermissions(Permissions.AttachFiles)] -public Task FooCommand(CommandContext ctx, int bar, int baz, string qux = "agony") { } - -[Command("foo")] -public Task FooCommand(CommandContext ctx, DiscordChannel bar, TimeSpan baz) { } -``` - -The additional attributes and checks applied to the first method will also be applied to the second method. - -## Further Reading - -Now that you've gotten an understanding of CommandsNext, it'd be a good idea check out the following: - -* [Command Attributes][14] -* [Help Formatter][15] -* [Dependency Injection][16] - - -[0]: xref:articles.basics.first_bot -[1]: ../../images/commands_next_intro_01.png -[2]: ../../images/commands_next_intro_02.png -[3]: xref:articles.basics.first_bot#spicing-up-your-bot -[4]: ../../images/commands_next_intro_03.png -[5]: https://www.youtube.com/watch?v=GsQXadrmhws -[6]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated -[7]: ../../images/commands_next_intro_04.png -[8]: #command-overloads -[9]: xref:articles.commands_next.argument_converters -[10]: ../../images/commands_next_intro_05.png -[11]: ../../images/commands_next_intro_06.png -[12]: ../../images/commands_next_intro_07.png -[13]: ../../images/commands_next_intro_08.png -[14]: xref:articles.commands_next.command_attributes -[15]: xref:articles.commands_next.help_formatter -[16]: xref:articles.commands_next.dependency_injection diff --git a/docs/articles/hosting.md b/docs/articles/hosting.md deleted file mode 100644 index 20be2ed8d2..0000000000 --- a/docs/articles/hosting.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -uid: articles.hosting -title: Hosting Solutions ---- - -## 24/7 Hosting Solutions - -### Free hosting - -If you're looking for free hosts, you've likely considered using [Heroku][0] or [Glitch][1]. We advise against using -these platforms as they are designed to host web services, not Discord bots, and instances from either of these -companies will shut down if there isn't enough internet traffic. Save yourself the headache and don't bother. - -Alternatively, some providers are offering Free Tiers which also allow application hosting. These services typically -have some sort of resource quota, and may charge money on exceeding these quotas. Make sure to carefully review the -fine-print, and understand that these services may come with strings attached. You can find examples below. - -### Self Hosting - -If you have access to an unused machine, have the technical know-how, and you also have a solid internet connection, you -might consider hosting your bot on your own. Even if you don't have a spare PC on hand, parts to build one are fairly -cheap in most regions. You could think of it as a one time investment with no monthly server fees. Any modern hardware -will work just fine, new or used. - -Depending on how complex your bot is, you may even consider purchasing a Raspberry Pi ($35). - -### Third-Party Hosting - -The simplest, and probably most hassle-free (and maybe cheapest in the long run for dedicated machines) option is to -find a provider that will lend you their machine or a virtual host so you can run your bot in there. - -Generally, cheapest hosting options are all GNU/Linux-based, so it's highly recommended you familiarize yourself with -the OS and its environment, particularly the shell (command line), and concepts such as SSH. - -There are several well-known, trusted, and cheap providers: - -* [Xenyth Cloud][2] - A hosting solution made by Discord bot developers. Based in Canada, starting from $2.49/mo. -* [Vultr][3] - Based in the US with datacenters in many regions, including APAC. Starting at $2.50/mo. -* [DigitalOcean][4] - The gold standard, US based. Locations available world wide. Starting from $5.00/mo. -* [Linode][5] - US based host with many datacenters around the world. Starting at $5.00/mo. -* [OVH][6] - Very popular VPS host. Worldwide locations available. Starting from $6.00/mo. -* [Contabo][7] - Based in Germany, US locations available; extremely good value for the price. Starting from 4.99€/mo. - -Things to keep in mind when looking for a hosting provider: - -* The majority of cheap VPS hosts will be running some variant of Linux, and not Windows. -* The primary Discord API server is located in East US. - * If latency matters for you application, choose a provider who is closer to this location. - -In addition to these, there are several hosting providers who offer free tiers, free trials, or in-service credit: - -* [**Microsoft Azure**][8]: $200 in-service credit, to be used within month of registration. There are also several - always-free services available, including various compute resources. Requires credit or debit card for validation. - Azure isn't cheap, but it supports both Windows and GNU/Linux-based servers. If you're enrolled in Microsoft Imagine, - it's possible to get these cheaper or free. -* [**Amazon Web Services**][9]: Free for 12 months (with 750 compute hours per month), with several always-free options - available. Not cheap once the trial runs out, but it's also considered industry standard in cloud services. -* [**Google Cloud Platform**][10]: $300 in-service credit, to be used within year of registration, and several - always-free resources available, albeit with heavy restrictions. GCP is based in the US, and offers very scalable - products. Like the above, it's not the cheapest of offerings. -* [**Oracle Cloud**][11] - $300 credit to be used within a month, and an always-free tier, which provides up to 4 ARM - cores, 24GB of ram, and 200GB of storage in compute resources, as well as some small x64 instances. There is no - monthly time limit. This service does require a valid credit card and offers no SLA. - -### Hosting on Cloud Native Services - -With most bots, unless if you host many of them, they dont require a whole machine to run them, just a slice of a -machine. This is where Docker and other cloud native hosting comes into play. There are many different options available -to you and you will need to chose which one will suit you best. Here are a few services that offer Docker or other cloud -native solutions that are cheaper than running a whole VM. - -* [**Azure App Service**][12]: Allows for Hosting Website, Continous Jobs, and Docker images on a Windows base or Linux - base machine. -* [**AWS Fargate**][13]: Allows for hosting Docker images within Amazon Web Services. -* [**Jelastic**][14]: Allows for hosting Docker images. - -### Making your publishing life easier - -Now that we have covered where you can possibly host your application, now lets cover how to make your life easier -publishing it. Many different source control solutions out there are free and also offer some type of CI/CD integration -(paid and free). Below are some of the solutions that we recommend: - -* [**Github**][15]: Offers Git repository hosting, as well as static page hosting (under \*.github.io domain) and basic - CI/CD in form of GitHub actions. -* [**GitLab**][16]: Another Git repository hosting, offers a far more advanced and flexible CI/CD. Can be self-hosted. -* [**BitBucket**][17]: Like the previous two, offers Git repository hosting, and CI/CD services. -* [**Azure Devops**][18]: Offers Git and Team Foundation Version Control repository hosting, in addition to full CI/CD - pipeline, similar to GitHub actions. The CI/CD offering can be attached to other services as well. - - -[0]: https://www.heroku.com/ -[1]: https://glitch.com/ -[2]: https://xenyth.net/ -[3]: https://www.vultr.com/products/cloud-compute/ -[4]: https://www.digitalocean.com/products/droplets/ -[5]: https://www.linode.com/products/shared/ -[6]: https://www.ovhcloud.com/en/vps/ -[7]: https://contabo.com/?show=vps -[8]: https://azure.microsoft.com/en-us/free/ -[9]: https://aws.amazon.com/free/ -[10]: https://cloud.google.com/free/ -[11]: https://www.oracle.com/cloud/free/ -[12]: https://azure.microsoft.com/en-us/services/app-service/ -[13]: https://aws.amazon.com/fargate/ -[14]: https://jelastic.com/docker/ -[15]: https://github.com/ -[16]: https://gitlab.com/ -[17]: https://bitbucket.org/ -[18]: https://azure.microsoft.com/en-us/services/devops/?nav=min diff --git a/docs/articles/interactivity.md b/docs/articles/interactivity.md deleted file mode 100644 index 03f66a0ce6..0000000000 --- a/docs/articles/interactivity.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -uid: articles.interactivity -title: Interactivity Introduction ---- - -# Introduction to Interactivity - -Interactivity will enable you to write commands which the user can interact with through reactions and messages. -The goal of this article is to introduce you to the general flow of this extension. - -Make sure to install the `DSharpPlus.Interactivity` package from NuGet before continuing. - -![Interactivity NuGet][0] - -## Enabling Interactivity - -Interactivity can be registered using the -@DSharpPlus.Interactivity.Extensions.ClientExtensions.UseInteractivity(DSharpPlus.DiscordClient,DSharpPlus.Interactivity.InteractivityConfiguration) -extension method. Optionally, you can also provide an instance of @DSharpPlus.Interactivity.InteractivityConfiguration -to modify default behaviors. - -```cs -var discord = new DiscordClient(); - -discord.UseInteractivity(new InteractivityConfiguration() -{ - PollBehaviour = PollBehaviour.KeepEmojis, - Timeout = TimeSpan.FromSeconds(30) -}); -``` - -## Using Interactivity - -There are two ways available to use interactivity: - -* Extension methods available for @DSharpPlus.Entities.DiscordChannel and @DSharpPlus.Entities.DiscordMessage. -* [Instance methods][1] available from @DSharpPlus.Interactivity.InteractivityExtension. - -We'll have a quick look at a few common interactivity methods along with an example of use for each. - -The first (and arguably most useful) extension method is -@DSharpPlus.Interactivity.InteractivityExtension.SendPaginatedMessageAsync* for @DSharpPlus.Entities.DiscordChannel - -This method displays a collection of *'pages'* which are selected one-at-a-time by the user through reaction buttons. -Each button click will move the page view in one direction or the other until the timeout is reached. - -You'll need to create a collection of pages before you can invoke this method. This can be done easily using the -@DSharpPlus.Interactivity.InteractivityExtension.GeneratePagesInEmbed* and -@DSharpPlus.Interactivity.InteractivityExtension.GeneratePagesInContent* instance methods from -@DSharpPlus.Interactivity.InteractivityExtension. -Alternatively, for pre-generated content, you can create and add individual instances of @DSharpPlus.Interactivity.Page -to a collection. - -This example will use the @DSharpPlus.Interactivity.InteractivityExtension.GeneratePagesInEmbed* method to generate the -pages. - -```cs -public async Task PaginationCommand(CommandContext ctx) -{ - var reallyLongString = "Lorem ipsum dolor sit amet, consectetur adipiscing..." - - var interactivity = ctx.Client.GetInteractivity(); - var pages = interactivity.GeneratePagesInEmbed(reallyLongString); - - await ctx.Channel.SendPaginatedMessageAsync(ctx.Member, pages); -} -``` - -![Pagination Pages][2] - -Next we'll look at the @DSharpPlus.Interactivity.Extensions.MessageExtensions.WaitForReactionAsync* extension method for -@DSharpPlus.Entities.DiscordMessage. This method waits for a reaction from a specific user and returns the emoji that -was used. - -An overload of this method also enables you to wait for a *specific* reaction, as shown in the example below. - -```cs -public async Task ReactionCommand(CommandContext ctx, DiscordMember member) -{ - var emoji = DiscordEmoji.FromName(ctx.Client, ":ok_hand:"); - var message = await ctx.RespondAsync($"{member.Mention}, react with {emoji}."); - - var result = await message.WaitForReactionAsync(member, emoji); - - if (!result.TimedOut) await ctx.RespondAsync("Thank you!"); -} -``` - -![Thank You!][3] - -Another reaction extension method for @DSharpPlus.Entities.DiscordMessage is -@DSharpPlus.Interactivity.InteractivityExtension.CollectReactionsAsync* As the name implies, this method collects all -reactions on a message until the timeout is reached. - -```cs -public async Task CollectionCommand(CommandContext ctx) -{ - var message = await ctx.RespondAsync("React here!"); - var reactions = await message.CollectReactionsAsync(); - - var strBuilder = new StringBuilder(); - foreach (var reaction in reactions) - { - strBuilder.AppendLine($"{reaction.Emoji}: {reaction.Total}"); - } - - await ctx.RespondAsync(strBuilder.ToString()); -} -``` - -![Reaction Count][4] - -The final one we'll take a look at is the @DSharpPlus.Interactivity.Extensions.ChannelExtensions.GetNextMessageAsync* -extension method for @DSharpPlus.Entities.DiscordMessage. - -This method will return the next message sent from the author of the original message. Our example here will use its -alternate overload which accepts an additional predicate. - -```cs -public async Task ActionCommand(CommandContext ctx) -{ - await ctx.RespondAsync("Respond with *confirm* to continue."); - var result = await ctx.Message.GetNextMessageAsync(m => - { - return m.Content.ToLower() == "confirm"; - }); - - if (!result.TimedOut) await ctx.RespondAsync("Action confirmed."); -} -``` - -![Confirmed][5] - - -[0]: ../images/interactivity_01.png -[1]: xref:DSharpPlus.Interactivity.InteractivityExtension#methods -[2]: ../images/interactivity_02.png -[3]: ../images/interactivity_03.png -[4]: ../images/interactivity_04.png -[5]: ../images/interactivity_05.png diff --git a/docs/articles/migration/2x_to_3x.md b/docs/articles/migration/2x_to_3x.md deleted file mode 100644 index 4ca634ac26..0000000000 --- a/docs/articles/migration/2x_to_3x.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -uid: articles.migration.2x_to_3x -title: Migration 2.x - 3.x ---- - -# Migration From 2.x to 3.x - -Major breaking changes: - -* Most classes were organized into namespaces. -* Several classes and methods were renamed to maintain consistency with the rest of the library. -* All events were renamed to be in the past tense. -* @DSharpPlus.Entities.DiscordEmbed instances are no longer constructed directly. - * Instead, they are built using a @DSharpPlus.Entities.DiscordEmbedBuilder. -* All colors are now passed as instances of @DSharpPlus.Entities.DiscordColor. -* Command modules are now based on an abstract class rather than an interface. -* A brand-new ratelimit handler has been implemented. - -## Fixing namespace issues - -Entities such as @DSharpPlus.Entities.DiscordUser, @DSharpPlus.Entities.DiscordChannel, and similar are in the -@DSharpPlus.Entities namespace, exceptions in @DSharpPlus.Exceptions, event arguments in @DSharpPlus.EventArgs, and -network components in @DSharpPlus.Net. - -Be sure to add these namespaces to your `using` directives as needed. - -## Class, Method, and Event Renames - -Several classes and methods were renamed to fit the current naming scheme in the library. - -2.x | 3.x -:----------------------------------:|:-----------------------------------: -`DiscordConfig` | `DiscordConfiguration` -`CommandExecutedEventArgs` | `CommandExecutionEventArgs` -`SnowflakeObject.CreationDate` | `SnowflakeObject.CreationTimestamp` -`VoiceReceivedEventArgs` | `VoiceReceiveEventArgs` -`DiscordMessage.EditAsync()` | `DiscordMessage.ModifyAsync()` -`SocketDisconnectEventArgs` | `SocketCloseEventArgs` -`DiscordMember.TakeRoleAsync()` | `DiscordMember.RevokeRoleAsync()` -`MessageReactionRemoveAllEventArgs` | `MessageReactionsClearEventArgs` - -Additionally, all events received a rename to maintain consistent naming across the library with many receiving an *d* -or *ed* to the end of their name. - -## Embed woes - -Embeds can no longer be constructed or modified directly. Instead, you have to use the embed builder. For the most part, -this can be achieved using Find/Replace and doing `new DiscordEmbed` -> `new DiscordEmbedBuilder`. - -On top of that, to add fields to an embed, you no longer create a new list for fields and assign it to -@DSharpPlus.Entities.DiscordEmbedBuilder.Fields, but instead you use the -@DSharpPlus.Entities.DiscordEmbedBuilder.AddField* method on the builder. - -To modify an existing embed, pass said embed to builder's constructor. The builder will use it as a prototype. - -## Color changes - -This one is easy to fix for the most part. For situation where you were doing e.g. `Color = 0xC0FFEE`, you now do -`Color = new DiscordColor(0xC0FFEE)`. This has the added advantage of letting you create a color from 3 RGB values or -parse an RGB string. - -## Default channel removal - -`DefaultChannel` no longer exists on guilds, and, as such, `DiscordGuild.CreateInviteAsync()` is also gone, as it relied -on that property. - -The new concept of "default" channel is a fallback, and is basically top channel the user can see. In the library this -is facilitated via `DiscordGuild.GetDefaultChannel()`. - -## Module changes - -The `IModule` interface was removed, and replaced with `BaseModule` class. The most notable change is that your module -should no longer define the field which holds your instance of @DSharpPlus.DiscordClient, as that's on the base class -itself. On top of that, you need to change modifiers of `.Setup()` from `public` to `protected internal override`. - -## New ratelimit handler - -This does not actually cause any in-code changes, however the behavior of the REST client and the way requests are -handled changes drastically. - -The new handler is thread-safe, and uses queueing to handle REST requests, and should bucket requests properly now. diff --git a/docs/articles/migration/3x_to_4x.md b/docs/articles/migration/3x_to_4x.md deleted file mode 100644 index f7af0bdc38..0000000000 --- a/docs/articles/migration/3x_to_4x.md +++ /dev/null @@ -1,234 +0,0 @@ ---- -uid: articles.migration.3x_to_4x -title: Migration 3.x - 4.x ---- - -## Migration From 3.x to 4.x - -### Proxy Support - -The client now supports proxies for both WebSocket and HTTP traffic. To proxy your traffic, create a new instance of -`System.Net.WebProxy` and assign it to @DSharpPlus.DiscordConfiguration.Proxy property. - -### Module Rename - -3.x | 4.x -:--------------------:|:------------------------: -`CommandsNextModule` | `CommandsNextExtension` -`InteractivityModule` | `InteractivityExtension` -`VoiceNextClient` | `VoiceNextExtension` -`BaseModule` | `BaseExtension` - -### Intents - -Due to a change by Discord on their V8 endpoint which DSharpPlus targets, in order to recieve events, intents will have -to be enabled in both the @DSharpPlus.DiscordConfiguration and the Discord Application Portal. We have an [article][0] -that covers all that has to be done to set this up. - -### Event Handlers - -The signitures for all the event handlers have changed to have 2 parameters instead of one. Please refer to this -[article][1] for the changes. - -### Entity mutation changes - -Entity updating methods now take an action which mutates the state of the object, instead of taking large lists of -arguments. This means that instead of updating e.g. a role like this: - -```cs -await role.UpdateAsync(name: "Modified Role", color: new DiscordColor(0xFF00FF)); -``` - -you will update it like this: - -```cs -await role.UpdateAsync(x => -{ - x.Name = "Modified Role"; - x.Color = new DiscordColor(0xFF00FF); -}); -``` - -### SendMessageAsync and SendFileAsync Methods - -We now have a message builder that will handle any advanced creating and modifing of messages. Below are the overloads -for sending and modifing messages: - -1. Sending Messages - -* `.SendMessageAsync(DiscordChannel, DiscordEmbed)` -* `.SendMessageAsync(DiscordChannel, System.String)` -* `.SendMessageAsync(DiscordChannel, System.String,DiscordEmbed)` -* `.SendMessageAsync(DiscordChannel, DiscordMessageBuilder)` - -2. Modifying Messages - -* `DiscordMessage.ModifyAsync(DSharpPlus.Entities.Optional)` -* `DiscordMessage.ModifyAsync(DSharpPlus.Entities.Optional)` -* `DiscordMessage.ModifyAsync(DSharpPlus.Entities.Optional, DSharpPlus.Entities.Optional)` -* `DiscordMessage.ModifyAsync(DSharpPlus.Entities.DiscordMessageBuilder)` - -Using the @DSharpPlus.Entities.DiscordMessageBuilder can be found [here][2]. - -### Logging Changes - -Logging was overhauled and now some of the Properties on @DSharpPlus.DiscordConfiguration along with some of the events -on @DSharpPlus.DiscordClient are Gone/Modified/Added. Below is a listing of what changed: - -* @DSharpPlus.DiscordConfiguration.LoggerFactory - this is where you can specify your own logging factory to help - augment the output of the log messages, redirect the output to other locations, etc. -* @DSharpPlus.DiscordConfiguration.MinimumLogLevel - this replaces LogLevel -* **DebugLogger** - this has been removed. -* **UseInternalLogHandler** - this has been removed. -* **DebugLogMessageEventArgs** - this event has been removed. - -We have also created an [article][3] on how to setup the new logger. - -### Other minor changes - -* **User DM handling** - Users can no longer be DM'd directly. Instead, you will need to find a member object for the - user you want to DM, then use the appropriate methods on the member object. -* **Channel permission override enhancements** - You can now query the member or role objects for each permission - override set on channels. Furthermore, the overwrite building is now more intuitive. -* **Indefinite reconnecting** - the client can now be configured to attempt reconnecting indefinitely. -* **Channel.Users** - you can now query users in voice and text channels by using - @DSharpPlus.Entities.DiscordChannel.Users property. -* **SendFileAsync argument reordering** - arguments for these methods were reordered to prevent overload confusion. -* **New Discord features** - support for animated emoji and slow mode. - -## CommandsNext - -There were several major changes made to CommandsNext extension. While basics remain the same, some finer details are -different. - -### Multiprefix support - -Prefixes are now configured via @DSharpPlus.CommandsNext.CommandsNextConfiguration.StringPrefixes instead of old -`StringPrefix` property. Prefixes passed in this array will all function at the same time. At the same time, -@DSharpPlus.CommandsNext.CommandContext class has been augmented with @DSharpPlus.CommandsNext.CommandContext.Prefix -property, which allows for checking which prefix was used to trigger the command. Furthermore, the new -@DSharpPlus.CommandsNext.Attributes.RequirePrefixesAttribute can be used as a check to require a specific prefix to be -used with a command. - -### Command hiding inheritance - -Much like checks, the @DSharpPlus.CommandsNext.Attributes.HiddenAttribute is now inherited in modules which are not -command groups. - -### Support for `Nullable` and `System.Uri` conversion - -The default argument converters have been augmented to allow for conversion of nullable value types. No further -configuration is required. - -Furthermore, native support for `System.Uri` type now exists as well. - -### Dependency Injection changes - -CommandsNext now uses Microsoft's Dependency Injection abstractions, which greatly enhances flexibility, as well as -allows 3rd party service containers to be used. For more information, see [Dependency injection][4] page. - -### Command overloads and group commands - -Command overloads are now implemented. This means you can create a command which takes multiple various argument type -configurations. This is done by creating several commands and giving them all the same name. - -Overloads need to have unique argument configurations, which means that it is possible to create commands which use the -same argument types in different order (e.g. `int, string` and `string, int`), however you cannot create two overloads -which have the same argument types and order. - -Checks are pooled between all overloads, which means that specifying the same check on every overload will make it run -several times; if you apply a check to a single overload, it will apply to all of them. - -Group command is also done by marking a command with @DSharpPlus.CommandsNext.Attributes.GroupCommandAttribute instead -of regular `CommandAttribute`. They can also be overloaded. - -### Common module base - -All command modules are now required to inherit from @DSharpPlus.CommandsNext.BaseCommandModule. This also enables the -modules to use @DSharpPlus.CommandsNext.BaseCommandModule.BeforeExecutionAsync(DSharpPlus.CommandsNext.CommandContext) -and @DSharpPlus.CommandsNext.BaseCommandModule.AfterExecutionAsync(DSharpPlus.CommandsNext.CommandContext). - -### Module lifespans - -It is now possible to create transient command modules. As opposed to regular singleton modules, which are instantiated -upon registration, these modules are instantiated before every command call, and are disposed shortly after. - -Combined with dependency injection changes, this enables the usage of transient and scoped modules. - -For more information, see [Module lifespans][5] page. - -### Help formatter changes - -Help formatter is now lower level, because it now receives a command object and a group object. Furthermore, they are -now also subject to dependency injection, receiving services and command context via DI. - -Default help module is also transient, allowing it to take advantage of more advanced DI usages. - -If you need to implement a custom help formatter, see [Custom Help Formatter][6]. - -### Custom command handlers - -You can now disabe the built-in command handler, and create your own. For more information, see -[Custom Command Handlers][7]. - -### Minor changes - -* **Case-insensitivity changes** - case insensitivity now applies to command name matching, prefix matching, and - argument conversions. -* **DM help** - Default help can now be routed to DMs. -* **Custom attributes on commands** - CommandsNext now exposes all custom attributes declared on commands, groups, and - modules. -* **Implicit naming** - Commands can be named from their method or class name, by not giving it a name in the Command or - Group attribute. -* **Argument converters are now asynchronous** - this allows using async code in converters. - -## Interactivity - -Interactivity went through an extensive rewrite and many methods were changed: - -Method | Change -:-----------------------------|:------- -`CollectReactionsAsync` | Different return value -`CreatePollAsync` | Changed to `DoPollAsync`. -`SendPaginatedMessage` | Changed to `SendPaginatedMessageAsync`. -`GeneratePagesInEmbeds` | New parameter. -`GeneratePagesInStrings` | New parameter. -`GeneratePaginationReactions` | Removed. -`DoPagination` | Removed. -`WaitForMessageReactionAsync` | Changed to `WaitForReactionAsync`. -`WaitForTypingUserAsync` | Changed to `WaitForUserTypingAsync`. -`WaitForTypingChannelAsync` | Changed to `WaitForTypingAsync`. - -## VoiceNext - -VoiceNext went through a substantial rewrite. Below are some of the key highlights: - -* Implemented support for Voice Gateway v4 -* Implemented support for lite and suffix encryption mode -* Improved performance -* Replaced old voice sending API with new stream-based transmit API that is non-blocking and has built-in support for - Changing volume levels. -* Automatic sending of silence packets on connection to enable incoming voice -* Incoming voice now properly maintains an Opus decoder per source -* Packet loss is now concealed via Opus FEC (if possible) or silence packets -* VoiceNext will now properly send and process UDP keepalive packets -* UDP and WebSocket ping values are now exposed on VoiceNextConnection objects -* Voice OP12 and 13 (user join and leave respectively) are now supported and exposed on VoiceNextConnection objects. - -## Lavalink - -The library now comes with a Lavalink client, which supports both Lavalink 2.x and 3.x. - -Lavalink is a standalone lightweight Java application, which handles downloading, transcoding, and transmitting audio to -Discord. For more information, see the [Lavalink][8] article. - - -[0]: xref:articles.beyond_basics.intents -[1]: xref:articles.beyond_basics.events -[2]: xref:articles.beyond_basics.messagebuilder -[3]: xref:articles.beyond_basics.logging.default -[4]: xref:articles.commands_next.dependency_injection -[5]: xref:articles.commands_next.dependency_injection#modules -[6]: xref:articles.commands_next.help_formatter -[7]: xref:articles.commands_next.command_handler -[8]: xref:articles.audio.lavalink.setup diff --git a/docs/articles/migration/4x_to_5x.md b/docs/articles/migration/4x_to_5x.md deleted file mode 100644 index cc2b81cd93..0000000000 --- a/docs/articles/migration/4x_to_5x.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -uid: articles.migration.4x_to_5x -title: Migration 4.x - 5.x ---- - -## Migration from 4.x to 5.x - -> [!NOTE] -> The API surface of DSharpPlus 5.x is not stable yet. This migration guide may be incomplete or outdated. It is recommended to cross-reference with the [tracking issue](https://github.com/DSharpPlus/DSharpPlus/issues/1585) when migrating. - -The first change you will likely encounter is a rewrite to how bots are set up. We now support two approaches instead of the old approach: - -# [DiscordClientBuilder](#tab/discordclientbuilder) - -The simplest way to get a bot running is to use `DSharpPlus.DiscordClientBuilder`. To get started, create a new builder as follows: - -```cs -DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault(string token, DiscordIntents intents); -``` - -Instead, if you are sharding, create it as follows: - -```cs -DiscordClientBuilder builder = DiscordClientBuilder.CreateSharded(string token, DiscordIntents intents, uint? shardCount); -``` - -You may omit the shard count to instead defer to Discord's recommended shard count. - -Then, migrate your configuration options. Rest-related settings from your old DiscordConfiguration are covered by `DiscordClientBuilder.ConfigureRestClient`, gateway-related settings are covered by `DiscordClientBuilder.ConfigureGatewayClient`. - -`LogLevel` has been migrated to `DiscordClientBuilder.SetLogLevel`, and configuring the gateway client is now done through either overriding or decorating the default client via `DiscordClientBuilder.ConfigureServices`. It is comprised of two parts, `ITransportService` and `IGatewayClient` - -Then, we will need to update event handling. For more information, see [the dedicated article](../beyond_basics/events.md), but in short, events have also been migrated to DiscordClientBuilder: events are now handled through `DiscordClientBuilder.ConfigureEventHandlers`. You can register handlers on the configuration delegate as follows: - -```cs -builder.ConfigureEventHandlers -( - b => b.HandleMessageCreated(async (s, e) => - { - if (e.Message.Content.ToLower().StartsWith("spiderman")) - { - await e.Message.RespondAsync("I want pictures of Spiderman!"); - } - }) - .HandleGuildMemberAdded(OnGuildMemberAdded) -); - -private Task OnGuildMemberAdded(DiscordClient sender, GuildMemberAddedEventArgs args) -{ - // non-asynchronous code here - return Task.CompletedTask; -} -``` - -Lastly, we'll need to migrate our extensions. Our extensions have an `UseExtensionName` method for DiscordClientBuilder, similar to how they previously had such extensions for DiscordClient. Some extensions take an additional `Action` parameter you can use to configure the extension, like so: - -```cs -builder.UseCommandsExtension -( - extension => - { - extension.AddCommands([typeof(MyCommands)]); - extension.AddParameterCheck(typeof(MyCheck)) - } -); -``` - -# [IServiceCollection](#tab/iservicecollection) - -If you need more advanced setup than DiscordClientBuilder facilitates, you can register DSharpPlus against an IServiceCollection. - -First, register all necessary services: - -```cs -serviceCollection.AddDiscordClient(string token, DiscordIntents intents); -``` - -Alternatively, if you are sharding, register them as such: - -```cs -serviceCollection.AddShardedDiscordClient(string token, DiscordIntents intents); -``` - -Then, migrate your configuration options to calls to `serviceCollection.Configure();`, `serviceCollection.Configure();` and `serviceCollection.Configure();`, respectively. `DiscordConfiguration` is a valid target to configure, however it only contains a few remaining configuration knobs not covered by the other configurations. - -When registering against a service collection, you are expected to provide your own logging setup, and DSharpPlus' default logging will not be registered. - -To handle events, use the extension method `ConfigureEventHandlers`: - -```cs -services.ConfigureEventHandlers -( - b => b.HandleMessageCreated(async (s, e) => - { - if (e.Message.Content.ToLower().StartsWith("spiderman")) - { - await e.Message.RespondAsync("I want pictures of Spiderman!"); - } - }) - .HandleGuildMemberAdded(OnGuildMemberAdded) -); - -private Task OnGuildMemberAdded(DiscordClient sender, GuildMemberAddedEventArgs args) -{ - // non-asynchronous code here - return Task.CompletedTask; -} -``` - -To register extensions, use their extension methods on IServiceCollection. These methods are named `AddXExtension` and accept a configuration object and optionally an `Action` for further configuration: - -```cs -services.AddCommandsExtension -( - extension => - { - extension.AddCommands([typeof(MyCommands)]); - extension.AddParameterCheck(typeof(MyCheck)); - }, - new CommandsConfiguration() - { - // ... - } -); -``` - ---- diff --git a/docs/articles/migration/dsharp.md b/docs/articles/migration/dsharp.md deleted file mode 100644 index 9136ed3bfe..0000000000 --- a/docs/articles/migration/dsharp.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -uid: articles.migration.dsharp -title: Migration From DiscordSharp ---- - -## Migration From DiscordSharp - -### Connecting - -```cs -// Old. -var discord = new DiscordClient("My First Token", true); - -discord.SendLoginRequest(); -discord.Connect(); -``` - -The constructor of the @DSharpPlus.DiscordClient now requires a @DSharpPlus.DiscordConfiguration object instead of a -simple string token and boolean. - -```cs -// New. -var discord = new DiscordClient(new DiscordConfiguration -{ - Token = "your token", - TokenType = TokenType.Bot -}); - -await discord.ConnectAsync(); -await Task.Delay(-1); -``` - -New versions of DSharpPlus implement [TAP][0], and the all DSharpPlus methods ending with *async* will need to be -`await`ed within an asynchronous method. - -### Events - -While the signature will look similar, many changes have been done to events behind the scenes. - -```cs -discord.MessageReceived += async (sender, arg) => -{ - // Code here -}; -``` - -We have a small article covering DSharpPlus events [here][1]. - -#### New events - -* ChannelPinsUpdated -* ClientErrored -* GuildEmojisUpdated -* GuildIntegrationsUpdated -* GuildMembersChunked -* GuildRoleCreated -* GuildUnavailable -* Heartbeated -* MessageAcknowledged -* MessageReactionAdded -* MessageReactionRemoved -* MessageReactionsCleared -* MessagesBulkDeleted -* SocketErrored -* UnknownEvent -* UserSettingsUpdated -* VoiceServerUpdated -* WebhooksUpdated - -#### Removed Events - -* TextClientDebugMessageReceived -* VoiceClientDebugMessageReceived - -#### Changed Event names - -Old DiscordSharp Event | DSharpPlus Equivalent -:----------------------|:---------------------- -MessageReceived | MessageCreated -Connected | Ready -PrivateChannelCreated | DmChannelCreated -PrivateMessageReceived | MessageCreated -MentionReceived | MessageCreated -UserTypingStart | TypingStarted -MessageEdited | MessageUpdated -URLMessageAutoUpdate | MessageUpdate -VoiceStateUpdate | VoiceStateUpdated -UserUpdate | UserUpdated -UserAddedToServer | GuildMemberAdded -UserRemovedFromServer | GuildMemberRemoved -RoleDeleted | GuildRoleDeleted -RoleUpdated | GuildRoleUpdated -GuildMemberBanned | GuildBanAdded -PrivateChannelDeleted | DMChannelDeleted -BanRemoved | GuildBanRemoved -PrivateMessageDeleted | MessageDeleted. - - -[0]: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap -[1]: xref:articles.beyond_basics.events diff --git a/docs/articles/migration/slashcommands_to_commands.md b/docs/articles/migration/slashcommands_to_commands.md deleted file mode 100644 index 4f3b398061..0000000000 --- a/docs/articles/migration/slashcommands_to_commands.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -uid: articles.migration.slashcommands_to_commands -title: DSharpPlus.SlashCommands to DSharpPlus.Commands ---- - -## Migrating from DSharpPlus.SlashCommands to DSharpPlus.Commands - -This section will focus on migrating existing code - there is a rough sketch of what to expect in new code at the end. - -> [!NOTE] -> This setup will register commands to both slash and text commands. If you want to use only slash commands, either disable the text command processor or mark your commands with `[AllowedProcessors(typeof(SlashCommandProcessor))]`. - -Before migrating to the shiny new command farmework, you should make sure to update to the latest available build of the library - migrating both at once will be considerably more challenging. Then, we'll need to do some setup. - -Remove the SlashCommands reference and install the package. Then, set up a [service collection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) containing all services your commands need, as well as a logger. Then, call `BuildServiceProvider()` to obtain a service provider. - -At the very least, you will need the following: -```cs -IServiceProvider serviceProvider = new ServiceCollection().AddLogging(x => x.AddConsole()).BuildServiceProvider(); -``` - -> [!IMPORTANT] -> If you want the commands extension to log anything, you must register a logger - it cannot currently use the default logger. Additionally, when designing your services, you should keep in mind that all commands are currently transient. - -Now that we're ready to change our code, go to all of your command classes and remove the reference to `ApplicationCommandModule` - we won't be needing that any more. - -If you previously used the functionality provided by that class, migrate it into a `CommandsExtension.CommandInvoked` event handler. - -> [!NOTE] -> DSharpPlus.Commands does not currently support pre-execution events. If you were using them to control execution of the command, use an [unconditional context check](../commands/custom_context_checks.md#advanced-features) - more on checks later. - -Next, change `InteractionContext` to `CommandContext`, change `SlashCommandAttribute` to just `CommandAttribute` and move the descriptions to their own `System.ComponentModel.DescriptionAttribute`s. The new extension will synthesize parameter names from the C# parameter names, but you can override the generated names using `ParameterAttribute`. Most other attributes follow the same naming and have been moved between namespaces or merged, such as `SlashMinValueAttribute` and `SlashMaxValueAttribute` -> `SlashMinMaxValueAttribute`. - -Any localization you have will need to be factored into a localization provider and applied to the command using `InteractionLocalizerAttribute`. - -If you previously specified default required permissions, use `RequirePermissionsAttribute`, which also provides a way to specify the permissions your bot needs for the command to successfully execute. - -Now, let's talk about checks. The library provides a fair few built-in checks, both on [parameters](../commands/custom_context_checks.md#parameter-checks) and on commands, similar to what you're used to. However, implementing your own checks works slightly different now. Checks are now comprised of two types - the check implementation and the attribute applied to the command. For more in-depth applications, you should refer to [the dedicated article](../commands/custom_context_checks.md), but on surface level it works as follows: first, paste your implementation into a check implementing `IContextCheck`, then change it to return error messages instead of exceptions if possible and lastly register the check with the extension using the `AddCheck` methods while keeping the attribute applied to the command. - -As for new features, DSharpPlus.Commands allows argument converters on slash commands, as well as broadening the range of available types: you can now use all integer types, additional Discord entities and more. Have a peek around `DSharpPlus.Commands.Converters`, or [implement your own converter](../commands/converters/custom_argument_converters.md). - -As a last step, we'll change how our commands are registered. To simply register commands, use the `AddCommands` methods on your `CommandsExtension`. If you wish to register commands to one guild for debugging/testing purposes, use `DebugGuildId` in `CommandsConfiguration`, and if you wish to register specific command to specific guilds, specify the IDs of those guilds in your `AddCommands` calls. It is generally recommended to not register guild-specific commands with the same name as global commands. - -## Changed Names and Concepts - -#### Attributes - -| DSharpPlus.SlashCommands | DSharpPlus.Commands | -| ------------------------ | ------------------- | -| `MinimumLengthAttribute` and `MaximumLengthAttribute` | `MinMaxLengthAttribute` | -| `MinimumAttribute` and `MaximumAttribute` | `MinMaxValueAttribute` | -| `NameLocalizationAttribute` and `DescriptionLocalizationAttribute` | `InteractionLocalizerAttribute` | -| `ChoiceNameAttribute` | `ChoiceDisplayNameAttribute` | -| `DSharpPlus.SlashCommands.DescriptionAttribute` | `System.ComponentModel.DescriptionAttribute` | -| `OptionAttribute` | `ParameterAttribute` | -| `SlashCommandAttribute` and `SlashCommandGroupAttribute` | `CommandAttribute` | - -#### Checks - -Checks are now split into two parts, with a changed error model - refer to [the dedicated article](../commands/custom_context_checks.md). - -#### Pre-Execution and Post-Execution Events - -Pre-execution events are not currently supported, but if you used them to control execution, you can use [an unconditional check](../commands/custom_context_checks.md#advanced-features) instead. `CommandsExtension.CommandInvoked` serves as post-execution event. `ApplicationCommandModule` no longer exists. - -#### Localization - -Instead of having an attribute for each locale for the name and description of a command or parameter, you now use `InteractionLocalizerAttribute` and implement a localizer. - -#### Argument Converters - -DSharpPlus.Commands supports argument converters - refer to [the dedicated article](../commands/converters/custom_argument_converters.md). diff --git a/docs/articles/misc/debug_symbols.md b/docs/articles/misc/debug_symbols.md deleted file mode 100644 index c90c387826..0000000000 --- a/docs/articles/misc/debug_symbols.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -uid: articles.misc.debug_symbols -title: Debug Symbols ---- - -# I want to diagnose a problem I believe originates from the library, how do? - -In the event you need to debug DSharpPlus, we offer debug symbols. They are available at the following locations: - -## Symbol sources - -All of our symbols can be found on [Nuget](https://www.nuget.org/packages/DSharpPlus/). Nightly builds have symbols and source included inside of the packages, while release builds will only contain the symbols. - -## Using the symbols - -In Visual Studio: - -1. Go to Tools > Options > Debugging and make sure "Just My Code" is disabled and "Source Server Support" is enabled. -2. Go to Tools > Options > Debugging > Symbols, and add the URL in there. diff --git a/docs/articles/misc/nightly_builds.md b/docs/articles/misc/nightly_builds.md deleted file mode 100644 index 6941202b16..0000000000 --- a/docs/articles/misc/nightly_builds.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -uid: articles.misc.nightly_builds -title: Nightly Builds ---- - -# I like living on the edge - give me the freshest builds - -We offer nightly builds for DSharpPlus. They contain bugfixes and new features before the stable releases, however they -are not guaranteed to be stable, or work at all. - -Simply open the NuGet interface for your project, check **Prerelease** and select **Latest prerelease** version of -the DSharpPlus packages, and install them. - -If you find any problems in the nightly versions of the packages, please follow the instructions in -[Reporting issues][0] article. - - -[0]: xref:articles.misc.reporting_issues diff --git a/docs/articles/misc/reporting_issues.md b/docs/articles/misc/reporting_issues.md deleted file mode 100644 index 125a04347a..0000000000 --- a/docs/articles/misc/reporting_issues.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -uid: articles.misc.reporting_issues -title: Reporting Issues ---- - -# I broke something, and I need it fixed - -We always try to fix bugs, and make sure that when we release the next version of DSharpPlus, everything is polished and -working. However, DSharpPlus is a large codebase, and we can't always catch all the bugs, or notice all the regressions -that happen while we fix bugs or implement new issues. - -## GitHub issue tracker - -If you find a bug, come up with a new idea, or just want to report something, you can open an issue on our -[GitHub Issue Tracker][0]. - -When opening an issue, make sure to include as much detail as possible. If at all possible, please include: - -* Steps to reproduce the issue -* What were you trying to achieve -* Expected/acutal result -* Stack traces, exception types, messages -* Attempted solutions - -## Discord - -Some questions, most notably questions on using the library, are better asked on Discord. You can find the server links -on the [preamble][1]. - -Make sure to ask for help in the `#! help-and-support!` forum. - -## Contributing - -Lastly, while we understand that not everyone is an expert programmer, we would appreciate it if you could fix any -issues you find and submit a Pull Request on GitHub. This would reduce the amount of work we would have to do. - -When contributing, ensure your code matches the style of the rest of the library, and that you test the changes you -make, and catch any possible regressions. - - -[0]: https://github.com/DSharpPlus/DSharpPlus/issues "DSharpPlus issues on GitHub" -[1]: xref:articles.preamble diff --git a/docs/articles/preamble.md b/docs/articles/preamble.md deleted file mode 100644 index e8c7920b50..0000000000 --- a/docs/articles/preamble.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -uid: articles.preamble -title: Article Preamble ---- - ->[!NOTE] -> These articles and the [API documentation][11] are built for the latest [nightly][18] version (`v5.0`). Please use v5.0 nightlies instead of v4.5.X. v5.0 is a major rewrite of the library and no support will be provided for v4.5.X. - -## Knowledge Prerequisites - -Before attempting to write a Discord bot, you should be familiar with the concepts of [Object Oriented Programing][0], -[the C# programming language][1], and [Task-based Asynchronous Pattern][2]. - -If you're brand new to C#, or programming in general, this library may prove difficult for you to use. Fortunately, -there are resources that can help you get started with the language! - -An excellent tutorial series to go through would be [C# Fundamentals for Absolute Beginners][3] by Bob Tabor. His videos -go through all the basics, from setting up your development environment up to some of the more advanced concepts. If -you're not sure what to do first, Bob's tutorial series should be your starting point! - -## Supported .NET Implementations - -There are multiple different branches of DSharpPlus targeting different [.NET][4] versions. - -See the table below for supported [.NET implementations][16]: - -| DSharpPlus Branch | .NET | .NET Core | .NET Framework | -| :---------- | :-----: | :-----: | :-----: | -| [Stable][17], `v4.5.X` | `v8.0` - `v9.0`
✔️ | `v3.1`
⚠️ | `v4.6.1` - `v4.8.1`
⚠️ | -| [Nightly][18], `v5.0` | `v9.0`
✔️ | ❌ | ❌ | -| [Future][19], `v6.0` | `v10.0`
✔️ | ❌ | ❌ | - - ✔️ `Recommended and supported`  ●  ⚠️ `Unsupported, might still work`  ●  ❌ `Unsupported, do not use` - -Generally, you should be targeting the latest version of .NET. - -.NET Core and [.NET Framework][5] are not directly targeted by DSharpPlus, but may work in some senarios because of the [.NET Standard][20]. - -Using [Unity][7], [Mono][6], [.NET Framework][5], or any other .NET implementation other than the ones listed with a `✔️` above are _not_ supported by DSharpPlus, and you will be on your own regarding any arising issues. - -If you are using a game engine with C# support (such as [Unity][7]), you should consider using the [Discord GameSDK][8] instead of DSharpPlus. - -## Getting Started - -If you're writing a Discord bot for the first time, you'll want to start with [creating a bot account][9]. Otherwise, if -you have a bot account already, start off with the [writing your first bot][10] article. - -Once you're up and running, feel free to browse through the [API Documentation][11]! - -## Support and Questions - -You can get in contact with us on Discord through one of the following guilds: - -**DSharpPlus Guild**:
-[![DSharpPlus Guild][12]][13] - -**#dotnet_dsharpplus** on **Discord API**:
-[![Discord API / #dotnet_dsharpplus][14]][15] - - - -[0]: https://en.wikipedia.org/wiki/Object-oriented_programming -[1]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/ -[2]: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap -[3]: https://channel9.msdn.com/Series/CSharp-Fundamentals-for-Absolute-Beginners -[4]: https://dotnet.microsoft.com/en-us/ -[5]: https://en.wikipedia.org/wiki/.NET_Framework -[6]: https://en.wikipedia.org/wiki/Mono_(software) -[7]: https://en.wikipedia.org/wiki/Unity_(game_engine) -[8]: https://discord.com/developers/docs/game-sdk/sdk-starter-guide -[9]: xref:articles.basics.bot_account -[10]: xref:articles.basics.first_bot -[11]: /api/ -[12]: https://discordapp.com/api/guilds/379378609942560770/embed.png?style=banner2 -[13]: https://discord.gg/dsharpplus -[14]: https://discordapp.com/api/guilds/81384788765712384/embed.png?style=banner2 -[15]: https://discord.gg/discord-api -[16]: https://learn.microsoft.com/en-us/dotnet/fundamentals/implementations -[17]: https://github.com/DSharpPlus/DSharpPlus/tree/release/4.5 -[18]: https://github.com/DSharpPlus/DSharpPlus/tree/master -[19]: https://github.com/DSharpPlus/DSharpPlus/tree/v6 -[20]: https://learn.microsoft.com/en-us/dotnet/standard/net-standard diff --git a/docs/articles/slash_commands.md b/docs/articles/slash_commands.md deleted file mode 100644 index 72a7c79299..0000000000 --- a/docs/articles/slash_commands.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -uid: articles.slash_commands -title: Slash Commands ---- - -# Slash Commands ->[!WARNING] -> `DSharpPlus.SlashCommands` has been replaced by [Commands](xref:articles.commands.introduction). Both this article and `DSharpPlus.SlashCommands` itself is no longer maintained and may contain outdated information. `DSharpPlus.SlashCommands` will be deprecated in version 5.1.0 of DSharpPlus. - -## Introduction - -This is the documentation for the slash commands extension for DSharpPlus (it also supports context menus). This is a direct merge of IDoEverything's slash command extension, so if you've been using that one you shouldn't need to make any changes in your code. - -There are some caveats to the usage of the library that you should note: - -It does not support registering or editing commands at runtime. While you can make commands at runtime using the methods on the client, if you have a command class registered for that guild/globally if you're making global commands, it will be overwritten (therefore probably deleted) on the next startup due to the limitations of the bulk overwrite endpoint. If your usage of slash commands depends on dynamically registering commands, this extension will not work for you. - -## Installation -To get started, make sure you have matching versions of both the [`DSharpPlus`](https://www.nuget.org/packages/DSharpPlus) and [`DSharpPlus.SlashCommands`](https://www.nuget.org/packages/DSharpPlus.SlashCommands) packages. - -``` -dotnet add package DSharpPlus -dotnet add package DSharpPlus.SlashCommands -``` - -## Important: Authorizing your bot - -For a bot to make slash commands in a server, it must be authorized with the applications.commands scope as well. In the OAuth2 section of the developer portal, you can check the applications.commands box to generate an invite link. You can check the bot box as well to generate a link that authorizes both. If a bot is already authorized with the bot scope, you can still authorize with just the applications.commands scope without having to kick out the bot. - -If your bot isn't properly authorized, a 403 exception will be thrown on startup. - -## Setup - -Add the using reference to your bot class: - -```cs -using DSharpPlus.SlashCommands; -``` - -You can then register a `SlashCommandsExtension` on your `DiscordClient`, similar to how you register a `CommandsNextExtension` - -```cs -var slash = discord.UseSlashCommands(); -``` - -## Making a command class - -Similar to CommandsNext, you can make a module for slash commands and make it inherit from `ApplicationCommandModule` - -```cs -public class SlashCommands : ApplicationCommandModule -{ - //commands -} -``` - -You have to then register it with your `SlashCommandsExtension`. - -Slash commands can be registered either globally or for a certain guild. However, if you try to register them globally, they can take up to an hour to cache across all guilds. So, it is recommended that you only register them for a certain guild for testing, and only register them globally once they're ready to be used. - -To register your command class, - -```cs -//To register them for a single server, recommended for testing -slash.RegisterCommands(guild_id); - -//To register them globally, once you're confident that they're ready to be used by everyone -slash.RegisterCommands(); -``` - -*Make sure that you register them before your `ConnectAsync`* - -## Making Slash Commands - -On to the exciting part. - -Slash command methods must be `Task`s and have the `SlashCommand` attribute. The first argument for the method must be an `InteractionContext`. Let's make a simple slash command: - -```cs -public class SlashCommands : ApplicationCommandModule -{ - [SlashCommand("test", "A slash command made to test the DSharpPlusSlashCommands library!")] - public async Task TestCommand(InteractionContext ctx) { } -} -``` - -To make a response, you must run `CreateResponseAsync` on your `InteractionContext`. `CreateResponseAsync` takes two arguments. The first is a [`InteractionResponseType`](https://dsharpplus.github.io/DSharpPlus/api/DSharpPlus.InteractionResponseType.html): - -* `DeferredChannelMessageWithSource` - Acknowledges the interaction, doesn't require any content. -* `ChannelMessageWithSource` - Sends a message to the channel, requires you to specify some data to send. - -An interaction expires in 3 seconds unless you make a response. If the code you execute before making a response has the potential to take more than 3 seconds, you should first create a `DeferredChannelMessageWithSource` response, and then edit it after your code executes. - -The second argument is a type of [`DiscordInteractionResponseBuilder`](https://dsharpplus.github.io/DSharpPlus/api/DSharpPlus.Entities.DiscordInteractionResponseBuilder.-ctor.html). It functions similarly to the DiscordMessageBuilder, except you cannot send files, and you can have multiple embeds. - -If you want to send a file, you'll have to edit the response. - -A simple response would be like: - -```cs -[SlashCommand("test", "A slash command made to test the DSharpPlus Slash Commands extension!")] -public async Task TestCommand(InteractionContext ctx) -{ - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Success!")); -} -``` - -If your code will take some time to execute: - -```cs -[SlashCommand("delaytest", "A slash command made to test the DSharpPlus Slash Commands extension!")] -public async Task DelayTestCommand(InteractionContext ctx) -{ - await ctx.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); - - //Some time consuming task like a database call or a complex operation - - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent("Thanks for waiting!")); -} -``` - -You can also override `BeforeExecutionAsync` and `AfterExecutionAsync` to run code before and after all the commands in a module. This does not apply to groups, you have to override them individually for the group's class. -`BeforeExecutionAsync` can also be used to prevent the command from running. - -### Arguments - -If you want the user to be able to give more data to the command, you can add some arguments. - -Arguments must have the `Option` attribute, and can be of type: - -* `string` -* `long` or `long?` -* `bool` or `bool?` -* `double` or `double?` -* `DiscordUser` - This can be cast to `DiscordMember` if the command is run in a guild -* `DiscordChannel` -* `DiscordRole` -* `DiscordAttachment` -* `SnowflakeObject` - This can accept both a user and a role; you can cast it `DiscordUser`, `DiscordMember` or `DiscordRole` to get the actual object -* `Enum` - This can used for choices through an enum; read further - -If you want to make them optional, you can assign a default value. - -You can also predefine some choices for the option. Custom choices only work for `string`, `long` or `double` arguments (for `DiscordChannel` arguments, `ChannelTypes` attribute can be used to limit the types of channels that can be chosen). There are several ways to use them: - -1. Using the `Choice` attribute. You can add multiple attributes to add multiple choices. -2. You can define choices using enums. See the example below. -3. You can use a `ChoiceProvider` to run code to get the choices from a database or similar. See the example below. - -(second and third method contributed by @Epictek) - -Some examples: - -```cs -//Attribute choices -[SlashCommand("ban", "Bans a user")] -public async Task Ban(InteractionContext ctx, [Option("user", "User to ban")] DiscordUser user, - [Choice("None", 0)] - [Choice("1 Day", 1)] - [Choice("1 Week", 7)] - [Option("deletedays", "Number of days of message history to delete")] long deleteDays = 0) -{ - await ctx.Guild.BanMemberAsync(user.Id, (int)deleteDays); - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Banned {user.Username}")); -} - -//Enum choices -public enum MyEnum -{ - [ChoiceName("Option 1")] - option1, - [ChoiceName("Option 2")] - option2, - [ChoiceName("Option 3")] - option3 -} - -[SlashCommand("enum", "Test enum")] -public async Task EnumCommand(InteractionContext ctx, [Option("enum", "enum option")]MyEnum myEnum = MyEnum.option1) -{ - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent(myEnum.GetName())); -} - -//ChoiceProvider choices -public class TestChoiceProvider : IChoiceProvider -{ - public async Task> Provider() - { - return new DiscordApplicationCommandOptionChoice[] - { - //You would normally use a database call here - new DiscordApplicationCommandOptionChoice("testing", "testing"), - new DiscordApplicationCommandOptionChoice("testing2", "test option 2") - }; - } -} - -[SlashCommand("choiceprovider", "test")] -public async Task ChoiceProviderCommand(InteractionContext ctx, - [ChoiceProvider(typeof(TestChoiceProvider))] - [Option("option", "option")] string option) -{ - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent(option)); -} -``` - -### Groups - -You can have slash commands in groups. Their structure is explained [here](https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups). You can simply mark your command class with the `[SlashCommandGroup]` attribute. - -```cs -//for regular groups -[SlashCommandGroup("group", "description")] -public class GroupContainer : ApplicationCommandModule -{ - [SlashCommand("command", "description")] - public async Task Command(InteractionContext ctx) {} - - [SlashCommand("command2", "description")] - public async Task Command2(InteractionContext ctx) {} -} - -//For subgroups inside groups -[SlashCommandGroup("group", "description")] -public class SubGroupContainer : ApplicationCommandModule -{ - [SlashCommandGroup("subgroup", "description")] - public class SubGroup : ApplicationCommandModule - { - [SlashCommand("command", "description")] - public async Task Command(InteractionContext ctx) {} - - [SlashCommand("command2", "description")] - public async Task Command2(InteractionContext ctx) {} - } - - [SlashCommandGroup("subgroup2", "description")] - public class SubGroup2 : ApplicationCommandModule - { - [SlashCommand("command", "description")] - public async Task Command(InteractionContext ctx) {} - - [SlashCommand("command2", "description")] - public async Task Command2(InteractionContext ctx) {} - } -} -``` - -## Context Menus - -Context menus are commands that show up when you right click on a user or a message. Their implementation is fairly similar to slash commands. - -```cs -//For user commands -[ContextMenu(ApplicationCommandType.UserContextMenu, "User Menu")] -public async Task UserMenu(ContextMenuContext ctx) { } - -//For message commands -[ContextMenu(ApplicationCommandType.MessageContextMenu, "Message Menu")] -public async Task MessageMenu(ContextMenuContext ctx) { } -``` - -Responding works exactly the same as slash commands. You cannot define any arguments. - -### Pre-execution checks - -You can define some custom attributes that function as pre-execution checks, working very similarly to `CommandsNext`. Simply create an attribute that inherits `SlashCheckBaseAttribute` for slash commands, and `ContextMenuCheckBaseAttribute` for context menus and override the methods. - -There are also some built in ones for slash commands, the same ones as on `CommandsNext` but prefix with `Slash` - for example the `SlashRequirePermissionsAttribute` - -```cs -public class RequireUserIdAttribute : SlashCheckBaseAttribute -{ - public ulong UserId; - - public RequireUserIdAttribute(ulong userId) - { - this.UserId = userId; - } - - public override async Task ExecuteChecksAsync(InteractionContext ctx) - { - if (ctx.User.Id == UserId) - return true; - else - return false; - } -} - -``` - -Then just apply it to your command - -```cs -[SlashCommand("admin", "runs sneaky admin things")] -[RequireUserId(0000000000000)] -public async Task Admin(InteractionContext ctx) { //secrets } -``` - -To provide a custom error message when an execution check fails, hook the `SlashCommandErrored` event for slash commands, and `ContextMenuErrored` event for context menus on your `SlashCommandsExtension` - -```cs -SlashCommandsExtension slash = //assigned; -slash.SlashCommandErrored += async (s, e) => -{ - if(e.Exception is SlashExecutionChecksFailedException slex) - { - foreach (var check in slex.FailedChecks) - if (check is RequireUserIdAttribute att) - await e.Context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Only <@{att.Id}> can run this command!")); - } -}; -``` - -Context menus throw `ContextMenuExecutionChecksFailedException`. - -To use a built in one: - -```cs -[SlashCommand("ban", "Bans a user")] -[SlashRequirePermissions(Permissions.BanMembers)] -public async Task Ban(InteractionContext ctx, [Option("user", "User to ban")] DiscordUser user, - [Choice("None", 0)] - [Choice("1 Day", 1)] - [Choice("1 Week", 7)] - [Option("deletedays", "Number of days of message history to delete")] long deleteDays = 0) -{ - await ctx.Guild.BanMemberAsync(user.Id, (int)deleteDays); - await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent($"Banned {user.Username}")); -} -``` - -### Dependency Injection - -To pass in a service collection, provide a `SlashCommandsConfiguration` in `UseSlashCommands`. - -```cs -var slash = discord.UseSlashCommands(new SlashCommandsConfiguration -{ - Services = new ServiceCollection().AddSingleton().BuildServiceProvider() -}); -``` - -Property injection is implemented, however static properties will not be replaced. If you wish for a non-static property to be left alone, assign it the `DontInject` attribute. Property Injection can be used like so: - -```cs -public class Commands : ApplicationCommandModule -{ - public Database Database { private get; set; } // The get accessor is optionally public, but the set accessor must be public. - - [SlashCommand("ping", "Checks the latency between the bot and it's database. Best used to see if the bot is lagging.")] - public async Task Ping(InteractionContext context) => await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new() - { - Content = $"Pong! Database latency is {Database.GetPing()}ms." - }); -} -``` - -### Sharding - -`UseSlashCommands` -> `UseSlashCommmandsAsync` which returns a dictionary. - -You'll have to foreach over it to register events. - -### Module Lifespans - -You can specify a module's lifespan by applying the `SlashModuleLifespan` attribute on it. Modules are transient by default. diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml deleted file mode 100644 index 191a8f3fcc..0000000000 --- a/docs/articles/toc.yml +++ /dev/null @@ -1,149 +0,0 @@ -- 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: Analyzers - items: - - name: DSharpPlus Core Rules - topicUid: articles.analyzers.core - - name: DSharpPlus.Commands Rules - topicUid: articles.analyzers.commands -- 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/docfx.json b/docs/docfx.json deleted file mode 100644 index 1c91bc4d40..0000000000 --- a/docs/docfx.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "metadata": [ - { - "src": [ - { - "src": "../", - "files": "**.slnx", - "exclude": [ - "**/obj/**", - "_site/**" - ] - } - ], - "dest": "api", - "filter": "filter_config.yml", - "memberLayout": "separatePages" - } - - ], - "build": { - "content": [ - { - "files": [ - "api/**.yml", - "api/index.md" - ] - }, - { - "files": [ - "articles/**.md", - "articles/**/toc.yml", - "natives/**.md", - "faq/**.md", - "toc.yml", - "*.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "xref": [ - "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" - ], - "resource": [ - { - "files": [ - "images/**", - "langword_mapping.yml" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "overwrite": [ - { - "files": [ - "apidoc/**.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "dest": "_site", - "globalMetadata": { - "_appTitle": "DSharpPlus", - "_appName": "DSharpPlus", - "_appFooter": "© 2016-2025 DSharpPlus Contributors", - "_appLogoPath": "images/logo.png", - "_appFaviconPath": "images/favicon.ico", - "_enableSearch": "true" - }, - "globalMetadataFiles": [], - "fileMetadataFiles": [], - "template": [ - "default", - "modern" - ], - "postProcessors": [ - "ExtractSearchIndex" - ], - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false - } -} diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 3c34fc5139..0000000000 --- a/docs/faq.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -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/docs/filter_config.yml b/docs/filter_config.yml deleted file mode 100644 index ffea09111f..0000000000 --- a/docs/filter_config.yml +++ /dev/null @@ -1,17 +0,0 @@ -apiRules: -- exclude: - uidRegex: ^DSharpPlus\.Test.*$ -- exclude: - uidRegex: ^System\..*$ -- exclude: - uidRegex: ^DSharpPlus\.RingBuffer.*$ -- exclude: - uidRegex: ^DSharpPlus\.Base(WebSocket|Udp)Client.*$ -- exclude: - uidRegex: ^DSharpPlus\.HttpRequestMethod.*$ -- exclude: - uidRegex: ^DSharpPlus\.VoiceNext\.Codec.*$ -- exclude: - uidRegex: ^DSharpPlus\.Tools.*$ -- exclude: - uidRegex: ^DSharpPlus\.Analyzers.*$ diff --git a/docs/images/Intents.png b/docs/images/Intents.png deleted file mode 100644 index f998cda526..0000000000 Binary files a/docs/images/Intents.png and /dev/null differ diff --git a/docs/images/advanced_topics_buttons_01.png b/docs/images/advanced_topics_buttons_01.png deleted file mode 100644 index 48c14b2ec9..0000000000 Binary files a/docs/images/advanced_topics_buttons_01.png and /dev/null differ diff --git a/docs/images/advanced_topics_buttons_02.png b/docs/images/advanced_topics_buttons_02.png deleted file mode 100644 index 02e3ee3eb2..0000000000 Binary files a/docs/images/advanced_topics_buttons_02.png and /dev/null differ diff --git a/docs/images/advanced_topics_selects_01.png b/docs/images/advanced_topics_selects_01.png deleted file mode 100644 index d4070c3007..0000000000 Binary files a/docs/images/advanced_topics_selects_01.png and /dev/null differ diff --git a/docs/images/advanced_topics_selects_02.png b/docs/images/advanced_topics_selects_02.png deleted file mode 100644 index fbe0ae7744..0000000000 Binary files a/docs/images/advanced_topics_selects_02.png and /dev/null differ diff --git a/docs/images/basics_bot_account_01.png b/docs/images/basics_bot_account_01.png deleted file mode 100644 index 8d0cbee303..0000000000 Binary files a/docs/images/basics_bot_account_01.png and /dev/null differ diff --git a/docs/images/basics_bot_account_02.png b/docs/images/basics_bot_account_02.png deleted file mode 100644 index 47b4955194..0000000000 Binary files a/docs/images/basics_bot_account_02.png and /dev/null differ diff --git a/docs/images/basics_bot_account_03.png b/docs/images/basics_bot_account_03.png deleted file mode 100644 index 5c972ad1ab..0000000000 Binary files a/docs/images/basics_bot_account_03.png and /dev/null differ diff --git a/docs/images/basics_bot_account_04.png b/docs/images/basics_bot_account_04.png deleted file mode 100644 index fef372d1b7..0000000000 Binary files a/docs/images/basics_bot_account_04.png and /dev/null differ diff --git a/docs/images/basics_bot_account_05.png b/docs/images/basics_bot_account_05.png deleted file mode 100644 index c67103e16b..0000000000 Binary files a/docs/images/basics_bot_account_05.png and /dev/null differ diff --git a/docs/images/basics_bot_account_06.png b/docs/images/basics_bot_account_06.png deleted file mode 100644 index fbaf2fa821..0000000000 Binary files a/docs/images/basics_bot_account_06.png and /dev/null differ diff --git a/docs/images/basics_bot_account_07.png b/docs/images/basics_bot_account_07.png deleted file mode 100644 index 33a42668fb..0000000000 Binary files a/docs/images/basics_bot_account_07.png and /dev/null differ diff --git a/docs/images/basics_bot_account_08.png b/docs/images/basics_bot_account_08.png deleted file mode 100644 index 04471e9103..0000000000 Binary files a/docs/images/basics_bot_account_08.png and /dev/null differ diff --git a/docs/images/basics_bot_account_09.png b/docs/images/basics_bot_account_09.png deleted file mode 100644 index a65baef83a..0000000000 Binary files a/docs/images/basics_bot_account_09.png and /dev/null differ diff --git a/docs/images/basics_bot_account_10.png b/docs/images/basics_bot_account_10.png deleted file mode 100644 index fcd205186d..0000000000 Binary files a/docs/images/basics_bot_account_10.png and /dev/null differ diff --git a/docs/images/basics_first_bot_01.png b/docs/images/basics_first_bot_01.png deleted file mode 100644 index 1585521dd7..0000000000 Binary files a/docs/images/basics_first_bot_01.png and /dev/null differ diff --git a/docs/images/basics_first_bot_02.png b/docs/images/basics_first_bot_02.png deleted file mode 100644 index a17cc518b4..0000000000 Binary files a/docs/images/basics_first_bot_02.png and /dev/null differ diff --git a/docs/images/basics_first_bot_03.png b/docs/images/basics_first_bot_03.png deleted file mode 100644 index a0ae365ac5..0000000000 Binary files a/docs/images/basics_first_bot_03.png and /dev/null differ diff --git a/docs/images/basics_first_bot_04.png b/docs/images/basics_first_bot_04.png deleted file mode 100644 index 908055fe89..0000000000 Binary files a/docs/images/basics_first_bot_04.png and /dev/null differ diff --git a/docs/images/basics_first_bot_05.png b/docs/images/basics_first_bot_05.png deleted file mode 100644 index 1b4029bcd0..0000000000 Binary files a/docs/images/basics_first_bot_05.png and /dev/null differ diff --git a/docs/images/basics_first_bot_06.png b/docs/images/basics_first_bot_06.png deleted file mode 100644 index 60e75f15cb..0000000000 Binary files a/docs/images/basics_first_bot_06.png and /dev/null differ diff --git a/docs/images/basics_first_bot_07.png b/docs/images/basics_first_bot_07.png deleted file mode 100644 index 2267ee35fa..0000000000 Binary files a/docs/images/basics_first_bot_07.png and /dev/null differ diff --git a/docs/images/basics_first_bot_08.png b/docs/images/basics_first_bot_08.png deleted file mode 100644 index da3db90934..0000000000 Binary files a/docs/images/basics_first_bot_08.png and /dev/null differ diff --git a/docs/images/basics_first_bot_10.png b/docs/images/basics_first_bot_10.png deleted file mode 100644 index 6a6d8af27e..0000000000 Binary files a/docs/images/basics_first_bot_10.png and /dev/null differ diff --git a/docs/images/basics_first_bot_11.png b/docs/images/basics_first_bot_11.png deleted file mode 100644 index d10364d74c..0000000000 Binary files a/docs/images/basics_first_bot_11.png and /dev/null differ diff --git a/docs/images/basics_first_bot_12.png b/docs/images/basics_first_bot_12.png deleted file mode 100644 index cc867d8145..0000000000 Binary files a/docs/images/basics_first_bot_12.png and /dev/null differ diff --git a/docs/images/basics_first_bot_13.png b/docs/images/basics_first_bot_13.png deleted file mode 100644 index 68333989c7..0000000000 Binary files a/docs/images/basics_first_bot_13.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_default_01.png b/docs/images/beyond_basics_logging_default_01.png deleted file mode 100644 index dc9233e3c0..0000000000 Binary files a/docs/images/beyond_basics_logging_default_01.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_default_02.png b/docs/images/beyond_basics_logging_default_02.png deleted file mode 100644 index 1ea32daf30..0000000000 Binary files a/docs/images/beyond_basics_logging_default_02.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_default_03.png b/docs/images/beyond_basics_logging_default_03.png deleted file mode 100644 index 870aaa9471..0000000000 Binary files a/docs/images/beyond_basics_logging_default_03.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_third_party_01.png b/docs/images/beyond_basics_logging_third_party_01.png deleted file mode 100644 index f101c81891..0000000000 Binary files a/docs/images/beyond_basics_logging_third_party_01.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_user_01.png b/docs/images/beyond_basics_logging_user_01.png deleted file mode 100644 index b92abca84a..0000000000 Binary files a/docs/images/beyond_basics_logging_user_01.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_user_02.png b/docs/images/beyond_basics_logging_user_02.png deleted file mode 100644 index 0ad9f284d9..0000000000 Binary files a/docs/images/beyond_basics_logging_user_02.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_user_03.png b/docs/images/beyond_basics_logging_user_03.png deleted file mode 100644 index 18104b9b14..0000000000 Binary files a/docs/images/beyond_basics_logging_user_03.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_user_04.png b/docs/images/beyond_basics_logging_user_04.png deleted file mode 100644 index 43f2bdc4d8..0000000000 Binary files a/docs/images/beyond_basics_logging_user_04.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_user_05.png b/docs/images/beyond_basics_logging_user_05.png deleted file mode 100644 index 683a37feb9..0000000000 Binary files a/docs/images/beyond_basics_logging_user_05.png and /dev/null differ diff --git a/docs/images/beyond_basics_logging_user_06.png b/docs/images/beyond_basics_logging_user_06.png deleted file mode 100644 index 7ddcf4bcc9..0000000000 Binary files a/docs/images/beyond_basics_logging_user_06.png and /dev/null differ diff --git a/docs/images/commands_autocomplete_example.png b/docs/images/commands_autocomplete_example.png deleted file mode 100644 index 4cc26fb58c..0000000000 Binary files a/docs/images/commands_autocomplete_example.png and /dev/null differ diff --git a/docs/images/commands_choice_provider_example.png b/docs/images/commands_choice_provider_example.png deleted file mode 100644 index 6d79fe30b0..0000000000 Binary files a/docs/images/commands_choice_provider_example.png and /dev/null differ diff --git a/docs/images/commands_next_argument_converters_01.png b/docs/images/commands_next_argument_converters_01.png deleted file mode 100644 index 17c224e903..0000000000 Binary files a/docs/images/commands_next_argument_converters_01.png and /dev/null differ diff --git a/docs/images/commands_next_command_attributes_01.png b/docs/images/commands_next_command_attributes_01.png deleted file mode 100644 index a5551aef1e..0000000000 Binary files a/docs/images/commands_next_command_attributes_01.png and /dev/null differ diff --git a/docs/images/commands_next_dependency_injection_01.png b/docs/images/commands_next_dependency_injection_01.png deleted file mode 100644 index 08e1c73263..0000000000 Binary files a/docs/images/commands_next_dependency_injection_01.png and /dev/null differ diff --git a/docs/images/commands_next_help_formatter_01.png b/docs/images/commands_next_help_formatter_01.png deleted file mode 100644 index 93ad412d35..0000000000 Binary files a/docs/images/commands_next_help_formatter_01.png and /dev/null differ diff --git a/docs/images/commands_next_help_formatter_02.png b/docs/images/commands_next_help_formatter_02.png deleted file mode 100644 index 64c544afe9..0000000000 Binary files a/docs/images/commands_next_help_formatter_02.png and /dev/null differ diff --git a/docs/images/commands_next_intro_01.png b/docs/images/commands_next_intro_01.png deleted file mode 100644 index 05b953173e..0000000000 Binary files a/docs/images/commands_next_intro_01.png and /dev/null differ diff --git a/docs/images/commands_next_intro_02.png b/docs/images/commands_next_intro_02.png deleted file mode 100644 index df4627a6aa..0000000000 Binary files a/docs/images/commands_next_intro_02.png and /dev/null differ diff --git a/docs/images/commands_next_intro_03.png b/docs/images/commands_next_intro_03.png deleted file mode 100644 index 50e9ca3fff..0000000000 Binary files a/docs/images/commands_next_intro_03.png and /dev/null differ diff --git a/docs/images/commands_next_intro_04.png b/docs/images/commands_next_intro_04.png deleted file mode 100644 index d9e1dc7f91..0000000000 Binary files a/docs/images/commands_next_intro_04.png and /dev/null differ diff --git a/docs/images/commands_next_intro_05.png b/docs/images/commands_next_intro_05.png deleted file mode 100644 index cf537a8e54..0000000000 Binary files a/docs/images/commands_next_intro_05.png and /dev/null differ diff --git a/docs/images/commands_next_intro_06.png b/docs/images/commands_next_intro_06.png deleted file mode 100644 index dc09489abf..0000000000 Binary files a/docs/images/commands_next_intro_06.png and /dev/null differ diff --git a/docs/images/commands_next_intro_07.png b/docs/images/commands_next_intro_07.png deleted file mode 100644 index ff48bc04c1..0000000000 Binary files a/docs/images/commands_next_intro_07.png and /dev/null differ diff --git a/docs/images/commands_next_intro_08.png b/docs/images/commands_next_intro_08.png deleted file mode 100644 index 34271b1177..0000000000 Binary files a/docs/images/commands_next_intro_08.png and /dev/null differ diff --git a/docs/images/commands_ping_command_demonstration.png b/docs/images/commands_ping_command_demonstration.png deleted file mode 100644 index 1d8eaabc67..0000000000 Binary files a/docs/images/commands_ping_command_demonstration.png and /dev/null differ diff --git a/docs/images/commands_text_command_processor_example.png b/docs/images/commands_text_command_processor_example.png deleted file mode 100644 index 54316210ef..0000000000 Binary files a/docs/images/commands_text_command_processor_example.png and /dev/null differ diff --git a/docs/images/faq_spiderman.png b/docs/images/faq_spiderman.png deleted file mode 100644 index b72cc86da8..0000000000 Binary files a/docs/images/faq_spiderman.png and /dev/null differ diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico deleted file mode 100644 index 81ddb2d35f..0000000000 Binary files a/docs/images/favicon.ico and /dev/null differ diff --git a/docs/images/interactivity_01.png b/docs/images/interactivity_01.png deleted file mode 100644 index 770628b1be..0000000000 Binary files a/docs/images/interactivity_01.png and /dev/null differ diff --git a/docs/images/interactivity_02.png b/docs/images/interactivity_02.png deleted file mode 100644 index 707b5efa7b..0000000000 Binary files a/docs/images/interactivity_02.png and /dev/null differ diff --git a/docs/images/interactivity_03.png b/docs/images/interactivity_03.png deleted file mode 100644 index c293193c0c..0000000000 Binary files a/docs/images/interactivity_03.png and /dev/null differ diff --git a/docs/images/interactivity_04.png b/docs/images/interactivity_04.png deleted file mode 100644 index c113d10197..0000000000 Binary files a/docs/images/interactivity_04.png and /dev/null differ diff --git a/docs/images/interactivity_05.png b/docs/images/interactivity_05.png deleted file mode 100644 index a3b5c97952..0000000000 Binary files a/docs/images/interactivity_05.png and /dev/null differ diff --git a/docs/images/logo.png b/docs/images/logo.png deleted file mode 100644 index 22465b4df2..0000000000 Binary files a/docs/images/logo.png and /dev/null differ diff --git a/docs/images/logobig.png b/docs/images/logobig.png deleted file mode 100644 index 28e743b656..0000000000 Binary files a/docs/images/logobig.png and /dev/null differ diff --git a/docs/images/voicenext_receive_01.png b/docs/images/voicenext_receive_01.png deleted file mode 100644 index caa27de363..0000000000 Binary files a/docs/images/voicenext_receive_01.png and /dev/null differ diff --git a/docs/images/voicenext_transmit_01.png b/docs/images/voicenext_transmit_01.png deleted file mode 100644 index 32494213ac..0000000000 Binary files a/docs/images/voicenext_transmit_01.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index c3a3e35422..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -uid: index -title: DSharpPlus Documentation ---- - -

DSharpPlus Documentation

-DSharpPlus Logo - -[DSharpPlus][1] (D#+) is an unofficial .NET wrapper for the [Discord API][2] which was originally a fork of -[DiscordSharp][3]. The library has since been rewritten to fit quality and API standards as well as target modern .NET implementations. - -## Getting Started - -New users will want to take a look through the [articles][4] for quick start guides, tutorials, and examples of use. -Once you've gotten through the articles, head on over to the [API Documentation][5] for all classes and methods provided -by this library. - -## Source and Contributors - -DSharpPlus is licensed under MIT License, as detailed in the [license][6] found in the repository.
-The repository containing the source code for this library can be found [here][1]. Contributions are welcomed. - -DSharpPlus is built by [Naamloos][7], [Emzi0767][8], [Axiom][9], [afroraydude][10], [DrCreo][11], [TiaqoY0][12], -[Neuheit][13], [SSG/Maxine][14], [and many others...][15] - - -[1]: https://github.com/DSharpPlus/DSharpPlus "DSharpPlus GitHub repository" -[2]: https://discordapp.com/developers/docs/intro "Discord API documentation" -[3]: https://github.com/suicvne/DiscordSharp "DiscordSharp GitHub repository" -[4]: xref:articles.preamble -[5]: xref:apidocs -[6]: https://github.com/DSharpPlus/DSharpPlus/blob/master/LICENSE -[7]: https://github.com/Naamloos -[8]: https://github.com/Emzi0767 -[9]: https://github.com/suicvne -[10]: https://github.com/afroraydude -[11]: https://github.com/DrCreo -[12]: https://github.com/nick-strohm -[13]: https://github.com/Neuheit/ -[14]: https://github.com/uwx/ -[15]: https://github.com/DSharpPlus/DSharpPlus/graphs/contributors diff --git a/docs/langword_mapping.yml b/docs/langword_mapping.yml deleted file mode 100644 index 9ddd00cab0..0000000000 --- a/docs/langword_mapping.yml +++ /dev/null @@ -1,61 +0,0 @@ -references: -- uid: langword_csharp_null - name.csharp: "null" - name.vb: "Nothing" -- uid: langword_vb_Nothing - name.csharp: "null" - name.vb: "Nothing" -- uid: langword_csharp_static - name.csharp: static - name.vb: Shared -- uid: langword_vb_Shared - name.csharp: static - name.vb: Shared -- uid: langword_csharp_virtual - name.csharp: virtual - name.vb: Overridable -- uid: langword_vb_Overridable - name.csharp: virtual - name.vb: Overridable -- uid: langword_csharp_true - name.csharp: "true" - name.vb: "True" -- uid: langword_vb_True - name.csharp: "true" - name.vb: "True" -- uid: langword_csharp_false - name.csharp: "false" - name.vb: "False" -- uid: langword_vb_False - name.csharp: "false" - name.vb: "False" -- uid: langword_csharp_abstract - name.csharp: abstract - name.vb: MustInherit -- uid: langword_vb_MustInherit - name.csharp: abstract - name.vb: MustInherit -- uid: langword_csharp_sealed - name.csharp: sealed - name.vb: NotInheritable -- uid: langword_vb_NotInheritable - name.csharp: sealed - name.vb: NotInheritable -- uid: langword_csharp_async - name.csharp: async - name.vb: Async -- uid: langword_vb_Async - name.csharp: async - name.vb: Async -- uid: langword_csharp_await - name.csharp: await - name.vb: Await -- uid: langword_vb_Await - name.csharp: await - name.vb: Await -- uid: langword_csharp_async/await - name.csharp: async/await - name.vb: Async/Await -- uid: langword_vb_Async/Await - name.csharp: async/await - name.vb: Async/Await \ No newline at end of file diff --git a/docs/natives/index.md b/docs/natives/index.md deleted file mode 100644 index 92cd624678..0000000000 --- a/docs/natives/index.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -uid: natives -title: Native Libraries ---- - -#### Downloads - -Operating System|Download|Checksums|GPG Signatures ----|---|---|--- -64-bit Windows|[Click Here](/docs/natives/vnext_natives_win32_x64.zip)|[Click Here](/docs/natives/vnext_natives_win32_x64.zip.checksums)|[Click Here](/docs/natives/vnext_natives_win32_x64.zip.checksums.sig) -32-bit Windows|[Click Here](/docs/natives/vnext_natives_win32_x86.zip)|[Click Here](/docs/natives/vnext_natives_win32_x86.zip.checksums)|[Click Here](/docs/natives/vnext_natives_win32_x86.zip.checksums.sig) - -Signatures use key 246AB92A3C22030A. - -#### Licenses - -Library|License -:---:|:--- -Opus| -libsodium| diff --git a/docs/natives/vnext_natives_win32_x64.zip b/docs/natives/vnext_natives_win32_x64.zip deleted file mode 100644 index a447803e52..0000000000 Binary files a/docs/natives/vnext_natives_win32_x64.zip and /dev/null differ diff --git a/docs/natives/vnext_natives_win32_x64.zip.checksums b/docs/natives/vnext_natives_win32_x64.zip.checksums deleted file mode 100644 index 054098490e..0000000000 --- a/docs/natives/vnext_natives_win32_x64.zip.checksums +++ /dev/null @@ -1,5 +0,0 @@ -MD5(./vnext_natives_win32_x64.zip) = "1c37d3bb23ed42cdaf17351e2b327671" -SHA1(./vnext_natives_win32_x64.zip) = "ae7813c230c5ce6c3e7c3a778bc29c6991a6e87c" -SHA256(./vnext_natives_win32_x64.zip) = "b48a59895ab70643d77e954d589b7cde033581099431a42c2fcce70042fdd665" -SHA384(./vnext_natives_win32_x64.zip) = "36d9eab64bfcb453811ac036c51d70e4ddf23f334f0208cc47beabf0e85d415db062caa1bfbb6dee65093aeb9a88ff81" -SHA512(./vnext_natives_win32_x64.zip) = "173e7c8a7be70b641d4b6359925ee3c5aad70e228cf2640d9116947e42152e8776b47e656c755462763efc30702cb79a044eedeac0263d95121c12d58c378c74" diff --git a/docs/natives/vnext_natives_win32_x64.zip.checksums.sig b/docs/natives/vnext_natives_win32_x64.zip.checksums.sig deleted file mode 100644 index 64d38ebbb9..0000000000 Binary files a/docs/natives/vnext_natives_win32_x64.zip.checksums.sig and /dev/null differ diff --git a/docs/natives/vnext_natives_win32_x86.zip b/docs/natives/vnext_natives_win32_x86.zip deleted file mode 100644 index 35522e1ec7..0000000000 Binary files a/docs/natives/vnext_natives_win32_x86.zip and /dev/null differ diff --git a/docs/natives/vnext_natives_win32_x86.zip.checksums b/docs/natives/vnext_natives_win32_x86.zip.checksums deleted file mode 100644 index 123334a5ab..0000000000 --- a/docs/natives/vnext_natives_win32_x86.zip.checksums +++ /dev/null @@ -1,5 +0,0 @@ -MD5(./vnext_natives_win32_x86.zip) = "583eb13ec50af4ffcf19ad7367d06f7b" -SHA1(./vnext_natives_win32_x86.zip) = "576928a83952b6a5e3700f7effd9045cefb5e426" -SHA256(./vnext_natives_win32_x86.zip) = "7ec08d6bfdb3d6192d98af31363cd4660f3593feee34efa2f386b6f979969e09" -SHA384(./vnext_natives_win32_x86.zip) = "de25e35d9c214b9c53940bd37cd6b1d0a715714970b68ed1cc99c6d81d1ab75151806901f34b8a99fd79d1159171f5b5" -SHA512(./vnext_natives_win32_x86.zip) = "74d377d44f3c63a0fecb9842c54dea7a58d78a01b03125ecf450e711ff1dd766944a2342b5958e01c3337b26b24f4e6c021c8bf5340c833b355be98ce0e9bb49" diff --git a/docs/natives/vnext_natives_win32_x86.zip.checksums.sig b/docs/natives/vnext_natives_win32_x86.zip.checksums.sig deleted file mode 100644 index 52751932a3..0000000000 Binary files a/docs/natives/vnext_natives_win32_x86.zip.checksums.sig and /dev/null differ diff --git a/docs/toc.yml b/docs/toc.yml deleted file mode 100644 index 1acd62cdb7..0000000000 --- a/docs/toc.yml +++ /dev/null @@ -1,10 +0,0 @@ -- name: Articles - href: articles/ - topicUid: articles.preamble -- name: FAQ - topicUid: faq -- name: API Documentation - href: api/ - topicUid: apidocs -- name: Voice Natives - topicUid: natives diff --git a/dsharpplus.csx b/dsharpplus.csx new file mode 100644 index 0000000000..065307ed70 --- /dev/null +++ b/dsharpplus.csx @@ -0,0 +1,239 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:Spectre.Console, 0.47.1-preview.0.26" + +using System; +using System.Diagnostics; +using System.Linq; +using System.IO; + +using Spectre.Console; + +enum ToolType +{ + None, + Generator, + Analyzer +} + +readonly record struct ToolMetadata +{ + public required string Name { get; init; } + + public required ToolType Type { get; init; } + + public required string Subset { get; init; } +} + +ToolMetadata[] tools = +{ + new() + { + Name = "generate-concrete-objects", + Subset = "core", + Type = ToolType.Generator + }, + new() + { + Name = "generate-rest-payloads", + Subset = "core", + Type = ToolType.Generator + }, + new() + { + Name = "copy-concrete-implementations", + Subset = "extensions", + Type = ToolType.Generator + }, + new() + { + Name = "generate-serialization-registration", + Subset = "core", + Type = ToolType.Generator + } +}; + +string[] subsets = +{ + "core", + "extensions" +}; + +// executes a given tool +void ExecuteTool(string tool, ToolType type) +{ + ProcessStartInfo info = new ProcessStartInfo("dotnet"); + info.Arguments = type switch + { + ToolType.Generator => $"script ./tools/generators/{tool}.csx -c Release", + ToolType.Analyzer => $"script ./tools/analyzers/{tool}.csx -c Release", + _ => throw new InvalidOperationException($"Expected a valid tool type, found {type}.") + }; + + Process process = Process.Start(info)!; + process.WaitForExit(); +} + +// executes all tools belonging to a subject +void ExecuteSubset(string[] subset, ToolType type) +{ + foreach (ToolMetadata metadata in tools) + { + if (type != ToolType.None && metadata.Type != type) + { + continue; + } + + if (!subset.Contains(metadata.Subset)) + { + continue; + } + + ExecuteTool(metadata.Name, metadata.Type); + } +} + +// main entrypoint is here, at the big ass help command + +if (Args.Count == 1 && Args is ["--help" or "-h" or "-?"]) +{ + AnsiConsole.MarkupLine + ( + """ + This is the primary script controlling the DSharpPlus build. + + [lightgoldenrod2_2]Usage: dsharpplus [[action]] [/] + + [lightgoldenrod2_2]Actions:[/] + run Runs the given tools or subset of tools. + publish Publishes the given subset of the library. + + [lightgoldenrod2_2]Groups:[/] + [grey50]NOTE: Groups are only valid when operating on tools.[/] + + tools Indiscriminately operates on all kinds of tools. + generators Operates on tools intended to generate code or metadata. + analyzers Operates on tools intended to analyze and communicate the validity of existing code and metadata + + [lightgoldenrod2_2]Options:[/] + + -s|--subset Specifies one or more subsets to operate on. + -n|--name Specifies the individual name of a tools to operate on. + + [lightgoldenrod2_2]Examples:[/] + + The following command will run all core tools: + [grey50]dsharpplus run tools --subset core[/] + + The following command will run all core generators: + [grey50]dsharpplus run generators --subset core[/] + + The following command will only run a single tool, generate-concrete-objects: + [grey50]dsharpplus run --name generate-concrete-objects[/] + + The following command will build the core library as well as the caching logic: + [grey50]dsharpplus publish --subset core,cache[/] + """ + ); + + return 0; +} + +if (Args.Count >= 1 && Args[0] == "clear") +{ + Directory.Delete("./artifacts/hashes", true); + + AnsiConsole.MarkupLine("Cleared persisting cached data."); + return 0; +} + +if (Args.Count >= 1 && Args[0] == "publish") +{ + if (Args[1] == "-s" || Args[1] == "--subset") + { + if (Args.Count != 3) + { + AnsiConsole.MarkupLine("[red]Expected one argument to --subset.[/]"); + return 1; + } + + string[] loadedSubsets = Args[2].Split(','); + + foreach (string s in loadedSubsets) + { + Process.Start("dotnet", $"pack ./src/{s} --tl"); + } + + return 0; + } + + Process.Start("dotnet", "pack --tl"); + + return 0; +} + +if (Args[0] != "run") +{ + AnsiConsole.MarkupLine($"[red]The only supported top-level verbs are 'clear', 'run' and 'publish', found {Args[0]}.[/]"); + return 1; +} + +// we're now firmly in tooling territory + +switch (Args[1]) +{ + case "tools": + + if (Args.Count >= 4 && (Args[2] == "-s" || Args[2] == "--subset")) + { + ExecuteSubset(Args[3].Split(','), ToolType.None); + } + else + { + ExecuteSubset(subsets, ToolType.None); + } + break; + + case "generators": + + if (Args.Count >= 4 && (Args[2] == "-s" || Args[2] == "--subset")) + { + ExecuteSubset(Args[3].Split(','), ToolType.Generator); + } + else + { + ExecuteSubset(subsets, ToolType.Generator); + } + break; + + case "analyzers": + + if (Args.Count >= 4 && (Args[2] == "-s" || Args[2] == "--subset")) + { + ExecuteSubset(Args[3].Split(','), ToolType.Analyzer); + } + else + { + ExecuteSubset(subsets, ToolType.Analyzer); + } + break; + + case "-n": + case "--name": + + ToolType type = tools.Where(t => t.Name == Args[2]).Select(t => t.Type).FirstOrDefault(); + + if (type == ToolType.None) + { + AnsiConsole.MarkupLine($"[red]The tool {Args[2]} could not be found.[/]"); + return 1; + } + + ExecuteTool(Args[2], type); + + break; +} + +return 0; diff --git a/gen/common/DSharpPlus.SourceGenerators.Common.csproj b/gen/common/DSharpPlus.SourceGenerators.Common.csproj new file mode 100644 index 0000000000..543498f19b --- /dev/null +++ b/gen/common/DSharpPlus.SourceGenerators.Common.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + enable + enable + + + + + + + diff --git a/gen/common/Extensions/TypeSymbolExtensions.cs b/gen/common/Extensions/TypeSymbolExtensions.cs new file mode 100644 index 0000000000..526e47febb --- /dev/null +++ b/gen/common/Extensions/TypeSymbolExtensions.cs @@ -0,0 +1,57 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using Microsoft.CodeAnalysis; + +namespace DSharpPlus.SourceGenerators.Common.Extensions; + +/// +/// Contains convenience extensions on ITypeSymbol +/// +public static class TypeSymbolExtensions +{ + /// + /// Returns all public properties of a type, including those inherited from base types. If this type is an + /// interface, this returns all public properties of any base interfaces. + /// + /// + /// WARNING: This method comes with a drastic performance penalty. + /// + public static IEnumerable GetPublicProperties + ( + this ITypeSymbol type + ) + { + IEnumerable symbols = type.GetMembers() + .Where + ( + xm => xm is IPropertySymbol + { + DeclaredAccessibility: Accessibility.Public + } + ) + .Cast(); + + if (type.BaseType is not null) + { + symbols = symbols.Concat + ( + type.BaseType.GetPublicProperties() + ); + } + + if (type is { TypeKind: TypeKind.Interface, Interfaces: not { IsDefaultOrEmpty: true } }) + { + foreach (INamedTypeSymbol baseInterface in type.Interfaces) + { + symbols = symbols.Concat + ( + baseInterface.GetPublicProperties() + ); + } + } + + return symbols.Distinct(SymbolEqualityComparer.IncludeNullability).Cast(); + } +} diff --git a/lib/bundles b/lib/bundles new file mode 160000 index 0000000000..c6212e3406 --- /dev/null +++ b/lib/bundles @@ -0,0 +1 @@ +Subproject commit c6212e340614aaeadc40d137c09010dbc860f89d diff --git a/lib/etf b/lib/etf new file mode 160000 index 0000000000..0423eefb10 --- /dev/null +++ b/lib/etf @@ -0,0 +1 @@ +Subproject commit 0423eefb1080c119d093a2e19772f9e37e020821 diff --git a/logo/d#+.png b/logo/d#+.png deleted file mode 100644 index 9c625d3148..0000000000 Binary files a/logo/d#+.png and /dev/null differ diff --git a/logo/d#+.psd b/logo/d#+.psd deleted file mode 100644 index 43c9757a55..0000000000 Binary files a/logo/d#+.psd and /dev/null differ diff --git a/logo/d#+_smaller.png b/logo/d#+_smaller.png deleted file mode 100644 index dcc642b3e5..0000000000 Binary files a/logo/d#+_smaller.png and /dev/null differ diff --git a/logo/ds+.png b/logo/ds+.png deleted file mode 100644 index e480da793e..0000000000 Binary files a/logo/ds+.png and /dev/null differ diff --git a/logo/ds+.psd b/logo/ds+.psd deleted file mode 100644 index 7071af5794..0000000000 Binary files a/logo/ds+.psd and /dev/null differ diff --git a/logo/ds+_smaller.png b/logo/ds+_smaller.png deleted file mode 100644 index ac9fb47622..0000000000 Binary files a/logo/ds+_smaller.png and /dev/null differ diff --git a/logo/dsharp+.png b/logo/dsharp+.png deleted file mode 100644 index 84d35e34ed..0000000000 Binary files a/logo/dsharp+.png and /dev/null differ diff --git a/logo/dsharp+.psd b/logo/dsharp+.psd deleted file mode 100644 index a47496ae0b..0000000000 Binary files a/logo/dsharp+.psd and /dev/null differ diff --git a/logo/dsharp+_smaller.png b/logo/dsharp+_smaller.png deleted file mode 100644 index cbbf93ca62..0000000000 Binary files a/logo/dsharp+_smaller.png and /dev/null differ diff --git a/logo/dsharpplus.png b/logo/dsharpplus.png deleted file mode 100644 index 9c625d3148..0000000000 Binary files a/logo/dsharpplus.png and /dev/null differ diff --git a/meta/toolbox-concrete-types.json b/meta/toolbox-concrete-types.json new file mode 100644 index 0000000000..ad555cfccf --- /dev/null +++ b/meta/toolbox-concrete-types.json @@ -0,0 +1,15 @@ +[ + "./src/core/DSharpPlus.Internal.Models/Messages/Embed.cs", + "./src/core/DSharpPlus.Internal.Models/Interactions/InteractionResponse.cs", + "./src/core/DSharpPlus.Internal.Models/Interactions/ModalCallbackData.cs", + "./src/core/DSharpPlus.Internal.Models/Interactions/MessageCallbackData.cs", + "./src/core/DSharpPlus.Internal.Models/Components/ActionRowComponent.cs", + "./src/core/DSharpPlus.Internal.Models/Components/TextInputComponent.cs", + "./src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceDay.cs", + "./src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceRule.cs", + "./src/core/DSharpPlus.Internal.Rest/Payloads/Messages/CreateMessagePayload.cs", + "./src/core/DSharpPlus.Internal.Rest/Payloads/Messages/EditMessagePayload.cs", + "./src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/CreateFollowupMessagePayload.cs", + "./src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditFollowupMessagePayload.cs", + "./src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditInteractionResponsePayload.cs" +] \ No newline at end of file diff --git a/obsolete/DSharpPlus.SlashCommands/ApplicationCommandModule.cs b/obsolete/DSharpPlus.SlashCommands/ApplicationCommandModule.cs deleted file mode 100644 index 6612fd1629..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/ApplicationCommandModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.SlashCommands; - -/// -/// Represents a base class for slash command modules. -/// -public abstract class ApplicationCommandModule -{ - /// - /// Called before the execution of a slash command in the module. - /// - /// The context. - /// Whether or not to execute the slash command. - public virtual Task BeforeSlashExecutionAsync(InteractionContext ctx) - => Task.FromResult(true); - - /// - /// Called after the execution of a slash command in the module. - /// - /// The context. - /// - public virtual Task AfterSlashExecutionAsync(InteractionContext ctx) - => Task.CompletedTask; - - /// - /// Called before the execution of a context menu in the module. - /// - /// The context. - /// Whether or not to execute the slash command. - public virtual Task BeforeContextMenuExecutionAsync(ContextMenuContext ctx) - => Task.FromResult(true); - - /// - /// Called after the execution of a context menu in the module. - /// - /// The context. - /// - public virtual Task AfterContextMenuExecutionAsync(ContextMenuContext ctx) - => Task.CompletedTask; - -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/AutocompleteAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/AutocompleteAttribute.cs deleted file mode 100644 index 56abc0ad4a..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/AutocompleteAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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; - -namespace DSharpPlus.SlashCommands; - -/// -/// Handles autocomplete choices for a slash command parameter. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class AutocompleteAttribute : Attribute -{ - /// - /// The provider for this autocomplete parameter. - /// - public Type Provider { get; } - - /// - /// Handles autocomplete choices for a slash command parameter. - /// - /// The type of the autocomplete provider. This should inherit from . - public AutocompleteAttribute(Type provider) => this.Provider = provider; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/ChannelTypesAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/ChannelTypesAttribute.cs deleted file mode 100644 index 3b46f8f6d4..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/ChannelTypesAttribute.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Defines allowed channel types for a channel parameter. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class ChannelTypesAttribute : Attribute -{ - /// - /// Allowed channel types. - /// - public IEnumerable ChannelTypes { get; } - - /// - /// Defines allowed channel types for a channel parameter. - /// - /// The channel types to allow. - public ChannelTypesAttribute(params DiscordChannelType[] channelTypes) => this.ChannelTypes = channelTypes; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceAttribute.cs deleted file mode 100644 index 8ddfb16801..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceAttribute.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Adds a choice for this slash command option. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] -public sealed class ChoiceAttribute : Attribute -{ - /// - /// Gets the name of the choice. - /// - public string Name { get; } - - /// - /// Gets the value of the choice. - /// - public object Value { get; } - - /// - /// Adds a choice to the slash command option. - /// - /// The name of the choice. - /// The value of the choice. - public ChoiceAttribute(string name, string value) - { - this.Name = name; - this.Value = value; - } - - /// - /// Adds a choice to the slash command option. - /// - /// The name of the choice. - /// The value of the choice. - public ChoiceAttribute(string name, long value) - { - this.Name = name; - this.Value = value; - } - - /// - /// Adds a choice to the slash command option. - /// - /// The name of the choice. - /// The value of the choice. - public ChoiceAttribute(string name, double value) - { - this.Name = name; - this.Value = value; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceNameAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceNameAttribute.cs deleted file mode 100644 index bb3ee29b82..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceNameAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Sets the name for this enum choice. -/// -[AttributeUsage(AttributeTargets.All, AllowMultiple = false)] -public sealed class ChoiceNameAttribute : Attribute -{ - /// - /// The name. - /// - public string Name { get; } - - /// - /// Sets the name for this enum choice. - /// - /// The name for this enum choice. - public ChoiceNameAttribute(string name) => this.Name = name; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceProviderAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceProviderAttribute.cs deleted file mode 100644 index bdf73a817e..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/ChoiceProviderAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Sets a IChoiceProvider for a command options. ChoiceProviders can be used to provide -/// DiscordApplicationCommandOptionChoice from external sources such as a database. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] -public sealed class ChoiceProviderAttribute : Attribute -{ - /// - /// The type of the provider. - /// - public Type ProviderType { get; } - - /// - /// Adds a choice provider to this command. - /// - /// The type of the provider. - public ChoiceProviderAttribute(Type providerType) => this.ProviderType = providerType; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/ContextMenuAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/ContextMenuAttribute.cs deleted file mode 100644 index 90a586df13..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/ContextMenuAttribute.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Marks this method as a context menu. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public sealed class ContextMenuAttribute : Attribute -{ - /// - /// Gets the name of this context menu. - /// - public string Name { get; internal set; } - - /// - /// Gets the type of this context menu. - /// - public DiscordApplicationCommandType Type { get; internal set; } - - /// - /// Gets whether this command is enabled by default. - /// - public bool DefaultPermission { get; internal set; } - - /// - /// Gets whether this command is age restricted. - /// - public bool NSFW { get; } - - /// - /// Marks this method as a context menu. - /// - /// The type of the context menu. - /// The name of the context menu. - /// Sets whether the command should be enabled by default. - /// Sets whether the command is age restricted. - public ContextMenuAttribute(DiscordApplicationCommandType type, string name, bool defaultPermission = true, bool nsfw = false) - { - if (type == DiscordApplicationCommandType.SlashCommand) - { - throw new ArgumentException("Context menus cannot be of type SlashCommand."); - } - - this.Type = type; - this.Name = name; - this.DefaultPermission = defaultPermission; - this.NSFW = nsfw; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/DescriptionLocalizationAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/DescriptionLocalizationAttribute.cs deleted file mode 100644 index c66a0aa744..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/DescriptionLocalizationAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Specifies a locale for a slash command description. The longest description is the one that counts toward character limits. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = true)] -public sealed class DescriptionLocalizationAttribute : Attribute -{ - public string Locale { get; } - - public string Description { get; } - - public DescriptionLocalizationAttribute(Localization locale, string description) - { - this.Description = description; - this.Locale = LocaleHelper.LocaleToStrings[locale]; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/DontInjectAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/DontInjectAttribute.cs deleted file mode 100644 index b352fc21cc..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/DontInjectAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Prevents this field or property from having its value injected by dependency injection. -/// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] -public sealed class DontInjectAttribute : Attribute { } diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/GuildOnlyAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/GuildOnlyAttribute.cs deleted file mode 100644 index eda7a52af7..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/GuildOnlyAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Indicates that a global application command cannot be invoked in DMs. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class GuildOnlyAttribute : Attribute { } diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/InteractionCommandAllowedContextsAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/InteractionCommandAllowedContextsAttribute.cs deleted file mode 100644 index 68e8a5754a..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/InteractionCommandAllowedContextsAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Specifies the allowed interaction contexts for a command. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] -public sealed class InteractionCommandAllowedContextsAttribute(params DiscordInteractionContextType[] allowedContexts) : Attribute -{ - /// - /// The contexts the command is allowed to be used in. - /// - public IReadOnlyList AllowedContexts { get; } = allowedContexts; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/InteractionCommandInstallTypeAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/InteractionCommandInstallTypeAttribute.cs deleted file mode 100644 index 834021a4c1..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/InteractionCommandInstallTypeAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Specifies the installation context for a command or module. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] -public class InteractionCommandInstallTypeAttribute(params DiscordApplicationIntegrationType[] installTypes) : Attribute -{ - /// - /// The contexts the command is allowed to be installed to. - /// - public IReadOnlyList InstallTypes { get; } = installTypes; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/MaximumAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/MaximumAttribute.cs deleted file mode 100644 index 93cedf5472..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/MaximumAttribute.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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; - -namespace DSharpPlus.SlashCommands; - -/// -/// Sets a maximum value for this slash command option. Only valid for or parameters. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class MaximumAttribute : Attribute -{ - /// - /// The value. - /// - public object Value { get; internal set; } - - /// - /// Sets a maximum value for this slash command option. Only valid for or parameters. - /// - public MaximumAttribute(long value) => this.Value = value; - - /// - /// Sets a maximum value for this slash command option. Only valid for or parameters. - /// - public MaximumAttribute(double value) => this.Value = value; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/MaximumLengthAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/MaximumLengthAttribute.cs deleted file mode 100644 index dec0817dc0..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/MaximumLengthAttribute.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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; - -namespace DSharpPlus.SlashCommands; - -/// -/// Sets a maximum allowed length for this slash command option. Only valid for parameters. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class MaximumLengthAttribute : Attribute -{ - /// - /// The value. - /// - public int Value { get; internal set; } - - /// - /// Sets a maximum allowed length for this slash command option. Only valid for parameters. - /// - public MaximumLengthAttribute(int value) => this.Value = value; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/MinimumAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/MinimumAttribute.cs deleted file mode 100644 index 01fae65169..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/MinimumAttribute.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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; - -namespace DSharpPlus.SlashCommands; - -/// -/// Sets a minimum value for this slash command option. Only valid for or parameters. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class MinimumAttribute : Attribute -{ - /// - /// The value. - /// - public object Value { get; internal set; } - - /// - /// Sets a minimum value for this slash command option. Only valid for or parameters. - /// - public MinimumAttribute(long value) => this.Value = value; - - /// - /// Sets a minimum value for this slash command option. Only valid for or parameters. - /// - public MinimumAttribute(double value) => this.Value = value; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/MinimumLengthAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/MinimumLengthAttribute.cs deleted file mode 100644 index 67da3f403e..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/MinimumLengthAttribute.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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; - -namespace DSharpPlus.SlashCommands; - -/// -/// Sets a minimum allowed length for this slash command option. Only valid for parameters. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class MinimumLengthAttribute : Attribute -{ - /// - /// The value. - /// - public int Value { get; internal set; } - - /// - /// Sets a minimum allowed length for this slash command option. Only valid for parameters. - /// - public MinimumLengthAttribute(int value) => this.Value = value; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/NameLocalizationAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/NameLocalizationAttribute.cs deleted file mode 100644 index 3d2351af66..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/NameLocalizationAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Specifies a locale for a slash command name. The longest name is the name that counts toward character limits. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = true)] -public sealed class NameLocalizationAttribute : Attribute -{ - public string Locale { get; } - - public string Name { get; } - - public NameLocalizationAttribute(Localization locale, string name) - { - this.Name = name; - this.Locale = LocaleHelper.LocaleToStrings[locale]; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/OptionAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/OptionAttribute.cs deleted file mode 100644 index daea351e01..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/OptionAttribute.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Marks this parameter as an option for a slash command. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public sealed class OptionAttribute : Attribute -{ - /// - /// Gets the name of this option. - /// - public string Name { get; } - - /// - /// Gets the description of this option. - /// - public string Description { get; } - - /// - /// Gets whether this option should autocomplete. - /// - public bool Autocomplete { get; } - - /// - /// Marks this parameter as an option for a slash command. - /// - /// The name of the option. - /// The description of the option. - /// Whether this option should autocomplete. - public OptionAttribute(string name, string description, bool autocomplete = false) - { - if (name.Length > 32) - { - throw new ArgumentException("Slash command option names cannot go over 32 characters."); - } - - if (description.Length > 100) - { - throw new ArgumentException("Slash command option descriptions cannot go over 100 characters."); - } - - this.Name = name.ToLower(); - this.Description = description; - this.Autocomplete = autocomplete; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandAttribute.cs deleted file mode 100644 index f1ef03d5ca..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandAttribute.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Marks this method as a slash command. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public sealed class SlashCommandAttribute : Attribute -{ - /// - /// Gets the name of this command. - /// - public string Name { get; } - - /// - /// Gets the description of this command. - /// - public string Description { get; } - - /// - /// Gets whether this command is enabled by default. - /// - public bool DefaultPermission { get; } - - /// - /// Gets whether this command is age restricted. - /// - public bool NSFW { get; } - - /// - /// Marks this method as a slash command. - /// - /// Sets the name of this slash command. - /// Sets the description of this slash command. - /// Sets whether the command should be enabled by default. - /// Sets whether the command is age restricted. - public SlashCommandAttribute(string name, string description, bool defaultPermission = true, bool nsfw = false) - { - this.Name = name.ToLower(); - this.Description = description; - this.DefaultPermission = defaultPermission; - this.NSFW = nsfw; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandGroupAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandGroupAttribute.cs deleted file mode 100644 index 5f4e1f5d9a..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandGroupAttribute.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Marks this class a slash command group. -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] -public sealed class SlashCommandGroupAttribute : Attribute -{ - /// - /// Gets the name of this slash command group. - /// - public string Name { get; } - - /// - /// Gets the description of this slash command group. - /// - public string Description { get; } - - /// - /// Gets whether this command is enabled on default. - /// - public bool DefaultPermission { get; } - - /// - /// Gets whether this command is age restricted. - /// - public bool NSFW { get; } - - /// - /// Marks this class as a slash command group. - /// - /// Sets the name of this command group. - /// Sets the description of this command group. - /// Sets whether this command group is enabled on default. - /// Sets whether the command group is age restricted. - public SlashCommandGroupAttribute(string name, string description, bool defaultPermission = true, bool nsfw = false) - { - this.Name = name.ToLower(); - this.Description = description; - this.DefaultPermission = defaultPermission; - this.NSFW = nsfw; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandPermissionsAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandPermissionsAttribute.cs deleted file mode 100644 index befe9c7fff..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashCommandPermissionsAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] -public class SlashCommandPermissionsAttribute : Attribute -{ - public DiscordPermission[] Permissions { get; } - - public SlashCommandPermissionsAttribute(params DiscordPermission[] permissions) => this.Permissions = permissions; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashModuleLifespanAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Attributes/SlashModuleLifespanAttribute.cs deleted file mode 100644 index 2cd914a8c2..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Attributes/SlashModuleLifespanAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace DSharpPlus.SlashCommands; - -/// -/// Defines this slash command module's lifespan. Module lifespans are transient by default. -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] -public sealed class SlashModuleLifespanAttribute : Attribute -{ - /// - /// Gets the lifespan. - /// - public SlashModuleLifespan Lifespan { get; } - - /// - /// Defines this slash command module's lifespan. - /// - /// The lifespan of the module. Module lifespans are transient by default. - public SlashModuleLifespanAttribute(SlashModuleLifespan lifespan) => this.Lifespan = lifespan; -} - -/// -/// Represents a slash command module lifespan. -/// -public enum SlashModuleLifespan -{ - /// - /// Whether this module should be initiated every time a command is run, with dependencies injected from a scope. - /// - Scoped, - - /// - /// Whether this module should be initiated every time a command is run. - /// - Transient, - - /// - /// Whether this module should be initiated at startup. - /// - Singleton -} diff --git a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/ContextMenuCheckBaseAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/ContextMenuCheckBaseAttribute.cs deleted file mode 100644 index 64e6190b82..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/ContextMenuCheckBaseAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.SlashCommands; - -/// -/// The base class for a pre-execution check for a context menu. -/// -public abstract class ContextMenuCheckBaseAttribute : Attribute -{ - /// - /// Checks whether this command can be executed within the current context. - /// - /// The context. - /// Whether the checks passed. - public abstract Task ExecuteChecksAsync(ContextMenuContext ctx); -} diff --git a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashCheckBaseAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashCheckBaseAttribute.cs deleted file mode 100644 index 0618b634bf..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashCheckBaseAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.SlashCommands; - -/// -/// The base class for a pre-execution check for a slash command. -/// -public abstract class SlashCheckBaseAttribute : Attribute -{ - /// - /// Checks whether this command can be executed within the current context. - /// - /// The context. - /// Whether the checks passed. - public abstract Task ExecuteChecksAsync(InteractionContext ctx); -} diff --git a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashCooldownAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashCooldownAttribute.cs deleted file mode 100644 index 5cfb7cbd24..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashCooldownAttribute.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.SlashCommands.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 SlashCooldownAttribute : SlashCheckBaseAttribute -{ - /// - /// 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 SlashCooldownBucketType 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 SlashCooldownAttribute(int maxUses, double resetAfter, SlashCooldownBucketType 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 SlashCommandCooldownBucket GetBucket(InteractionContext ctx) - { - string bid = GetBucketId(ctx, out _, out _, out _); - buckets.TryGetValue(bid, out SlashCommandCooldownBucket? 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(InteractionContext ctx) - { - SlashCommandCooldownBucket? 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(InteractionContext ctx, out ulong userId, out ulong channelId, out ulong guildId) - { - userId = 0ul; - if (this.BucketType.HasFlag(SlashCooldownBucketType.User)) - { - userId = ctx.User.Id; - } - - channelId = 0ul; - if (this.BucketType.HasFlag(SlashCooldownBucketType.Channel)) - { - channelId = ctx.Channel.Id; - } - - guildId = 0ul; - if (this.BucketType.HasFlag(SlashCooldownBucketType.Guild)) - { - if (ctx.Guild == null) - { - channelId = ctx.Channel.Id; - } - else - { - guildId = ctx.Guild.Id; - } - } - - string bucketId = SlashCommandCooldownBucket.MakeId(ctx.QualifiedName, ctx.Client.CurrentUser.Id, userId, channelId, guildId); - return bucketId; - } - - public override async Task ExecuteChecksAsync(InteractionContext ctx) - { - string bucketId = GetBucketId(ctx, out ulong userId, out ulong channelId, out ulong guildId); - if (!buckets.TryGetValue(bucketId, out SlashCommandCooldownBucket? bucket)) - { - bucket = new SlashCommandCooldownBucket(ctx.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 SlashCooldownBucketType : 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 SlashCommandCooldownBucket : IEquatable -{ - /// - /// The command's full name (includes groups and subcommands). - /// - public string FullCommandName { get; } - - /// - /// The bot's ID. - /// - 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 SlashCommandCooldownBucket(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 succeeded 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 SlashCommandCooldownBucket cooldownBucket && Equals(cooldownBucket); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(SlashCommandCooldownBucket 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 ==(SlashCommandCooldownBucket bucket1, SlashCommandCooldownBucket 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 !=(SlashCommandCooldownBucket bucket1, SlashCommandCooldownBucket 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/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireBotPermissionsAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireBotPermissionsAttribute.cs deleted file mode 100644 index 15b7095f26..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireBotPermissionsAttribute.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands.Attributes; - -/// -/// Defines that usage of this slash command is only possible when the bot is granted a specific permission. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class SlashRequireBotPermissionsAttribute : SlashCheckBaseAttribute -{ - /// - /// Gets the permissions required by this attribute. - /// - public DiscordPermission[] Permissions { get; } - - /// - /// Gets or 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 bool IgnoreDms { get; } = true; - - /// - /// Defines that usage of this slash 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 SlashRequireBotPermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) - { - this.Permissions = permissions; - this.IgnoreDms = ignoreDms; - } - - /// - /// Runs checks. - /// - public override async Task ExecuteChecksAsync(InteractionContext ctx) - { - 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/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireDirectMessageAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireDirectMessageAttribute.cs deleted file mode 100644 index 96abe8d42e..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireDirectMessageAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands.Attributes; - -/// -/// Defines that this slash command is only usable within a direct message channel. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class SlashRequireDirectMessageAttribute : SlashCheckBaseAttribute -{ - /// - /// Defines that this command is only usable within a direct message channel. - /// - public SlashRequireDirectMessageAttribute() { } - - /// - /// Runs checks. - /// - public override Task ExecuteChecksAsync(InteractionContext ctx) => Task.FromResult(ctx.Channel is DiscordDmChannel); -} diff --git a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireGuildAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireGuildAttribute.cs deleted file mode 100644 index 56f6a40c9d..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireGuildAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.SlashCommands.Attributes; - -/// -/// Defines that this slash command is only usable within a guild. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class SlashRequireGuildAttribute : SlashCheckBaseAttribute -{ - /// - /// Defines that this command is only usable within a guild. - /// - public SlashRequireGuildAttribute() { } - - /// - /// Runs checks. - /// - public override Task ExecuteChecksAsync(InteractionContext ctx) => Task.FromResult(ctx.Guild != null); -} diff --git a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireOwnerAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireOwnerAttribute.cs deleted file mode 100644 index 27ef6e5971..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireOwnerAttribute.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.SlashCommands.Attributes; - -/// -/// Defines that this slash command is restricted to the owner of the bot. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class SlashRequireOwnerAttribute : SlashCheckBaseAttribute -{ - /// - /// Defines that this slash command is restricted to the owner of the bot. - /// - public SlashRequireOwnerAttribute() { } - - /// - /// Runs checks. - /// - public override Task ExecuteChecksAsync(InteractionContext ctx) - { - Entities.DiscordApplication app = ctx.Client.CurrentApplication; - 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/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequirePermissionsAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequirePermissionsAttribute.cs deleted file mode 100644 index 068b12bdbf..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequirePermissionsAttribute.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands.Attributes; - -/// -/// Defines that usage of this slash 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 SlashRequirePermissionsAttribute : SlashCheckBaseAttribute -{ - /// - /// Gets the permissions required by this attribute. - /// - public DiscordPermission[] Permissions { get; } - - /// - /// Gets or 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 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 SlashRequirePermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) - { - this.Permissions = permissions; - this.IgnoreDms = ignoreDms; - } - - /// - /// Runs checks. - /// - public override async Task ExecuteChecksAsync(InteractionContext ctx) - { - if (ctx.Guild == null) - { - return this.IgnoreDms; - } - - DiscordMember usr = ctx.Member; - if (usr == null) - { - return false; - } - - DiscordPermissions pusr = ctx.Channel.PermissionsFor(usr); - - DiscordMember bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id); - if (bot == null) - { - return false; - } - - DiscordPermissions pbot = ctx.Channel.PermissionsFor(bot); - - bool usrok = ctx.Guild.OwnerId == usr.Id; - bool botok = ctx.Guild.OwnerId == bot.Id; - - if (!usrok) - { - usrok = pusr.HasAllPermissions([..this.Permissions]); - } - - if (!botok) - { - botok = pbot.HasAllPermissions([..this.Permissions]); - } - - return usrok && botok; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireUserPermissionsAttribute.cs b/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireUserPermissionsAttribute.cs deleted file mode 100644 index dc05b3e7f2..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Built-in Checks/SlashRequireUserPermissionsAttribute.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands.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 SlashRequireUserPermissionsAttribute : SlashCheckBaseAttribute -{ - /// - /// Gets the permissions required by this attribute. - /// - public DiscordPermission[] Permissions { get; } - - /// - /// Gets or 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 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 SlashRequireUserPermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) - { - this.Permissions = permissions; - this.IgnoreDms = ignoreDms; - } - - /// - /// Runs checks. - /// - public override Task ExecuteChecksAsync(InteractionContext ctx) - { - 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/obsolete/DSharpPlus.SlashCommands/ChoiceProvider.cs b/obsolete/DSharpPlus.SlashCommands/ChoiceProvider.cs deleted file mode 100644 index 6a6af85b59..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/ChoiceProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Implementation of with access to service collection. -/// -public abstract class ChoiceProvider : IChoiceProvider -{ - /// - /// Sets the choices for the slash command. - /// - public abstract Task> Provider(); - - /// - /// Sets the service provider. - /// - public IServiceProvider Services { get; set; } - - /// - /// The optional ID of the Guild the command got registered for. - /// - public ulong? GuildId { get; set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/ContextMenuExecutionChecksFailedException.cs b/obsolete/DSharpPlus.SlashCommands/ContextMenuExecutionChecksFailedException.cs deleted file mode 100644 index 3c3159af59..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/ContextMenuExecutionChecksFailedException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.SlashCommands; - -/// -/// Thrown when a pre-execution check for a slash command fails. -/// -public sealed class ContextMenuExecutionChecksFailedException : Exception -{ - /// - /// The list of failed checks. - /// - public IReadOnlyList FailedChecks; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Contexts/AutocompleteContext.cs b/obsolete/DSharpPlus.SlashCommands/Contexts/AutocompleteContext.cs deleted file mode 100644 index 20f992d440..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Contexts/AutocompleteContext.cs +++ /dev/null @@ -1,94 +0,0 @@ -// 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 DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.SlashCommands; - -/// -/// Represents a context for an autocomplete interaction. -/// -public class AutocompleteContext -{ - /// - /// The interaction created. - /// - public DiscordInteraction Interaction { get; internal set; } - - /// - /// Gets the client for this interaction. - /// - public DiscordClient Client { get; internal set; } - - /// - /// Gets the guild this interaction was executed in. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the channel this interaction was executed in. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the user which executed this interaction. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the member which executed this interaction, or null if the command is in a DM. - /// - public DiscordMember Member - => this.User is DiscordMember member ? member : null; - - /// - /// Gets the slash command module this interaction was created in. - /// - public SlashCommandsExtension SlashCommandsExtension { get; internal set; } - - /// - /// Gets the service provider. - /// This allows passing data around without resorting to static members. - /// Defaults to null. - /// - public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true); - - /// - /// The options already provided. - /// - public IReadOnlyList Options { get; internal set; } - - /// - /// The option to auto-fill for. - /// - public DiscordInteractionDataOption FocusedOption { get; internal set; } - - /// - /// The given value of the focused option. - /// - public object OptionValue - => this.FocusedOption.Value; -} diff --git a/obsolete/DSharpPlus.SlashCommands/Contexts/BaseContext.cs b/obsolete/DSharpPlus.SlashCommands/Contexts/BaseContext.cs deleted file mode 100644 index 29368965e2..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Contexts/BaseContext.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.SlashCommands; - -/// -/// Represents a base context for application command contexts. -/// -public class BaseContext -{ - /// - /// Gets the interaction that was created. - /// - public DiscordInteraction Interaction { get; internal set; } - - /// - /// Gets the client for this interaction. - /// - public DiscordClient Client { get; internal set; } - - /// - /// Gets the guild this interaction was executed in. - /// - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the channel this interaction was executed in. - /// - public DiscordChannel Channel { get; internal set; } - - /// - /// Gets the user which executed this interaction. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the member which executed this interaction, or null if the command is in a DM. - /// - public DiscordMember Member - => this.User is DiscordMember member ? member : null; - - /// - /// Gets the slash command module this interaction was created in. - /// - public SlashCommandsExtension SlashCommandsExtension { get; internal set; } - - /// - /// Gets the token for this interaction. - /// - public string Token { get; internal set; } - - /// - /// Gets the id for this interaction. - /// - public ulong InteractionId { get; internal set; } - - /// - /// Gets the name of the command. - /// - public string CommandName { get; internal set; } - - /// - /// Gets the qualified name of the command. - /// - public string QualifiedName { get; internal set; } - - /// - /// Gets the type of this interaction. - /// - public DiscordApplicationCommandType Type { get; internal set; } - - /// - /// Gets the service provider. - /// This allows passing data around without resorting to static members. - /// Defaults to null. - /// - public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true); - - /// - /// 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. - /// - /// The type of the response. - /// The data to be sent, if any. - public Task CreateResponseAsync(DiscordInteractionResponseType type, DiscordInteractionResponseBuilder builder = null) - => this.Interaction.CreateResponseAsync(type, builder); - - /// - public Task CreateResponseAsync(DiscordInteractionResponseBuilder builder) - => CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, builder); - - /// - /// 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. - /// Whether the response should be ephemeral. - public Task CreateResponseAsync(string content, DiscordEmbed embed, bool ephemeral = false) - => CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent(content).AddEmbed(embed).AsEphemeral(ephemeral)); - - /// - public Task CreateResponseAsync(string content, bool ephemeral = false) - => CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent(content).AsEphemeral(ephemeral)); - - /// - public Task CreateResponseAsync(DiscordEmbed embed, bool ephemeral = false) - => CreateResponseAsync(new DiscordInteractionResponseBuilder().AddEmbed(embed).AsEphemeral(ephemeral)); - - /// - /// 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)); - - /// - /// Edits the interaction response. - /// - /// The data to edit the response with. - /// Attached files to keep. - /// - public Task EditResponseAsync(DiscordWebhookBuilder builder, IEnumerable attachments = default) - => this.Interaction.EditOriginalResponseAsync(builder, attachments); - - /// - /// Deletes the interaction response. - /// - /// - public Task DeleteResponseAsync() - => this.Interaction.DeleteOriginalResponseAsync(); - - /// - /// Creates a follow up message to the interaction. - /// - /// The message to be sent, in the form of a webhook. - /// The created message. - public Task FollowUpAsync(DiscordFollowupMessageBuilder builder) - => this.Interaction.CreateFollowupMessageAsync(builder); - - /// - /// Edits a followup message. - /// - /// The id of the followup message to edit. - /// The webhook builder. - /// Attached files to keep. - /// - public Task EditFollowupAsync(ulong followupMessageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - => this.Interaction.EditFollowupMessageAsync(followupMessageId, builder, attachments); - - /// - /// Deletes a followup message. - /// - /// The id of the followup message to delete. - /// - public Task DeleteFollowupAsync(ulong followupMessageId) - => this.Interaction.DeleteFollowupMessageAsync(followupMessageId); - - /// - /// Gets the original interaction response. - /// - /// The original interaction response. - public Task GetOriginalResponseAsync() - => this.Interaction.GetOriginalResponseAsync(); -} diff --git a/obsolete/DSharpPlus.SlashCommands/Contexts/ContextMenuContext.cs b/obsolete/DSharpPlus.SlashCommands/Contexts/ContextMenuContext.cs deleted file mode 100644 index 4852b6c90e..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Contexts/ContextMenuContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Represents a context for a context menu. -/// -public sealed class ContextMenuContext : BaseContext -{ - /// - /// The user this command targets, if applicable. - /// - public DiscordUser TargetUser { get; internal set; } - - /// - /// The member this command targets, if applicable. - /// - public DiscordMember TargetMember { get; internal set; } - - /// - /// The message this command targets, if applicable. - /// - public DiscordMessage TargetMessage { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/Contexts/InteractionContext.cs b/obsolete/DSharpPlus.SlashCommands/Contexts/InteractionContext.cs deleted file mode 100644 index 54a888bda3..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Contexts/InteractionContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// Represents a context for an interaction. -/// -public sealed class InteractionContext : BaseContext -{ - /// - /// Gets the users mentioned in the command parameters. - /// - public IReadOnlyList ResolvedUserMentions { get; internal set; } - - /// - /// Gets the roles mentioned in the command parameters. - /// - public IReadOnlyList ResolvedRoleMentions { get; internal set; } - - /// - /// Gets the channels mentioned in the command parameters. - /// - public IReadOnlyList ResolvedChannelMentions { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/DSharpPlus.SlashCommands.csproj b/obsolete/DSharpPlus.SlashCommands/DSharpPlus.SlashCommands.csproj deleted file mode 100644 index af9d99eb5a..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/DSharpPlus.SlashCommands.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - DSharpPlus.SlashCommands - An extension for DSharpPlus to make slash commands easy - $(PackageTags), commands, slash-commands, interactions - IDoEverything, Epictek, SakuraIsayeki, sssvt-drabek-stepan, tygore587, VelvetThePanda, OoLunar - true - CS0618 - - - - - \ No newline at end of file diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/AutocompleteErrorEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/AutocompleteErrorEventArgs.cs deleted file mode 100644 index 4a926acff3..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/AutocompleteErrorEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents arguments for a event. -/// -public class AutocompleteErrorEventArgs : AsyncEventArgs -{ - /// - /// The exception thrown. - /// - public Exception Exception { get; internal set; } - - /// - /// The context of the autocomplete. - /// - public AutocompleteContext Context { get; internal set; } - - /// - /// The type of the provider. - /// - public Type ProviderType { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/AutocompleteExecutedEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/AutocompleteExecutedEventArgs.cs deleted file mode 100644 index bdf5915125..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/AutocompleteExecutedEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents arguments for a event. -/// -public class AutocompleteExecutedEventArgs : AsyncEventArgs -{ - /// - /// The context of the autocomplete. - /// - public AutocompleteContext Context { get; internal set; } - - /// - /// The type of the provider. - /// - public Type ProviderType { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuErrorEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuErrorEventArgs.cs deleted file mode 100644 index 6bc4ea9574..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuErrorEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents arguments for a -/// -public class ContextMenuErrorEventArgs : AsyncEventArgs -{ - /// - /// The context of the command. - /// - public ContextMenuContext Context { get; internal set; } - - /// - /// The exception thrown. - /// - public Exception Exception { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuExecutedEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuExecutedEventArgs.cs deleted file mode 100644 index 002fee3842..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuExecutedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents arguments for a -/// -public sealed class ContextMenuExecutedEventArgs : AsyncEventArgs -{ - /// - /// The context of the command. - /// - public ContextMenuContext Context { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuInvokedEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuInvokedEventArgs.cs deleted file mode 100644 index db05027659..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/ContextMenuInvokedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents arguments for a -/// -public sealed class ContextMenuInvokedEventArgs : AsyncEventArgs -{ - /// - /// The context of the command. - /// - public ContextMenuContext Context { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandErrorEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandErrorEventArgs.cs deleted file mode 100644 index a68375407e..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandErrorEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents arguments for a event. -/// -public sealed class SlashCommandErrorEventArgs : AsyncEventArgs -{ - /// - /// The context of the command. - /// - public InteractionContext Context { get; internal set; } - - /// - /// The exception thrown. - /// - public Exception Exception { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandExecutedEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandExecutedEventArgs.cs deleted file mode 100644 index 5fc450e437..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandExecutedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents the arguments for a event. -/// -public sealed class SlashCommandExecutedEventArgs : AsyncEventArgs -{ - /// - /// The context of the command. - /// - public InteractionContext Context { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandInvokedEventArgs.cs b/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandInvokedEventArgs.cs deleted file mode 100644 index 592f587e66..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/EventArgs/SlashCommandInvokedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.SlashCommands.EventArgs; - -/// -/// Represents the arguments for a event. -/// -public sealed class SlashCommandInvokedEventArgs : AsyncEventArgs -{ - /// - /// The context of the command. - /// - public InteractionContext Context { get; internal set; } -} diff --git a/obsolete/DSharpPlus.SlashCommands/ExtensionMethods.cs b/obsolete/DSharpPlus.SlashCommands/ExtensionMethods.cs deleted file mode 100644 index 0b2fccf011..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/ExtensionMethods.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; - -using DSharpPlus.Extensions; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.SlashCommands; - -/// -/// Defines various extension methods for slash commands. -/// -public static class ExtensionMethods -{ - /// - /// Adds the slash commands extension to the provided service collection. - /// - /// The service collection to register into. - /// Any setup code you want to run on the extension, such as registering commands. - /// The same service collection for chaining. - [Obsolete("DSharpPlus.SlashCommands is obsolete. Please consider using the new DSharpPlus.Commands extension instead.")] - public static IServiceCollection AddSlashCommandsExtension - ( - this IServiceCollection services, - Action setup - ) - { - services.ConfigureEventHandlers(b => b.AddEventHandlers()) - .AddSingleton(provider => - { - DiscordClient client = provider.GetRequiredService(); - - SlashCommandsExtension extension = new(provider); - extension.Setup(client); - setup(extension); - - return extension; - }); - - return services; - } - - /// - /// Adds the slash commands extension to the provided DiscordClientBuilder. - /// - /// The client builder to register with. - /// Any setup code you want to run on the extension, such as registering commands. - /// The same client builder for chaining. - [Obsolete("DSharpPlus.SlashCommands is obsolete. Please consider using the new DSharpPlus.Commands extension instead.")] - public static DiscordClientBuilder UseSlashCommands - ( - this DiscordClientBuilder builder, - Action setup - ) - => builder.ConfigureServices(s => s.AddSlashCommandsExtension(setup)); - - /// - /// Gets the name from the for this enum value. - /// - /// The name. - public static string GetName(this T e) where T : IConvertible - { - if (e is Enum) - { - Type type = e.GetType(); - Array values = Enum.GetValues(type); - - foreach (int val in values) - { - if (val == e.ToInt32(CultureInfo.InvariantCulture)) - { - System.Reflection.MemberInfo[] memInfo = type.GetMember(type.GetEnumName(val)); - - return memInfo[0].GetCustomAttributes(typeof(ChoiceNameAttribute), false).FirstOrDefault() is ChoiceNameAttribute nameAttribute - ? nameAttribute.Name - : type.GetEnumName(val); - } - } - } - return null; - } -} diff --git a/obsolete/DSharpPlus.SlashCommands/IAutocompleteProvider.cs b/obsolete/DSharpPlus.SlashCommands/IAutocompleteProvider.cs deleted file mode 100644 index 6c558d0bc0..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/IAutocompleteProvider.cs +++ /dev/null @@ -1,40 +0,0 @@ -// 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; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// All autocomplete providers must inherit from this interface. -/// -public interface IAutocompleteProvider -{ - /// - /// Provides autocomplete choices. - /// - /// The autocomplete context. - public Task> Provider(AutocompleteContext ctx); -} diff --git a/obsolete/DSharpPlus.SlashCommands/IChoiceProvider.cs b/obsolete/DSharpPlus.SlashCommands/IChoiceProvider.cs deleted file mode 100644 index 95799091fe..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/IChoiceProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.SlashCommands; - -/// -/// All choice providers must inherit from this interface. -/// -public interface IChoiceProvider -{ - /// - /// Sets the choices for the slash command. - /// - public Task> Provider(); -} diff --git a/obsolete/DSharpPlus.SlashCommands/LICENSE b/obsolete/DSharpPlus.SlashCommands/LICENSE deleted file mode 100644 index c239fcd5f2..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Adi Mathur - -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. diff --git a/obsolete/DSharpPlus.SlashCommands/Localization.cs b/obsolete/DSharpPlus.SlashCommands/Localization.cs deleted file mode 100644 index 359ee79e22..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/Localization.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Collections.Generic; - -namespace DSharpPlus.SlashCommands; - -/// -/// Supported locals for slash command localizations. -/// -public enum Localization -{ - AmericanEnglish, - BritishEnglish, - Bulgarian, - Chinese, - Taiwanese, - Croatian, - Czech, - Danish, - Dutch, - Finnish, - French, - German, - Greek, - Hindi, - Hungarian, - Italian, - Japanese, - Korean, - Lithuanian, - Norwegian, - Polish, - Portuguese, - Romanian, - Spanish, - Swedish, - Thai, - Turkish, - Ukrainian, - Vietnamese, - Russian -} - -/// -/// A helper class that provides a list of supported localizations. -/// -public static class LocaleHelper -{ - /// - /// A dictionary of supported localizations. - /// - public static readonly Dictionary LocaleToStrings = new() - { - [Localization.AmericanEnglish] = "en-US", - [Localization.BritishEnglish] = "en-GB", - [Localization.Bulgarian] = "bg", - [Localization.Chinese] = "zh-CN", - [Localization.Taiwanese] = "zh-TW", - [Localization.Croatian] = "hr", - [Localization.Czech] = "cs", - [Localization.Danish] = "da", - [Localization.Dutch] = "nl", - [Localization.Finnish] = "fi", - [Localization.French] = "fr", - [Localization.German] = "de", - [Localization.Greek] = "el", - [Localization.Hindi] = "hi", - [Localization.Hungarian] = "hu", - [Localization.Italian] = "it", - [Localization.Japanese] = "ja", - [Localization.Korean] = "ko", - [Localization.Lithuanian] = "lt", - [Localization.Norwegian] = "no", - [Localization.Polish] = "pl", - [Localization.Portuguese] = "pt-BR", - [Localization.Romanian] = "ro", - [Localization.Spanish] = "es-ES", - [Localization.Swedish] = "sv-SE", - [Localization.Thai] = "th", - [Localization.Turkish] = "tr", - [Localization.Ukrainian] = "uk", - [Localization.Vietnamese] = "vi", - [Localization.Russian] = "ru" - }; - - public static readonly Dictionary StringsToLocale = new() - { - ["en-US"] = Localization.AmericanEnglish, - ["en-GB"] = Localization.BritishEnglish, - ["bg"] = Localization.Bulgarian, - ["zh-CN"] = Localization.Chinese, - ["zh-TW"] = Localization.Taiwanese, - ["hr"] = Localization.Croatian, - ["cs"] = Localization.Czech, - ["da"] = Localization.Danish, - ["nl"] = Localization.Dutch, - ["fi"] = Localization.Finnish, - ["fr"] = Localization.French, - ["de"] = Localization.German, - ["el"] = Localization.Greek, - ["hi"] = Localization.Hindi, - ["hu"] = Localization.Hungarian, - ["it"] = Localization.Italian, - ["ja"] = Localization.Japanese, - ["ko"] = Localization.Korean, - ["lt"] = Localization.Lithuanian, - ["no"] = Localization.Norwegian, - ["pl"] = Localization.Polish, - ["pt-BR"] = Localization.Portuguese, - ["ro"] = Localization.Romanian, - ["es-ES"] = Localization.Spanish, - ["sv-SE"] = Localization.Swedish, - ["th"] = Localization.Thai, - ["tr"] = Localization.Turkish, - ["uk"] = Localization.Ukrainian, - ["vi"] = Localization.Vietnamese, - ["ru"] = Localization.Russian - }; -} diff --git a/obsolete/DSharpPlus.SlashCommands/SlashCommandsEventHandler.cs b/obsolete/DSharpPlus.SlashCommands/SlashCommandsEventHandler.cs deleted file mode 100644 index 3006ec751a..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/SlashCommandsEventHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -using DSharpPlus.EventArgs; - -namespace DSharpPlus.SlashCommands; - -internal class SlashCommandsEventHandler - : IEventHandler, - IEventHandler, - IEventHandler -{ - private readonly SlashCommandsExtension extension; - - public SlashCommandsEventHandler(SlashCommandsExtension ext) - => this.extension = ext; - - public async Task HandleEventAsync(DiscordClient sender, SessionCreatedEventArgs eventArgs) - => await this.extension.Update(sender, eventArgs); - - public async Task HandleEventAsync(DiscordClient sender, InteractionCreatedEventArgs eventArgs) - => await this.extension.InteractionHandler(sender, eventArgs); - - public async Task HandleEventAsync(DiscordClient sender, ContextMenuInteractionCreatedEventArgs eventArgs) - => await this.extension.ContextMenuHandler(sender, eventArgs); -} diff --git a/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs b/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs deleted file mode 100644 index e90214ce01..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs +++ /dev/null @@ -1,2044 +0,0 @@ -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/obsolete/DSharpPlus.SlashCommands/SlashExecutionChecksFailedException.cs b/obsolete/DSharpPlus.SlashCommands/SlashExecutionChecksFailedException.cs deleted file mode 100644 index 948c37c539..0000000000 --- a/obsolete/DSharpPlus.SlashCommands/SlashExecutionChecksFailedException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.SlashCommands; - -/// -/// Thrown when a pre-execution check for a slash command fails. -/// -public sealed class SlashExecutionChecksFailedException : Exception -{ - /// - /// The list of failed checks. - /// - public IReadOnlyList FailedChecks; -} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000000..a0ad4a73ee --- /dev/null +++ b/readme.md @@ -0,0 +1,18 @@ +# DSharpPlus, version 6 + +DSharpPlus v6 is a ground-up rewrite of the library, focused on performance, modernity, maintainability and correctness. +Development of this version is occurring simultaneously with development of DSharpPlus v5, and is tracked on this branch, +while DSharpPlus v5 is tracked on the master branch. + +## Licensing + +DSharpPlus is based off DiscordSharp, whose license can be found [here](./DiscordSharp.Old.License). + +DSharpPlus, up to version 5.x, uses the MIT License, which can be found [here](./DSharpPlus.Old.License) + +DSharpPlus, starting with version 6.x, uses the Mozilla Public License, v2.0, which can be found [here](./LICENSE). +This version does not contain any code from previous versions, and is built from ground up. + +The following contributor(s) to v6 and up have chosen to license their code under the [MIT License](./LICENSE-MIT) +in addition to MPL-2.0: +- [akiraveliara](https://github.com/akiraveliara) diff --git a/src/core/DSharpPlus.Core.sln b/src/core/DSharpPlus.Core.sln new file mode 100644 index 0000000000..ac0da204da --- /dev/null +++ b/src/core/DSharpPlus.Core.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSharpPlus.Internal.Abstractions.Models", "DSharpPlus.Internal.Abstractions.Models\DSharpPlus.Internal.Abstractions.Models.csproj", "{AF8283F9-AC6C-46AB-A4FC-D331C64BFFC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSharpPlus.Internal.Models", "DSharpPlus.Internal.Models\DSharpPlus.Internal.Models.csproj", "{9699C432-CC00-48D3-8534-D949C77C6B87}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSharpPlus.Shared", "DSharpPlus.Shared\DSharpPlus.Shared.csproj", "{E9B2D26F-5E11-4198-8E00-A7780572C480}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSharpPlus.Internal.Abstractions.Rest", "DSharpPlus.Internal.Abstractions.Rest\DSharpPlus.Internal.Abstractions.Rest.csproj", "{16EFED50-A695-48C3-8B05-C52381C13C9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSharpPlus.Internal.Rest", "DSharpPlus.Internal.Rest\DSharpPlus.Internal.Rest.csproj", "{4ECD4676-81FD-4E16-B025-D6AE11638E02}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AF8283F9-AC6C-46AB-A4FC-D331C64BFFC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF8283F9-AC6C-46AB-A4FC-D331C64BFFC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF8283F9-AC6C-46AB-A4FC-D331C64BFFC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF8283F9-AC6C-46AB-A4FC-D331C64BFFC9}.Release|Any CPU.Build.0 = Release|Any CPU + {9699C432-CC00-48D3-8534-D949C77C6B87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9699C432-CC00-48D3-8534-D949C77C6B87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9699C432-CC00-48D3-8534-D949C77C6B87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9699C432-CC00-48D3-8534-D949C77C6B87}.Release|Any CPU.Build.0 = Release|Any CPU + {E9B2D26F-5E11-4198-8E00-A7780572C480}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9B2D26F-5E11-4198-8E00-A7780572C480}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9B2D26F-5E11-4198-8E00-A7780572C480}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9B2D26F-5E11-4198-8E00-A7780572C480}.Release|Any CPU.Build.0 = Release|Any CPU + {16EFED50-A695-48C3-8B05-C52381C13C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16EFED50-A695-48C3-8B05-C52381C13C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16EFED50-A695-48C3-8B05-C52381C13C9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16EFED50-A695-48C3-8B05-C52381C13C9B}.Release|Any CPU.Build.0 = Release|Any CPU + {4ECD4676-81FD-4E16-B025-D6AE11638E02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4ECD4676-81FD-4E16-B025-D6AE11638E02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4ECD4676-81FD-4E16-B025-D6AE11638E02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4ECD4676-81FD-4E16-B025-D6AE11638E02}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/.editorconfig b/src/core/DSharpPlus.Internal.Abstractions.Models/.editorconfig new file mode 100644 index 0000000000..3e37f7b31f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/.editorconfig @@ -0,0 +1,2 @@ +# don't force namespaces matching folder paths +dotnet_style_namespace_match_folder = false \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommand.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommand.cs new file mode 100644 index 0000000000..c8a2d3baf9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommand.cs @@ -0,0 +1,90 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an application command, either as a chat input, a message context menu or an user context +/// menu command. +/// +public interface IApplicationCommand +{ + /// + /// The snowflake identifier of this command. + /// + public Snowflake Id { get; } + + /// + /// The type of this command, default: . + /// + public Optional Type { get; } + + /// + /// The snowflake identifier of the application owning this command. + /// + public Snowflake ApplicationId { get; } + + /// + /// If this command is a guild command, the snowflake identifier of its home guild. + /// + public Optional GuildId { get; } + + /// + /// The name of this command, between 1 and 32 characters. + /// + public string Name { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> NameLocalizations { get; } + + /// + /// If this command is a command, the + /// description of this command, between 1 and 100 characters. This is an empty string for all + /// other command types. + /// + public string Description { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> DescriptionLocalizations { get; } + + /// + /// The parameters of this (chat input) command, up to 25. + /// + public Optional> Options { get; } + + /// + /// The permissions needed to gain default access to this command. + /// + public DiscordPermissions? DefaultMemberPermissions { get; } + + /// + /// Indicates whether this command is age-restricted. + /// + public Optional Nsfw { get; } + + /// + /// Specifies installation contexts where this command is available; only for globally-scoped commands. Defaults to + /// . + /// + public Optional> IntegrationTypes { get; } + + /// + /// Specifies contexts where this command can be used; only for globally-scoped commands. Defaults to including all + /// context types. + /// + public Optional?> Contexts { get; } + + /// + /// An autoincrementing version identifier updated during substantial changes. + /// + public Snowflake Version { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandOption.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandOption.cs new file mode 100644 index 0000000000..fd7f826380 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandOption.cs @@ -0,0 +1,98 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +using OneOf; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single option for a chat input application command. +/// +public interface IApplicationCommandOption +{ + /// + /// Signifies the type of this option. + /// + public DiscordApplicationCommandOptionType Type { get; } + + /// + /// The name of this option or subcommand, between 1 and 32 characters. + /// + public string Name { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> NameLocalizations { get; } + + /// + /// The description of this option or subcommand, between 1 and 100 characters. + /// + public string Description { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> DescriptionLocalizations { get; } + + /// + /// Specifies whether this parameter is required or optional, default: optional. + /// + public Optional Required { get; } + + /// + /// If this application command option is of , + /// or + /// , up to 25 options to choose from. + /// These options will be the only valid options for this command. + /// + public Optional> Choices { get; } + + /// + /// If this option is of or + /// , these options will be the + /// parameters (or subcommands if this is a subcommand group). + /// + public Optional> Options { get; } + + /// + /// If this option is of , shown + /// channels will be restricted to these types. + /// + public Optional> ChannelTypes { get; } + + /// + /// If this option is of or + /// , the minimum value permitted. + /// + public Optional> MinValue { get; } + + /// + /// If this option is of or + /// , the maximum value permitted. + /// + public Optional> MaxValue { get; } + + /// + /// If this option is of , the minimum + /// length permitted, between 0 and 6000. + /// + public Optional MinLength { get; } + + /// + /// If this option is of , the maximum + /// length permitted, between 1 and 6000. + /// + public Optional MaxLength { get; } + + /// + /// Indicates whether this option is subject to autocomplete. This is mutually exclusive with + /// being defined. + /// + public Optional Autocomplete { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandOptionChoice.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandOptionChoice.cs new file mode 100644 index 0000000000..045aaf4b96 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandOptionChoice.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using OneOf; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Specifies one choice for a . +/// +public interface IApplicationCommandOptionChoice +{ + /// + /// The name of this choice, 1 to 100 characters. + /// + public string Name { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> NameLocalizations { get; } + + /// + /// The value of this choice, up to 100 characters if this is a string. + /// + public OneOf Value { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandPermission.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandPermission.cs new file mode 100644 index 0000000000..8454c429dc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandPermission.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single permission override for an application command inside a guild. +/// +public interface IApplicationCommandPermission +{ + /// + /// The snowflake identifier of the target of this override, or a permission constant. + /// + /// + /// The snowflake identifier of the current guild targets the @everyone role, the + /// snowflake identifier - 1 targets all channels in the guild. + /// + public Snowflake Id { get; } + + /// + /// The type of the entity this override targets. + /// + public DiscordApplicationCommandPermissionType Type { get; } + + /// + /// Indicates whether this command is allowed or not. + /// + public bool Permission { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandPermissions.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandPermissions.cs new file mode 100644 index 0000000000..14314093dc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IApplicationCommandPermissions.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a collection of permissions for an application command in a guild +/// +public interface IApplicationCommandPermissions : IPartialApplicationCommandPermissions +{ + /// + public new Snowflake Id { get; } + + /// + public new Snowflake ApplicationId { get; } + + /// + public new Snowflake GuildId { get; } + + /// + public new IReadOnlyList Permissions { get; } + + // partial access routes + + /// + Optional IPartialApplicationCommandPermissions.Id => this.Id; + + /// + Optional IPartialApplicationCommandPermissions.ApplicationId => this.ApplicationId; + + /// + Optional IPartialApplicationCommandPermissions.GuildId => this.GuildId; + + /// + Optional> IPartialApplicationCommandPermissions.Permissions => new(this.Permissions); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IPartialApplicationCommandPermissions.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IPartialApplicationCommandPermissions.cs new file mode 100644 index 0000000000..03e11454f9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ApplicationCommands/IPartialApplicationCommandPermissions.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partial set of application command permissions. +/// +public interface IPartialApplicationCommandPermissions +{ + /// + /// The snowflake identifier of this command. + /// + public Optional Id { get; } + + /// + /// The snowflake identifier of the application this command belongs to. + /// + public Optional ApplicationId { get; } + + /// + /// The snowflake identifier of the guild to which these permissions apply. + /// + public Optional GuildId { get; } + + /// + /// The permission overrides for this command in this guild. + /// + public Optional> Permissions { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IActivityInstance.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IActivityInstance.cs new file mode 100644 index 0000000000..a975058d25 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IActivityInstance.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a live instance of a running activity. +/// +public interface IActivityInstance +{ + /// + /// The snowflake identifier of the application executing the activity. + /// + public Snowflake ApplicationId { get; } + + /// + /// An unique identifier of the current activity instance. + /// + public string InstanceId { get; } + + /// + /// A snowflake identifier created for the launch of this activity. + /// + public Snowflake LaunchId { get; } + + /// + /// The guild and channel this activity is running in. + /// + public IActivityLocation Location { get; } + + /// + /// The snowflake identifiers of users currently connected to this instance. + /// + public IReadOnlyList Users { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IActivityLocation.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IActivityLocation.cs new file mode 100644 index 0000000000..e437ada735 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IActivityLocation.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Specifies the location an activity is running in. +/// +public interface IActivityLocation +{ + /// + /// A unique identifier for this location. + /// + public string Id { get; } + + /// + /// Specifies what this location is: "gc" for a guild channel, "pc" for a private channel such as direct message + /// or group chat. + /// + public string Kind { get; } + + /// + /// The snowflake identifier of the channel hosting the activity. + /// + public Snowflake ChannelId { get; } + + /// + /// The snowflake identifier of the guild hosting the activity. + /// + public Optional GuildId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IApplication.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IApplication.cs new file mode 100644 index 0000000000..c3b871705c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IApplication.cs @@ -0,0 +1,55 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an application, such as a bot, on Discord. +/// +public interface IApplication : IPartialApplication +{ + /// + public new Snowflake Id { get; } + + /// + public new string Name { get; } + + /// + public new string? Icon { get; } + + /// + public new bool BotPublic { get; } + + /// + public new bool BotRequireCodeGrant { get; } + + /// + public new string VerifyKey { get; } + + /// + public new ITeam? Team { get; } + + // explicit routes for partial application access + + /// + Optional IPartialApplication.Id => this.Id; + + /// + Optional IPartialApplication.Name => this.Name; + + /// + Optional IPartialApplication.Icon => this.Icon; + + /// + Optional IPartialApplication.BotPublic => this.BotPublic; + + /// + Optional IPartialApplication.BotRequireCodeGrant => this.BotRequireCodeGrant; + + /// + Optional IPartialApplication.VerifyKey => this.VerifyKey; + + /// + Optional IPartialApplication.Team => new(this.Team); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IApplicationIntegrationTypeConfiguration.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IApplicationIntegrationTypeConfiguration.cs new file mode 100644 index 0000000000..8198529b53 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IApplicationIntegrationTypeConfiguration.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Specifies the default scopes and permissions for a given application. +/// +public interface IApplicationIntegrationTypeConfiguration +{ + /// + /// The installation parameters for each context's default in-app installation link. + /// + public Optional Oauth2InstallParams { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IInstallParameters.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IInstallParameters.cs new file mode 100644 index 0000000000..2718eddf53 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IInstallParameters.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Stores metadata about the installation process for an application. +/// +public interface IInstallParameters +{ + /// + /// The OAuth2 scopes to add the application to the present context with. + /// + public IReadOnlyList Scopes { get; } + + /// + /// The permissions to request for the present context. + /// + public DiscordPermissions Permissions { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IPartialApplication.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IPartialApplication.cs new file mode 100644 index 0000000000..08c6d01167 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Applications/IPartialApplication.cs @@ -0,0 +1,157 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated application object. +/// +public interface IPartialApplication +{ + /// + /// The snowflake identifier of this application. + /// + public Optional Id { get; } + + /// + /// The name of this application. + /// + public Optional Name { get; } + + /// + /// The icon hash of this application. + /// + public Optional Icon { get; } + + /// + /// The description of this application. This doubles as the associated bot's about me section. + /// + public Optional Description { get; } + + /// + /// An array of RPC origin urls, if RPC is enabled. + /// + public Optional> RpcOrigins { get; } + + /// + /// Indicates whether this application's bot is publicly invitable. + /// + public Optional BotPublic { get; } + + /// + /// Indicates whether the bot will require completion of the OAuth2 code grant flow to join. + /// + public Optional BotRequireCodeGrant { get; } + + /// + /// A partial user object for the bot user associated with this app. + /// + public Optional Bot { get; } + + /// + /// The URL to this application's terms of service. + /// + public Optional TermsOfServiceUrl { get; } + + /// + /// The URL to this application's privacy policy. + /// + public Optional PrivacyPolicyUrl { get; } + + /// + /// A partial user object containing information on the owner of the application. + /// + public Optional Owner { get; } + + /// + /// The verification key for interactions and GameSDK functions. + /// + public Optional VerifyKey { get; } + + /// + /// The team owning this application. + /// + public Optional Team { get; } + + /// + /// The snowflake identifier of the guild associated with this app, for instance, its support server. + /// + public Optional GuildId { get; } + + /// + /// A partial object of the associated guild. + /// + public Optional Guild { get; } + + /// + /// If this application is a game sold on discord, this is the snowflake identifier of the + /// game SKU created, if it exists. + /// + public Optional PrimarySkuId { get; } + + /// + /// If this application is a game sold on discord, this is the URL slug that links to the store page. + /// + public Optional Slug { get; } + + /// + /// The image hash of this application's default rich presence invite cover image. + /// + public Optional CoverImage { get; } + + /// + /// The public flags for this application. + /// + public Optional Flags { get; } + + /// + /// An approximate count of this application's guild membership. + /// + public Optional ApproximateGuildCount { get; } + + /// + /// An array of redirect URIs for this application. + /// + public Optional> RedirectUris { get; } + + /// + /// The interactions endpoint url for this app, if it uses HTTP interactions. + /// + public Optional InteractionsEndpointUrl { get; } + + /// + /// This application's role connection verification entry point; which, when configured, will render + /// the application as a verification method in the guild role verification configuration. + /// + public Optional RoleConnectionsVerificationUrl { get; } + + /// + /// Up to five tags describing content and functionality of the application. + /// + public Optional> Tags { get; } + + /// + /// The installation parameters for this app. + /// + public Optional InstallParams { get; } + + /// + /// The default scopes and permissions for each supported installation context. + /// + public Optional> IntegrationTypesConfig { get; } + + /// + /// The default custom authorization link for this application, if enabled. + /// + public Optional CustomInstallUrl { get; } + + /// + /// An approximate to the amount of users who installed this app. + /// + public Optional ApproximateUserInstallCount { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLog.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLog.cs new file mode 100644 index 0000000000..64c5101f66 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLog.cs @@ -0,0 +1,53 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents the guild audit log. +/// +public interface IAuditLog +{ + /// + /// The application commands referenced in the audit log. + /// + public IReadOnlyList ApplicationCommands { get; } + + /// + /// The audit log entires, sorted from most recent to last recent. + /// + public IReadOnlyList AuditLogEntries { get; } + + /// + /// The auto moderation rules referenced in the audit log. + /// + public IReadOnlyList AutoModerationRules { get; } + + /// + /// The scheduled events referenced in the audit log. + /// + public IReadOnlyList GuildScheduledEvents { get; } + + /// + /// The integrations referenced in the audit log. + /// + public IReadOnlyList Integrations { get; } + + /// + /// The threads referenced in the audit log. + /// + public IReadOnlyList Threads { get; } + + /// + /// The users referenced in the audit log. + /// + public IReadOnlyList Users { get; } + + /// + /// The webhooks referenced in the audit log. + /// + public IReadOnlyList Webhooks { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogChange.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogChange.cs new file mode 100644 index 0000000000..e6f6fbecb3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogChange.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single logged change made to an entity. Cast to a more specific type to access the changes made. +/// +public interface IAuditLogChange +{ + /// + /// The new value of this field. + /// + public Optional NewValue { get; } + + /// + /// The old value of this field. + /// + public Optional OldValue { get; } + + /// + /// The name of the changed field, with a few exceptions: see + /// + /// the documentation. + /// + public string Key { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogEntry.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogEntry.cs new file mode 100644 index 0000000000..34b6c364c9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogEntry.cs @@ -0,0 +1,52 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +using OneOf; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single entry within the audit log. +/// +public interface IAuditLogEntry +{ + /// + /// The identifier of the affected entity. + /// + public OneOf? TargetId { get; } + + /// + /// The changes made to the affected entity. + /// + public Optional> Changes { get; } + + /// + /// The user or application that made these changes. + /// + public Snowflake? UserId { get; } + + /// + /// The snowflake identifier of this entry. + /// + public Snowflake Id { get; } + + /// + /// The type of the action that occurred. + /// + public DiscordAuditLogEvent ActionType { get; } + + /// + /// Additional information sent for certain event types. + /// + public Optional Options { get; } + + /// + /// The reason for this change. + /// + public Optional Reason { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogEntryInfo.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogEntryInfo.cs new file mode 100644 index 0000000000..5d3246bc2b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AuditLogs/IAuditLogEntryInfo.cs @@ -0,0 +1,72 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains additional metadata for an audit log entry. +/// +/// +/// Presence of a field is dictated by its parent . When deciding +/// what to access, refer to the +/// +/// Event Types section in the docs. +/// +public interface IAuditLogEntryInfo +{ + /// + /// The snowflake identifier of the application whose permissions were targeted. + /// + public Optional ApplicationId { get; } + + /// + /// The name of the auto moderation rule that was triggered. + /// + public Optional AutoModerationRuleName { get; } + + /// + /// The trigger type of the auto moderation rule that was triggered. + /// + public Optional AutoModerationRuleTriggerType { get; } + + /// + /// The snowflake identifier of the channel in which entities were targeted. + /// + public Optional ChannelId { get; } + + /// + /// The amount of entities that were targeted. + /// + public Optional Count { get; } + + /// + /// The amount of days after which inactive members were kicked. + /// + public Optional DeleteMemberDays { get; } + + /// + /// The snowflake identifier of the overwritten entry. + /// + public Optional Id { get; } + + /// + /// The amount of members removed by the server prune. + /// + public Optional MembersRemoved { get; } + + /// + /// The snowflake identifier of the message that was targeted. + /// + public Optional MessageId { get; } + + /// + /// The name of the targeted role. + /// + public Optional RoleName { get; } + + /// + /// The type of the overwritten entity. + /// + public Optional Type { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationAction.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationAction.cs new file mode 100644 index 0000000000..d31fc797f7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationAction.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an action which will execute whenever a rule is triggered. +/// +public interface IAutoModerationAction +{ + /// + /// The type of this action. + /// + public DiscordAutoModerationActionType Type { get; } + + /// + /// Additional metadata for executing this action. + /// + public Optional Metadata { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationActionMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationActionMetadata.cs new file mode 100644 index 0000000000..60a329f641 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationActionMetadata.cs @@ -0,0 +1,11 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents additional metadata provided to automod actions. Cast to a specialized interface to +/// retrieve this metadata. +/// +public interface IAutoModerationActionMetadata; diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationRule.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationRule.cs new file mode 100644 index 0000000000..181f276ee6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationRule.cs @@ -0,0 +1,83 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an auto moderation rule within a guild. +/// +public interface IAutoModerationRule : IPartialAutoModerationRule +{ + /// + public new Snowflake Id { get; } + + /// + public new Snowflake GuildId { get; } + + /// + public new string Name { get; } + + /// + public new Snowflake CreatorId { get; } + + /// + public new DiscordAutoModerationEventType EventType { get; } + + /// + public new DiscordAutoModerationTriggerType TriggerType { get; } + + /// + public new IAutoModerationTriggerMetadata TriggerMetadata { get; } + + /// + public new IReadOnlyList Actions { get; } + + /// + public new bool Enabled { get; } + + /// + public new IReadOnlyList ExemptRoles { get; } + + /// + public new IReadOnlyList ExemptChannels { get; } + + // partial access routes + + /// + Optional IPartialAutoModerationRule.Id => this.Id; + + /// + Optional IPartialAutoModerationRule.GuildId => this.GuildId; + + /// + Optional IPartialAutoModerationRule.Name => this.Name; + + /// + Optional IPartialAutoModerationRule.CreatorId => this.CreatorId; + + /// + Optional IPartialAutoModerationRule.EventType => this.EventType; + + /// + Optional IPartialAutoModerationRule.TriggerType => this.TriggerType; + + /// + Optional IPartialAutoModerationRule.TriggerMetadata => new(this.TriggerMetadata); + + /// + Optional> IPartialAutoModerationRule.Actions => new(this.Actions); + + /// + Optional IPartialAutoModerationRule.Enabled => this.Enabled; + + /// + Optional> IPartialAutoModerationRule.ExemptRoles => new(this.ExemptRoles); + + /// + Optional> IPartialAutoModerationRule.ExemptChannels => new(this.ExemptChannels); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationTriggerMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationTriggerMetadata.cs new file mode 100644 index 0000000000..5200bd3813 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IAutoModerationTriggerMetadata.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents additional rule metadata, based on the configured trigger. +/// +/// +/// Which fields may be meaningful depends on the trigger type of the parent rule, see +/// +/// the documentation for additional information. +/// +public interface IAutoModerationTriggerMetadata +{ + /// + /// Substrings which the automod will search for in message content, up to 1000. + /// + public Optional> KeywordFilter { get; } + + /// + /// Rust-flavoured regex patterns which will be matched against message content, up to 10. + /// + public Optional> RegexPatterns { get; } + + /// + /// The pre-defined wordsets that will be searched for in message content. + /// + public Optional> Presets { get; } + + /// + /// Substrings which will not trigger the rule, even if otherwise filtered out. The maximum + /// depends on the parent trigger type. + /// + public Optional> AllowList { get; } + + /// + /// The total number of unique role and user mentions allowed per message, up to 50. + /// + public Optional MentionTotalLimit { get; } + + /// + /// Indicates whether mention raid detection is enabled. + /// + public Optional MentionRaidProtectionEnabled { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IBlockMessageActionMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IBlockMessageActionMetadata.cs new file mode 100644 index 0000000000..95b8d05039 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IBlockMessageActionMetadata.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata for auto moderation actions of type +/// . +/// +public interface IBlockMessageActionMetadata : IAutoModerationActionMetadata +{ + /// + /// An explanation that will be shown to members whenever their message is blocked, up to 150 characters. + /// + public Optional CustomMessage { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IPartialAutoModerationRule.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IPartialAutoModerationRule.cs new file mode 100644 index 0000000000..5af07466d2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/IPartialAutoModerationRule.cs @@ -0,0 +1,70 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partial auto-moderation rule. +/// +public interface IPartialAutoModerationRule +{ + /// + /// The snowflake identifier of this rule. + /// + public Optional Id { get; } + + /// + /// The snowflake identifier of the guild this rule belongs to. + /// + public Optional GuildId { get; } + + /// + /// The display name of this rule. + /// + public Optional Name { get; } + + /// + /// The snowflake identifier of the user who created this rule. + /// + public Optional CreatorId { get; } + + /// + /// The type of event that causes this rule to fire. + /// + public Optional EventType { get; } + + /// + /// The trigger type of this rule. + /// + public Optional TriggerType { get; } + + /// + /// Additional metadata for this rule trigger. + /// + public Optional TriggerMetadata { get; } + + /// + /// The actions which wille xecute when this rule is triggered. + /// + public Optional> Actions { get; } + + /// + /// Indicates whether this rule should be enabled. + /// + public Optional Enabled { get; } + + /// + /// Up to 20 role IDs that should be exempted from this rule. + /// + public Optional> ExemptRoles { get; } + + /// + /// Up to 50 channel IDs that should be exempted from this rule. + /// + public Optional> ExemptChannels { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/ISendAlertMessageActionMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/ISendAlertMessageActionMetadata.cs new file mode 100644 index 0000000000..1fceec8262 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/ISendAlertMessageActionMetadata.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata for auto moderation actions of type +/// . +/// +public interface ISendAlertMessageActionMetadata : IAutoModerationActionMetadata +{ + /// + /// The snowflake identifier of the channel to which content should be logged. + /// + public Snowflake ChannelId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/ITimeoutActionMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/ITimeoutActionMetadata.cs new file mode 100644 index 0000000000..0cfd9dac4b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/AutoModeration/ITimeoutActionMetadata.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata for auto moderation actions of type +/// . +/// +public interface ITimeoutActionMetadata : IAutoModerationActionMetadata +{ + /// + /// The timeout duration in seconds, up to 2419200 seconds, or 28 days. + /// + public int DurationSeconds { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IChannel.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IChannel.cs new file mode 100644 index 0000000000..83bb69a004 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IChannel.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a fully populated channel object. +/// +public interface IChannel : IPartialChannel +{ + /// + public new Snowflake Id { get; } + + /// + public new DiscordChannelType Type { get; } + + // partial access routes + + /// + Optional IPartialChannel.Id => this.Id; + + /// + Optional IPartialChannel.Type => this.Type; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IChannelOverwrite.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IChannelOverwrite.cs new file mode 100644 index 0000000000..e9115acfe2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IChannelOverwrite.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a permission overwrite on a channel. +/// +public interface IChannelOverwrite : IPartialChannelOverwrite +{ + /// + public new Snowflake Id { get; } + + /// + public new DiscordChannelOverwriteType Type { get; } + + /// + public new DiscordPermissions Allow { get; } + + /// + public new DiscordPermissions Deny { get; } + + // partial access routes + + /// + Optional IPartialChannelOverwrite.Id => this.Id; + + /// + Optional IPartialChannelOverwrite.Type => this.Type; + + /// + Optional IPartialChannelOverwrite.Allow => this.Allow; + + /// + Optional IPartialChannelOverwrite.Deny => this.Deny; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IDefaultReaction.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IDefaultReaction.cs new file mode 100644 index 0000000000..ca38a57e70 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IDefaultReaction.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Indicates the default emoji to react to a forum post with. +/// +public interface IDefaultReaction +{ + /// + /// The snowflake identifier of a custom emoji to react with. Mutually exclusive with + /// . + /// + public Snowflake? EmojiId { get; } + + /// + /// The unicode representation of a default emoji to react with. Mutually exclusive with + /// . + /// + public string? EmojiName { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IFollowedChannel.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IFollowedChannel.cs new file mode 100644 index 0000000000..87a229dd94 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IFollowedChannel.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a followed channel, whose messages will be crossposted. +/// +public interface IFollowedChannel +{ + /// + /// The snowflake identifier of the source channel. + /// + public Snowflake ChannelId { get; } + + /// + /// The snowflake identifier of the crossposting webhook. + /// + public Snowflake WebhookId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IForumTag.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IForumTag.cs new file mode 100644 index 0000000000..adc7b3d8e6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IForumTag.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single forum tag, applicable to forum or media channel posts. +/// +public interface IForumTag +{ + /// + /// The snowflake identifier of this tag. + /// + public Snowflake Id { get; } + + /// + /// The name of this tag, up to 20 characters. + /// + public string Name { get; } + + /// + /// Indicates whether this tag can only be added or removed by a member with the manage threads permission. + /// + public bool Moderated { get; } + + /// + /// The snowflake identifier of a custom emoji to be applied to this tag. Mutually exclusive with + /// . + /// + public Snowflake? EmojiId { get; } + + /// + /// The unicode representation of a default emoji to be applied to this tag. Mutually exclusive with + /// . + /// + public string? EmojiName { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IPartialChannel.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IPartialChannel.cs new file mode 100644 index 0000000000..da1d17eb5a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IPartialChannel.cs @@ -0,0 +1,199 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partial channel of any given type. +/// +public interface IPartialChannel +{ + /// + /// The snowflake identifier of this channel. + /// + public Optional Id { get; } + + /// + /// The type of this channel. + /// + public Optional Type { get; } + + /// + /// The snowflake identifier of the guild this channel belongs to, if it is a guild channel. + /// + public Optional GuildId { get; } + + /// + /// The sorting position in the guild channel list of this channel, if it is a guild channel. + /// + public Optional Position { get; } + + /// + /// A collection of explicit permission overwrites for members and roles. + /// + public Optional> PermissionOverwrites { get; } + + /// + /// The name of this channel, 1 to 100 characters. + /// + public Optional Name { get; } + + /// + /// The channel topic/description. For and + /// , up to 4096 characters are allowed, for all other + /// types up to 1024 characters. + /// + public Optional Topic { get; } + + /// + /// Indicates whether the channel is considered a NSFW channel. + /// + public Optional Nsfw { get; } + + /// + /// The snowflake identifier of the last message sent in this channel. This is a thread for forum and media + /// channels, and may not point to an existing or valid message or thread. + /// + public Optional LastMessageId { get; } + + /// + /// The bitrate of this channel, if it is a voice channel. + /// + public Optional Bitrate { get; } + + /// + /// The user limit of this channel, if it is a voice channel. + /// + public Optional UserLimit { get; } + + /// + /// The slowmode of the current channel in seconds, between 0 and 21600. Bots, as well as users + /// with manage messages or manage channel permissions, are unaffected. + /// + public Optional RateLimitPerUser { get; } + + /// + /// The recipients of this DM channel. + /// + public Optional> Recipients { get; } + + /// + /// The icon hash of this group DM channel. + /// + public Optional Icon { get; } + + /// + /// The snowflake identifier of the creator of this group DM or thread channel. + /// + public Optional OwnerId { get; } + + /// + /// The snowflake identifier of the application that created this group DM, if it was created by a bot. + /// + public Optional ApplicationId { get; } + + /// + /// Indicates whether this channel is managed by an application via OAuth2. + /// + public Optional Managed { get; } + + /// + /// The parent channel of the current channel; either the containing category if this is a standalone + /// guild channel, or the containing text channel if this is a thread channel. + /// + public Optional ParentId { get; } + + /// + /// The timestamp at which the last message was pinned. + /// + public Optional LastPinTimestamp { get; } + + /// + /// The voice region ID of this voice channel, automatic when set to null. + /// + public Optional RtcRegion { get; } + + /// + /// The camera video quality mode of this voice channel, automatic when not present. + /// + public Optional VideoQualityMode { get; } + + /// + /// The number of messages, excluding the original message and deleted messages, in a thread. + /// + public Optional MessageCount { get; } + + /// + /// The approximate amount of users in this thread, stops counting at 50. + /// + public Optional MemberCount { get; } + + /// + /// A thread-specific metadata object containing data not needed by other channel types. + /// + public Optional ThreadMetadata { get; } + + /// + /// A thread member object for the current user, if they have joined this thread. + /// + public Optional Member { get; } + + /// + /// The default thread archive duration in minutes, applied to all threads created within this channel + /// where the default was not overridden on creation. + /// + public Optional DefaultAutoArchiveDuration { get; } + + /// + /// Computed permissions for the invoking user in this channel, including permission overwrites. This + /// is only sent as part of application command resolved data. + /// + public Optional Permissions { get; } + + /// + /// Additional flags for this channel. + /// + public Optional Flags { get; } + + /// + /// The total number of messages sent in this thread, including deleted messages. + /// + public Optional TotalMessageSent { get; } + + /// + /// The set of tags that can be used in this forum or media channel. + /// + public Optional> AvailableTags { get; } + + /// + /// The snowflake identifiers of the set of tags that have been applied to this forum or media thread channel. + /// + public Optional> AppliedTags { get; } + + /// + /// The default emoji to show in the add reaction button on a thread in this forum or media channel. + /// + public Optional DefaultReactionEmoji { get; } + + /// + /// The initial slowmode to set on newly created threads in this channel. This is populated at creation + /// time and does not sync. + /// + public Optional DefaultThreadRateLimitPerUser { get; } + + /// + /// The default sort order for posts in this forum or media channel. + /// + public Optional DefaultSortOrder { get; } + + /// + /// The default layout view used to display posts in this forum channel. + /// + public Optional DefaultForumLayout { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IPartialChannelOverwrite.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IPartialChannelOverwrite.cs new file mode 100644 index 0000000000..5a5b1cc308 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IPartialChannelOverwrite.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated channel overwrite. +/// +public interface IPartialChannelOverwrite +{ + /// + /// The snowflake identifier of the role or user this overwrite targtes. + /// + public Optional Id { get; } + + /// + /// Specifies what kind of entity this overwrite targets. + /// + public Optional Type { get; } + + /// + /// The permissions explicitly granted by this overwrite. + /// + public Optional Allow { get; } + + /// + /// The permissions explicitly denied by this overwrite. + /// + public Optional Deny { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IThreadMember.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IThreadMember.cs new file mode 100644 index 0000000000..4ba3a5d03a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IThreadMember.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Includes additional metadata about a member's presence inside a thread. +/// +public interface IThreadMember +{ + /// + /// The snowflake identifier of the thread this object belongs to. + /// + public Optional Id { get; } + + /// + /// The snowflake identifier of the user this object belongs to. + /// + public Optional UserId { get; } + + /// + /// The timestamp at which this user last joined the thread. + /// + public DateTimeOffset JoinTimestamp { get; } + + /// + /// User thread settings used for notifications. + /// + public int Flags { get; } + + /// + /// Additional information about this thread member. + /// + public Optional Member { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IThreadMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IThreadMetadata.cs new file mode 100644 index 0000000000..42b2f2f6d9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Channels/IThreadMetadata.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains additional metadata about thread channels. +/// +public interface IThreadMetadata +{ + /// + /// Indicates whether this thread is considered archived. + /// + public bool Archived { get; } + + /// + /// The time in minutes of inactivity before this thread stops showing in the channel list. + /// Legal values are 60, 1440, 4320 and 10080. + /// + public int AutoArchiveDuration { get; } + + /// + /// The timestamp at which this thread's archive status was last changed. + /// + public DateTimeOffset ArchiveTimestamp { get; } + + /// + /// Indicates whether this thread is locked, if it is, only users with the manage threads permission + /// can unlock it. + /// + public bool Locked { get; } + + /// + /// Indicates whether non-moderators can add other non-moderators to this thread. + /// + public Optional Invitable { get; } + + /// + /// The timestamp at which this thread was created. + /// + public Optional CreateTimestamp { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IActionRowComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IActionRowComponent.cs new file mode 100644 index 0000000000..a757aec900 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IActionRowComponent.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a container component for other components. +/// +public interface IActionRowComponent : IComponent +{ + /// + /// + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The child components of this action row: up to five buttons, or one non-button component. + /// + public IReadOnlyList Components { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IButtonComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IButtonComponent.cs new file mode 100644 index 0000000000..5e27d492b4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IButtonComponent.cs @@ -0,0 +1,59 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a button attached to a message. +/// +public interface IButtonComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The visual style of this button. + /// + public DiscordButtonStyle Style { get; } + + /// + /// The text to render on this button, up to 80 characters. + /// + public Optional Label { get; } + + /// + /// The emoji to render on this button, if any. + /// + public Optional Emoji { get; } + + /// + /// A developer-defined identifier for this button, up to 100 characters. This is mutually + /// exclusive with . + /// + public Optional CustomId { get; } + + /// + /// An URL for link-style buttons. This is mutually exclusive with . + /// + public Optional Url { get; } + + /// + /// Indicates whether this button is disabled, default false. + /// + public Optional Disabled { get; } + + /// + /// A snowflake identifier of a purchasable SKU to link to. This is only available on premium buttons. + /// + public Optional SkuId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IChannelSelectComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IChannelSelectComponent.cs new file mode 100644 index 0000000000..75cabd974d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IChannelSelectComponent.cs @@ -0,0 +1,62 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a dropdown menu from where users can select discord-supplied channels, +/// optionally restricted by channel type. +/// +public interface IChannelSelectComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The developer-defined ID for this select menu, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// The channel types by which to filter listed channels. + /// + public IReadOnlyList ChannelTypes { get; } + + /// + /// Placeholder text if nothing is selected, up to 150 characters. + /// + public Optional Placeholder { get; } + + /// + /// A list of default values for this select; the number of default values must be within the range defined by + /// and . + /// + public Optional> DefaultValues { get; } + + /// + /// The minimum number of items that must be chosen, between 0 and 25. + /// + public Optional MinValues { get; } + + /// + /// The maximum number of items that can be chosen, between 1 and 25. + /// + public Optional MaxValues { get; } + + /// + /// Indicates whether this select menu is disabled. + /// + public Optional Disabled { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IComponent.cs new file mode 100644 index 0000000000..8ae2b183a6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IComponent.cs @@ -0,0 +1,10 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a message component that users can interact with. +/// +public interface IComponent; diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IContainerComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IContainerComponent.cs new file mode 100644 index 0000000000..efbfa5ee33 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IContainerComponent.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// A top-level layout-only component that is visually distinct from the background and has a customizable colour bar akin to an embed. +/// +public interface IContainerComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The components to display in this container. + /// + public IReadOnlyList Components { get; } + + /// + /// The colour for the colour bar. Unset blends it with the container's background, which depends on client theme. + /// + public Optional AccentColor { get; } + + /// + /// Indicates whether the container should be spoilered, that is, blurred out until clicked or if the user automatically shows all spoilers. + /// Defaults to false. + /// + public Optional Spoiler { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IDefaultSelectValue.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IDefaultSelectValue.cs new file mode 100644 index 0000000000..f9317bb22b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IDefaultSelectValue.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a default value for an auto-populated select menu. +/// +public interface IDefaultSelectValue +{ + /// + /// The snowflake identifier of a user, role or channel. + /// + public Snowflake Id { get; } + + /// + /// The type of the value represented by ; either "user", "role" or "channel". + /// + public string Type { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IFileComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IFileComponent.cs new file mode 100644 index 0000000000..eca86f6401 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IFileComponent.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// A top-level component that allows displaying an uploaded file. +/// +public interface IFileComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The file to display. In this case, only uploads are supported via attachment:// reference, not other arbitrary URLs. + /// + public IUnfurledMediaItem File { get; } + + /// + /// Indicates whether the file should be spoilered. Defaults to false. + /// + public Optional Spoiler { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMediaGalleryComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMediaGalleryComponent.cs new file mode 100644 index 0000000000..e9453ad6f4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMediaGalleryComponent.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a gallery of one to ten media items to display. +/// +public interface IMediaGalleryComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// 1-10 media items. + /// + public IReadOnlyList Items { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMediaGalleryItem.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMediaGalleryItem.cs new file mode 100644 index 0000000000..20fca1f5ff --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMediaGalleryItem.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single media item inside a media gallery. +/// +public interface IMediaGalleryItem +{ + /// + /// The underlying media item to display. + /// + public IUnfurledMediaItem Media { get; } + + /// + /// Alt text for this media item. + /// + public Optional Description { get; } + + /// + /// Indicates whether this media item should be spoilered; that is, blurred out and only showed on click or if the user automatically shows all spoilers. + /// + public Optional Spoiler { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMentionableSelectComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMentionableSelectComponent.cs new file mode 100644 index 0000000000..f192b8a6f0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IMentionableSelectComponent.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a dropdown menu from where users can select discord-supplied users and roles. +/// +public interface IMentionableSelectComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The developer-defined ID for this select menu, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// Placeholder text if nothing is selected, up to 150 characters. + /// + public Optional Placeholder { get; } + + /// + /// A list of default values for this select; the number of default values must be within the range defined by + /// and . + /// + public Optional> DefaultValues { get; } + + /// + /// The minimum number of items that must be chosen, between 0 and 25. + /// + public Optional MinValues { get; } + + /// + /// The maximum number of items that can be chosen, between 1 and 25. + /// + public Optional MaxValues { get; } + + /// + /// Indicates whether this select menu is disabled. + /// + public Optional Disabled { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IRoleSelectComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IRoleSelectComponent.cs new file mode 100644 index 0000000000..89ced9a516 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IRoleSelectComponent.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a dropdown menu from where users can select discord-supplied roles. +/// +public interface IRoleSelectComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The developer-defined ID for this select menu, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// Placeholder text if nothing is selected, up to 150 characters. + /// + public Optional Placeholder { get; } + + /// + /// A list of default values for this select; the number of default values must be within the range defined by + /// and . + /// + public Optional> DefaultValues { get; } + + /// + /// The minimum number of items that must be chosen, between 0 and 25. + /// + public Optional MinValues { get; } + + /// + /// The maximum number of items that can be chosen, between 1 and 25. + /// + public Optional MaxValues { get; } + + /// + /// Indicates whether this select menu is disabled. + /// + public Optional Disabled { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISectionComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISectionComponent.cs new file mode 100644 index 0000000000..1608193ae3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISectionComponent.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// A top-level layout component that allows combining text with an accessory. +/// +public interface ISectionComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// One to three text components to send within this section. + /// + public IReadOnlyList Components { get; } + + /// + /// A thumbnail or button component to group with the text in this component. + /// + public IComponent Accessory { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISelectOption.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISelectOption.cs new file mode 100644 index 0000000000..2039698dac --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISelectOption.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single text option in a . +/// +public interface ISelectOption +{ + /// + /// The user-facing name of this option, up to 100 characters. + /// + public string Label { get; } + + /// + /// The developer-defined value of this option, up to 100 characters. + /// + public string Value { get; } + + /// + /// An additional description of this option, up to 100 characters. + /// + public Optional Description { get; } + + /// + /// The emoji to render with this option + /// + public Optional Emoji { get; } + + /// + /// Indicates whether this option will be selected by default. + /// + public Optional Default { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISeparatorComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISeparatorComponent.cs new file mode 100644 index 0000000000..d232d32d3b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ISeparatorComponent.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// A top-level component that adds vertical padding and/or visual division between components. +/// +public interface ISeparatorComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// Indicates whether a visual divider should be displayed in this component. Defaults to true. + /// + public Optional Divider { get; } + + /// + /// The amount of space this separator takes up. + /// + public Optional Spacing { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IStringSelectComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IStringSelectComponent.cs new file mode 100644 index 0000000000..d3e5a906e4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IStringSelectComponent.cs @@ -0,0 +1,55 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a dropdown menu from where users can select dev-supplied strings. +/// +public interface IStringSelectComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The developer-defined ID for this select menu, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// Up to 25 specified choices for this select menu. + /// + public IReadOnlyList Options { get; } + + /// + /// Placeholder text if nothing is selected, up to 150 characters. + /// + public Optional Placeholder { get; } + + /// + /// The minimum number of items that must be chosen, between 0 and 25. + /// + public Optional MinValues { get; } + + /// + /// The maximum number of items that can be chosen, between 1 and 25. + /// + public Optional MaxValues { get; } + + /// + /// Indicates whether this select menu is disabled. + /// + public Optional Disabled { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ITextDisplayComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ITextDisplayComponent.cs new file mode 100644 index 0000000000..1efc318996 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ITextDisplayComponent.cs @@ -0,0 +1,29 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// A top-level component displaying markdown text. Mentions in the text are controlled by the message's +/// . +/// +public interface ITextDisplayComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The text to display with this component. + /// + public string Content { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ITextInputComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ITextInputComponent.cs new file mode 100644 index 0000000000..b748649617 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/ITextInputComponent.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a text input field in a modal. +/// +public interface ITextInputComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The identifier of this input field, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// Indicates whether this input field requests short-form or long-form input. + /// + public DiscordTextInputStyle Style { get; } + + /// + /// The label of this input field, up to 45 charcters. + /// + public string Label { get; } + + /// + /// The minimum length for a text input, between 0 and 4000 characters. + /// + public Optional MinLength { get; } + + /// + /// The maximum length for a text input, between 1 and 4000 characters. + /// + public Optional MaxLength { get; } + + /// + /// Indicates whether this text input field is required to be filled, defaults to true. + /// + public Optional Required { get; } + + /// + /// A pre-filled value for this component, up to 4000 characters. + /// + public Optional Value { get; } + + /// + /// A custom placeholder if the input field is empty, up to 100 characters. + /// + public Optional Placeholder { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IThumbnailComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IThumbnailComponent.cs new file mode 100644 index 0000000000..c527db03ae --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IThumbnailComponent.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +public interface IThumbnailComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The media item to be displayed in this thumbnail. + /// + public IUnfurledMediaItem Media { get; } + + /// + /// Alt text for this thumbnail. + /// + public Optional Description { get; } + + /// + /// Indicates whether this thumbnail should be spoilered; that is, blurred out and only showed on click or if the user automatically shows all spoilers. + /// + public Optional Spoiler { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUnfurledMediaItem.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUnfurledMediaItem.cs new file mode 100644 index 0000000000..98a0960619 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUnfurledMediaItem.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an attachment reference for use in components. +/// +public interface IUnfurledMediaItem +{ + /// + /// The URL to the attachment; supports arbitrary URLs and attachemnt:// references to files uploaded along the message. + /// + public string Url { get; } + + /// + /// The proxied URL of this media item. + /// + public Optional ProxyUrl { get; } + + /// + /// The height of this media item. + /// + public Optional Height { get; } + + /// + /// The width of this media item. + /// + public Optional Width { get; } + + /// + /// The MIME/content type of the content. + /// + public Optional ContentType { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUnknownComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUnknownComponent.cs new file mode 100644 index 0000000000..3f3881cf3a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUnknownComponent.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Specifies a component of unknown type. +/// +public interface IUnknownComponent : IComponent +{ + /// + /// Gets the type of this component. You should expect to handle values not specified by the enum type. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// Gets the raw string represented by this component. + /// + public string RawPayload { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUserSelectComponent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUserSelectComponent.cs new file mode 100644 index 0000000000..4119dacdfb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Components/IUserSelectComponent.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a dropdown menu from where users can select discord-supplied users. +/// +public interface IUserSelectComponent : IComponent +{ + /// + /// The type of this component. + /// + public DiscordMessageComponentType Type { get; } + + /// + /// An optional numeric identifier for this component. + /// + public Optional Id { get; } + + /// + /// The developer-defined ID for this select menu, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// Placeholder text if nothing is selected, up to 150 characters. + /// + public Optional Placeholder { get; } + + /// + /// A list of default values for this select; the number of default values must be within the range defined by + /// and . + /// + public Optional> DefaultValues { get; } + + /// + /// The minimum number of items that must be chosen, between 0 and 25. + /// + public Optional MinValues { get; } + + /// + /// The maximum number of items that can be chosen, between 1 and 25. + /// + public Optional MaxValues { get; } + + /// + /// Indicates whether this select menu is disabled. + /// + public Optional Disabled { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/DSharpPlus.Internal.Abstractions.Models.csproj b/src/core/DSharpPlus.Internal.Abstractions.Models/DSharpPlus.Internal.Abstractions.Models.csproj new file mode 100644 index 0000000000..a6c52ba8cf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/DSharpPlus.Internal.Abstractions.Models.csproj @@ -0,0 +1,19 @@ + + + + $(_DSharpPlusInternalAbstractionsModelsVersion) + $(Description) This package specifies a contract for the serialization models to implement. This definition is library-agnostic. + Library + + $(NoWarn);CA1040;CA1056 + + + + + + + + + + + diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Emojis/IEmoji.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Emojis/IEmoji.cs new file mode 100644 index 0000000000..d438688f7d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Emojis/IEmoji.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a fully populated emoji object. +/// +public interface IEmoji : IPartialEmoji +{ + /// + public new Snowflake? Id { get; } + + /// + public new string? Name { get; } + + // direct partial access routes + + /// + Optional IPartialEmoji.Id => this.Id; + + /// + Optional IPartialEmoji.Name => this.Name; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Emojis/IPartialEmoji.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Emojis/IPartialEmoji.cs new file mode 100644 index 0000000000..fd5768014f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Emojis/IPartialEmoji.cs @@ -0,0 +1,54 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partial emoji object, where any or all properties may be missing. +/// +public interface IPartialEmoji +{ + /// + /// The snowflake identifier of this emoji, if it belongs to a guild. + /// + public Optional Id { get; } + + /// + /// The name of this emoji. + /// + public Optional Name { get; } + + /// + /// A list of roles allowed to use this emoji, if applicable. + /// + public Optional> Roles { get; } + + /// + /// The user who created this emoji. + /// + public Optional User { get; } + + /// + /// Indicates whether this emoji requires to be wrapped in colons. + /// + public Optional RequireColons { get; } + + /// + /// Indicates whether this emoji is managed by an app. + /// + public Optional Managed { get; } + + /// + /// Indicates whether this emoji is an animated emoji. Animated emojis have an a prefix in text form. + /// + public Optional Animated { get; } + + /// + /// Indicates whether this emoji is currently available for use. This may be false when losing server + /// boosts. + /// + public Optional Available { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Entitlements/IEntitlement.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Entitlements/IEntitlement.cs new file mode 100644 index 0000000000..80c833ccff --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Entitlements/IEntitlement.cs @@ -0,0 +1,47 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an entitlement to a premium offering in an application. +/// +public interface IEntitlement : IPartialEntitlement +{ + /// + public new Snowflake SkuId { get; } + + /// + public new Snowflake ApplicationId { get; } + + /// + public new DiscordEntitlementType Type { get; } + + /// + public new bool Deleted { get; } + + /// + public new DateTimeOffset? StartsAt { get; } + + /// + public new DateTimeOffset? EndsAt { get; } + + // partial access + + Optional IPartialEntitlement.SkuId => this.SkuId; + + Optional IPartialEntitlement.ApplicationId => this.ApplicationId; + + Optional IPartialEntitlement.Type => this.Type; + + Optional IPartialEntitlement.Deleted => this.Deleted; + + Optional IPartialEntitlement.StartsAt => this.StartsAt; + + Optional IPartialEntitlement.EndsAt => this.EndsAt; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Entitlements/IPartialEntitlement.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Entitlements/IPartialEntitlement.cs new file mode 100644 index 0000000000..b84649f2b0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Entitlements/IPartialEntitlement.cs @@ -0,0 +1,65 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partial entitlement object. +/// +public interface IPartialEntitlement +{ + /// + /// The snowflake identifier of this entitlement. + /// + public Snowflake Id { get; } + + /// + /// The snowflake identifier of the SKU. + /// + public Optional SkuId { get; } + + /// + /// The snowflake identifier of the user that is granted access to the entitlement's SKU. + /// + public Optional UserId { get; } + + /// + /// The snowflake identifier of the guild that is granted access to the entitlement's SKU. + /// + public Optional GuildId { get; } + + /// + /// The snowflake identifier of the parent application. + /// + public Optional ApplicationId { get; } + + /// + /// The type of this entitlement. + /// + public Optional Type { get; } + + /// + /// Indicates whether this entitlement was deleted. + /// + public Optional Deleted { get; } + + /// + /// The starting date at which this entitlement is valid. Not present when using test entitlements. + /// + public Optional StartsAt { get; } + + /// + /// The date at which this entitlement is no longer valid. Not present when using test entitlements. + /// + public Optional EndsAt { get; } + + /// + /// Indicates whether this entitlement has been consumed. + /// + public Optional Consumed { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/GuildTemplates/ITemplate.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/GuildTemplates/ITemplate.cs new file mode 100644 index 0000000000..46b808e897 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/GuildTemplates/ITemplate.cs @@ -0,0 +1,69 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a guild template that, when used, creates a guild with default settings taken from +/// a snapshot of an existing guild. +/// +public interface ITemplate +{ + /// + /// The unique template code. + /// + public string Code { get; } + + /// + /// The name of this template. + /// + public string Name { get; } + + /// + /// The description for this template. + /// + public string? Description { get; } + + /// + /// The amount of times this template has been used. + /// + public int UsageCount { get; } + + /// + /// The snowflake identifier of the user who created this template. + /// + public Snowflake CreatorId { get; } + + /// + /// The user who created this template. + /// + public IUser Creator { get; } + + /// + /// Indicates when this template was created. + /// + public DateTimeOffset CreatedAt { get; } + + /// + /// Indicates when this template was last synced to the source guild. + /// + public DateTimeOffset UpdatedAt { get; } + + /// + /// The snowflake identifier of the guild this template is based on. + /// + public Snowflake SourceGuildId { get; } + + /// + /// The guild snapshot this template contains. + /// + public IPartialGuild SerializedSourceGuild { get; } + + /// + /// Indicates whether this template has unsynced changes. + /// + public bool? IsDirty { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IBan.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IBan.cs new file mode 100644 index 0000000000..c3999d580f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IBan.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents information about a ban from a guild. +/// +public interface IBan +{ + /// + /// The ban reason. + /// + public string? Reason { get; } + + /// + /// The banned user. + /// + public IUser User { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuild.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuild.cs new file mode 100644 index 0000000000..28a3c4d5c3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuild.cs @@ -0,0 +1,190 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a fully populated guild object. +/// +public interface IGuild : IPartialGuild +{ + /// + public new Snowflake Id { get; } + + /// + public new string Name { get; } + + /// + public new string? Icon { get; } + + /// + public new string? Splash { get; } + + /// + public new string? DiscoverySplash { get; } + + /// + public new Snowflake OwnerId { get; } + + /// + public new Snowflake? AfkChannelId { get; } + + /// + public new int AfkTimeout { get; } + + /// + public new DiscordVerificationLevel VerificationLevel { get; } + + /// + public new DiscordMessageNotificationLevel DefaultMessageNotifications { get; } + + /// + public new DiscordExplicitContentFilterLevel ExplicitContentFilter { get; } + + /// + public new IReadOnlyList Roles { get; } + + /// + public new IReadOnlyList Emojis { get; } + + /// + public new IReadOnlyList Features { get; } + + /// + public new DiscordMfaLevel MfaLevel { get; } + + /// + public new Snowflake? ApplicationId { get; } + + /// + public new Snowflake? SystemChannelId { get; } + + /// + public new DiscordSystemChannelFlags SystemChannelFlags { get; } + + /// + public new Snowflake? RulesChannelId { get; } + + /// + public new string? VanityUrlCode { get; } + + /// + public new string? Description { get; } + + /// + public new string? Banner { get; } + + /// + public new int PremiumTier { get; } + + /// + public new string PreferredLocale { get; } + + /// + public new Snowflake? PublicUpdatesChannelId { get; } + + /// + public new DiscordNsfwLevel NsfwLevel { get; } + + /// + public new bool PremiumProgressBarEnabled { get; } + + /// + public new Snowflake? SafetyAlertsChannelId { get; } + + /// + public new IIncidentsData? IncidentsData { get; } + + // routes for partial access + + /// + Optional IPartialGuild.Id => this.Id; + + /// + Optional IPartialGuild.Name => this.Name; + + /// + Optional IPartialGuild.Icon => this.Icon; + + /// + Optional IPartialGuild.Splash => this.Splash; + + /// + Optional IPartialGuild.DiscoverySplash => this.DiscoverySplash; + + /// + Optional IPartialGuild.OwnerId => this.OwnerId; + + /// + Optional IPartialGuild.AfkChannelId => this.AfkChannelId; + + /// + Optional IPartialGuild.AfkTimeout => this.AfkTimeout; + + /// + Optional IPartialGuild.VerificationLevel => this.VerificationLevel; + + /// + Optional IPartialGuild.DefaultMessageNotifications => this.DefaultMessageNotifications; + + /// + Optional IPartialGuild.ExplicitContentFilter => this.ExplicitContentFilter; + + /// + Optional> IPartialGuild.Roles => new(this.Roles); + + /// + Optional> IPartialGuild.Emojis => new(this.Emojis); + + /// + Optional> IPartialGuild.Features => new(this.Features); + + /// + Optional IPartialGuild.MfaLevel => this.MfaLevel; + + /// + Optional IPartialGuild.ApplicationId => this.ApplicationId; + + /// + Optional IPartialGuild.SystemChannelId => this.SystemChannelId; + + /// + Optional IPartialGuild.SystemChannelFlags => this.SystemChannelFlags; + + /// + Optional IPartialGuild.RulesChannelId => this.RulesChannelId; + + /// + Optional IPartialGuild.VanityUrlCode => this.VanityUrlCode; + + /// + Optional IPartialGuild.Description => this.Description; + + /// + Optional IPartialGuild.Banner => this.Banner; + + /// + Optional IPartialGuild.PremiumTier => this.PremiumTier; + + /// + Optional IPartialGuild.PreferredLocale => this.PreferredLocale; + + /// + Optional IPartialGuild.PublicUpdatesChannelId => this.PublicUpdatesChannelId; + + /// + Optional IPartialGuild.NsfwLevel => this.NsfwLevel; + + /// + Optional IPartialGuild.PremiumProgressBarEnabled => this.PremiumProgressBarEnabled; + + /// + Optional IPartialGuild.SafetyAlertsChannelId => this.SafetyAlertsChannelId; + + Optional IPartialGuild.IncidentsData => new(this.IncidentsData); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildMember.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildMember.cs new file mode 100644 index 0000000000..abfdc15005 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildMember.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a fully populated guild member object. +/// +public interface IGuildMember : IPartialGuildMember +{ + /// + public new IReadOnlyList Roles { get; } + + /// + public new DateTimeOffset JoinedAt { get; } + + /// + public new bool Deaf { get; } + + /// + public new bool Mute { get; } + + /// + public new DiscordGuildMemberFlags Flags { get; } + + // explicit routes for partial guild member access + + /// + Optional> IPartialGuildMember.Roles => new(this.Roles); + + /// + Optional IPartialGuildMember.JoinedAt => this.JoinedAt; + + /// + Optional IPartialGuildMember.Deaf => this.Deaf; + + /// + Optional IPartialGuildMember.Mute => this.Mute; + + /// + Optional IPartialGuildMember.Flags => this.Flags; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildPreview.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildPreview.cs new file mode 100644 index 0000000000..d050a65cc7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildPreview.cs @@ -0,0 +1,68 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a guild preview object. +/// +public interface IGuildPreview +{ + /// + /// The snowflake identifier of the guild this preview belongs to. + /// + public Snowflake Id { get; } + + /// + /// The name of this guild, between 2 and 100 characters. + /// + public string Name { get; } + + /// + /// The icon hash for this guild. + /// + public string? Icon { get; } + + /// + /// The splash hash for this guild. + /// + public string? Splash { get; } + + /// + /// The discovery splash hash for this guild. + /// + public string? DiscoverySplash { get; } + + /// + /// The custom emojis in this guild. + /// + public IReadOnlyList Emojis { get; } + + /// + /// The enabled guild features. + /// + public IReadOnlyList Features { get; } + + /// + /// The approximate amount of members in this guild. + /// + public int ApproximateMemberCount { get; } + + /// + /// The approximate amount of online members in this guild. + /// + public int ApproximatePresenceCount { get; } + + /// + /// The description of this guild. + /// + public string? Description { get; } + + /// + /// The custom stickers in this guild. + /// + public IReadOnlyList Stickers { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildWidget.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildWidget.cs new file mode 100644 index 0000000000..dd82d213db --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildWidget.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a guild's widget object. +/// +public interface IGuildWidget +{ + /// + /// The snowflake identifier of the guild this widget belongs to. + /// + public Snowflake Id { get; } + + /// + /// The name of the guild, 2 to 100 characters. + /// + public string Name { get; } + + /// + /// An instant invite code for the guild's specified widget invite channel. + /// + public string? InstantInvite { get; } + + /// + /// Voice and stage channels accessible by everyone. + /// + public IReadOnlyList Channels { get; } + + /// + /// Up to 100 users including their presences. + /// + public IReadOnlyList Members { get; } + + /// + /// The number of online members in this guild. + /// + public int PresenceCount { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildWidgetSettings.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildWidgetSettings.cs new file mode 100644 index 0000000000..46ddbdece8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IGuildWidgetSettings.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains settings for this guild's widget. +/// +public interface IGuildWidgetSettings +{ + /// + /// Indicates whether the widget is enabled. + /// + public bool Enabled { get; } + + /// + /// The snowflake identifier of the widget channel. + /// + public Snowflake? ChannelId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIncidentsData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIncidentsData.cs new file mode 100644 index 0000000000..d2048f6dd0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIncidentsData.cs @@ -0,0 +1,34 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains metadata for a guild pertaining to recent incidents and moderation actions, such as temporarily disabling invites or detected raids. +/// +public interface IIncidentsData +{ + /// + /// Indicates when invites get enabled again. + /// + public DateTimeOffset? InvitesDisabledUntil { get; } + + /// + /// Indicates when direct messages between guild members get enabled again. Note that they can still message each other if they can do so through other + /// means, such as being friends or sharing another mutual server. + /// + public DateTimeOffset? DmsDisabledUntil { get; } + + /// + /// Indicates when DM spam was last detected in the guild. + /// + public Optional DmSpamDetectedAt { get; } + + /// + /// Indicates when a raid was last detected in the guild. + /// + public Optional RaidDetectedAt { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegration.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegration.cs new file mode 100644 index 0000000000..740e7b1fa7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegration.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an integration between an external service and a guild. +/// +public interface IIntegration : IPartialIntegration +{ + /// + public new Snowflake Id { get; } + + /// + public new string Name { get; } + + /// + public new string Type { get; } + + /// + public new bool Enabled { get; } + + /// + public new IIntegrationAccount Account { get; } + + // partial access routes + + /// + Optional IPartialIntegration.Id => this.Id; + + /// + Optional IPartialIntegration.Name => this.Name; + + /// + Optional IPartialIntegration.Type => this.Type; + + /// + Optional IPartialIntegration.Enabled => this.Enabled; + + /// + Optional IPartialIntegration.Account => new(this.Account); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegrationAccount.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegrationAccount.cs new file mode 100644 index 0000000000..8035afaa67 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegrationAccount.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains additional information about integration accounts. +/// +public interface IIntegrationAccount +{ + /// + /// The ID of this account. + /// + public string Id { get; } + + /// + /// The name of this account. + /// + public string Name { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegrationApplication.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegrationApplication.cs new file mode 100644 index 0000000000..8543242d81 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IIntegrationApplication.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents application-specific metadata for an integration. +/// +public interface IIntegrationApplication +{ + /// + /// The snowflake identifier of this application. + /// + public Snowflake Id { get; } + + /// + /// The name of this application. + /// + public string Name { get; } + + /// + /// The icon hash of this application. + /// + public string? Icon { get; } + + /// + /// The description of this application. + /// + public string Description { get; } + + /// + /// The bot user associated with this application. + /// + public Optional Bot { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboarding.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboarding.cs new file mode 100644 index 0000000000..5216b7e8ee --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboarding.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents the guild onboarding flow for the given guild. +/// +public interface IOnboarding +{ + /// + /// The snowflake identifier of the guild this onboarding is part of. + /// + public Snowflake GuildId { get; } + + /// + /// Prompts shown during onboarding and in Customize Community + /// + public IReadOnlyList Prompts { get; } + + /// + /// Snowflake identifiers of channels that members are "opted into" automatically. + /// + public IReadOnlyList DefaultChannelIds { get; } + + /// + /// Indicates whether onboarding is enabled in the guild. + /// + public bool Enabled { get; } + + /// + /// The current onboarding mode. + /// + public DiscordGuildOnboardingPromptType Mode { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboardingPrompt.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboardingPrompt.cs new file mode 100644 index 0000000000..3cfd252cca --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboardingPrompt.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an user prompt during the onboarding flow. +/// +public interface IOnboardingPrompt +{ + /// + /// The snowflake identifier of the prompt option. + /// + public Snowflake Id { get; } + + /// + /// The type of this prompt. + /// + public DiscordGuildOnboardingPromptType Type { get; } + + /// + /// The options available within this prompt. + /// + public IReadOnlyList Options { get; } + + /// + /// The title of this prompt. + /// + public string Title { get; } + + /// + /// Indicates whether users are limited to selecting only one of the . + /// + public bool SingleSelect { get; } + + /// + /// Indicates whether this prompt is required to be completed before the user can complete the onboarding flow. + /// + public bool Required { get; } + + /// + /// Indicates whether this prompt is present in the onboarding flow. If , the + /// prompt will only appear in Customize Community. + /// + public bool InOnboarding { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboardingPromptOption.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboardingPromptOption.cs new file mode 100644 index 0000000000..8fa6e0a918 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IOnboardingPromptOption.cs @@ -0,0 +1,59 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an option for a guild onboarding prompt. +/// +public interface IOnboardingPromptOption +{ + /// + /// The snowflake identifier of the prompt option. + /// + public Snowflake Id { get; } + + /// + /// The snowflake identifiers for channels a member is added to when this option is selected. + /// + public IReadOnlyList ChannelIds { get; } + + /// + /// The snowflake identifiers for roles assigned to a member when this option is selected. + /// + public IReadOnlyList RoleIds { get; } + + /// + /// The emoji for this option. This field is only ever returned, and must not be sent when creating + /// or updating a prompt option. + /// + public Optional Emoji { get; } + + /// + /// The snowflake identifier of the emoji for this option. + /// + public Optional EmojiId { get; } + + /// + /// The name of the emoji for this option. + /// + public Optional EmojiName { get; } + + /// + /// Indicates whether the emoji for this option is animated. + /// + public Optional EmojiAnimated { get; } + + /// + /// The title of this option. + /// + public string Title { get; } + + /// + /// The description of this option. + /// + public string? Description { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialGuild.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialGuild.cs new file mode 100644 index 0000000000..94e5b6642e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialGuild.cs @@ -0,0 +1,233 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated guild object. +/// +public interface IPartialGuild +{ + /// + /// The snowflake identifier of this guild. + /// + public Optional Id { get; } + + /// + /// The name of this guild, between 2 and 100 characters. + /// + public Optional Name { get; } + + /// + /// The icon hash for this guild. + /// + public Optional Icon { get; } + + /// + /// The icon hash for this guild. This field, unlike , is only sent when a part of + /// the guild template object. + /// + public Optional IconHash { get; } + + /// + /// The splash hash for this guild. + /// + public Optional Splash { get; } + + /// + /// The discovery splash hash for this guild, only present for discoverable guilds. + /// + public Optional DiscoverySplash { get; } + + /// + /// Indicates whether the current user owns this guild. + /// + public Optional Owner { get; } + + /// + /// The snowflake identifier of the user owning this guild. + /// + public Optional OwnerId { get; } + + /// + /// The permissions for the current user, excluding overwrites. + /// + public Optional Permissions { get; } + + /// + /// The snowflake identifier of the afk voice channel. + /// + public Optional AfkChannelId { get; } + + /// + /// The voice afk timeout in seconds. + /// + public Optional AfkTimeout { get; } + + /// + /// Indicates whether the guild widget is enabled. + /// + public Optional WidgetEnabled { get; } + + /// + /// The snowflake identifier of the channel that the guild widget will generate an invite to. + /// + public Optional WidgetChannelId { get; } + + /// + /// Indicates the verification level required to chat in this guild. + /// + public Optional VerificationLevel { get; } + + /// + /// Indicates the default message notification setting. + /// + public Optional DefaultMessageNotifications { get; } + + /// + /// Indicates the default severity level of the explicit content filter. + /// + public Optional ExplicitContentFilter { get; } + + /// + /// The roles within this guild. + /// + public Optional> Roles { get; } + + /// + /// The custom guild emojis for this guild. + /// + public Optional> Emojis { get; } + + /// + /// The enabled guild feature identifiers for this guild. + /// + public Optional> Features { get; } + + /// + /// The required MFA level for moderation actions in this guild. + /// + public Optional MfaLevel { get; } + + /// + /// The snowflake identifier of the application that created this guild, if it was created by a bot. + /// + public Optional ApplicationId { get; } + + /// + /// The snowflake identifier of the channel where guild notices such as welcome messages and boost + /// messages are sent. + /// + public Optional SystemChannelId { get; } + + /// + /// Additional settings for the system channel in this guild, represented as flags. + /// + public Optional SystemChannelFlags { get; } + + /// + /// The snowflake identifier of the channel where community servers display rules and guidelines. + /// + public Optional RulesChannelId { get; } + + /// + /// The maximum number of presences for this guild. This will nearly always be , + /// except for the largest guilds. + /// + public Optional MaxPresences { get; } + + /// + /// The member limit for this guild. + /// + public Optional MaxMembers { get; } + + /// + /// The vanity invite code for this guild. + /// + public Optional VanityUrlCode { get; } + + /// + /// The description of this guild. + /// + public Optional Description { get; } + + /// + /// The banner image hash for this guild. + /// + public Optional Banner { get; } + + /// + /// The server boost level for this guild. + /// + public Optional PremiumTier { get; } + + /// + /// The amount of server boosts this guild has. + /// + public Optional PremiumSubscriptionCount { get; } + + /// + /// The preferred locale of a community guild, defaults to "en-US". + /// + public Optional PreferredLocale { get; } + + /// + /// The snowflake identifier of the channel where official notices from Discord are sent to. + /// + public Optional PublicUpdatesChannelId { get; } + + /// + /// The maximum amount of users in a video channel. + /// + public Optional MaxVideoChannelUsers { get; } + + /// + /// The maximum amount of users in a stage video channel. + /// + public Optional MaxStageVideoChannelUsers { get; } + + /// + /// The approximate number of members in this guild. + /// + public Optional ApproximateMemberCount { get; } + + /// + /// The approximate number of non-offline members in this guild. + /// + public Optional ApproximatePresenceCount { get; } + + /// + /// The welcome screen of a community guild, shown to new members. + /// + public Optional WelcomeScreen { get; } + + /// + /// The NSFW level of this guild. + /// + public Optional NsfwLevel { get; } + + /// + /// The custom guild stickers. + /// + public Optional> Stickers { get; } + + /// + /// Indicates whether this guild has the boost progress bar enabled. + /// + public Optional PremiumProgressBarEnabled { get; } + + /// + /// The snowflake identifier of the channel where community servers receive safety alerts. + /// + public Optional SafetyAlertsChannelId { get; } + + /// + /// Guild metadata pertaining to raids and spam incidents and actions taken to resolve them. + /// + public Optional IncidentsData { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialGuildMember.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialGuildMember.cs new file mode 100644 index 0000000000..35d30a67ac --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialGuildMember.cs @@ -0,0 +1,88 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single member of a guild. +/// +public interface IPartialGuildMember +{ + /// + /// The underlying user account. + /// + public Optional User { get; } + + /// + /// This user's guild nickname. + /// + public Optional Nick { get; } + + /// + /// The user's guild avatar hash. + /// + public Optional Avatar { get; } + + /// + /// The user's guild banner hash. + /// + public Optional Banner { get; } + + /// + /// This user's list of roles. + /// + public Optional> Roles { get; } + + /// + /// Stores when this user last joined this guild. + /// + public Optional JoinedAt { get; } + + /// + /// Stores when this user started boosting this guild. + /// + public Optional PremiumSince { get; } + + /// + /// Indicates whether this user is server deafened. + /// + public Optional Deaf { get; } + + /// + /// Indicates whether this user is server muted. + /// + public Optional Mute { get; } + + /// + /// Additional flags for this guild member. + /// + public Optional Flags { get; } + + /// + /// Indicates whether the user is in the process of passing the guild membership + /// screening requirements. + /// + public Optional Pending { get; } + + /// + /// Total permissions of this guild member including channel overwrites. This is only set in + /// interaction-related objects. + /// + public Optional Permissions { get; } + + /// + /// The timestamp at which this user's timeout expires. + /// + public Optional CommunicationDisabledUntil { get; } + + /// + /// If present, contains data for the member's avatar decoration. + /// + public Optional AvatarDecorationData { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialIntegration.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialIntegration.cs new file mode 100644 index 0000000000..c5d2ec47df --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialIntegration.cs @@ -0,0 +1,97 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated integration object. +/// +public interface IPartialIntegration +{ + /// + /// The snowflake identifier of the integration. + /// + public Optional Id { get; } + + /// + /// The name of this integration. + /// + public Optional Name { get; } + + /// + /// The type of this integration; twitch, youtube, discord or guild_subscription. + /// + public Optional Type { get; } + + /// + /// Indicates whether this integration is enabled. + /// + public Optional Enabled { get; } + + /// + /// Indicates whether this integration is synchronizing. + /// + public Optional Syncing { get; } + + /// + /// The snowflake identifier of the role that this integration uses for "subscribers". + /// + public Optional RoleId { get; } + + /// + /// Indicates whether emoticons should be synced for this integration. This is currently only + /// applicable to twitch. + /// + public Optional EnableEmoticons { get; } + + /// + /// Indicates how this integration should behave when a subscription expires. + /// + public Optional ExpireBehaviour { get; } + + /// + /// The grace period, in days, before expiring subscribers. + /// + public Optional ExpireGracePeriod { get; } + + /// + /// The user for this integration. + /// + public Optional User { get; } + + /// + /// Contains additional integration account metadata. + /// + public Optional Account { get; } + + /// + /// Indicates when this integration was last synced. + /// + public Optional SyncedAt { get; } + + /// + /// The amount of subscribers this integration has. + /// + public Optional SubscriberCount { get; } + + /// + /// Indicates whether this integration has been revoked. + /// + public Optional Revoked { get; } + + /// + /// The bot/oauth2 application for discord integrations. + /// + public Optional Application { get; } + + /// + /// The OAuth2 scopes this application has been authorized for. + /// + public Optional> Scopes { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialRole.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialRole.cs new file mode 100644 index 0000000000..b3bc50b06a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IPartialRole.cs @@ -0,0 +1,73 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated role object. +/// +public interface IPartialRole +{ + /// + /// The snowflake identifier of this role. + /// + public Optional Id { get; } + + /// + /// The name of this role. + /// + public Optional Name { get; } + + /// + /// The RGB color code of this role, #000000 represents a transparent role. + /// + public Optional Color { get; } + + /// + /// Indicates whether users with this role are hoisted in the member list. + /// + public Optional Hoist { get; } + + /// + /// This role's role icon hash, if applicable. + /// + public Optional Hash { get; } + + /// + /// The unicode emoji serving as this role's role icon, if applicable. + /// + public Optional UnicodeEmoji { get; } + + /// + /// The position of this role in the role list. + /// + public Optional Position { get; } + + /// + /// The permissions associated with this role. + /// + public Optional Permissions { get; } + + /// + /// Indicates whether this role is managed by an integration. + /// + public Optional Managed { get; } + + /// + /// Indicates whether this role can be mentioned by users without the permission. + /// + public Optional Mentionable { get; } + + /// + /// Additional tags added to this role. + /// + public Optional Tags { get; } + + /// + /// Flags for this role, combined as a bitfield. + /// + public Optional Flags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IRole.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IRole.cs new file mode 100644 index 0000000000..d3061c69a2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IRole.cs @@ -0,0 +1,69 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a role within a guild. +/// +public interface IRole : IPartialRole +{ + /// + public new Snowflake Id { get; } + + /// + public new string Name { get; } + + /// + public new int Color { get; } + + /// + public new bool Hoist { get; } + + /// + public new int Position { get; } + + /// + public new DiscordPermissions Permissions { get; } + + /// + public new bool Managed { get; } + + /// + public new bool Mentionable { get; } + + /// + public new DiscordRoleFlags Flags { get; } + + // partial access routes + + /// + Optional IPartialRole.Id => this.Id; + + /// + Optional IPartialRole.Name => this.Name; + + /// + Optional IPartialRole.Color => this.Color; + + /// + Optional IPartialRole.Hoist => this.Hoist; + + /// + Optional IPartialRole.Position => this.Position; + + /// + Optional IPartialRole.Permissions => this.Permissions; + + /// + Optional IPartialRole.Managed => this.Managed; + + /// + Optional IPartialRole.Mentionable => this.Mentionable; + + /// + Optional IPartialRole.Flags => this.Flags; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IRoleTags.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IRoleTags.cs new file mode 100644 index 0000000000..2d84a67ad6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IRoleTags.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains additional tags associated with a given role. +/// +public interface IRoleTags +{ + /// + /// The snowflake identifier of the bot this role belongs to. + /// + public Optional BotId { get; } + + /// + /// The snowflake identifier of the integration this role belongs to. + /// + public Optional IntegrationId { get; } + + /// + /// Indicates whether this is the guild's booster role. + /// + public bool PremiumSubscriber { get; } + + /// + /// The snowflake identifier of this role's subscription SKU and listing. + /// + public Optional SubscriptionListingId { get; } + + /// + /// Indicates whether this role is available for purchase. + /// + public bool AvailableForPurchase { get; } + + /// + /// Indicates whether this role is a linked role. + /// + public bool GuildConnections { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IWelcomeScreen.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IWelcomeScreen.cs new file mode 100644 index 0000000000..ab7b87c0e9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IWelcomeScreen.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a guild welcome screen, showing the user a brief description and a few channels to check out. +/// +public interface IWelcomeScreen +{ + /// + /// The server description as shown in the welcome screen. + /// + public string? Description { get; } + + /// + /// The channels shown in the welcome screen, up to five. + /// + public IReadOnlyList WelcomeChannels { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IWelcomeScreenChannel.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IWelcomeScreenChannel.cs new file mode 100644 index 0000000000..ecdaaf7983 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Guilds/IWelcomeScreenChannel.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single shown in the welcome screen. +/// +public interface IWelcomeScreenChannel +{ + /// + /// The snowflake identifier of the channel. + /// + public Snowflake ChannelId { get; } + + /// + /// The description shown for this channel. + /// + public string Description { get; } + + /// + /// The snowflake identifier of the associated emoji, if this is a custom emoji. + /// + public Snowflake? EmojiId { get; } + + /// + /// The emoji name of the associated emoji if this is a custom emoji; the unicode character if this is + /// a default emoji, or if no emoji is set. + /// + public string? EmojiName { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IApplicationCommandInteractionData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IApplicationCommandInteractionData.cs new file mode 100644 index 0000000000..ef175e5f26 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IApplicationCommandInteractionData.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains the interaction metadata for application commands. +/// +public interface IApplicationCommandInteractionData +{ + /// + /// The snowflake identifier of the invoked command. + /// + public Snowflake Id { get; } + + /// + /// The name of the invoked command. + /// + public string Name { get; } + + /// + /// The type of the invoked command. + /// + public DiscordApplicationCommandType Type { get; } + + /// + /// Contains resolved users, guild members, roles, channels, messages and attachments related to this + /// interaction. + /// + public Optional Resolved { get; } + + /// + /// The parameters and passed values from the user. + /// + public Optional> Options { get; } + + /// + /// The snowflake identifier of the guild this command is being registered to. + /// + public Optional GuildId { get; } + + /// + /// The snowflake identifier of the user or message targeted by this user/message command. + /// + public Optional TargetId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IApplicationCommandInteractionDataOption.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IApplicationCommandInteractionDataOption.cs new file mode 100644 index 0000000000..c2cbe552df --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IApplicationCommandInteractionDataOption.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +using OneOf; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents submitted data for a single application command option. +/// +public interface IApplicationCommandInteractionDataOption +{ + /// + /// The name of the option. + /// + public string Name { get; } + + /// + /// The type of this option. + /// + public DiscordApplicationCommandOptionType Type { get; } + + /// + /// The value passed to this option by the user. + /// + public Optional> Value { get; } + + /// + /// If this is a subcommand, the values passed to its parameters. If this is a subcommand group, + /// its subcommands. + /// + public Optional> Options { get; } + + /// + /// Indicates whether this option is currently focused for autocomplete. + /// + public Optional Focused { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IAutocompleteCallbackData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IAutocompleteCallbackData.cs new file mode 100644 index 0000000000..111329f1ba --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IAutocompleteCallbackData.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents callback data for responding to an autocomplete interaction. +/// +public interface IAutocompleteCallbackData +{ + /// + /// Up to 25 choices for the end user to choose from. + /// + public IReadOnlyList Choices { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IInteraction.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IInteraction.cs new file mode 100644 index 0000000000..302d3498de --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IInteraction.cs @@ -0,0 +1,122 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +using OneOf; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an incoming interaction, received when an user submits an application command +/// or uses a message component.
+/// For application commands, it includes the submitted options;
+/// For context menu commands it includes the context;
+/// For message components it includes information about the component as well as metadata +/// about how the interaction was triggered. +///
+public interface IInteraction +{ + /// + /// The snowflake identifier of this interaction. + /// + public Snowflake Id { get; } + + /// + /// The snowflake identifier of the application this interaction is sent to. + /// + public Snowflake ApplicationId { get; } + + /// + /// The type of this interaction. + /// + public DiscordInteractionType Type { get; } + + /// + /// The data payload, depending on the . + /// + public Optional> Data { get; } + + /// + /// The guild this interaction was sent from. + /// + public Optional Guild { get; } + + /// + /// The snowflake identifier of the guild this interaction was sent from, if applicable. + /// + public Optional GuildId { get; } + + /// + /// The channel this interaction was sent from. + /// + public Optional Channel { get; } + + /// + /// The snowflake identifier of the channel this interaction was sent from, if applicable. + /// + public Optional ChannelId { get; } + + /// + /// The guild member object for the invoking user, if applicable. + /// + public Optional Member { get; } + + /// + /// The user object for the invokign user, if invoked in a DM. + /// + public Optional User { get; } + + /// + /// The continuation token for responding to this interaction. + /// + public string Token { get; } + + /// + /// Always 1. + /// + public int Version { get; } + + /// + /// For components, the message they were attached to. + /// + public Optional Message { get; } + + /// + /// The permissions the application has within the channel the interaction was sent from. + /// + public Optional AppPermissions { get; } + + /// + /// The selected locale of the invoking user. + /// + public Optional Locale { get; } + + /// + /// The preferred locale of the guild, if invoked in a guild. + /// + public Optional GuildLocale { get; } + + /// + /// For monetized apps, any entitlements for the invoking user. + /// + public IReadOnlyList Entitlements { get; } + + /// + /// The user or guild IDs the integration is authorized in for the given context. + /// + public IReadOnlyDictionary AuthorizingIntegrationOwners { get; } + + /// + /// The context the interaction was triggered in. + /// + public Optional Context { get; } + + /// + /// The size limit for attachments this interaction can be responded to with, in bytes. + /// + public int AttachmentSizeLimit { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IInteractionResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IInteractionResponse.cs new file mode 100644 index 0000000000..2fc521c1f6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IInteractionResponse.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +using OneOf; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an outgoing response to an interaction. +/// +public interface IInteractionResponse +{ + /// + /// The type of this response. + /// + public DiscordInteractionCallbackType Type { get; } + + /// + /// An additional response message for this interaction. + /// + public Optional> Data { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IMessageCallbackData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IMessageCallbackData.cs new file mode 100644 index 0000000000..d402c84d06 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IMessageCallbackData.cs @@ -0,0 +1,57 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a response payload for creating or updating a message. +/// +public interface IMessageCallbackData +{ + /// + /// Indicates whether the response is a TTS message. + /// + public Optional Tts { get; } + + /// + /// The message content. + /// + public Optional Content { get; } + + /// + /// An array of up to 10 embeds to be attached to the message. + /// + public Optional> Embeds { get; } + + /// + /// An allowed mentions object controlling mention behaviour for this method. + /// + public Optional AllowedMentions { get; } + + /// + /// Message flags for this message; only SuppressEmbeds and Ephemeral can be set. + /// + public Optional Flags { get; } + + /// + /// Up to five action rows of components to attach to this message. + /// + public Optional> Components { get; } + + /// + /// Attachments to this message. + /// + public Optional> Attachments { get; } + + /// + /// The poll to create along with this message. + /// + public Optional Poll { get; } + + public IReadOnlyList? Files { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IMessageComponentInteractionData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IMessageComponentInteractionData.cs new file mode 100644 index 0000000000..0736c8cf3f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IMessageComponentInteractionData.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata for a message component interaction. +/// +public interface IMessageComponentInteractionData +{ + /// + /// The developer-defined ID of the component. + /// + public string CustomId { get; } + + /// + /// The type of this component. + /// + public DiscordMessageComponentType ComponentType { get; } + + /// + /// The values selected in the associated select menu, if applicable. + /// + public Optional> Values { get; } + + /// + /// Resolved entities from the selected options. + /// + public Optional Resolved { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IModalCallbackData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IModalCallbackData.cs new file mode 100644 index 0000000000..24c6015901 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IModalCallbackData.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents callback data for creating a modal. +/// +public interface IModalCallbackData +{ + /// + /// The developer-defined custom identifier for this modal, up to 100 characters. + /// + public string CustomId { get; } + + /// + /// The title of the modal, up to 45 characters. + /// + public string Title { get; } + + /// + /// Between 1 and 5 action rows containing each one text input component. + /// + public IReadOnlyList Components { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IModalInteractionData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IModalInteractionData.cs new file mode 100644 index 0000000000..e9551a74dc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IModalInteractionData.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains metadata for a modal submission interaction. +/// +public interface IModalInteractionData +{ + /// + /// The developer-defined ID of this modal. + /// + public string CustomId { get; } + + /// + /// The values submitted by the user. + /// + public IReadOnlyList Components { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IResolvedData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IResolvedData.cs new file mode 100644 index 0000000000..6fc6ade4ca --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Interactions/IResolvedData.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents data resolved by Discord from s. +/// +public interface IResolvedData +{ + /// + /// Maps snowflakes to resolved user objects. + /// + public Optional> Users { get; } + + /// + /// Maps snowflakes to resolved guild member objects. + /// + public Optional> Members { get; } + + /// + /// Maps snowflakes to role objects. + /// + public Optional> Roles { get; } + + /// + /// Maps snowflakes to channel objects. + /// + public Optional> Channels { get; } + + /// + /// Maps snowflakes to message objects. + /// + public Optional> Messages { get; } + + /// + /// Maps snowflakes to attachment objects. + /// + public Optional> Attachments { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Invites/IInvite.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Invites/IInvite.cs new file mode 100644 index 0000000000..439d1c846d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Invites/IInvite.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a discord invite object. +/// +public interface IInvite : IPartialInvite +{ + /// + public new DiscordInviteType Type { get; } + + /// + public new string Code { get; } + + /// + public new IPartialChannel? Channel { get; } + + // partial access routes + + /// + Optional IPartialInvite.Type => this.Type; + + /// + Optional IPartialInvite.Code => this.Code; + + /// + Optional IPartialInvite.Channel => new(this.Channel); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Invites/IPartialInvite.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Invites/IPartialInvite.cs new file mode 100644 index 0000000000..49274510a2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Invites/IPartialInvite.cs @@ -0,0 +1,100 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated invite object. +/// +public interface IPartialInvite +{ + /// + /// Specifies the type of this invite. + /// + public Optional Type { get; } + + /// + /// The unique invite code. + /// + public Optional Code { get; } + + /// + /// The guild this invite points to. + /// + public Optional Guild { get; } + + /// + /// The channel this invite points to. + /// + public Optional Channel { get; } + + /// + /// The user who created this invite. + /// + public Optional Inviter { get; } + + /// + /// The target type of this voice channel invite. + /// + public Optional TargetType { get; } + + /// + /// The user whose stream to display for this stream invite. + /// + public Optional TargetUser { get; } + + /// + /// The embedded application to open for this embedded application invite. + /// + public Optional TargetApplication { get; } + + /// + /// The approximate count of online members for this guild. + /// + public Optional ApproximatePresenceCount { get; } + + /// + /// The approximate count of total members in this guild. + /// + public Optional ApproximateMemberCount { get; } + + /// + /// The expiration date of this invite. + /// + public Optional ExpiresAt { get; } + + /// + /// Guild scheduled event data for the guild this invite points to. + /// + public Optional GuildScheduledEvent { get; } + + /// + /// The number of times this invite has been used. + /// + public Optional Uses { get; } + + /// + /// The number of times this invite can be used. + /// + public Optional MaxUses { get; } + + /// + /// The duration in seconds after which this invite expires. + /// + public Optional MaxAge { get; } + + /// + /// Indicates whether this invite only grants temporary membership. + /// + public Optional Temporary { get; } + + /// + /// Indicates when this invite was created. + /// + public Optional CreatedAt { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IAllowedMentions.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IAllowedMentions.cs new file mode 100644 index 0000000000..417280b57f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IAllowedMentions.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Stores information about what mentions should be allowed and which ones should be +/// ignored when handling read states and notifications. Refer to +/// +/// the Discord docs for further information. +/// +public interface IAllowedMentions +{ + /// + /// An array of allowed mention types to parse from the message content. This may contain + /// "roles" for parsing role mentions, "users" for parsing user mentions and + /// "everyone" for parsing @everyone and @here mentions. + /// + public Optional> Parse { get; } + + /// + /// An array of role IDs to mention, up to 100. + /// + public Optional> Roles { get; } + + /// + /// An array of user IDs to mention, up to 100. + /// + public Optional> Users { get; } + + /// + /// For replies, this controls whether to mention the author of the replied message. + /// + public Optional RepliedUser { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IAttachment.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IAttachment.cs new file mode 100644 index 0000000000..ed88efb70d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IAttachment.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an attachment to a message. +/// +public interface IAttachment : IPartialAttachment +{ + /// + public new Snowflake Id { get; } + + /// + public new string Filename { get; } + + /// + public new int Size { get; } + + /// + public new string Url { get; } + + /// + public new string ProxyUrl { get; } + + // partial access routing + + /// + Optional IPartialAttachment.Id => this.Id; + + /// + Optional IPartialAttachment.Filename => this.Filename; + + /// + Optional IPartialAttachment.Size => this.Size; + + /// + Optional IPartialAttachment.Url => this.Url; + + /// + Optional IPartialAttachment.ProxyUrl => this.ProxyUrl; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IChannelMention.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IChannelMention.cs new file mode 100644 index 0000000000..0f362d19e4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IChannelMention.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a mention of a guild channel. +/// +public interface IChannelMention +{ + /// + /// The snowflake identifier of this channel. + /// + public Snowflake Id { get; } + + /// + /// The snowflake identifier of the guild containing this channel. + /// + public Snowflake GuildId { get; } + + /// + /// The type of this channel. + /// + public DiscordChannelType Type { get; } + + /// + /// The name of this channel. + /// + public string Name { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbed.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbed.cs new file mode 100644 index 0000000000..cb0e0f1369 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbed.cs @@ -0,0 +1,79 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents embedded content in a message. +/// +public interface IEmbed +{ + /// + /// The title of this embed. + /// + public Optional Title { get; } + + /// + /// The type of this embed, always rich for bot/webhook embeds. + /// + public Optional Type { get; } + + /// + /// The main content field of this embed. + /// + public Optional Description { get; } + + /// + /// The url of this embed. + /// + public Optional Url { get; } + + /// + /// The timestamp of this embed content. + /// + public Optional Timestamp { get; } + + /// + /// The color code for the event sidebar. + /// + public Optional Color { get; } + + /// + /// The embed footer. + /// + public Optional Footer { get; } + + /// + /// The embed image. + /// + public Optional Image { get; } + + /// + /// The embed thumbnail. + /// + public Optional Thumbnail { get; } + + /// + /// The embed video. + /// + public Optional Video { get; } + + /// + /// The embed provider. + /// + public Optional Provider { get; } + + /// + /// The embed author. + /// + public Optional Author { get; } + + /// + /// Up to 25 embed fields. + /// + public Optional> Fields { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedAuthor.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedAuthor.cs new file mode 100644 index 0000000000..81d0047fa9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedAuthor.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an embed author object. +/// +public interface IEmbedAuthor +{ + /// + /// The name of the author. + /// + public string Name { get; } + + /// + /// The URL of the author, only supports http(s). + /// + public Optional Url { get; } + + /// + /// The URL of the author icon. + /// + public Optional IconUrl { get; } + + /// + /// The proxied URL of the author icon. + /// + public Optional ProxyIconUrl { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedField.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedField.cs new file mode 100644 index 0000000000..d44dbc9e2b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedField.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an embed field. +/// +public interface IEmbedField +{ + /// + /// The name of the field. This does not support markdown. + /// + public string Name { get; } + + /// + /// The value of this field. + /// + public string Value { get; } + + /// + /// Indicates whether this field is rendered inline. + /// + public Optional Inline { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedFooter.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedFooter.cs new file mode 100644 index 0000000000..ae396fc249 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedFooter.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an embed footer object. +/// +public interface IEmbedFooter +{ + /// + /// The footer text. + /// + public string Text { get; } + + /// + /// The URL of the footer icon. + /// + public Optional IconUrl { get; } + + /// + /// The proxied URL of the footer icon. + /// + public Optional ProxyIconUrl { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedImage.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedImage.cs new file mode 100644 index 0000000000..ee938a2a66 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedImage.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an embedded image. +/// +public interface IEmbedImage +{ + /// + /// The source URL of this image. + /// + public string Url { get; } + + /// + /// The proxied URL of this image. + /// + public Optional ProxyUrl { get; } + + /// + /// The height of the image. + /// + public Optional Height { get; } + + /// + /// The width of the image. + /// + public Optional Width { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedProvider.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedProvider.cs new file mode 100644 index 0000000000..205400b76e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedProvider.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an embed provider. +/// +public interface IEmbedProvider +{ + /// + /// The name of this provider. + /// + public Optional Name { get; } + + /// + /// The URL of this provider. + /// + public Optional Url { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedThumbnail.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedThumbnail.cs new file mode 100644 index 0000000000..778fa84b6b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedThumbnail.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a thumbnail in an embed. +/// +public interface IEmbedThumbnail +{ + /// + /// The source URL of this thumbnail. + /// + public string Url { get; } + + /// + /// The proxied URL of this thumbnail. + /// + public Optional ProxyUrl { get; } + + /// + /// The height of the thumbnail. + /// + public Optional Height { get; } + + /// + /// The width of the thumbnail. + /// + public Optional Width { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedVideo.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedVideo.cs new file mode 100644 index 0000000000..7eefb0f325 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IEmbedVideo.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an embedded video. +/// +public interface IEmbedVideo +{ + /// + /// The source URL of this video. + /// + public Optional Url { get; } + + /// + /// The proxied URL of this video. + /// + public Optional ProxyUrl { get; } + + /// + /// The height of the video. + /// + public Optional Height { get; } + + /// + /// The width of the video. + /// + public Optional Width { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessage.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessage.cs new file mode 100644 index 0000000000..04bbb08ef9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessage.cs @@ -0,0 +1,96 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a complete message object. +/// +public interface IMessage : IPartialMessage +{ + /// + public new Snowflake ChannelId { get; } + + /// + public new IUser Author { get; } + + /// + public new string Content { get; } + + /// + public new DateTimeOffset Timestamp { get; } + + /// + public new DateTimeOffset? EditedTimestamp { get; } + + /// + public new bool Tts { get; } + + /// + public new bool MentionEveryone { get; } + + /// + public new IReadOnlyList Mentions { get; } + + /// + public new IReadOnlyList MentionRoles { get; } + + /// + public new IReadOnlyList Attachments { get; } + + /// + public new IReadOnlyList Embeds { get; } + + /// + public new bool Pinned { get; } + + /// + public new DiscordMessageType Type { get; } + + // explicit routes for partial access + + /// + Optional IPartialMessage.ChannelId => this.ChannelId; + + /// + Optional IPartialMessage.Author => new(this.Author); + + /// + Optional IPartialMessage.Content => this.Content; + + /// + Optional IPartialMessage.Timestamp => this.Timestamp; + + /// + Optional IPartialMessage.EditedTimestamp => this.EditedTimestamp; + + /// + Optional IPartialMessage.Tts => this.Tts; + + /// + Optional IPartialMessage.MentionEveryone => this.MentionEveryone; + + /// + Optional> IPartialMessage.Mentions => new(this.Mentions); + + /// + Optional> IPartialMessage.MentionRoles => new(this.MentionRoles); + + /// + Optional> IPartialMessage.Attachments => new(this.Attachments); + + /// + Optional> IPartialMessage.Embeds => new(this.Embeds); + + /// + Optional IPartialMessage.Pinned => this.Pinned; + + /// + Optional IPartialMessage.Type => this.Type; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageActivity.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageActivity.cs new file mode 100644 index 0000000000..ead2234827 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageActivity.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents activity data encoded in a message. +/// +public interface IMessageActivity +{ + /// + /// The type of this activity. + /// + public DiscordMessageActivityType Type { get; } + + /// + /// The party ID from a rich presence event. + /// + public Optional PartyId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageCall.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageCall.cs new file mode 100644 index 0000000000..5f47337d1c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageCall.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains information about a call in a private channel. +/// +public interface IMessageCall +{ + /// + /// The snowflake IDs of participating users. + /// + public IReadOnlyList Participants { get; } + + /// + /// The timestamp at which the call ended. + /// + public Optional EndedTimestamp { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageInteractionMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageInteractionMetadata.cs new file mode 100644 index 0000000000..b887151512 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageInteractionMetadata.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata about an interaction a message was sent in response to. +/// +public interface IMessageInteractionMetadata +{ + /// + /// The snowflake identifier of the interaction. + /// + public Snowflake Id { get; } + + /// + /// The type of this interaction. + /// + public DiscordInteractionType Type { get; } + + /// + /// The user who triggered this interaction. + /// + public IUser User { get; } + + /// + /// The installation contexts related to this interaction. + /// + public IReadOnlyDictionary AuthorizingIntegrationOwners { get; } + + /// + /// The snowflake identifier of the original response to the interaction. This is only present on follow-up responses. + /// + public Optional OriginalResponseMessageId { get; } + + /// + /// If this interaction was created from a component, this is the snowflake identifier of the message containing the component. + /// + public Optional InteractedMessageId { get; } + + /// + /// If this message was sent in response to a modal interaction, this specifies metadata for the parent interaction the modal + /// was created in response to. + /// + public Optional TriggeringInteractionMetadata { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageReference.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageReference.cs new file mode 100644 index 0000000000..739746b9a3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageReference.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a message reference, used for replies, crossposts, pins, thread created and thread +/// starter messages, and channel following messages. +/// +public interface IMessageReference +{ + /// + /// The type of this reference. If this is unset, should be assumed. + /// + public Optional Type { get; } + + /// + /// The snowflake identifier of the originating message. + /// + public Optional MessageId { get; } + + /// + /// The snowflake identifier of the originating message's parent channel. + /// + public Optional ChannelId { get; } + + /// + /// The snowflake identifier of the originating message's parent guild. + /// + public Optional GuildId { get; } + + /// + /// Indicates whether this reference will be checked for validity when sending the message; and + /// whether failing this check should result in this message not sending. + /// + public Optional FailIfNotExists { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageSnapshot.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageSnapshot.cs new file mode 100644 index 0000000000..c062547eb0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageSnapshot.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a snapshot taken of a message. +/// +public interface IMessageSnapshot +{ + // this is documented as IPartialMessage, however, per the documentation it is not explicitly guaranteed to + // have an ID - therefore, we represent it as a distinct type that does not assume an ID is present + + /// + /// A subset of the data contained within the referenced message. + /// + public IMessageSnapshotContent Message { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageSnapshotContent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageSnapshotContent.cs new file mode 100644 index 0000000000..26770f21c2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IMessageSnapshotContent.cs @@ -0,0 +1,71 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using System.Collections.Generic; +using System; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents the contents of a . +/// +public interface IMessageSnapshotContent +{ + /// + /// The text contents of this message. This will be empty if your application does not have the + /// message content intent. + /// + public Optional Content { get; } + + /// + /// The timestamp at which this message was sent. + /// + public Optional Timestamp { get; } + + /// + /// The timestamp at which this message was last edited. + /// + public Optional EditedTimestamp { get; } + + /// + /// The users specifically mentioned in this message. + /// + public Optional> Mentions { get; } + + /// + /// The roles specifically mentioned in this message. + /// + public Optional> MentionRoles { get; } + + /// + /// The files attached to this message. + /// + public Optional> Attachments { get; } + + /// + /// The embeds added to this message. + /// + public Optional> Embeds { get; } + + /// + /// The type of this message. + /// + public Optional Type { get; } + + /// + /// Additional flags for this message. + /// + public Optional Flags { get; } + + /// + /// The components attached to this message. + /// + public Optional> Components { get; } + + /// + /// The stickers sent along with this message. + /// + public Optional StickerItems { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IPartialAttachment.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IPartialAttachment.cs new file mode 100644 index 0000000000..dee13d4c1f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IPartialAttachment.cs @@ -0,0 +1,85 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated message attachment. +/// +public interface IPartialAttachment +{ + /// + /// The snowflake identifier of this attachment. + /// + public Optional Id { get; } + + /// + /// The attachment's filename. + /// + public Optional Filename { get; } + + /// + /// The attachment's title. + /// + public Optional Title { get; } + + /// + /// The file description, up to 1024 characters. + /// + public Optional Description { get; } + + /// + /// This attachment's media type. + /// + public Optional ContentType { get; } + + /// + /// The file size in bytes. + /// + public Optional Size { get; } + + /// + /// The source URL of this file. + /// + public Optional Url { get; } + + /// + /// A proxied URL of this file. + /// + public Optional ProxyUrl { get; } + + /// + /// The height of this file, if this is an image. + /// + public Optional Height { get; } + + /// + /// The width of this file, if this is an image. + /// + public Optional Width { get; } + + /// + /// Indicates whether this is an ephemeral attachment. + /// + public Optional Ephemeral { get; } + + /// + /// The duration of this voice message in seconds. + /// + public Optional DurationSecs { get; } + + /// + /// base64-encoded byte array representing a sampled waveform for this voice message. + /// + public Optional> Waveform { get; } + + /// + /// Additional flags for this attachment. + /// + public Optional Flags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IPartialMessage.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IPartialMessage.cs new file mode 100644 index 0000000000..9bc1ab659a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IPartialMessage.cs @@ -0,0 +1,192 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated message object. +/// +public interface IPartialMessage +{ + /// + /// The snowflake identifier of this message. + /// + public Snowflake Id { get; } + + /// + /// The snowflake identifier of the channel this message was sent in. + /// + public Optional ChannelId { get; } + + /// + /// The author of this message. This is not guaranteed to be a valid user; if this message was sent + /// by a webhook it will contain webhook metadata instead. Check to see + /// whether this message was generated by a webhook. + /// + public Optional Author { get; } + + /// + /// The text contents of this message. This will be empty if your application does not have the + /// message content intent. + /// + public Optional Content { get; } + + /// + /// The timestamp at which this message was sent. + /// + public Optional Timestamp { get; } + + /// + /// The timestamp at which this message was last edited. + /// + public Optional EditedTimestamp { get; } + + /// + /// Indicates whether this is a text-to-speech message. + /// + public Optional Tts { get; } + + /// + /// Indicates whether this message mentions everyone. + /// + public Optional MentionEveryone { get; } + + /// + /// The users specifically mentioned in this message. + /// + public Optional> Mentions { get; } + + /// + /// The roles specifically mentioned in this message. + /// + public Optional> MentionRoles { get; } + + /// + /// The channels specifically mentioned in this message. + /// + public Optional> MentionChannels { get; } + + /// + /// The files attached to this message. + /// + public Optional> Attachments { get; } + + /// + /// The embeds added to this message. + /// + public Optional> Embeds { get; } + + /// + /// The reactions added to this message. + /// + public Optional> Reactions { get; } + + /// + /// Used for validating whether a message was sent. + /// + public Optional Nonce { get; } + + /// + /// Indicates whether this message is pinned. + /// + public Optional Pinned { get; } + + /// + /// The snowflake identifier of the webhook that generated this message. + /// + public Optional WebhookId { get; } + + /// + /// The type of this message. + /// + public Optional Type { get; } + + /// + /// Sent with rich-presence related chat embeds, encodes an activity. + /// + public Optional Activity { get; } + + /// + /// Sent with rich-presence related chat embeds, encodes an associated application. + /// + public Optional Application { get; } + + /// + /// If this message is an interaction-owned or application-owned webhook, this is the snowflake + /// identifier of its parent application. + /// + public Optional ApplicationId { get; } + + /// + /// Additional flags for this message. + /// + public Optional Flags { get; } + + /// + /// A message reference showing the source of a crosspost, channel follow, pin, reply or thread + /// creation/start message. + /// + public Optional MessageReference { get; } + + /// + /// Contains message snapshot objects associated with the . + /// + public Optional> MessageSnapshots { get; } + + /// + /// The message associated with the . + /// + public Optional ReferencedMessage { get; } + + /// + /// Additional metadata if this message is the original response to an interaction. + /// + public Optional InteractionMetadata { get; } + + /// + /// The thread that was started from this message. + /// + public Optional Thread { get; } + + /// + /// The components attached to this message. + /// + public Optional> Components { get; } + + /// + /// The stickers sent along with this message. + /// + public Optional StickerItems { get; } + + /// + /// An approximate position of this message in a thread. This can be used to estimate the relative + /// position of this message in its parent thread. + /// + public Optional Position { get; } + + /// + /// Metadata for the role subscription purchase or renewal that prompted this message. + /// + public Optional RoleSubscriptionData { get; } + + /// + /// Resolved data for users, members, channels and roles in this messages auto-populated select menus. + /// + public Optional Resolved { get; } + + /// + /// A poll attached to this message. + /// + public Optional Poll { get; } + + /// + /// A call associated with this message. + /// + public Optional Call { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IReaction.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IReaction.cs new file mode 100644 index 0000000000..dff8fbb3dd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IReaction.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a reaction to a message. +/// +public interface IReaction +{ + /// + /// The amount of times this emoji has been reacted with. + /// + public int Count { get; } + + /// + /// Contains additional information about how often this emoji has been reacted with. + /// + public IReactionCountDetails CountDetails { get; } + + /// + /// Indicates whether the current user has reacted using this emoji. + /// + public bool Me { get; } + + /// + /// Indicates whether the current user has super-reacted using this emoji. + /// + public bool MeBurst { get; } + + /// + /// The emoji that is being reacted with. + /// + public IPartialEmoji Emoji { get; } + + /// + /// The color codes used for super reactions. + /// + public IReadOnlyList BurstColors { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IReactionCountDetails.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IReactionCountDetails.cs new file mode 100644 index 0000000000..c72ca780a1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IReactionCountDetails.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Provides additional information about how many times a reaction was applied to a message. +/// +public interface IReactionCountDetails +{ + /// + /// Specifies the amount of super reactions of this emoji applied to the message. + /// + public int Burst { get; } + + /// + /// Specifies the amount of standard reactions of this emoji applied to the message. + /// + public int Normal { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IRoleSubscriptionData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IRoleSubscriptionData.cs new file mode 100644 index 0000000000..ed0979da87 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Messages/IRoleSubscriptionData.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains metadata about a role subscription. +/// +public interface IRoleSubscriptionData +{ + /// + /// The snowflake identifier of the SKU and listing that the user is subscribed to. + /// + public Snowflake RoleSubscriptionListingId { get; } + + /// + /// The name of the tier that the user is subscribed to. + /// + public string TierName { get; } + + /// + /// The cumulative number of months that the user has been subscribed for. + /// + public int TotalMonthsSubscribed { get; } + + /// + /// Indicates whether this notification is for a renewal, rather than a new purchase. + /// + public bool IsRenewal { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/ICreatePoll.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/ICreatePoll.cs new file mode 100644 index 0000000000..8aeff9c5bd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/ICreatePoll.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a poll currently being created by this application. +/// +public interface ICreatePoll +{ + /// + /// The question this poll asks. The text is limited to 300 characters. + /// + public IPollMedia Question { get; } + + /// + /// The answers available within this poll, between 1 and 10. + /// + public IReadOnlyList Answers { get; } + + /// + /// The duration in hours this poll should last; up to 32 days or 768 hours. Defaults to one day or 24 hours. + /// + public Optional Duration { get; } + + /// + /// Specifies whether this poll allows selecting multiple answers. Defaults to false. + /// + public Optional AllowMultiselect { get; } + + /// + /// The layout type of this poll. "Defaults to... DEFAULT!" - Discord. + /// + public Optional LayoutType { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPoll.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPoll.cs new file mode 100644 index 0000000000..edbe6b8ed3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPoll.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a poll object. +/// +public interface IPoll +{ + /// + /// The question this poll asks. The text is limited to 300 characters. + /// + public IPollMedia Question { get; } + + /// + /// The answers available within this poll, between 1 and 10. + /// + public IReadOnlyList Answers { get; } + + /// + /// The timestamp at which this poll expires. + /// + public DateTimeOffset Expiry { get; } + + /// + /// Specifies whether this poll allows selecting multiple answers. + /// + public bool AllowMultiselect { get; } + + /// + /// The layout type of this poll. "Defaults to... DEFAULT!" - Discord. + /// + public Optional LayoutType { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollAnswer.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollAnswer.cs new file mode 100644 index 0000000000..6a2cdc4418 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollAnswer.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an answer option to a poll. +/// +public interface IPollAnswer +{ + /// + /// The numeric ID of this answer. + /// + /// + /// This is only sent as part of responses from the API or gateway. + /// + public int AnswerId { get; } + + /// + /// The text and optional emoji of this answer. The text is limited to 55 characters. + /// + public IPollMedia PollMedia { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollAnswerCount.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollAnswerCount.cs new file mode 100644 index 0000000000..300b6737b2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollAnswerCount.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents the answer count for a poll. +/// +public interface IPollAnswerCount +{ + /// + /// The ID of the answer, corresponds to . + /// + public int Id { get; } + + /// + /// The amount of votes this answer received. + /// + public int Count { get; } + + /// + /// Indicates whether the current user has voted on this poll. Applications are not allowed to vote on polls. + /// + public bool MeVoted { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollMedia.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollMedia.cs new file mode 100644 index 0000000000..d8f9faf9be --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollMedia.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// The backing text field object for polls. Questions can only contain , while answers can also contain +/// . +/// +public interface IPollMedia +{ + /// + /// The contents of the text field. + /// + public Optional Text { get; } + + /// + /// An optional emoji attached to poll answers. + /// + public Optional Emoji { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollResults.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollResults.cs new file mode 100644 index 0000000000..4b458dbbb4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Polls/IPollResults.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents the results from a poll. +/// +/// +/// While a poll is in progress, the results may not be entirely accurate, though they shouldn't deviate by much. +/// After a poll finishes, Discord performs a final, accurate tally. If is set to true, +/// this tally has concluded and the results are accurate. +/// +public interface IPollResults +{ + /// + /// Indicates whether the votes have been fully counted. + /// + public bool IsFinalized { get; } + + /// + /// The counts for each answer. If an answer is missing, it received 0 votes. + /// + public IReadOnlyList AnswerCounts { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/RoleConnections/IRoleConnectionMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/RoleConnections/IRoleConnectionMetadata.cs new file mode 100644 index 0000000000..524432b8ef --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/RoleConnections/IRoleConnectionMetadata.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains role connection metadata for an application. +/// +public interface IRoleConnectionMetadata +{ + /// + /// The type and comparison type of the metadata value. + /// + public DiscordRoleConnectionMetadataType Type { get; } + + /// + /// The dictionary key for the metadata field, between 1 and 50 characters. + /// + public string Key { get; } + + /// + /// The name of this metadata field, between 1 and 100 characters. + /// + public string Name { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> NameLocalizations { get; } + + /// + /// A description for this metadata field, between 1 and 200 characters. + /// + public string Description { get; } + + /// + /// A localization dictionary for , with the keys being locales. + /// + public Optional?> DescriptionLocalizations { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IPartialScheduledEvent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IPartialScheduledEvent.cs new file mode 100644 index 0000000000..7cfef139a1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IPartialScheduledEvent.cs @@ -0,0 +1,93 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated scheduled event. +/// + +// we deliberately ignore some fields listed in the documentation, because the documentation is outdated. +// it still documents data for external events (status: 2023-07-01) despite having been removed a long +// time ago... +// the following changes are thereby made from the documentation: +// - remove entity_metadata +// - remove scheduled_end_time +// - change channel_id to non-nullable +public interface IPartialScheduledEvent +{ + /// + /// The snowflake identifier of the scheduled event. + /// + public Optional Id { get; } + + /// + /// The snowflake identifier of the guild this event belongs to. + /// + public Optional GuildId { get; } + + /// + /// The snowflake identifier of the channel in which this event will be hosted. + /// + public Optional ChannelId { get; } + + /// + /// The snowflake identifier of the user that created this event. + /// + public Optional CreatorId { get; } + + /// + /// The name of this event, 1 to 100 characters. + /// + public Optional Name { get; } + + /// + /// The description of this event, 1 to 1000 characters. + /// + public Optional Description { get; } + + /// + /// The time at which this scheduled event will start. + /// + public Optional ScheduledStartTime { get; } + + /// + /// The privacy level of this event. + /// + public Optional PrivacyLevel { get; } + + /// + /// The status of this scheduled event. + /// + public Optional Status { get; } + + /// + /// The type of this scheduled event. + /// + public Optional EntityType { get; } + + /// + /// The user that created this event. + /// + public Optional Creator { get; } + + /// + /// The number of users subscribed to this event. + /// + public Optional UserCount { get; } + + /// + /// The cover image hash of this event. + /// + public Optional Image { get; } + + /// + /// A definition for how often and at what dates this event should recur. + /// + public Optional RecurrenceRule { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEvent.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEvent.cs new file mode 100644 index 0000000000..7069682ab7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEvent.cs @@ -0,0 +1,71 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a guild scheduled event. +/// +public interface IScheduledEvent : IPartialScheduledEvent +{ + /// + public new Snowflake Id { get; } + + /// + public new Snowflake GuildId { get; } + + /// + public new Snowflake ChannelId { get; } + + /// + public new string Name { get; } + + /// + public new DateTimeOffset ScheduledStartTime { get; } + + /// + public new DiscordScheduledEventPrivacyLevel PrivacyLevel { get; } + + /// + public new DiscordScheduledEventStatus Status { get; } + + /// + public new DiscordScheduledEventType EntityType { get; } + + /// + public new IScheduledEventRecurrenceRule? RecurrenceRule { get; } + + // partial access routes + + /// + Optional IPartialScheduledEvent.Id => this.Id; + + /// + Optional IPartialScheduledEvent.GuildId => this.GuildId; + + /// + Optional IPartialScheduledEvent.ChannelId => this.ChannelId; + + /// + Optional IPartialScheduledEvent.Name => this.Name; + + /// + Optional IPartialScheduledEvent.ScheduledStartTime => this.ScheduledStartTime; + + /// + Optional IPartialScheduledEvent.PrivacyLevel => this.PrivacyLevel; + + /// + Optional IPartialScheduledEvent.Status => this.Status; + + /// + Optional IPartialScheduledEvent.EntityType => this.EntityType; + + /// + Optional IPartialScheduledEvent.RecurrenceRule => new(this.RecurrenceRule); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventMetadata.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventMetadata.cs new file mode 100644 index 0000000000..9cb22e50e7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventMetadata.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata for a scheduled event. +/// +public interface IScheduledEventMetadata +{ + /// + /// The location of the event, up to 100 characters. + /// + public Optional Location { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventRecurrenceDay.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventRecurrenceDay.cs new file mode 100644 index 0000000000..286074fab8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventRecurrenceDay.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Describes which day within a month an event should recur on. +/// +public interface IScheduledEventRecurrenceDay +{ + /// + /// The week of the month this recurrence point refers to. Restricted to 1-5. + /// + public int N { get; } + + /// + /// The day of the specified week to recur at. + /// + public DiscordScheduledEventRecurrenceWeekday Day { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventRecurrenceRule.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventRecurrenceRule.cs new file mode 100644 index 0000000000..26992d04a4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventRecurrenceRule.cs @@ -0,0 +1,104 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a rule for recurring scheduled events. This is a subset of the +/// iCalendar specification and has some rather specific +/// limitations, documented where appropriate. +/// +public interface IScheduledEventRecurrenceRule +{ + /// + /// Specifies the starting time of the recurrence interval. + /// + public DateTimeOffset Start { get; } + + /// + /// Specifies the ending time of the recurrence interval. This cannot be set by the application. + /// + public DateTimeOffset? End { get; } + + /// + /// Specifies how often this event should occur. + /// + public DiscordScheduledEventRecurrenceFrequency Frequency { get; } + + /// + /// Specifies the interval according to . For example, a frequency of + /// with an interval of 2 would mean "every other week". + /// + /// + /// This can only be set to 1 or 2 (not higher) for weekly events, and exactly 1 for all other events. + /// + public int Interval { get; } + + /// + /// Specifies a set of specific days within a week to recur on. This is mutually exclusive with + /// and + . + /// + /// + /// This field is only valid for daily and weekly events according to . However, it also + /// behaves differently depending on the frequency:

+ /// IF the frequency is set to , the values must be a + /// "known set" of weekdays. The following sets are currently allowed:
+ /// - Monday to Friday
+ /// - Tuesday to Saturday
+ /// - Sunday to Thursday
+ /// - Friday and Saturday
+ /// - Saturday and Sunday
+ /// - Sunday and Monday

+ /// IF the frequency is set to , the array must have + /// a length of 1, so only one weekday can have a recurring event on. If you wish to have an event recur on multiple + /// days within a week, use a daily-frequency event. + ///
+ public IReadOnlyList? ByWeekday { get; } + + /// + /// Specifies a set of specific days within a month to recur on. This is mutually exclusive with + /// and + + /// + /// + /// This field is only valid for monthly events according to . It may only contain + /// a single element. + /// + public IReadOnlyList? ByNWeekday { get; } + + /// + /// Specifies a set of months within a year to recur in. This is mutually exclusive with + /// and and requires to also be specified. + /// + /// + /// This field is only valid for yearly events according to . It may only contain a single + /// element. + /// + public IReadOnlyList? ByMonth { get; } + + /// + /// Specifies a date within a month to recur on. This is mutually exclusive with and + /// and requires to also be specified. + /// + /// + /// This field is only valid for yearly events according to . It may only contain a single + /// element. + /// + public IReadOnlyList? ByMonthDay { get; } + + /// + /// Specifies the set of days within the year to recur on. This cannot be set by the application. + /// + public IReadOnlyList? ByYearDay { get; } + + /// + /// Specifies the total amount of times this event is allowed to recur before stopping. This cannot be set by the + /// application. + /// + public int? Count { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventUser.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventUser.cs new file mode 100644 index 0000000000..bed5baa77e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/ScheduledEvents/IScheduledEventUser.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Serves as a container object for users and the scheduled event they have subscribed to. +/// +public interface IScheduledEventUser +{ + /// + /// The snowflake identifier of the event this user has subscribed to. + /// + public Snowflake GuildScheduledEventId { get; } + + /// + /// The user which subscribed to the event. + /// + public IUser User { get; } + + /// + /// Guild member data for this user for the guild which this event belongs to. + /// + public Optional Member { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Skus/ISku.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Skus/ISku.cs new file mode 100644 index 0000000000..d8379da8bd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Skus/ISku.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a premium offering that can be made available to your application's users or guilds. +/// +public interface ISku +{ + /// + /// The identifier of this SKU. + /// + public Snowflake Id { get; } + + /// + /// The type of this SKU. + /// + public DiscordSkuType Type { get; } + + /// + /// The snowflake identifier of the parent application. + /// + public Snowflake ApplicationId { get; } + + /// + /// The user-facing name of the offering. + /// + public string Name { get; } + + /// + /// A system-generated URL slug based on the SKU's name. + /// + public string Slug { get; } + + /// + /// Additional flags for this SKU. + /// + public DiscordSkuFlags Flags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Soundboard/IPartialSoundboardSound.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Soundboard/IPartialSoundboardSound.cs new file mode 100644 index 0000000000..c268cee76d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Soundboard/IPartialSoundboardSound.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a playable soundboard sound. +/// +public interface IPartialSoundboardSound +{ + /// + /// The snowflake identifier of this sound. + /// + public Snowflake SoundId { get; } + + /// + /// The user-readable name of this sound. + /// + public Optional Name { get; } + + /// + /// The volume of this sound, in fractions from 0 to 1. + /// + public Optional Volume { get; } + + /// + /// The snowflake identifier of this sound's associated custom emoji. Mutually exclusive with . + /// + public Optional EmojiId { get; } + + /// + /// The name of this sound's associated unicode emoji. Mutually exclusive with . + /// + public Optional EmojiName { get; } + + /// + /// The snowflake identifier of the guild this sound lives in. This may be missing for global soundboard sounds. + /// + public Optional GuildId { get; } + + /// + /// Indicates whether this sound is currently available for use. + /// + public Optional Available { get; } + + /// + /// The user who uploaded this sound, if applicable. + /// + public Optional User { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Soundboard/ISoundboardSound.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Soundboard/ISoundboardSound.cs new file mode 100644 index 0000000000..aa93e76a73 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Soundboard/ISoundboardSound.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +public interface ISoundboardSound : IPartialSoundboardSound +{ + /// + public new string Name { get; } + + /// + public new double Volume { get; } + + /// + public new Snowflake? EmojiId { get; } + + /// + public new string? EmojiName { get; } + + /// + public new bool Available { get; } + + // partial access + + /// + Optional IPartialSoundboardSound.Name => Name; + + /// + Optional IPartialSoundboardSound.Volume => Volume; + + /// + Optional IPartialSoundboardSound.EmojiId => EmojiId; + + /// + Optional IPartialSoundboardSound.EmojiName => EmojiName; + + /// + Optional IPartialSoundboardSound.Available => Available; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/StageInstances/IPartialStageInstance.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/StageInstances/IPartialStageInstance.cs new file mode 100644 index 0000000000..5b7c144b41 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/StageInstances/IPartialStageInstance.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partial stage instance. +/// +public interface IPartialStageInstance +{ + /// + /// The snowflake identifier of this stage instance. + /// + public Optional Id { get; } + + /// + /// The snowflake identifier of the guild this stage instance is held in. + /// + public Optional GuildId { get; } + + /// + /// The snowflake identifier of the associated stage channel. + /// + public Optional ChannelId { get; } + + /// + /// The topic of this stage instance, 1 to 120 characters. + /// + public Optional Topic { get; } + + /// + /// The privacy level of this stage instance. + /// + public Optional PrivacyLevel { get; } + + /// + /// The snowflake identifier of the scheduled event for this stage instance. + /// + public Optional GuildScheduledEventId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/StageInstances/IStageInstance.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/StageInstances/IStageInstance.cs new file mode 100644 index 0000000000..ef0a76747f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/StageInstances/IStageInstance.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a live stage channel. +/// +public interface IStageInstance : IPartialStageInstance +{ + /// + public new Snowflake Id { get; } + + /// + public new Snowflake GuildId { get; } + + /// + public new Snowflake ChannelId { get; } + + /// + public new string Topic { get; } + + /// + public new DiscordStagePrivacyLevel PrivacyLevel { get; } + + /// + public new Snowflake? GuildScheduledEventId { get; } + + // partial access routes + + /// + Optional IPartialStageInstance.Id => this.Id; + + /// + Optional IPartialStageInstance.GuildId => this.GuildId; + + /// + Optional IPartialStageInstance.ChannelId => this.ChannelId; + + /// + Optional IPartialStageInstance.Topic => this.Topic; + + /// + Optional IPartialStageInstance.PrivacyLevel => this.PrivacyLevel; + + /// + Optional IPartialStageInstance.GuildScheduledEventId => this.GuildScheduledEventId; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IPartialSticker.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IPartialSticker.cs new file mode 100644 index 0000000000..61fe95b165 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IPartialSticker.cs @@ -0,0 +1,72 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated sticker object. +/// +public interface IPartialSticker +{ + /// + /// The snowflake identifier of this sticker. + /// + public Optional Id { get; } + + /// + /// For standard stickers, the snowflake identifier of the pack the sticker is from. + /// + public Optional PackId { get; } + + /// + /// The name of this sticker. + /// + public Optional Name { get; } + + /// + /// The description of this sticker. + /// + public Optional Description { get; } + + /// + /// Autocomplete/suggestion tags for this sticker, up to 200 characters. + /// + /// + /// For standard stickers, this is a comma separated list of keywords. When creating or modifying a guild + /// sticker, the client will always use a name generated from an emoji here. + /// + public Optional Tags { get; } + + /// + /// The type of this sticker. + /// + public Optional Type { get; } + + /// + /// The type of this sticker file format. + /// + public Optional FormatType { get; } + + /// + /// Indicates whether this sticker can be used. + /// + public Optional Available { get; } + + /// + /// The snowflake identifier of the guild that owns this object. + /// + public Optional GuildId { get; } + + /// + /// The user that uploaded this sticker. + /// + public Optional User { get; } + + /// + /// If this is a standard sticker, the sort order within its pack. + /// + public Optional SortValue { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/ISticker.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/ISticker.cs new file mode 100644 index 0000000000..37a97286d5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/ISticker.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a discord sticker that can be sent in messages. +/// +public interface ISticker : IPartialSticker +{ + /// + public new Snowflake Id { get; } + + /// + public new string Name { get; } + + /// + public new string? Description { get; } + + /// + public new string Tags { get; } + + /// + public new DiscordStickerType Type { get; } + + /// + public new DiscordStickerFormatType FormatType { get; } + + // partial access routes + + /// + Optional IPartialSticker.Id => this.Id; + + /// + Optional IPartialSticker.Name => this.Name; + + /// + Optional IPartialSticker.Description => this.Description; + + /// + Optional IPartialSticker.Tags => this.Tags; + + /// + Optional IPartialSticker.Type => this.Type; + + /// + Optional IPartialSticker.FormatType => this.FormatType; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IStickerItem.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IStickerItem.cs new file mode 100644 index 0000000000..56e3ee5586 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IStickerItem.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains the information needed to render a sticker. +/// +public interface IStickerItem +{ + /// + /// The snowflake identifier of the sticker + /// + public Snowflake Id { get; } + + /// + /// The name of this sticker. + /// + public string Name { get; } + + /// + /// The file format of this sticker. + /// + public DiscordStickerFormatType FormatType { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IStickerPack.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IStickerPack.cs new file mode 100644 index 0000000000..3b1df219a5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Stickers/IStickerPack.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a pack of standard stickers. +/// +public interface IStickerPack +{ + /// + /// The snowflake identifier of this sticker pack. + /// + public Snowflake Id { get; } + + /// + /// The stickers in this pack. + /// + public IReadOnlyList Stickers { get; } + + /// + /// The name of this sticker pack. + /// + public string Name { get; } + + /// + /// The snowflake identifier of the pack's SKU. + /// + public Snowflake SkuId { get; } + + /// + /// The snowflake of the sticker shown as the pack's icon. + /// + public Optional CoverStickerId { get; } + + /// + /// The description of this sticker pack. + /// + public string Description { get; } + + /// + /// The snowflake identifier of this pack's banner image. + /// + public Optional BannerAssetId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Subscriptions/ISubscription.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Subscriptions/ISubscription.cs new file mode 100644 index 0000000000..f1fdeaaddf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Subscriptions/ISubscription.cs @@ -0,0 +1,67 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a subscription to an . +/// +public interface ISubscription +{ + /// + /// The snowflake identifier of this subscription. + /// + public Snowflake Id { get; } + + /// + /// The snowflake identifier of the subscribing user. + /// + public Snowflake UserId { get; } + + /// + /// The list of SKUs this subscription applies to. + /// + public IReadOnlyList SkuIds { get; } + + /// + /// The list of entitlements this subscription applies to. + /// + public IReadOnlyList EntitlementIds { get; } + + /// + /// The list of SKUs this user will be subscribed to at renewal. + /// + public IReadOnlyList? RenewalSkuIds { get; } + + /// + /// The starting timestamp of the current subscription period. + /// + public DateTimeOffset CurrentPeriodStart { get; } + + /// + /// The ending timestamp of the current subscription period. + /// + public DateTimeOffset CurrentPeriodEnd { get; } + + /// + /// Specifies the status of this subscription. + /// + public DiscordSubscriptionStatus Status { get; } + + /// + /// Indicates when the subscription was cancelled, if applicable. + /// + public DateTimeOffset? CanceledAt { get; } + + /// + /// Specifies the ISO3166-1-alpha-2 country code of the payment source used to purchase the subscription. + /// Missing unless queried with a private OAuth2 scope. + /// + public Optional Country { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Teams/ITeam.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Teams/ITeam.cs new file mode 100644 index 0000000000..c8eaf4dea2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Teams/ITeam.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a developer team on Discord. Teams can collectively own applications +/// and thereby bots. +/// +public interface ITeam +{ + /// + /// The icon hash for this team. + /// + public string? Icon { get; } + + /// + /// The snowflake identifier of this team. + /// + public Snowflake Id { get; } + + /// + /// The members of this team. + /// + public IReadOnlyList Members { get; } + + /// + /// The name of this team. + /// + public string Name { get; } + + /// + /// The user ID of the current team owner. + /// + public Snowflake OwnerUserId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Teams/ITeamMember.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Teams/ITeamMember.cs new file mode 100644 index 0000000000..81f6e6ecf9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Teams/ITeamMember.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a single member of a . +/// +public interface ITeamMember +{ + /// + /// This user's membership state on the team. + /// + public DiscordTeamMembershipState MembershipState { get; } + + /// + /// This will always be a single string; "*". + /// + public IReadOnlyList Permissions { get; } + + /// + /// The snowflake identifier of the parent team. + /// + public Snowflake TeamId { get; } + + /// + /// The snowflake identifier, username and avatar of this user's discord account. + /// + public IPartialUser User { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IApplicationRoleConnection.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IApplicationRoleConnection.cs new file mode 100644 index 0000000000..2ce2b7bd99 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IApplicationRoleConnection.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a role connection object that an application has attached to a user. +/// +public interface IApplicationRoleConnection +{ + /// + /// The vanity name of the platform the application has connected, up to 50 characters. + /// + public string? PlatformName { get; } + + /// + /// The username of this user on the platform the application has connected, up to 100 characters. + /// + public string? PlatformUsername { get; } + + /// + /// The metadata keys and values for this user on the given platform. + /// + public IReadOnlyDictionary Metadata { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IAvatarDecorationData.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IAvatarDecorationData.cs new file mode 100644 index 0000000000..d583687a47 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IAvatarDecorationData.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Contains the data for an user's avatar decoration. +/// +public interface IAvatarDecorationData +{ + /// + /// The hash of the avatar decoration. + /// + public string Asset { get; } + + /// + /// The snowflake identifier of the associated SKU. + /// + public Snowflake SkuId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IConnection.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IConnection.cs new file mode 100644 index 0000000000..f48d2f11d2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IConnection.cs @@ -0,0 +1,65 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an account connection on an user. +/// +public interface IConnection +{ + /// + /// The identifier of the connection account. + /// + public string Id { get; } + + /// + /// The name of the connection acount. + /// + public string Name { get; } + + /// + /// The service for this connection. + /// + public string Type { get; } + + /// + /// Indicates whether this connection has been revoked. + /// + public Optional Revoked { get; } + + /// + /// Corresponding partial integrations. + /// + public Optional> Integrations { get; } + + /// + /// Indicates whether this connection is verified. + /// + public bool Verified { get; } + + /// + /// Indicates whether friend sync is enabled for this connection. + /// + public bool FriendSync { get; } + + /// + /// Indicates whether activities related to this connection will be shown in presences. + /// + public bool ShowActivity { get; } + + /// + /// Indicates whether this connection has a corresponding third-party OAuth2 token. + /// + public bool TwoWayLink { get; } + + /// + /// The visibility of this connection. + /// + public DiscordConnectionVisibility Visibility { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IPartialUser.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IPartialUser.cs new file mode 100644 index 0000000000..937e043d34 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IPartialUser.cs @@ -0,0 +1,98 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated user object. +/// +public interface IPartialUser +{ + /// + /// The snowflake identifier of this user. + /// + public Optional Id { get; } + + /// + /// The username of this user, unique across the platform. + /// + public Optional Username { get; } + + /// + /// The four-digit discriminator of this user if it is a bot; "0" if this is an user account. + /// + public Optional Discriminator { get; } + + /// + /// The global display name of this user. + /// + public Optional GlobalName { get; } + + /// + /// The user's avatar hash. + /// + public Optional Avatar { get; } + + /// + /// Indicates whether this user is a bot user. + /// + public Optional Bot { get; } + + /// + /// Indicates whether this user is part of Discords urgent message system. + /// + public Optional System { get; } + + /// + /// Indicates whether this user has multi-factor authentication enabled on their account. + /// + public Optional MfaEnabled { get; } + + /// + /// The user's banner hash. + /// + public Optional Banner { get; } + + /// + /// The user's banner color code. + /// + public Optional AccentColor { get; } + + /// + /// The user's chosen language option. + /// + public Optional Locale { get; } + + /// + /// Indicates whether the email address linked to this user account has been verified. + /// + public Optional Verified { get; } + + /// + /// The user's email address. + /// + public Optional Email { get; } + + /// + /// The flags on this user's account. + /// + public Optional Flags { get; } + + /// + /// The level of nitro subscription on this user's account. + /// + public Optional PremiumType { get; } + + /// + /// The publicly visible flags on this user's account. + /// + public Optional PublicFlags { get; } + + /// + /// The user's avatar decoration data. + /// + public Optional AvatarDecorationData { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IUser.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IUser.cs new file mode 100644 index 0000000000..435a6a4bb5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Users/IUser.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents an user object. +/// +public interface IUser : IPartialUser +{ + /// + public new Snowflake Id { get; } + + /// + public new string Username { get; } + + /// + public new string Discriminator { get; } + + /// + public new string? GlobalName { get; } + + /// + public new string? Avatar { get; } + + // explicit routes for partial user access + + /// + Optional IPartialUser.Id => this.Id; + + /// + Optional IPartialUser.Username => this.Username; + + /// + Optional IPartialUser.Discriminator => this.Discriminator; + + /// + Optional IPartialUser.GlobalName => this.GlobalName; + + /// + Optional IPartialUser.Avatar => this.Avatar; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Voice/IVoiceRegion.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Voice/IVoiceRegion.cs new file mode 100644 index 0000000000..9daf95fee5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Voice/IVoiceRegion.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents metadata about a voice region. +/// +public interface IVoiceRegion +{ + /// + /// The unique identifier of this region. + /// + public string Id { get; } + + /// + /// The name of this region. + /// + public string Name { get; } + + /// + /// Indicates whether this is the closest server to the current user's client. + /// + public bool Optimal { get; } + + /// + /// Indicates whether this is a deprecated voice region. + /// + public bool Deprecated { get; } + + /// + /// Indicates whether this is a custom voice region, used for official events etc. + /// + public bool Custom { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Voice/IVoiceState.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Voice/IVoiceState.cs new file mode 100644 index 0000000000..c4985e3e90 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Voice/IVoiceState.cs @@ -0,0 +1,78 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a user's voice connection status. +/// +public interface IVoiceState +{ + /// + /// The snowflake identifier of the guild this voice state is for. + /// + public Optional GuildId { get; } + + /// + /// The snowflake identifier of the channel this user is connected to, if none. + /// + public Snowflake? ChannelId { get; } + + /// + /// The snowflake identifier of the user this voice state is for. + /// + public Snowflake UserId { get; } + + /// + /// The guild member this voice state is for. + /// + public Optional Member { get; } + + /// + /// The session identifier for this voice state. + /// + public string SessionId { get; } + + /// + /// Indicates whether this user is deafened by the server. + /// + public bool Deaf { get; } + + /// + /// Indicates whether this user is muted by the server. + /// + public bool Mute { get; } + + /// + /// Indicates whether this user has deafened themselves. + /// + public bool SelfDeaf { get; } + + /// + /// Indicates whether this user has muted themselves. + /// + public bool SelfMute { get; } + + /// + /// Indicates whether this user is streaming in the voice channel. + /// + public Optional SelfStream { get; } + + /// + /// Indicates whether this user's camera is enabled. + /// + public bool SelfVideo { get; } + + /// + /// Indicates whether this user's permission to speak is denied. + /// + public bool Suppress { get; } + + /// + /// The time at which this user requested to speak. + /// + public DateTimeOffset? RequestToSpeakTimestamp { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Webhooks/IPartialWebhook.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Webhooks/IPartialWebhook.cs new file mode 100644 index 0000000000..9384c25aa2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Webhooks/IPartialWebhook.cs @@ -0,0 +1,73 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a partially populated webhook object. +/// +public interface IPartialWebhook +{ + /// + /// The snowflake identifier of this webhook. + /// + public Optional Id { get; } + + /// + /// The type of this webhook. + /// + public Optional Type { get; } + + /// + /// The snowflake identifier of the guild this webhook is for, if any. + /// + public Optional GuildId { get; } + + /// + /// The snowflake identifier of the channel this webhook is for, if any. + /// + public Optional ChannelId { get; } + + /// + /// The user who created this webhook. + /// + public Optional User { get; } + + /// + /// The default name of this webhook. + /// + public Optional Name { get; } + + /// + /// The default avatar hash of this webhook. + /// + public Optional Avatar { get; } + + /// + /// The secure token of this webhook. + /// + public Optional Token { get; } + + /// + /// The snowflake identifier of the bot/oauth2 application which created this webhook. + /// + public Optional ApplicationId { get; } + + /// + /// The guild containing the channel that this webhook is following. + /// + public Optional SourceGuild { get; } + + /// + /// The channel that this webhook is following. + /// + public Optional SourceChannel { get; } + + /// + /// The url used to execute this webhook. + /// + public Optional Url { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Models/Webhooks/IWebhook.cs b/src/core/DSharpPlus.Internal.Abstractions.Models/Webhooks/IWebhook.cs new file mode 100644 index 0000000000..6c963272e5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Models/Webhooks/IWebhook.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Models; + +/// +/// Represents a discord webhook. +/// +public interface IWebhook : IPartialWebhook +{ + /// + public new Snowflake Id { get; } + + /// + public new DiscordWebhookType Type { get; } + + /// + public new Snowflake? ChannelId { get; } + + /// + public new string? Name { get; } + + /// + public new string? Avatar { get; } + + /// + public new Snowflake? ApplicationId { get; } + + // partial access routes + + /// + Optional IPartialWebhook.Id => this.Id; + + /// + Optional IPartialWebhook.Type => this.Type; + + /// + Optional IPartialWebhook.ChannelId => this.ChannelId; + + /// + Optional IPartialWebhook.Name => this.Name; + + /// + Optional IPartialWebhook.Avatar => this.Avatar; + + /// + Optional IPartialWebhook.ApplicationId => this.ApplicationId; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IApplicationCommandsRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IApplicationCommandsRestAPI.cs new file mode 100644 index 0000000000..83840e6b4f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IApplicationCommandsRestAPI.cs @@ -0,0 +1,275 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to application commands-related API calls. +/// +// https://discord.com/developers/docs/interactions/application-commands +public interface IApplicationCommandsRestAPI +{ + /// + /// Fetches all global application commands for your application. + /// + /// The snowflake identifier of your application. + /// Indicates whether to include full localizations in the returned objects. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// An array of application commands. + public ValueTask>> GetGlobalApplicationCommandsAsync + ( + Snowflake applicationId, + LocalizationQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new global application command for your application. + /// + /// The snowflake identifier of your application. + /// The command you wish to create. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// A value indicating whether this command was newly created as well as the command object. + public ValueTask> CreateGlobalApplicationCommandAsync + ( + Snowflake applicationId, + ICreateGlobalApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches a global application command for your application. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the command to fetch. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The requested application command. + public ValueTask> GetGlobalApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits a global application command for your application. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the command to edit. + /// A payload containing the fields to edit with their new values. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The edited application command. + public ValueTask> EditGlobalApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake commandId, + IEditGlobalApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a global application command for your application. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the command to delete. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// A value indicating the success of this operation. + public ValueTask DeleteGlobalApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Bulk-overwrites global application commands for your application with the provided commands. + /// + /// + /// This will overwrite all types of application commands: slash/chat input commands, user context menu + /// commands and message context menu commands. Commands that did not already exist will count towards the + /// daily application command creation limits, commands that did exist will not. + /// + /// The snowflake identifier of your application. + /// The application commands to create. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The full list of application commands for your application after overwriting. + public ValueTask>> BulkOverwriteGlobalApplicationCommandsAsync + ( + Snowflake applicationId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches application commands for the specified guild. + /// + /// + /// This does not fetch global commands accessible in the guild, only comands registered to specifically + /// that guild using the related API calls. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild containing the application commands. + /// + /// Indicates whether to include localization dictionaries with the commands. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildApplicationCommandsAsync + ( + Snowflake applicationId, + Snowflake guildId, + LocalizationQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates an application command specific to the given guild. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild to own this command. + /// The command you wish to create. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The created application command object. + public ValueTask> CreateGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + ICreateGuildApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches a guild-specific application command by snowflake. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild owning this command. + /// The snowflake identifier of the command to fetch. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits a guild-specific application command. Updates will be available immediately. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild owning this command. + /// The snowflake identifier of this command. + /// A payload containing new properties for this command. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The edited application command object. + public ValueTask> EditGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + IEditGuildApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a guild-specific application command. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild owning this command. + /// The snowflake identifier of this command. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Bulk-overwrites guild-specific application commands for your application with the provided commands. + /// + /// + /// This will overwrite all types of application commands: slash/chat input commands, user context menu + /// commands and message context menu commands. Commands that did not already exist will count towards the + /// daily application command creation limits, commands that did exist will not. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild to own these commands. + /// The application commands to create. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The full list of application commands for your application after overwriting. + public ValueTask>> BulkOverwriteGuildApplicationCommandsAsync + ( + Snowflake applicationId, + Snowflake guildId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches permissions for all commands owned by your application in the given guild. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildApplicationCommandPermissionsAsync + ( + Snowflake applicationId, + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches permissions for the given command in the given guild. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the guild. + /// The snowflake identifier of the command. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetApplicationCommandPermissionsAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IApplicationRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IApplicationRestAPI.cs new file mode 100644 index 0000000000..fa46a80a44 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IApplicationRestAPI.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to application-related rest API calls. +/// +public interface IApplicationRestAPI +{ + /// + /// Returns the application object associated with the requesting bot user. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetCurrentApplicationAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits the application associated with the requesting bot user. + /// + /// A payload object containing properties to update. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The updated application object. + public ValueTask> EditCurrentApplicationAsync + ( + IEditCurrentApplicationPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IAuditLogsRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IAuditLogsRestAPI.cs new file mode 100644 index 0000000000..2719933295 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IAuditLogsRestAPI.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to audit log related rest API calls. +/// +public interface IAuditLogsRestAPI +{ + /// + /// Lists audit log entries for a given guild. + /// + /// The snowflake identifier of the guild to list audit logs for. + /// + /// Contains the query parameters for this request, comprised of request pagination and log entry + /// filtering options. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// An audit log object, containing all relevant other entities and the main audit log. + public ValueTask> ListGuildAuditLogEntriesAsync + ( + Snowflake guildId, + ListGuildAuditLogEntriesQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IAutoModerationRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IAutoModerationRestAPI.cs new file mode 100644 index 0000000000..65b5f8861d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IAutoModerationRestAPI.cs @@ -0,0 +1,102 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to managing the built-in auto moderator via the API. +/// +public interface IAutoModerationRestAPI +{ + /// + /// Fetches the auto moderation rules in the given guild. + /// + /// The snowflake identifier of the guild. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListAutoModerationRulesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches the specified auto moderation rule belonging to the specified guild. + /// + /// The snowflake identifier of the guild owning the rule. + /// The snowflake identifier of the rule to fetch. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetAutoModerationRuleAsync + ( + Snowflake guildId, + Snowflake ruleId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new auto moderation rule in the specified guild. + /// + /// The snowflake identifier of the guild to create the rule in. + /// A payload object containing the necessary information to create the rule. + /// An optional reason to list in the audit log. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created auto moderation rule. + public ValueTask> CreateAutoModerationRuleAsync + ( + Snowflake guildId, + ICreateAutoModerationRulePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the specified auto moderation rule. + /// + /// The snowflake identifier of the guild owning the rule to modify. + /// The snowflake identifier of the rule to modify. + /// A payload object containing the necessary information to modify the rule. + /// An optional reason to list in the audit log. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified auto moderation rule. + public ValueTask> ModifyAutoModerationRuleAsync + ( + Snowflake guildId, + Snowflake ruleId, + IModifyAutoModerationRulePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the specified auto moderation rule. + /// + /// The snowflake identifier of the guild owning the rule to delete. + /// The snowflake identifier of the rule to delete. + /// An optional reason to list in the audit log. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteAutoModerationRuleAsync + ( + Snowflake guildId, + Snowflake ruleId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IChannelRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IChannelRestAPI.cs new file mode 100644 index 0000000000..95be20ccba --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IChannelRestAPI.cs @@ -0,0 +1,488 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to channel-related rest API calls. +/// +public interface IChannelRestAPI +{ + /// + /// Returns a channel object for the given ID. If the channel is a thread channel, a + /// object is included in the returned channel. + /// + /// The snowflake identifier of the channel in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetChannelAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies a group DM channel with the given parameters. + /// + /// The snowflake identifier of the group DM in question. + /// Payload object containing the modification parameters. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The modified channel object. + public ValueTask> ModifyChannelAsync + ( + Snowflake channelId, + IModifyGroupDMPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies a guild channel with the given parameters. + /// + /// The snowflake identifier of the channel in question. + /// Payload object containing the modification parameters. + /// Optional audit log reason for the edit. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The modified channel object. + public ValueTask> ModifyChannelAsync + ( + Snowflake channelId, + IModifyGuildChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies a thread channel with the given parameters. + /// + /// The snowflake identifier of the channel in question. + /// Payload object containing the modification parameters. + /// Optional audit log reason for the edit. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The modified channel object. + public ValueTask> ModifyChannelAsync + ( + Snowflake channelId, + IModifyThreadChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a channel. Deleting guild channels cannot be undone. DM channels, however, cannot be deleted + /// and are restored by opening a direct message channel again. + /// + /// The snowflake identifier of the channel in question. + /// Optional audit log reason if this is a guild channel. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The associated channel object. + public ValueTask> DeleteChannelAsync + ( + Snowflake channelId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits a permission overwrite for a guild channel. + /// + /// The snowflake identifier for the channel in question. + /// The snowflake identifier of the entity (role/user) this overwrite targets. + /// The overwrite data to apply. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the overwrite was successfully edited. + public ValueTask EditChannelPermissionsAsync + ( + Snowflake channelId, + Snowflake overwriteId, + IEditChannelPermissionsPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of invite objects with invite metadata pointing to this channel. + /// + /// The snowflake identifier of the channel in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetChannelInvitesAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates an invite on the specified channel. + /// + /// Snowflake identifier of the channel in question. + /// A payload containing information on the invite. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created invite object. + public ValueTask> CreateChannelInviteAsync + ( + Snowflake channelId, + ICreateChannelInvitePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a channel permission overwrite. + /// + /// The snowflake identifier of the channel in question. + /// The snowflake identifier of the object this overwrite points to. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the deletion was successful. + public ValueTask DeleteChannelPermissionAsync + ( + Snowflake channelId, + Snowflake overwriteId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Follows an announcement channel. + /// + /// The snowflake identifier of the news channel to follow. + /// + /// The payload, containing the snowflake identifier of the channel you want messages to be cross-posted into. + /// + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The created followed channel object. + public ValueTask> FollowAnnouncementChannelAsync + ( + Snowflake channelId, + IFollowAnnouncementChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Triggers the typing indicator for the current user in the given channel. + /// + /// The snowflake identifier of the channel in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask TriggerTypingIndicatorAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns all pinned messages as message objects. + /// + /// The snowflake identifier of the messages' parent channel. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetPinnedMessagesAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Pins a message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the message was successfully pinned. + public ValueTask PinMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Unpins a message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the message was successfully unpinned. + public ValueTask UnpinMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Adds the given user to a specified group DM channel. + /// + /// The snowflake identifier of the group DM channel in question. + /// The snowflake identifier of the user in question. + /// Request payload, containing the access token needed. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask GroupDMAddRecipientAsync + ( + Snowflake channelId, + Snowflake userId, + IGroupDMAddRecipientPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Removes the given user from the given group DM channel. + /// + /// The snowflake identifier of the group DM channel in question. + /// The snowflake identifier of the user in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask GroupDMRemoveRecipientAsync + ( + Snowflake channelId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new thread channel from the given message. + /// + /// The snowflake identifier of the thread's parent channel. + /// The snowflake identifier of the thread's parent message. + /// Request payload for this request. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created thread channel. + public ValueTask> StartThreadFromMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + IStartThreadFromMessagePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new thread channel without a message. + /// + /// The snowflake identifier of the thread's parent channel. + /// Request payload for this request. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created thread channel. + public ValueTask> StartThreadWithoutMessageAsync + ( + Snowflake channelId, + IStartThreadWithoutMessagePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new thread with a starting message in a forum channel. + /// + /// The snowflake identifier of the parent forum channel. + /// + /// A payload object for starting a thread from a message containing a . + /// A new message is created, then a thread is started from it. + /// + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created thread channel. + public ValueTask> StartThreadInForumOrMediaChannelAsync + ( + Snowflake channelId, + IStartThreadInForumOrMediaChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Joins the current user into a thread. + /// + /// The snowflake identifier of the thread channel to be joined. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the operation was successful. + public ValueTask JoinThreadAsync + ( + Snowflake threadId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Adds another member into a thread. + /// + /// The snowflake identifier of the thread to be joined. + /// The snowflake identifier of the user to join into the thread. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the operation was successful. + public ValueTask AddThreadMemberAsync + ( + Snowflake threadId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Leaves a thread as the current bot. + /// + /// The snowflake identifier of the thread to be left. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the operation was successful. + public ValueTask LeaveThreadAsync + ( + Snowflake threadId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Removes another user from a thread. + /// + /// The snowflake identifier of the thread to be left. + /// The snowflake identifier of the user to be removed. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the operation was successful. + public ValueTask RemoveThreadMemberAsync + ( + Snowflake threadId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a thread member object for the specified user. + /// + /// The snowflake identifier of the thread to obtain data from. + /// The snowflake identifier of the user to obtain data for. + /// + /// Specifies whether the returned thread member object should contain guild member data. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetThreadMemberAsync + ( + Snowflake threadId, + Snowflake userId, + GetThreadMemberQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of all thread members for the specified thread. + /// + /// The snowflake identifier fo the thread to obtain data from. + /// + /// Specifies additional query information pertaining to pagination and optional additional data. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListThreadMembersAsync + ( + Snowflake threadId, + ListThreadMembersQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns all public, archived threads for this channel including respective thread member objects. + /// + /// The snowflake identifier of the thread's parent channel. + /// Contains pagination information for this request. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> ListPublicArchivedThreadsAsync + ( + Snowflake channelId, + ListArchivedThreadsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns all private, accessible, archived threads for this channel including respective thread member objects. + /// + /// The snowflake identifier of the thread's parent channel. + /// Contains pagination information for this request. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> ListPrivateArchivedThreadsAsync + ( + Snowflake channelId, + ListArchivedThreadsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of joined, private, archived threads. + /// + /// The nowflake identifier of their parent channel. + /// Contains pagination information for this request. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> ListJoinedPrivateArchivedThreadsAsync + ( + Snowflake channelId, + ListJoinedPrivateArchivedThreadsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); +} + diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IEmojiRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IEmojiRestAPI.cs new file mode 100644 index 0000000000..2cb109e90d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IEmojiRestAPI.cs @@ -0,0 +1,182 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to emoji-related rest API calls. +/// +public interface IEmojiRestAPI +{ + /// + /// Fetches a list of emojis for the given guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListGuildEmojisAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the specified emoji. + /// + /// The snowflake identifier of the guild owning the emoji. + /// The snowflake identifier of the emoji in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildEmojiAsync + ( + Snowflake guildId, + Snowflake emojiId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new guild emoji in the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// The payload containing information on the emoji. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created emoji. + public ValueTask> CreateGuildEmojiAsync + ( + Snowflake guildId, + ICreateGuildEmojiPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given emoji. + /// + /// The snowflake identifier of the guild owning the emoji. + /// The snowflake identifier of the emoji in question. + /// A payload detailing the edit to make to this emoji. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated emoji. + public ValueTask> ModifyGuildEmojiAsync + ( + Snowflake guildId, + Snowflake emojiId, + IModifyGuildEmojiPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given emoji. + /// + /// The snowflake identifier of the guild owning this emoji. + /// The snowflake identifier of the emoji to be deleted. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the deletion was successful. + public ValueTask DeleteGuildEmojiAsync + ( + Snowflake guildId, + Snowflake emojiId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Lists all emojis belonging to the given application. + /// + /// The snowflake identifier of the current application. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> ListApplicationEmojisAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a specified emoji from the given application. + /// + /// The snowflake identifier of the current application. + /// The snowflake identifier of the requested emoji. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetApplicationEmojiAsync + ( + Snowflake applicationId, + Snowflake emojiId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new emoji for the given application. + /// + /// The snowflake identifier of the current application. + /// The payload containing the new emoji. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created emoji. + public ValueTask> CreateApplicationEmojiAsync + ( + Snowflake applicationId, + ICreateApplicationEmojiPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies an existing emoji of the given application. + /// + /// The snowflake identifier of the current application. + /// The snowflake identifier of the emoji to modify. + /// The payload containing updated information. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified emoji. + public ValueTask> ModifyApplicationEmojiAsync + ( + Snowflake applicationId, + Snowflake emojiId, + IModifyApplicationEmojiPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes an existing emoji of the given application. + /// + /// The snowflake identifier of the current application. + /// The snowflake identifier of the emoji to delete. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// A value indicating whether the deletion was successful. + public ValueTask DeleteApplicationEmojiAsync + ( + Snowflake applicationId, + Snowflake emojiId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IEntitlementsRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IEntitlementsRestAPI.cs new file mode 100644 index 0000000000..97bf028508 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IEntitlementsRestAPI.cs @@ -0,0 +1,87 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to entitlements-related API calls. +/// +public interface IEntitlementsRestAPI +{ + /// + /// Returns all entitlements for a given app, according to the query parameters. + /// + /// The snowflake identifier of the current application. + /// Contains filtering and pagination options for this request. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListEntitlementsAsync + ( + Snowflake applicationId, + ListEntitlementsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a test entitlement to a given SKU for a given guild or user. Discord will act as though that user + /// or guild has entitlement to your offering.
+ /// After creating a test entitlement, you will need to reload your Discord client. After that, you will + /// have premium access. + ///
+ /// The snowflake identifier of your application. + /// The target of your test entitlement. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created test entitlement. + public ValueTask> CreateTestEntitlementAsync + ( + Snowflake applicationId, + ICreateTestEntitlementPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a currently active test entitlement. Discord will act as though that user or guild no longer + /// has entitlement to your offering. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the test entitlement to delete. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteTestEntitlementAsync + ( + Snowflake applicationId, + Snowflake entitlementId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Marks an entitlement as consumed. The entitlement will have set to true + /// if queried. + /// + /// The snowflake identifier of your application. + /// The snowflake identifier of the entitlement to consume. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// + public ValueTask ConsumeEntitlementAsync + ( + Snowflake applicationId, + Snowflake entitlementId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildRestAPI.cs new file mode 100644 index 0000000000..f941dd8992 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildRestAPI.cs @@ -0,0 +1,753 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to guild-related rest API calls. +/// +public interface IGuildRestAPI +{ + /// + /// Fetches a guild from its snowflake identifier. + /// + /// The snowflake identifier of the guild in question. + /// Specifies whether the response should include total and online member counts. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildAsync + ( + Snowflake guildId, + GetGuildQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches the guild preview for the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildPreviewAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies a guild. + /// + /// The snowflake identifier of the guild in question. + /// The fields of this guild to modify. + /// An optional audit log reason for the changes. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The updated guild. + public ValueTask> ModifyGuildAsync + ( + Snowflake guildId, + IModifyGuildPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Permanently deletes a guild. This user must own the guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether or not the request succeeded. + public ValueTask DeleteGuildAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Requests all channels for this guild from the API. This excludes thread channels. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildChannelsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a channel in this guild. + /// + /// The snowflake identifier of the parent guild. + /// The shannel creation payload, containing all initializing data. + /// An audit log reason for this operation. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created channel. + public ValueTask> CreateGuildChannelAsync + ( + Snowflake guildId, + ICreateGuildChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Moves channels in a guild. + /// + /// The snowflake identifier of the parent guild. + /// Array of new channel data payloads, containing IDs and some optional data. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask ModifyGuildChannelPositionsAsync + ( + Snowflake guildId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Queries all active thread channels in the given guild. + /// + /// The snowflake identifier of the queried guild. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// + /// A response payload object containing an array of thread channels and an array of thread member information + /// for all threads the current user has joined. + /// + public ValueTask> ListActiveGuildThreadsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the given users associated guild member object. + /// + /// The snowflake identifier of the queried guild. + /// The snowflake identifier of the user in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of guild member objects. + /// + /// The snowflake identifier of the guild to be queried. + /// + /// Contains information pertaining to request pagination. Up to 1000 users are allowed per request. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListGuildMembersAsync + ( + Snowflake guildId, + ForwardsPaginatedQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of guild member objects whose username or nickname starts with the given string. + /// + /// The snowflake identifier of the string in question. + /// The string to search for and the maximum amount of members to return; 1 - 1000. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> SearchGuildMembersAsync + ( + Snowflake guildId, + SearchGuildMembersQuery query, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Adds a discord user to the given guild using their oauth2 access token. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// A payload containing the OAuth2 token and initial information for the user. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created guild member, or null if the member had already joined the guild. + public ValueTask> AddGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + IAddGuildMemberPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies a given user in the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// The edits to make to this member. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The modified guild member. + public ValueTask> ModifyGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + IModifyGuildMemberPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the current user in the given guild. Currently, only setting the nickname is supported. + /// + /// The snowflake identifier of the guild in question. + /// The payload containing the new nickname for the current user. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The new current user event. + public ValueTask> ModifyCurrentMemberAsync + ( + Snowflake guildId, + IModifyCurrentMemberPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Adds a role to a guild member in a given guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// The snowflake identifier of the role in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask AddGuildMemberRoleAsync + ( + Snowflake guildId, + Snowflake userId, + Snowflake roleId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Removes the given role from the given member in the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// The snowflake identifier of the role in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask RemoveGuildMemberRoleAsync + ( + Snowflake guildId, + Snowflake userId, + Snowflake roleId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Kicks the given user from the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask RemoveGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of bans from the given guild. This endpoint is paginated. + /// + /// Snowflake identifier of the guild in question. + /// The query parameters used for pagination. Up to 1000 bans can be returned. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// An array of objects, representing the bans in the guild. + public ValueTask>> GetGuildBansAsync + ( + Snowflake guildId, + PaginatedQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the ban object for the given user. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildBanAsync + ( + Snowflake guildId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Bans the given user from the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// + /// Specifies how many seconds of message history from this user shall be purged, between 0 and + /// 604800, which equals 7 days. + /// + /// Specifies an audit log reason for the ban. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask CreateGuildBanAsync + ( + Snowflake guildId, + Snowflake userId, + CreateGuildBanQuery query = default, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Removes a ban from the given guild for the given user. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the user in question. + /// An optional audit log reason for the ban. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask RemoveGuildBanAsync + ( + Snowflake guildId, + Snowflake userId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Bans up to 200 users from the given guild. + /// + /// The snowflake identifier of the guild in question. + /// + /// The snowflake identifiers of the users to ban, and the amount of seconds to delete messages from. + /// + /// An optional audit log reason for the bans. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The snowflake identifiers of users that were banned and users that could not be banned. + public ValueTask> BulkGuildBanAsync + ( + Snowflake guildId, + IBulkGuildBanPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches the role list of the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildRolesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches the specified role from the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the role in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildRoleAsync + ( + Snowflake guildId, + Snowflake roleId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a role in a given guild. + /// + /// The snowflake identifier of the guild in question. + /// The information to initialize the role with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created role. + public ValueTask> CreateGuildRoleAsync + ( + Snowflake guildId, + ICreateGuildRolePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the positions of roles in the role list. + /// + /// The snowflake identifier of the guild in question. + /// The new positions for the roles. + /// An optional audit log reason for this action. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly ordered list of roles for this guild. + public ValueTask>> ModifyGuildRolePositionsAsync + ( + Snowflake guildId, + IReadOnlyList payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the settings of a specific role. + /// + /// The snowflake identifier of the guild the role belongs to. + /// The snowflake identifier of the role in question. + /// The new role settings for this role. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The modified role object. + public ValueTask> ModifyGuildRoleAsync + ( + Snowflake guildId, + Snowflake roleId, + IModifyGuildRolePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies a guild's MFA level. + /// + /// The snowflake identifier of the guild in question. + /// The new MFA level for this guild. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The updated MFA level. + public ValueTask> ModifyGuildMFALevelAsync + ( + Snowflake guildId, + IModifyGuildMfaLevelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a role from a guild. + /// + /// The snowflake identifier of the guild the role belongs to. + /// The snowflake identifier of the role in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteGuildRoleAsync + ( + Snowflake guildId, + Snowflake roleId, + string? reason, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Queries how many users would be kicked from a given guild in a prune. + /// + /// The snowflake identifier of the guild in question. + /// Provides additional information on which members to count. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildPruneCountAsync + ( + Snowflake guildId, + GetGuildPruneCountQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Initiates a prune from the guild in question. + /// + /// The snowflake identifier of the guild in question. + /// Contains additional information on which users to consider. + /// Optional audit log reason for the prune. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The amount of users pruned. + public ValueTask> BeginGuildPruneAsync + ( + Snowflake guildId, + IBeginGuildPrunePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Queries all available voice regions for this guild, including VIP regions. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildVoiceRegionsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of all active invites for this guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildInvitesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of up to 50 active integrations for this guild. If a guild has more integrations, + /// they cannot be accessed. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildIntegrationsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes an integration from the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The snowflake identifier of the integration to be deleted. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteGuildIntegrationAsync + ( + Snowflake guildId, + Snowflake integrationId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Queries the guild widget settings for the specified guild. + /// + /// The snowflake identifier of the guild to be queried. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildWidgetSettingsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the for the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// The new settings for this guild widget. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The new guild widget object. + public ValueTask> ModifyGuildWidgetAsync + ( + Snowflake guildId, + IGuildWidgetSettings settings, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the guild widget for the specified guild. + /// + /// The snowflake identifier for the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildWidgetAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Queries the vanity invite URL for this guild, if available. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildVanityUrlAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the guild widget image as a binary stream. + /// + /// The snowflake identifier of the guild in question. + /// The widget style, either "shield" (default) or "banner1" through "banner4". + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildWidgetImageAsync + ( + Snowflake guildId, + GetGuildWidgetImageQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the welcome screen of the given guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildWelcomeScreenAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the welcome screen of the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The information to modify the welcome screen with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated welcome screen. + public ValueTask> ModifyGuildWelcomeScreenAsync + ( + Snowflake guildId, + IModifyGuildWelcomeScreenPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the guild onboarding object for the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildOnboardingAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the onboarding configuration of the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The information to modify the onboarding configuration with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated onboarding configuration. + public ValueTask> ModifyGuildOnboardingAsync + ( + Snowflake guildId, + IModifyGuildOnboardingPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the actions taken in response to a raid or spam incident in the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The new action data to modify the guild with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated actions taken and, if applicable, ongoing incidents. + public ValueTask> ModifyGuildIncidentActionsAsync + ( + Snowflake guildId, + IModifyGuildIncidentActionsPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildScheduledEventRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildScheduledEventRestAPI.cs new file mode 100644 index 0000000000..1dd9ee095b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildScheduledEventRestAPI.cs @@ -0,0 +1,125 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to guild-scheduled-event-related rest API calls. +/// +public interface IGuildScheduledEventRestAPI +{ + /// + /// Returns a list of scheduled events taking place in this guild. + /// + /// The snowflake identifier of the guild in question. + /// Specifies whether the answer should include user counts. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListScheduledEventsForGuildAsync + ( + Snowflake guildId, + WithUserCountQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new scheduled event in the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// The data to intialize the event with. + /// An optional audit log reason + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created scheduled event. + public ValueTask> CreateGuildScheduledEventAsync + ( + Snowflake guildId, + ICreateGuildScheduledEventPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the requested scheduled event. + /// + /// The snowflake identifier of the guild this scheduled event takes place in. + /// The snowflake identifier of the scheduled event in qeustion. + /// + /// Specifies whether the number of users subscribed to this event should be included. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetScheduledEventAsync + ( + Snowflake guildId, + Snowflake eventId, + WithUserCountQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given scheduled event. + /// + /// The snowflake identifier of the guild this event takes place in. + /// The snowflake identifier of the event to be modified. + /// The new information for this event. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified scheduled event. + public ValueTask> ModifyScheduledEventAsync + ( + Snowflake guildId, + Snowflake eventId, + IModifyGuildScheduledEventPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given scheduled event. + /// + /// The snowflake identifier of the guild this event takes place in. + /// The snowflake identifier of the event to be modified. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the deletion was successful. + public ValueTask DeleteScheduledEventAsync + ( + Snowflake guildId, + Snowflake eventId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns objects for each participant of the given scheduled event. + /// + /// The snowflake identifier of the guild this scheduled event belongs to. + /// The snowflake identifier of the scheduled event in question. + /// Additional information regarding request pagination and member data in the response. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetScheduledEventUsersAsync + ( + Snowflake guildId, + Snowflake eventId, + GetScheduledEventUsersQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildTemplateRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildTemplateRestAPI.cs new file mode 100644 index 0000000000..6bf8e94682 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IGuildTemplateRestAPI.cs @@ -0,0 +1,130 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to guild-template-related API calls. +/// +public interface IGuildTemplateRestAPI +{ + /// + /// Fetches the guild template object corresponding to the given template code. + /// + /// The template code in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildTemplateAsync + ( + string templateCode, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new guild from the given guild template. + /// + /// + /// This endpoint can only be used by bots in less than 10 guilds. + /// + /// A template code to create the guild from. + /// Additional information to initialize this guild with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created guild. + public ValueTask> CreateGuildFromGuildTemplateAsync + ( + string templateCode, + ICreateGuildFromGuildTemplatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns all guild templates associated with this guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildTemplatesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new guild template from the given guild. + /// + /// The snowflake identifier of the guild in question. + /// The information to initialize this request with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created guild template. + public ValueTask> CreateGuildTemplateAsync + ( + Snowflake guildId, + ICreateGuildTemplatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Syncs the given template to the given guild's current state. + /// + /// The snowflake identifier of the guild in question. + /// The code of the template in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified guild template. + public ValueTask> SyncGuildTemplateAsync + ( + Snowflake guildId, + string templateCode, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given guild template. + /// + /// The snowflake identifier of the guild in question. + /// Template code of the template in question. + /// The new contents of this template. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified guild template. + public ValueTask> ModifyGuildTemplateAsync + ( + Snowflake guildId, + string templateCode, + IModifyGuildTemplatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given guild template. + /// + /// The snowflake identifier of the guild in question. + /// The code of the template to be deleted. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The deleted guild template. + public ValueTask> DeleteGuildTemplateAsync + ( + Snowflake guildId, + string templateCode, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IInteractionRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IInteractionRestAPI.cs new file mode 100644 index 0000000000..f4a4f62481 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IInteractionRestAPI.cs @@ -0,0 +1,159 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to interaction-related rest API calls. +/// +public interface IInteractionRestAPI +{ + /// + /// Creates a response to an interaction from the gateway. + /// + /// The snowflake identifier of the interaction. + /// The interaction token received with the interaction. + /// The response to this interaction. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask CreateInteractionResponseAsync + ( + Snowflake interactionId, + string interactionToken, + IInteractionResponse payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Gets the original response to this interaction, if it was a message. + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetInteractionResponseAsync + ( + Snowflake applicationId, + string interactionToken, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits the original response to this interaction, if it was a message. + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The message editing payload. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly edited message. + public ValueTask> EditInteractionResponseAsync + ( + Snowflake applicationId, + string interactionToken, + IEditInteractionResponsePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the original response to this interaction, if it was a message. + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteInteractionResponseAsync + ( + Snowflake applicationId, + string interactionToken, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a followup message for an interaction. If this is the first followup to a deferred interaction + /// response as created by + /// , + /// ephemerality of this message will be dictated by the supplied + /// originally instead of . + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// A payload containing data to create a message from. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created followup message. + public ValueTask> CreateFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + ICreateFollowupMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches a followup message created for this interaction. + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The snowflake identifier of the followup message. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits a followup message for this interaction. + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The snowflake identifier of the followup message. + /// A payload containing data to create a message from. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly edited followup message. + public ValueTask> EditFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + IEditFollowupMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a followup message created for this interaction. + /// + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The snowflake identifier of the followup message. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IInviteRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IInviteRestAPI.cs new file mode 100644 index 0000000000..d90f0d467b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IInviteRestAPI.cs @@ -0,0 +1,49 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to invite-related API calls. +/// +public interface IInviteRestAPI +{ + /// + /// Returns the queried invite. + /// + /// Invite code identifying this invite. + /// Specifies what additional information the invite object should contain. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetInviteAsync + ( + string inviteCode, + GetInviteQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given invite. + /// + /// The code identifying the invite. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The deleted invite object. + public ValueTask> DeleteInviteAsync + ( + string inviteCode, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IMessageRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IMessageRestAPI.cs new file mode 100644 index 0000000000..ad0d039bb5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IMessageRestAPI.cs @@ -0,0 +1,248 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to message-related rest API calls. +/// +public interface IMessageRestAPI +{ + /// + /// Returns a set amount of messages, optionally before, after or around a certain message. + /// + /// + /// around, before and after are mutually exclusive. Only one may be passed. If multiple are passed, + /// only the first one in the parameter list is respected, independent of the order they are passed in client code. + /// + /// The snowflake identifier of the channel in question. + /// Specifies where to get messages from, used for paginating. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetChannelMessagesAsync + ( + Snowflake channelId, + GetChannelMessagesQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Gets a message by its snowflake identifier. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetChannelMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new message in a channel. + /// + /// The snowflake identifier of the message's target channel. + /// Message creation payload including potential attachment files. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created message object. + public ValueTask> CreateMessageAsync + ( + Snowflake channelId, + ICreateMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Publishes a message in an announcement channel to following channels. + /// + /// The origin announcement channel of this message. + /// The snowflake identifier of the message. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> CrosspostMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a reaction with the given emoji on the specified message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// The string representation of the emoji. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the reaction was added successfully. + public ValueTask CreateReactionAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes your own reaction with the specified emoji on the specified message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// The string representation of the emoji. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the reaction was removed successfully. + public ValueTask DeleteOwnReactionAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the specified user's reaction with the specified emoji on the specified message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// The snowflake identifier of the user in question. + /// The string representation of the emoji. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the reaction was removed successfully. + public ValueTask DeleteUserReactionAsync + ( + Snowflake channelId, + Snowflake messageId, + Snowflake userId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Gets a list of users that reacted with the given emoji. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// The string representation of the queried emoji. + /// Contains query information related to request pagination. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetReactionsAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + GetReactionsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes all reactions on the given message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteAllReactionsAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes all reactions with a specific emoji from the specified message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// The string representation of the emoji in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteAllReactionsForEmojiAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits the given message. + /// + /// The snowflake identifier of the message's parent channel. + /// The snowflake identifier of the message in question. + /// A payload object containing information on how to edit this message. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> EditMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + IEditMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a given message. + /// + /// The nowflake identifier of the message's parent channel. + /// The snowflake identifier of the message. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the message was successfully deleted. + public ValueTask DeleteMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Bulk-deletes the provided messages. + /// + /// The snowflake identifier of the message's parent channel. + /// + /// Up to 100 message IDs to delete. If any messages older than two weeks are included, + /// or any of the IDs are duplicated, the entire request will fail. + /// + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the messages were deleted successfully. + public ValueTask BulkDeleteMessagesAsync + ( + Snowflake channelId, + IBulkDeleteMessagesPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IPollRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IPollRestAPI.cs new file mode 100644 index 0000000000..22b1b0838f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IPollRestAPI.cs @@ -0,0 +1,53 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to poll-related REST API calls. +/// +public interface IPollRestAPI +{ + /// + /// Gets a list of users who voted for the given answer. + /// + /// The snowflake identifier of the channel containing this poll. + /// The snowflake identifier of the message containing this poll. + /// The numeric identifier of the answer to query users for. + /// Pagination info for this request. The default limit is 25, but it may range between 1 and 100. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetAnswerVotersAsync + ( + Snowflake channelId, + Snowflake messageId, + int answerId, + ForwardsPaginatedQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Immediately ends the provided poll. You cannot end polls from other users. + /// + /// The snowflake identifier of the channel containing this poll. + /// The snowflake identifier of the message containing this poll. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> EndPollAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IRoleConnectionsRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IRoleConnectionsRestAPI.cs new file mode 100644 index 0000000000..062407488c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IRoleConnectionsRestAPI.cs @@ -0,0 +1,47 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to managing role connections via the API. +/// +public interface IRoleConnectionsRestAPI +{ + /// + /// Returns the role connection metadata records for the given application. + /// + /// The snowflake identifier of your application. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Updates the role connection metadata records for the given application. + /// + /// The snowflake identifier of your application. + /// The new metadata records for this application. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated metadata records. + public ValueTask>> UpdateRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISkusRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISkusRestAPI.cs new file mode 100644 index 0000000000..8b9cd1719c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISkusRestAPI.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to SKUs-related API calls. +/// +public interface ISkusRestAPI +{ + /// + /// Returns all SKUs for a given applications. You will see two SKUs for your premium offering, "because of + /// how our SKU and subscription systems work" - Discord.
+ /// For integration and testing entitlements you should use the SKU with type + /// . + ///
+ /// The snowflake identifier of the parent application. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListSkusAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISoundboardRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISoundboardRestAPI.cs new file mode 100644 index 0000000000..4a1a53350b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISoundboardRestAPI.cs @@ -0,0 +1,135 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to soundboard-related REST API calls. +/// +public interface ISoundboardRestAPI +{ + /// + /// Sends a soundboard sound in the voice channel the current user is connected to. + /// + /// + /// In addition to the relevant soundboard permissions, this also requires that the user + /// is not muted, suppressed, or, importantly, deafened/self-deafened. + /// + /// The snowflake identifier of the channel the user is connected to. + /// The sound ID and source. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// A value indicating whether the operation was successful. + public ValueTask SendSoundboardSoundAsync + ( + Snowflake channelId, + ISendSoundboardSoundPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Lists the default soundboard sounds that can be used by all users. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListDefaultSoundboardSoundsAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Lists the soundboard sounds available in the given guild. + /// + /// The snowflake identifier of the guild to query. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> ListGuildSoundboardSoundsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Gets a soundboard sound from the specified guild. + /// + /// The snowflake identifier of the given guild. + /// The snowflake identifier of the given sound. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildSoundboardSoundAsync + ( + Snowflake guildId, + Snowflake soundId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a soundboard sound in the specified guild. + /// + /// The snowflake identifier of the given guild. + /// The infomration necessary to create the sound. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created soundboard sound. + public ValueTask> CreateGuildSoundboardSoundAsync + ( + Snowflake guildId, + ICreateGuildSoundboardSoundPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the specified soundboard sound in the specified guild. + /// + /// The snowflake identifier of the given guild. + /// The snowflake identifier of the sound to modify. + /// The infomration necessary to create the sound. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The modified soundboard sound. + public ValueTask> ModifyGuildSoundboardSoundAsync + ( + Snowflake guildId, + Snowflake soundId, + IModifyGuildSoundboardSoundPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the specified soundboard sound in the specified guild. + /// + /// The snowflake identifier of the given guild. + /// The snowflake identifier of the sound to delete. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// A value indicating whether deletion was successful. + public ValueTask DeleteGuildSoundboardSoundAsync + ( + Snowflake guildId, + Snowflake soundId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IStageInstanceRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IStageInstanceRestAPI.cs new file mode 100644 index 0000000000..574cbda68f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IStageInstanceRestAPI.cs @@ -0,0 +1,80 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to stage-instance-related API calls. +/// +public interface IStageInstanceRestAPI +{ + /// + /// Creates a new stage instance associated to a stage channel. + /// + /// The information to initialize the stage instance with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created stage instance. + public ValueTask> CreateStageInstanceAsync + ( + ICreateStageInstancePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the stage instance associated with the stage channel, if one exists. + /// + /// Snowflake identifier of the associated stage channel. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetStageInstanceAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given stage instance. + /// + /// The snowflake identifier of the parent channel. + /// The updated information for this stage instance. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified stage instance. + public ValueTask> ModifyStageInstanceAsync + ( + Snowflake channelId, + IModifyStageInstancePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given stage instance. + /// + /// The snowflake identifier of its parent channel. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteStageInstanceAsync + ( + Snowflake channelId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IStickerRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IStickerRestAPI.cs new file mode 100644 index 0000000000..5901bc3e8c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IStickerRestAPI.cs @@ -0,0 +1,140 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to sticker-related API calls. +/// +public interface IStickerRestAPI +{ + /// + /// Fetches a sticker by its identifier. + /// + /// The snowflake identifier of the sticker in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetStickerAsync + ( + Snowflake stickerId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of available sticker packs. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> ListStickerPacksAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches the sticker objects for the given guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListGuildStickersAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the specified guild sticker. + /// + /// The snowflake identifier of the guild owning this sticker. + /// The snowflake identifier of the sticker in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetGuildStickerAsync + ( + Snowflake guildId, + Snowflake stickerId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a sticker in the specified guild. + /// + /// The snowflake identifier of the guild in question. + /// The information to initialize the sticker with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created sticker object. + public ValueTask> CreateGuildStickerAsync + ( + Snowflake guildId, + ICreateGuildStickerPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given sticker. + /// + /// The snowflake identifier of the guild owning the sticker. + /// The snowflake identifier of the sticker in question. + /// The new information for this sticker. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated sticker object. + public ValueTask> ModifyGuildStickerAsync + ( + Snowflake guildId, + Snowflake stickerId, + IModifyGuildStickerPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the specified sticker. + /// + /// The snowflake identifier of the guild owning the sticker. + /// The snowflake identifier of the sticker in question. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteGuildStickerAsync + ( + Snowflake guildId, + Snowflake stickerId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Gets the specified sticker pack. + /// + /// The snowflake identifier of the sticker pack in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetStickerPackAsync + ( + Snowflake packId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISubscriptionRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISubscriptionRestAPI.cs new file mode 100644 index 0000000000..108a61f819 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/ISubscriptionRestAPI.cs @@ -0,0 +1,49 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to subscription-related rest API calls. +/// +public interface ISubscriptionRestAPI +{ + /// + /// Fetches all subscriptions pertaining to the relevant SKU, filtered by user. + /// + /// The snowflake identifier of the queried SKU. + /// Additional query arguments for pagination and user filtering. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListSkuSubscriptionsAsync + ( + Snowflake skuId, + ListSkuSubscriptionsQuery query, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Fetches a subscription by ID. + /// + /// The snowflake identifier of the SKU this subscription corresponds to. + /// The snowflake identifier of the subscription to query. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetSkuSubscriptionAsync + ( + Snowflake skuId, + Snowflake subscriptionId, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IUserRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IUserRestAPI.cs new file mode 100644 index 0000000000..ab9ef33f40 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IUserRestAPI.cs @@ -0,0 +1,182 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to user-related API calls. +/// +public interface IUserRestAPI +{ + /// + /// Returns the current user. + /// + /// + /// For OAuth2, this requires the identify scope, which will return the object without an email, + /// and optionally the email scope, which will return the object with an email. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetCurrentUserAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the requested user. + /// + /// The snowflake identifier of the user in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetUserAsync + ( + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the current user. + /// + /// The new information for the current user. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified current user. + public ValueTask> ModifyCurrentUserAsync + ( + IModifyCurrentUserPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of partial guild objects representing the guilds the current user has joined. + /// + /// + /// defaults to 200 guilds, which is the maximum + /// number of guilds an user account can join. Pagination is therefore not needed for obtaining user + /// guilds, but may be needed for obtaining bot guilds. + /// + /// + /// Specifies request pagination info, as well as whether guild objects should include member counts. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetCurrentUserGuildsAsync + ( + GetCurrentUserGuildsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a guild member object for the current user for the given guild. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetCurrentUserGuildMemberAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Leaves a guild as the current user. + /// + /// The snowflake identifier of the guild to be left. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// Whether the operation was successful. + public ValueTask LeaveGuildAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new DM channel with a user. + /// + /// + /// As per Discord's documentation, you should not use this endpoint to DM everyone in a server about + /// something. DMs should generally be initiated by user action. If you open a significant amount of DMs + /// too quickly, your bot may be rate limited or blocked from opening new ones. + /// + /// The identifier of the user you want to create a DM with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created channel object, or the existing DM channel if one existed. + public ValueTask> CreateDmAsync + ( + ICreateDmPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Creates a new group DM with multiple users. This is limited to 10 active group DMs. + /// + /// The access tokens and nicks of the users to add. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created channel, or the existing DM channel if one existed. + public ValueTask> CreateGroupDmAsync + ( + ICreateGroupDmPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of connection objects for the current user. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetCurrentUserConnectionsAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the application role connection for the given application for the current user. + /// + /// The snowflake identifier of the application administering the connection. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetCurrentUserApplicationRoleConnectionAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Updates the application role connection for the given application for the current user. + /// + /// The snowflake identifier of the application administering the connection. + /// The new information for this role connection. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly updated connection object. + public ValueTask> UpdateCurrentUserApplicationRoleConnectionAsync + ( + Snowflake applicationId, + IUpdateCurrentUserApplicationRoleConnectionPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IVoiceRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IVoiceRestAPI.cs new file mode 100644 index 0000000000..a36746472b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IVoiceRestAPI.cs @@ -0,0 +1,90 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to voice-related API calls. +/// +public interface IVoiceRestAPI +{ + /// + /// Returns an array of voice region objects that can be used when setting a voice or stage channel's rtc region. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> ListVoiceRegionsAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the current user's voice state in the specified guild. + /// + /// The snowflake identifier of the specified guild. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetCurrentUserVoiceStateAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the specified user's voice state in the specified guild. + /// + /// The snowflake identifier specifying the guild. + /// The snowflake identifier specifying the user. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetUserVoiceStateAsync + ( + Snowflake guildId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the current user's voice state. + /// + /// The snowflake identifier of the guild everything takes place in. + /// Information on how to update the current voice state. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask ModifyCurrentUserVoiceStateAsync + ( + Snowflake guildId, + IModifyCurrentUserVoiceStatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies another user's voice state. + /// + /// The snowflake identifier of the guild everything takes place in. + /// The snowflake identifier of the user whose voice state to modify. + /// Information on how to modify the user's voice state. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask ModifyUserVoiceStateAsync + ( + Snowflake guildId, + Snowflake userId, + IModifyUserVoiceStatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IWebhookRestAPI.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IWebhookRestAPI.cs new file mode 100644 index 0000000000..a533cedae2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/API/IWebhookRestAPI.cs @@ -0,0 +1,272 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest.API; + +/// +/// Provides access to webhook-related API calls. +/// +public interface IWebhookRestAPI +{ + /// + /// Creates a new webhook in the specified channel. + /// + /// The snowflake identifier of the channel in question. + /// Request payload. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created webhook object. + public ValueTask> CreateWebhookAsync + ( + Snowflake channelId, + ICreateWebhookPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of channel webhook objects. + /// + /// The snowflake identifier of the channel in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetChannelWebhooksAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a list of guild webhook objects. + /// + /// The snowflake identifier of the guild in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask>> GetGuildWebhooksAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the specified webhook object. + /// + /// The snowflake identifier of the webhook in question. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetWebhookAsync + ( + Snowflake webhookId, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns the specified webhook object. + /// + /// + /// This endpoint does not require authentication and is not counted to your global ratelimits. + /// + /// The snowflake identifier of the webhook in question. + /// The access token to this webhook. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetWebhookWithTokenAsync + ( + Snowflake webhookId, + string webhookToken, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given webhook. + /// + /// The snowflake identifier of the webhook to edit. + /// The information to modify this webhook with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The updated webhook object. + public ValueTask> ModifyWebhookAsync + ( + Snowflake webhookId, + IModifyWebhookPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Modifies the given webhook. + /// + /// + /// This endpoint does not require authentication and is not counted to your global ratelimits. + /// + /// The snowflake identifier of the webhook to edit. + /// The webhook token of the webhook to edit. + /// The information to modify this webhook with. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The updated webhook object. + public ValueTask> ModifyWebhookWithTokenAsync + ( + Snowflake webhookId, + string webhookToken, + IModifyWebhookWithTokenPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given webhook. + /// + /// The snowflake identifier of the webhook to delete. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteWebhookAsync + ( + Snowflake webhookId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes the given webhook. + /// + /// + /// This endpoint does not require authentication and is not counted to your global ratelimits. + /// + /// The snowflake identifier of the webhook to delete. + /// The webhook token of the webhook to delete. + /// An optional audit log reason. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteWebhookWithTokenAsync + ( + Snowflake webhookId, + string webhookToken, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Executes the given webhook. + /// + /// The snowflake identifier of the webhook to execute. + /// The webhook token of the webhook to execute. + /// A payload of information on the message to send. + /// + /// Specifies the waiting behaviour of the request as well as whether this webhook should post to a thread. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// + /// If was set to , a + /// object. If was set to + /// , . + /// + public ValueTask> ExecuteWebhookAsync + ( + Snowflake webhookId, + string webhookToken, + IExecuteWebhookPayload payload, + ExecuteWebhookQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Returns a previously-sent webhook message from the same token. + /// + /// The snowflake identifier of your webhook. + /// + /// The webhook token for your webhook. This must match the token of the original author. + /// + /// The snowflake identifier of the message in question. + /// + /// Specifies the thread to search in rather than the parent channel. Only threads with the same parent channel + /// as the webhook can be passed. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask> GetWebhookMessageAsync + ( + Snowflake webhookId, + string webhookToken, + Snowflake messageId, + ThreadIdQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Edits a previously-sent webhook message from the same token. + /// + /// The snowflake identifier of your webhook. + /// + /// The webhook token for your webhook. This must match the token of the original author. + /// + /// The snowflake identifier of the message in question. + /// The information to update this message with. + /// + /// Specifies the thread to search in rather than the parent channel. Only threads with the same parent channel + /// as the webhook can be passed. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly edited message. + public ValueTask> EditWebhookMessageAsync + ( + Snowflake webhookId, + string webhookToken, + Snowflake messageId, + IEditWebhookMessagePayload payload, + EditWebhookMessageQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Deletes a previously-sent webhook message from the same token. + /// + /// The snowflake identifier of your webhook. + /// + /// The webhook token for your webhook. This must match the token of the original author. + /// + /// The snowflake identifier of the message in question. + /// + /// Specifies the thread to search in rather than the parent channel. Only threads with the same parent channel + /// as the webhook can be passed. + /// + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public ValueTask DeleteWebhookMessageAsync + ( + Snowflake webhookId, + string webhookToken, + Snowflake messageId, + ThreadIdQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/DSharpPlus.Internal.Abstractions.Rest.csproj b/src/core/DSharpPlus.Internal.Abstractions.Rest/DSharpPlus.Internal.Abstractions.Rest.csproj new file mode 100644 index 0000000000..30e6b8165c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/DSharpPlus.Internal.Abstractions.Rest.csproj @@ -0,0 +1,11 @@ + + + + $(_DSharpPlusInternalAbstractionsRestVersion) + + + + + + + diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Errors/HttpError.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Errors/HttpError.cs new file mode 100644 index 0000000000..d7f0e5e72c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Errors/HttpError.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Net; + +using DSharpPlus.Results.Errors; + +namespace DSharpPlus.Internal.Abstractions.Rest.Errors; + +/// +/// Represents a HTTP error returned by an API call. +/// +public sealed record HttpError : Error +{ + /// + /// The encountered status code. + /// + public required HttpStatusCode StatusCode { get; init; } + + /// + /// Initializes a new HttpError from the provided status code and message. + /// + /// The HTTP status encountered. + /// + /// The error message, either a synthesized human-readable error or the error string returned + /// by Discord, which may be parsed programmatically. + /// + public HttpError + ( + HttpStatusCode statusCode, + string? message = null + ) + : base(message ?? $"Encountered HTTP status code {(ulong)statusCode}: {statusCode}") + => this.StatusCode = statusCode; +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Errors/ValidationError.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Errors/ValidationError.cs new file mode 100644 index 0000000000..e742d53fcb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Errors/ValidationError.cs @@ -0,0 +1,13 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Results.Errors; + +namespace DSharpPlus.Internal.Abstractions.Rest.Errors; + +/// +/// Represents an error encountered during parameter validation. +/// +/// The human-readable error message. +public record ValidationError(string message) : Error(message); diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/IRatelimitCallbackInfo.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/IRatelimitCallbackInfo.cs new file mode 100644 index 0000000000..b8628ad692 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/IRatelimitCallbackInfo.cs @@ -0,0 +1,53 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Rest; + +/// +/// Represents information and operations on a callback. +/// +public interface IRatelimitCallbackInfo +{ + /// + /// Indicates whether the encountered ratelimit is a global ratelimit. + /// + public bool IsGlobalRatelimit { get; } + + /// + /// The now-exhausted limit of this bucket. + /// + public int Limit { get; } + + /// + /// The timestamp at which this ratelimit bucket will reset and be freed up. + /// + public DateTimeOffset Reset { get; } + + /// + /// The route this request sent over. + /// + public string Route { get; } + + /// + /// The hash of the encountered ratelimit bucket. + /// + public string Hash { get; } + + /// + /// Indicates whether this request can retry without user interaction. + /// + public bool MayRetry { get; } + + /// + /// Cancels all retries and forcibly ends this request's lifespan. + /// + public void CancelRetries(); + + /// + /// Forces this request to retry, even if it wouldn't have otherwise. + /// + public void Retry(); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/IRestClient.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/IRestClient.cs new file mode 100644 index 0000000000..1b7d2b8a5d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/IRestClient.cs @@ -0,0 +1,74 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Abstractions.Rest; + +/// +/// Represents the central rest client handling the requests made to Discord. +/// +public interface IRestClient +{ + /// + /// Sends a request to the Discord API. + /// + /// The type to deserialize into. + /// The HTTP method this request should be sent to. + /// The path this request will take. + /// Constructs the request to be sent to Discord. + /// Specifies additional parameters for this request. + /// A cancellation token for this operation. + /// The response from Discord, or an appropriate error. + public ValueTask> ExecuteRequestAsync + ( + HttpMethod method, + string path, + Action? request = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Sends a request to the Discord API, serializing every payload element into a separate form parameter. + /// + /// The type to deserialize into. + /// The HTTP method this request should be sent to. + /// The path this request will take. + /// Constructs the request to be sent to Discord. + /// Specifies additional parameters for this request. + /// A cancellation token for this operation. + /// The response from Discord, or an appropriate error. + public ValueTask> ExecuteMultipartPayloadRequestAsync + ( + HttpMethod method, + string path, + Action? request = null, + RequestInfo info = default, + CancellationToken ct = default + ); + + /// + /// Sends a request to the Discord API. + /// + /// The HTTP method this request should be sent to. + /// The path this request will take. + /// Constructs the request to be sent to Discord. + /// Specifies additional parameters for this request. + /// A cancellation token for this operation. + /// The response from Discord, or an appropriate error. + public ValueTask> ExecuteRequestAsync + ( + HttpMethod method, + string path, + Action? request = null, + RequestInfo info = default, + CancellationToken ct = default + ); +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/ICreateGlobalApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/ICreateGlobalApplicationCommandPayload.cs new file mode 100644 index 0000000000..8de89b845c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/ICreateGlobalApplicationCommandPayload.cs @@ -0,0 +1,68 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /applications/:application-id/commands. +/// +public interface ICreateGlobalApplicationCommandPayload +{ + /// + /// The name of this command, between 1 and 32 characters. + /// + public string Name { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> NameLocalizations { get; } + + /// + /// The description for this chat input command, between 1 and 100 characters. + /// + public Optional Description { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> DescriptionLocalizations { get; } + + /// + /// Up to 25 parameters for this command, or its subcommands. + /// + public Optional> Options { get; } + + /// + /// The default permissions needed to see this command. + /// + public Optional DefaultMemberPermissions { get; } + + /// + /// The type of this command. + /// + public Optional Type { get; } + + /// + /// Indicates whether this command is age-restricted. + /// + public Optional Nsfw { get; } + + /// + /// Specifies installation contexts where this command is available; only for globally-scoped commands. Defaults to + /// . + /// + public IReadOnlyList IntegrationTypes { get; } + + /// + /// Specifies contexts where this command can be used; only for globally-scoped commands. Defaults to including all + /// context types. + /// + public IReadOnlyList Contexts { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/ICreateGuildApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/ICreateGuildApplicationCommandPayload.cs new file mode 100644 index 0000000000..5ba13e7218 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/ICreateGuildApplicationCommandPayload.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /applications/:application-id/guilds/:guild-id/commands +/// +public interface ICreateGuildApplicationCommandPayload +{ + /// + /// The name of this command, between 1 and 32 characters. + /// + public string Name { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> NameLocalizations { get; } + + /// + /// Up to 25 parameters for this command, or its subcommands. + /// + public Optional> Options { get; } + + /// + /// The description for this chat input command, between 1 and 100 characters. + /// + public Optional Description { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> DescriptionLocalizations { get; } + + /// + /// The default permissions needed to see this command. + /// + public Optional DefaultMemberPermissions { get; } + + /// + /// The type of this command. + /// + public Optional Type { get; } + + /// + /// Indicates whether this command is age-restricted. + /// + public Optional Nsfw { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/IEditGlobalApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/IEditGlobalApplicationCommandPayload.cs new file mode 100644 index 0000000000..6d0d3189aa --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/IEditGlobalApplicationCommandPayload.cs @@ -0,0 +1,68 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /applications/:application-id/commands/:command-id. +/// +public interface IEditGlobalApplicationCommandPayload +{ + /// + /// The name of this command, between 1 and 32 characters. + /// + public Optional Name { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> NameLocalizations { get; } + + /// + /// The description for this chat input command, between 1 and 100 characters. + /// + public Optional Description { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> DescriptionLocalizations { get; } + + /// + /// Up to 25 parameters for this command, or its subcommands. + /// + public Optional> Options { get; } + + /// + /// The default permissions needed to see this command. + /// + public Optional DefaultMemberPermissions { get; } + + /// + /// The type of this command. + /// + public Optional Type { get; } + + /// + /// Indicates whether this command is age-restricted. + /// + public Optional Nsfw { get; } + + /// + /// Specifies installation contexts where this command is available; only for globally-scoped commands. Defaults to + /// . + /// + public IReadOnlyList IntegrationTypes { get; } + + /// + /// Specifies contexts where this command can be used; only for globally-scoped commands. Defaults to including all + /// context types. + /// + public IReadOnlyList Contexts { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/IEditGuildApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/IEditGuildApplicationCommandPayload.cs new file mode 100644 index 0000000000..4b9b76b7b2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ApplicationCommands/IEditGuildApplicationCommandPayload.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /applications/:application-id/guilds/:guild-id/commands/:command-id. +/// +public interface IEditGuildApplicationCommandPayload +{ + /// + /// The name of this command, between 1 and 32 characters. + /// + public Optional Name { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> NameLocalizations { get; } + + /// + /// The description for this chat input command, between 1 and 100 characters. + /// + public Optional Description { get; } + + /// + /// A localization dictionary for the field. Values follow the same restrictions. + /// + public Optional?> DescriptionLocalizations { get; } + + /// + /// Up to 25 parameters for this command, or its subcommands. + /// + public Optional> Options { get; } + + /// + /// The default permissions needed to see this command. + /// + public Optional DefaultMemberPermissions { get; } + + /// + /// The type of this command. + /// + public Optional Type { get; } + + /// + /// Indicates whether this command is age-restricted. + /// + public Optional Nsfw { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Applications/IEditCurrentApplicationPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Applications/IEditCurrentApplicationPayload.cs new file mode 100644 index 0000000000..38a7cfc280 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Applications/IEditCurrentApplicationPayload.cs @@ -0,0 +1,62 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /applications/@me. +/// +public interface IEditCurrentApplicationPayload +{ + /// + /// The default custom authorization URL for this application, if the feature is enabled. + /// + public Optional CustomInstallUrl { get; } + + /// + /// The description of this application. + /// + public Optional Description { get; } + + /// + /// The role connection verification URL for this application. + /// + public Optional RoleConnectionsVerificationUrl { get; } + + /// + /// Settings for this application's default in-app authorization link, if enabled. + /// + public Optional InstallParams { get; } + + /// + /// The public flags for this application. + /// + public Optional Flags { get; } + + /// + /// The icon for this application. + /// + public Optional Icon { get; } + + /// + /// The default rich presence invite cover image for this application. + /// + public Optional CoverImage { get; } + + /// + /// The interactions endpoint url for this application. + /// + public Optional InteractionsEndpointUrl { get; } + + /// + /// A list of tags describing the content and functionality of this application, with a maximum of + /// five tags and a maximum of 20 characters per tag. + /// + public Optional> Tags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/AutoModeration/ICreateAutoModerationRulePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/AutoModeration/ICreateAutoModerationRulePayload.cs new file mode 100644 index 0000000000..795f8a70cf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/AutoModeration/ICreateAutoModerationRulePayload.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/auto-moderation/rules. +/// +public interface ICreateAutoModerationRulePayload +{ + /// + /// The name of this rule. + /// + public string Name { get; } + + /// + /// The type of event to trigger evaluation of this rule. + /// + public DiscordAutoModerationEventType EventType { get; } + + /// + /// The type of trigger for this rule. + /// + public DiscordAutoModerationTriggerType TriggerType { get; } + + /// + /// Additional trigger metadata for this rule. + /// + public Optional TriggerMetadata { get; } + + /// + /// The actions to execute when this rule is triggered. + /// + public IReadOnlyList Actions { get; } + + /// + /// Indicates whether the rule is enabled. Defaults to false. + /// + public Optional Enabled { get; } + + /// + /// Up to 20 snowflake identifiers of roles to exempt from this rule. + /// + public Optional> ExemptRoles { get; } + + /// + /// Up to 50 snowflake identifiers of channels to exempt from this rule. + /// + public Optional> ExemptChannels { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/AutoModeration/IModifyAutoModerationRulePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/AutoModeration/IModifyAutoModerationRulePayload.cs new file mode 100644 index 0000000000..c173c3fb7c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/AutoModeration/IModifyAutoModerationRulePayload.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/auto-moderation/rules/:automod-rule-id. +/// +public interface IModifyAutoModerationRulePayload +{ + /// + /// The name of this rule. + /// + public Optional Name { get; } + + /// + /// The type of event to trigger evaluation of this rule. + /// + public Optional EventType { get; } + + /// + /// Additional trigger metadata for this rule. + /// + public Optional TriggerMetadata { get; } + + /// + /// The actions to execute when this rule is triggered. + /// + public Optional> Actions { get; } + + /// + /// Indicates whether the rule is enabled. Defaults to false. + /// + public Optional Enabled { get; } + + /// + /// Up to 20 snowflake identifiers of roles to exempt from this rule. + /// + public Optional> ExemptRoles { get; } + + /// + /// Up to 50 snowflake identifiers of channels to exempt from this rule. + /// + public Optional> ExemptChannels { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/ICreateChannelInvitePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/ICreateChannelInvitePayload.cs new file mode 100644 index 0000000000..27eb370112 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/ICreateChannelInvitePayload.cs @@ -0,0 +1,50 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/invites. +/// +public interface ICreateChannelInvitePayload +{ + /// + /// Specifies the expiry time in seconds for this invite. Setting it to 0 means the invite never expires. + /// + public Optional MaxAge { get; init; } + + /// + /// Specifies the maximum amount of uses for this invite. Setting it to 0 means the invite can be used infinitely. + /// + public Optional MaxUses { get; init; } + + /// + /// Indicates whether this invite only grants temporary membership. + /// + public Optional Temporary { get; init; } + + /// + /// Specifies whether this invite is unique. If true, Discord will not try to reuse a similar invite. + /// + public Optional Unique { get; init; } + + /// + /// Specifies the target type of this voice channel invite. + /// + public Optional TargetType { get; init; } + + /// + /// Snowflake identifier of the invite's target user if is + /// . + /// + public Optional TargetUserId { get; init; } + + /// + /// Snowflake identifier of the invite's target embedded application if is + /// . + /// + public Optional TargetApplicationId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IEditChannelPermissionsPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IEditChannelPermissionsPayload.cs new file mode 100644 index 0000000000..e87a1c02a0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IEditChannelPermissionsPayload.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload for PUT /channels/:channel-id/permissions/:overwrite-id. +/// +public interface IEditChannelPermissionsPayload +{ + /// + /// The overwrite type - either role or member. + /// + public DiscordChannelOverwriteType Type { get; } + + /// + /// The permissions this overwrite should grant. + /// + public Optional Allow { get; } + + /// + /// The permissions this overwrite should deny. + /// + public Optional Deny { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IFollowAnnouncementChannelPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IFollowAnnouncementChannelPayload.cs new file mode 100644 index 0000000000..a91be97937 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IFollowAnnouncementChannelPayload.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/followers +/// +public interface IFollowAnnouncementChannelPayload +{ + /// + /// The snowflake identifier of the channel to cross-post messages into. + /// + public Snowflake WebhookChannelId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IForumAndMediaThreadMessage.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IForumAndMediaThreadMessage.cs new file mode 100644 index 0000000000..ab8906840c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IForumAndMediaThreadMessage.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +public interface IForumAndMediaThreadMessage +{ + /// + /// The message content to send. + /// + public Optional Content { get; } + + /// + /// Up to 10 embeds to be attached to this message. + /// + public Optional> Embeds { get; } + + /// + /// Specifies which mentions should be resolved. + /// + public Optional AllowedMentions { get; } + + /// + /// A list of components to include with the message. + /// + public Optional> Components { get; } + + /// + /// Up to 3 snowflake identifiers of stickers to be attached to this message. + /// + public Optional> StickerIds { get; } + + /// + /// Attachment metadata for this message. + /// + /// + /// The files have to be attached to the parent object. + /// + public Optional> Attachments { get; } + + /// + /// Message flags, combined as bitfield. Only can be set. + /// + public Optional Flags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IGroupDMAddRecipientPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IGroupDMAddRecipientPayload.cs new file mode 100644 index 0000000000..6999f059da --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IGroupDMAddRecipientPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PUT /channel/:channel-id/recipients/:user-id. +/// +public interface IGroupDMAddRecipientPayload +{ + /// + /// The access token of the user, which must have granted you the gdm.join oauth scope. + /// + public string AccessToken { get; } + + /// + /// The nickname of the user, to be given on join. + /// + public string Nick { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyGroupDMPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyGroupDMPayload.cs new file mode 100644 index 0000000000..cfbdf06143 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyGroupDMPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /channels/:channel-id. +/// +public interface IModifyGroupDMPayload +{ + /// + /// The name of this group DM channel. + /// + public Optional Name { get; } + + /// + /// The icon of this group DM channel. + /// + public Optional Icon { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyGuildChannelPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyGuildChannelPayload.cs new file mode 100644 index 0000000000..69a1775232 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyGuildChannelPayload.cs @@ -0,0 +1,113 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /channels/:channel-id. +/// +public interface IModifyGuildChannelPayload +{ + /// + /// The new channel name. + /// + public Optional Name { get; } + + /// + /// The new channel type for this channel. Only converting between and + /// is supported. + /// + public Optional Type { get; } + + /// + /// The new position for this channel in the channel list. + /// + public Optional Position { get; } + + /// + /// The new channel topic. + /// + public Optional Topic { get; } + + /// + /// Indicates whether this channel permits NSFW content. + /// + public Optional Nsfw { get; } + + /// + /// The new slowmode for this channel in seconds. + /// + public Optional RateLimitPerUser { get; } + + /// + /// The new bitrate for this voice channel. + /// + public Optional Bitrate { get; } + + /// + /// The new user limit for this voice channel. 0 represents no limit, 1 - 99 represents a limit. + /// + public Optional UserLimit { get; } + + /// + /// New permission overwrites for this channel or category. + /// + public Optional?> PermissionOverwrites { get; } + + /// + /// Snowflake identifier of the new parent category channel. + /// + public Optional ParentId { get; } + + /// + /// Channel voice region ID, automatic when set to null. + /// + public Optional RtcRegion { get; } + + /// + /// The new camera video quality mode for this channel. + /// + public Optional VideoQualityMode { get; } + + /// + /// The new default auto archive duration for threads as used by the discord client. + /// + public Optional DefaultAutoArchiveDuration { get; } + + /// + /// The new channel flags. Currently only and + /// are supported. + /// + public Optional Flags { get; } + + /// + /// The set of tags that can be used in this channel. + /// + public Optional> AvailableTags { get; } + + /// + /// The default emoji to react with. + /// + public Optional DefaultReactionEmoji { get; } + + /// + /// The default slowmode in threads created from this channel. + /// + public Optional DefaultThreadRateLimitPerUser { get; } + + /// + /// The default sort order used to order posts in this channel. + /// + public Optional DefaultSortOrder { get; } + + /// + /// The default layout type used to display posts in this forum. + /// + public Optional DefaultForumLayout { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyThreadChannelPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyThreadChannelPayload.cs new file mode 100644 index 0000000000..4e908e9121 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IModifyThreadChannelPayload.cs @@ -0,0 +1,55 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /channels/:channel-id. +/// +public interface IModifyThreadChannelPayload +{ + /// + /// The new name for this thread channel. + /// + public Optional Name { get; } + + /// + /// Indicates whether this thread is archived. This must either be false or be set to false. + /// + public Optional Archived { get; } + + /// + /// The new auto archive duration for this thread, in seconds. + /// + public Optional AutoArchiveDuration { get; } + + /// + /// Indicates whether this thread is locked. + /// + public Optional Locked { get; } + + /// + /// Indicates whether non-moderators can add other non-moderators to this private thread. + /// + public Optional Invitable { get; } + + /// + /// The new slowmode duration for this thread, in seconds. + /// + public Optional RateLimitPerUser { get; } + + /// + /// Flags for this thread. + /// + public Optional Flags { get; } + + /// + /// The snowflake IDs of the tags that have been applied to this thread. + /// + public Optional> AppliedTags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadFromMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadFromMessagePayload.cs new file mode 100644 index 0000000000..5270f0a558 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadFromMessagePayload.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/messages/:message-id/threads. +/// +public interface IStartThreadFromMessagePayload +{ + /// + /// 1-100 characters, channel name for this thread. + /// + public string Name { get; } + + /// + /// Auto archive duration for this thread in minutes. + /// + public Optional AutoArchiveDuration { get; } + + /// + /// Slowmode for users in seconds. + /// + public Optional RateLimitPerUser { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadInForumOrMediaChannelPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadInForumOrMediaChannelPayload.cs new file mode 100644 index 0000000000..e8cf50cb31 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadInForumOrMediaChannelPayload.cs @@ -0,0 +1,43 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/threads. +/// +public interface IStartThreadInForumOrMediaChannelPayload +{ + /// + /// 1-100 characters, channel name for this thread. + /// + public string Name { get; } + + /// + /// Auto archive duration for this thread in minutes. + /// + public Optional AutoArchiveDuration { get; } + + /// + /// Slowmode for users in seconds. + /// + public Optional RateLimitPerUser { get; } + + /// + /// The first message in this forum/media thread. + /// + public IForumAndMediaThreadMessage Message { get; } + + /// + /// The snowflake identifiers of tags to apply to this thread. + /// + public Optional> AppliedTags { get; } + + /// + /// The contents of the files to send. + /// + public IReadOnlyList? Files { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadWithoutMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadWithoutMessagePayload.cs new file mode 100644 index 0000000000..afa89696d5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Channels/IStartThreadWithoutMessagePayload.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/threads. +/// +public interface IStartThreadWithoutMessagePayload +{ + /// + /// 1-100 characters, channel name for this thread. + /// + public string Name { get; } + + /// + /// Auto archive duration for this thread in minutes. + /// + public Optional AutoArchiveDuration { get; } + + /// + /// Slowmode for users in seconds. + /// + public Optional RateLimitPerUser { get; } + + /// + /// The type of thread to be created. + /// + // This field is currently technically optional as per API spec, but this behaviour is slated for removal in the future. + // Therefore, it is kept as a required field here. + public DiscordChannelType Type { get; } + + /// + /// Indicates whether non-moderators can add members to this private thread. + /// + public Optional Invitable { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/ICreateApplicationEmojiPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/ICreateApplicationEmojiPayload.cs new file mode 100644 index 0000000000..ce0b6c2a65 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/ICreateApplicationEmojiPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /applications/:application-id/emojis. +/// +public interface ICreateApplicationEmojiPayload +{ + /// + /// The name of the new emoji. + /// + public string Name { get; } + + /// + /// The 128x128 emoji image. + /// + public InlineMediaData Image { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/ICreateGuildEmojiPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/ICreateGuildEmojiPayload.cs new file mode 100644 index 0000000000..65d363ea6f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/ICreateGuildEmojiPayload.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/emojis. +/// +public interface ICreateGuildEmojiPayload +{ + /// + /// The name of the new emoji. + /// + public string Name { get; } + + /// + /// The 128x128 emoji image. + /// + public InlineMediaData Image { get; } + + /// + /// The snowflake identifiers of roles allowed to use this emoji. + /// + public Optional> Roles { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/IModifyApplicationEmojiPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/IModifyApplicationEmojiPayload.cs new file mode 100644 index 0000000000..0f1881119e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/IModifyApplicationEmojiPayload.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /applications/:application-id/emojis/:emoji-id +/// +public interface IModifyApplicationEmojiPayload +{ + /// + /// The new name of the emoji. + /// + public string Name { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/IModifyGuildEmojiPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/IModifyGuildEmojiPayload.cs new file mode 100644 index 0000000000..03cc3d17c1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Emojis/IModifyGuildEmojiPayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/emojis/:emoji-id. +/// +public interface IModifyGuildEmojiPayload +{ + /// + /// The new name of the emoji. + /// + public Optional Name { get; } + + /// + /// The snowflake identifiers of roles allowed to use this emoji. + /// + public Optional> Roles { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Entitlements/ICreateTestEntitlementPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Entitlements/ICreateTestEntitlementPayload.cs new file mode 100644 index 0000000000..fe8082b1ac --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Entitlements/ICreateTestEntitlementPayload.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /applications/:application-id/entitlements. +/// +public interface ICreateTestEntitlementPayload +{ + /// + /// The identifier of the string to grant the entitlement to. + /// + public Snowflake SkuId { get; } + + /// + /// The identifier of the guild or user to grant the entitlement to. + /// + public Snowflake OwnerId { get; } + + /// + /// Specifies what kind of entity the owner is. + /// + public DiscordEntitlementOwnerType OwnerType { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/ICreateGuildFromGuildTemplatePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/ICreateGuildFromGuildTemplatePayload.cs new file mode 100644 index 0000000000..b58aae10f2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/ICreateGuildFromGuildTemplatePayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/templates/:template-code. +/// +public interface ICreateGuildFromGuildTemplatePayload +{ + /// + /// The name of the guild, 2 to 100 characters. + /// + public string Name { get; } + + /// + /// The 128x128 icon for this guild. + /// + public Optional Icon { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/ICreateGuildTemplatePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/ICreateGuildTemplatePayload.cs new file mode 100644 index 0000000000..53ed8ffe28 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/ICreateGuildTemplatePayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/templates +/// +public interface ICreateGuildTemplatePayload +{ + /// + /// The name of this template, up to 100 characters. + /// + public string Name { get; } + + /// + /// The description of this template, up to 120 characters. + /// + public Optional Description { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/IModifyGuildTemplatePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/IModifyGuildTemplatePayload.cs new file mode 100644 index 0000000000..b68e789309 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/GuildTemplates/IModifyGuildTemplatePayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/templates +/// +public interface IModifyGuildTemplatePayload +{ + /// + /// The name of this template, up to 100 characters. + /// + public Optional Name { get; } + + /// + /// The description of this template, up to 120 characters. + /// + public Optional Description { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IAddGuildMemberPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IAddGuildMemberPayload.cs new file mode 100644 index 0000000000..39e72488ea --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IAddGuildMemberPayload.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PUT /guilds/:guild-id/members/:user-id. +/// +public interface IAddGuildMemberPayload +{ + /// + /// An OAuth2 access token granted with the guilds.join scope. + /// + public string AccessToken { get; } + + /// + /// The nickname to initialize the user with. + /// + public Optional Nickname { get; } + + /// + /// An array of role IDs to assign immediately upon join. + /// + public Optional> Roles { get; } + + /// + /// Whether to immediately mute the user upon join. + /// + public Optional Mute { get; } + + /// + /// Whether to immediately deafen the user upon join. + /// + public Optional Deaf { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IBeginGuildPrunePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IBeginGuildPrunePayload.cs new file mode 100644 index 0000000000..23a1aec711 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IBeginGuildPrunePayload.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST guilds/:guild-id/prune. +/// +public interface IBeginGuildPrunePayload +{ + /// + /// The days of inactivity to measure, from 0 to 30. + /// + public int? Days { get; } + + /// + /// A comma-separated list of snowflake identifiers of roles to include in the prune. + /// + /// + /// Any user with a subset of these roles will be considered for the prune. Any user with any role + /// not listed here will not be included in the count. + /// + public string? IncludeRoles { get; } + + /// + /// Specifies whether the amount of users pruned should be computed and returned. + /// + public bool? ComputeCount { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IBulkGuildBanPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IBulkGuildBanPayload.cs new file mode 100644 index 0000000000..ff5b2fbf10 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IBulkGuildBanPayload.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/bulk-ban. +/// +public interface IBulkGuildBanPayload +{ + /// + /// The snowflake identifiers of users to bulk ban. + /// + public IReadOnlyList UserIds { get; } + + /// + /// If any of the users have sent messages in the specified amount of seconds, these messages will be deleted if the + /// ban succeeds. + /// + public Optional DeleteMessageSeconds { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildChannelPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildChannelPayload.cs new file mode 100644 index 0000000000..d93bc64b1c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildChannelPayload.cs @@ -0,0 +1,107 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/channels. +/// +public interface ICreateGuildChannelPayload +{ + /// + /// The name of the channel to be created. + /// + public string Name { get; } + + /// + /// The channel type. + /// + public Optional Type { get; } + + /// + /// The channel topic. + /// + public Optional Topic { get; } + + /// + /// The voice channel bitrate. + /// + public Optional Bitrate { get; } + + /// + /// The voice channel user limit. + /// + public Optional UserLimit { get; } + + /// + /// The user slowmode in seconds. + /// + public Optional RateLimitPerUser { get; } + + /// + /// The sorting position in the channel list for this channel. + /// + public Optional Position { get; } + + /// + /// The permission overwrites for this channel. + /// + public Optional?> PermissionOverwrites { get; } + + /// + /// The category channel ID for this channel. + /// + public Optional ParentId { get; } + + /// + /// Indicates whether this channel is a NSFW channel. + /// + public Optional Nsfw { get; } + + /// + /// Channel voice region ID for this voice/stage channel. + /// + public Optional RtcRegion { get; } + + /// + /// Indicates the camera video quality mode of this channel. + /// + public Optional VideoQualityMode { get; } + + /// + /// The default auto archive duration clients use for newly created threads in this channel. + /// + public Optional DefaultAutoArchiveDuration { get; } + + /// + /// Default reaction for threads in this forum channel. + /// + public Optional DefaultReactionEmoji { get; } + + /// + /// The set of tags that can be used in this forum channel. + /// + public Optional?> AvailableTags { get; } + + /// + /// The default sort order for this forum channel. + /// + public Optional DefaultSortOrder { get; } + + /// + /// The default forum layout view used to display posts in this forum channel. + /// + public Optional DefaultForumLayout { get; } + + /// + /// The initial slowmode to set on newly created threads in this channel. This field is only used on creation + /// and does not live update. + /// + public Optional DefaultThreadRateLimitPerUser { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildPayload.cs new file mode 100644 index 0000000000..9795d0b5d0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildPayload.cs @@ -0,0 +1,85 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds. +/// +public interface ICreateGuildPayload +{ + /// + /// The name of the guild, from 2 to 100 characters. + /// + public string Name { get; } + + /// + /// The 128x128 icon of this guild. + /// + public Optional Icon { get; } + + /// + /// The verification level required by this guild. + /// + public Optional VerificationLevel { get; } + + /// + /// The default message notification level for this guild. + /// + public Optional DefaultMessageNotifications { get; } + + /// + /// The explicit content filter level for this guild. + /// + public Optional ExplicitContentFilter { get; } + + /// + /// The roles this guild will have. + /// + /// + /// ICreateGuildPayload.Roles[0] is used to configure the @everyone role. If you are trying + /// to bootstrap a guild with additional roles, you can set this first role to a placeholder.
+ /// The field is a placeholder to allow you to reference the role + /// elsewhere, namely when passing in default channels to and specifying + /// overwrites for them. + ///
+ public Optional> Roles { get; } + + /// + /// The channels to create this guild with. If this is set, none of the default channels will be created. + /// + /// + /// The field is ignored.
+ /// The field is a placeholder to allow creating category channels by + /// setting the field to the parents' ID. Category channels must + /// be listed before any of their children. The ID also serves for other fields to reference channels. + ///
+ public Optional> Channels { get; } + + /// + /// The identifier of the AFK voice channel, referring to a placeholder ID in . + /// + public Optional AfkChannelId { get; } + + /// + /// The AFK timeout in seconds, can be set to 60, 300, 900, 1800 or 3600. + /// + public Optional AfkTimeout { get; } + + /// + /// The identifier of the system channel where guild notices such as welcome messages are posted, referring + /// to a placeholder ID in . + /// + public Optional SystemChannelId { get; } + + /// + /// Default flags for the system channel. + /// + public Optional SystemChannelFlags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildRolePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildRolePayload.cs new file mode 100644 index 0000000000..ea5aa37c41 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/ICreateGuildRolePayload.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/roles +/// +public interface ICreateGuildRolePayload +{ + /// + /// The name of the to-be-created role. + /// + public Optional Name { get; init; } + + /// + /// Permissions for this role. Defaults to the @everyone permissions. + /// + public Optional Permissions { get; init; } + + /// + /// RGB color value for this role. + /// + public Optional Color { get; init; } + + /// + /// Whether the role should be hoisted in the sidebar. Defaults to . + /// + public Optional Hoist { get; init; } + + /// + /// The role's icon image, if it is a custom icon. + /// + public Optional Icon { get; init; } + + /// + /// The role's unicode emoji as role icon, if applicable. + /// + public Optional UnicodeEmoji { get; init; } + + /// + /// Indicates whether the role should be mentionable by everyone. + /// + public Optional Mentionable { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyCurrentMemberPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyCurrentMemberPayload.cs new file mode 100644 index 0000000000..0a2c699bf1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyCurrentMemberPayload.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/members/@me. +/// +public interface IModifyCurrentMemberPayload +{ + /// + /// The nickname of the current user. + /// + public Optional Nick { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildChannelPositionsPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildChannelPositionsPayload.cs new file mode 100644 index 0000000000..f97e5a2b30 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildChannelPositionsPayload.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/channels. +/// +public interface IModifyGuildChannelPositionsPayload +{ + /// + /// The snowflake identifier of the channel to be moved. + /// + public Snowflake ChannelId { get; } + + /// + /// The new sorting position for this channel. + /// + public Optional Position { get; } + + /// + /// Whether this channel should sync permissions with its new parent, if moving to a new parent category. + /// + public Optional LockPermissions { get; } + + /// + /// Snowflake identifier of this channels new parent channel. + /// + public Optional ParentChannelId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildIncidentActionsPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildIncidentActionsPayload.cs new file mode 100644 index 0000000000..e77d88158c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildIncidentActionsPayload.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PUT /guilds/:guild-id/incident-actions. +/// +public interface IModifyGuildIncidentActionsPayload +{ + /// + /// Disables invites up until the specified time. + /// + public Optional InvitesDisabledUntil { get; } + + /// + /// Disables direct messages between guild members up until the specified time. Note that this does not prevent members from messaging each other + /// if they have another avenue of doing so, such as another mutual server or being friends. + /// + public Optional DmsDisabledUntil { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildMemberPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildMemberPayload.cs new file mode 100644 index 0000000000..223cc0a1b1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildMemberPayload.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/members/:user-id. +/// +public interface IModifyGuildMemberPayload +{ + /// + /// The nickname to force the user to assume. + /// + public Optional Nickname { get; } + + /// + /// An array of role IDs to assign. + /// + public Optional?> Roles { get; } + + /// + /// Whether to mute the user. + /// + public Optional Mute { get; } + + /// + /// Whether to deafen the user. + /// + public Optional Deaf { get; } + + /// + /// The voice channel ID to move the user into. + /// + public Optional ChannelId { get; } + + /// + /// The timestamp at which the user's timeout is supposed to expire. Set to null to remove the timeout. + /// Must be no more than 28 days in the future. + /// + public Optional CommunicationDisabledUntil { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildMfaLevelPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildMfaLevelPayload.cs new file mode 100644 index 0000000000..e00f81213d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildMfaLevelPayload.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/mfa. +/// +public interface IModifyGuildMfaLevelPayload +{ + /// + /// The new MFA level for this guild. + /// + public DiscordMfaLevel Level { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildOnboardingPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildOnboardingPayload.cs new file mode 100644 index 0000000000..a3e5ab6228 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildOnboardingPayload.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PUT /guilds/:guild-id/onboarding. +/// +public interface IModifyGuildOnboardingPayload +{ + /// + /// Prompts shown during onboarding and in Customize Community. + /// + public IReadOnlyList Prompts { get; } + + /// + /// The snowflake identifiers of channels that members get opted into automatically. + /// + public IReadOnlyList DefaultChannelIds { get; } + + /// + /// Indicates whether onboarding is enabled in the guild. + /// + public bool Enabled { get; } + + /// + /// The current onboarding mode in this guild. + /// + public DiscordGuildOnboardingMode Mode { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildPayload.cs new file mode 100644 index 0000000000..ba00aa2052 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildPayload.cs @@ -0,0 +1,115 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id. +/// +public interface IModifyGuildPayload +{ + /// + /// The new name for this guild. + /// + public Optional Name { get; } + + /// + /// The new verification level for this guild. + /// + public Optional VerificationLevel { get; } + + /// + /// The new default message notification level for this guild. + /// + public Optional DefaultMessageNotifications { get; } + + /// + /// The new explicit content filter level for this guild. + /// + public Optional ExplicitContentFilter { get; } + + /// + /// The new snowflake identifier of the AFK channel of this guild. + /// + public Optional AfkChannelId { get; } + + /// + /// The new AFK timeout for this guild. + /// + public Optional AfkTimeout { get; } + + /// + /// The new icon for this guild. + /// + public Optional Icon { get; } + + /// + /// The snowflake identifier of this guild's new owner. Used to transfer guild ownership. + /// + public Optional OwnerId { get; } + + /// + /// The new splash for this guild. + /// + public Optional Splash { get; } + + /// + /// The new guild discovery splash for this guild. + /// + public Optional DiscoverySplash { get; } + + /// + /// The new banner for this guild. + /// + public Optional Banner { get; } + + /// + /// The snowflake identifier of the new system channel. + /// + public Optional SystemChannelId { get; } + + /// + /// The new system channel flags for this guild. + /// + public Optional SystemChannelFlags { get; } + + /// + /// The snowflake identifier of the new rules channel. + /// + public Optional RulesChannelId { get; } + + /// + /// The snowflake identifier of the new public update channel. + /// + public Optional PublicUpdatesChannelId { get; } + + /// + /// The new preferred locale for this community guild. + /// + public Optional PreferredLocale { get; } + + /// + /// The new enabled guild features for this guild. + /// + public Optional> Features { get; } + + /// + /// The new description for this guild, if it is discoverable. + /// + public Optional Description { get; } + + /// + /// Indicates whether the guild should have a boost progress bar. + /// + public Optional PremiumProgressBarEnabled { get; } + + /// + /// The snowflake identifier of the new safety alerts channel. + /// + public Optional SafetyAlertsChannelId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildRolePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildRolePayload.cs new file mode 100644 index 0000000000..26c8d92448 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildRolePayload.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/roles/:role-id +/// +public interface IModifyGuildRolePayload +{ + /// + /// The name of the to-be-created role. + /// + public Optional Name { get; init; } + + /// + /// Permissions for this role. Defaults to the @everyone permissions. + /// + public Optional Permissions { get; init; } + + /// + /// RGB color value for this role. + /// + public Optional Color { get; init; } + + /// + /// Whether the role should be hoisted in the sidebar. Defaults to . + /// + public Optional Hoist { get; init; } + + /// + /// The role's icon image, if it is a custom icon. + /// + public Optional Icon { get; init; } + + /// + /// The role's unicode emoji as role icon, if applicable. + /// + public Optional UnicodeEmoji { get; init; } + + /// + /// Indicates whether the role should be mentionable by everyone. + /// + public Optional Mentionable { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildRolePositionsPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildRolePositionsPayload.cs new file mode 100644 index 0000000000..6e10722554 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildRolePositionsPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/roles. +/// +public interface IModifyGuildRolePositionsPayload +{ + /// + /// The snowflake identifier of the role to move. + /// + public Snowflake Id { get; } + + /// + /// The new sorting position of the role. + /// + public Optional Position { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildWelcomeScreenPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildWelcomeScreenPayload.cs new file mode 100644 index 0000000000..6af4b8f7e0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Guilds/IModifyGuildWelcomeScreenPayload.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/welcome-screen. +/// +public interface IModifyGuildWelcomeScreenPayload +{ + /// + /// Indicates whether the welcome screen is enabled. + /// + public Optional Enabled { get; } + + /// + /// The channels linked in the welcome screen with their display options. + /// + public Optional?> WelcomeChannels { get; } + + /// + /// The guild description to show in the welcome screen. + /// + public Optional Description { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/ICreateFollowupMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/ICreateFollowupMessagePayload.cs new file mode 100644 index 0000000000..6e5af568ac --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/ICreateFollowupMessagePayload.cs @@ -0,0 +1,64 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /webhooks/:application-id/:interaction-token. +/// +/// +/// Either , or must be set. +/// +public interface ICreateFollowupMessagePayload +{ + /// + /// The text contents for this message, up to 2000 characters. + /// + public Optional Content { get; } + + /// + /// Indicates whether this is a TTS message. + /// + public Optional Tts { get; } + + /// + /// Embeds attached to this message. + /// + public Optional> Embeds { get; } + + /// + /// An allowed mentions object for this message. + /// + public Optional AllowedMentions { get; } + + /// + /// Up to five action rows worth of components to include with this message. + /// + public Optional> Components { get; } + + /// + /// Files to upload with this message. + /// + public IReadOnlyList? Files { get; } + + /// + /// Attachment metadata for files uploaded with this message. + /// + public Optional> Attachments { get; } + + /// + /// Additional message flags for this message. SuppressEmbeds and Ephemeral can be set. + /// + public Optional Flags { get; } + + /// + /// The poll object created with this message. + /// + public Optional Poll { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/IEditFollowupMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/IEditFollowupMessagePayload.cs new file mode 100644 index 0000000000..13df1502e6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/IEditFollowupMessagePayload.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /webhooks/:application-id/:interaction-token/messages/:message-id. +/// +public interface IEditFollowupMessagePayload +{ + /// + /// The message contents, up to 2000 characters. + /// + public Optional Content { get; } + + /// + /// Up to 10 embeds, subject to embed length limits. + /// + public Optional?> Embeds { get; } + + /// + /// Allowed mentions for this message. + /// + public Optional AllowedMentions { get; } + + /// + /// The components for this message. + /// + public Optional?> Components { get; } + + /// + /// Attached files to keep and possible descriptions for new files to upload. + /// + public Optional?> Attachments { get; } + + /// + /// File contents to send or edit. + /// + public IReadOnlyList? Files { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/IEditInteractionResponsePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/IEditInteractionResponsePayload.cs new file mode 100644 index 0000000000..00fbeb5933 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Interactions/IEditInteractionResponsePayload.cs @@ -0,0 +1,50 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /webhooks/:application-id/:interaction-token/messages/@original. +/// +public interface IEditInteractionResponsePayload +{ + /// + /// The message contents, up to 2000 characters. + /// + public Optional Content { get; } + + /// + /// Up to 10 embeds, subject to embed length limits. + /// + public Optional?> Embeds { get; } + + /// + /// Allowed mentions for this message. + /// + public Optional AllowedMentions { get; } + + /// + /// The components for this message. + /// + public Optional?> Components { get; } + + /// + /// Attached files to keep and possible descriptions for new files to upload. + /// + public Optional?> Attachments { get; } + + /// + /// File contents to send or edit. + /// + public IReadOnlyList? Files { get; } + + /// + /// A poll to add to this message. + /// + public Optional Poll { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/IBulkDeleteMessagesPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/IBulkDeleteMessagesPayload.cs new file mode 100644 index 0000000000..9f38eff052 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/IBulkDeleteMessagesPayload.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/messages/bulk-delete. +/// +public interface IBulkDeleteMessagesPayload +{ + /// + /// The message IDs to bulk delete, between 2 and 100. + /// + public IReadOnlyList Messages { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/ICreateMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/ICreateMessagePayload.cs new file mode 100644 index 0000000000..4a5887c564 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/ICreateMessagePayload.cs @@ -0,0 +1,83 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/messages. +/// +public interface ICreateMessagePayload +{ + /// + /// The message content to send. + /// + public Optional Content { get; } + + /// + /// An identifier for this message. This will be sent in the MESSAGE_CREATE event. + /// + public Optional Nonce { get; } + + /// + /// Indicates whether this message is a text-to-speech message. + /// + public Optional Tts { get; } + + /// + /// Up to 10 embeds to be attached to this message. + /// + public Optional> Embeds { get; } + + /// + /// Specifies which mentions should be resolved. + /// + public Optional AllowedMentions { get; } + + /// + /// A reference to the message this message shall reply to. + /// + public Optional MessageReference { get; } + + /// + /// A list of components to include with the message. + /// + public Optional> Components { get; } + + /// + /// Up to 3 snowflake identifiers of stickers to be attached to this message. + /// + public Optional> StickerIds { get; } + + /// + /// Files to be attached to this message. + /// + public IReadOnlyList? Files { get; } + + /// + /// Attachment metadata for this message. + /// + public Optional> Attachments { get; } + + /// + /// Message flags, combined as bitfield. Only can be set. + /// + public Optional Flags { get; } + + /// + /// If this is set to true and a is present, it will be checked for uniqueness in the past few + /// minutes. If another message was created by this user with the same nonce, that message will be returned and no new + /// message will be created. + /// + public Optional EnforceNonce { get; } + + /// + /// A poll attached to this message. + /// + public Optional Poll { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/IEditMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/IEditMessagePayload.cs new file mode 100644 index 0000000000..d9a6639b62 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Messages/IEditMessagePayload.cs @@ -0,0 +1,54 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /channels/:channel-id/messages/:message-id. +/// +public interface IEditMessagePayload +{ + /// + /// New string content of the message, up to 2000 characters. + /// + public Optional Content { get; } + + /// + /// Up to 10 embeds for this message. + /// + public Optional?> Embeds { get; } + + /// + /// New flags for this message. Only can currently be set + /// or unset. + /// + public Optional Flags { get; } + + /// + /// Authoritative allowed mentions object for this message. Passing resets + /// the object to default. + /// + public Optional AllowedMentions { get; } + + /// + /// New components for this message. + /// + public Optional?> Components { get; } + + /// + /// Attached files to this message. This must include old attachments to be retained and new attachments, + /// if passed. + /// + public IReadOnlyList? Files { get; } + + /// + /// Attachments to this message. + /// + public Optional?> Attachments { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ScheduledEvents/ICreateGuildScheduledEventPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ScheduledEvents/ICreateGuildScheduledEventPayload.cs new file mode 100644 index 0000000000..8f8d4b8b6e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ScheduledEvents/ICreateGuildScheduledEventPayload.cs @@ -0,0 +1,66 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /guilds/:guild-id/scheduled-events. +/// +public interface ICreateGuildScheduledEventPayload +{ + /// + /// The channel ID the scheduled event will take place in. + /// + public Optional ChannelId { get; } + + /// + /// Represents metadata about the scheduled event. + /// + public Optional EntityMetadata { get; } + + /// + /// Name of the scheduled event. + /// + public string Name { get; } + + /// + /// Privacy level for this scheduled event. + /// + public DiscordScheduledEventPrivacyLevel PrivacyLevel { get; } + + /// + /// Indicates the time at which this event is scheduled to start. + /// + public DateTimeOffset ScheduledStartTime { get; } + + /// + /// Indicates the time at which this event is scheduled to end. + /// + public Optional ScheduledEndTime { get; } + + /// + /// Description for this scheduled event. + /// + public Optional Description { get; } + + /// + /// The event type of this event. + /// + public DiscordScheduledEventType EntityType { get; } + + /// + /// Image data representing the cover image of this scheduled event. + /// + public Optional Image { get; } + + /// + /// A definition for how often and at what dates this event should recur. + /// + public Optional RecurrenceRule { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ScheduledEvents/IModifyGuildScheduledEventPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ScheduledEvents/IModifyGuildScheduledEventPayload.cs new file mode 100644 index 0000000000..d4ad414172 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/ScheduledEvents/IModifyGuildScheduledEventPayload.cs @@ -0,0 +1,71 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/scheduled-events/:event-id. +/// +public interface IModifyGuildScheduledEventPayload +{ + /// + /// The channel ID the scheduled event will take place in. + /// + public Optional ChannelId { get; } + + /// + /// Represents metadata about the scheduled event. + /// + public Optional EntityMetadata { get; } + + /// + /// Name of the scheduled event. + /// + public Optional Name { get; } + + /// + /// Privacy level for this scheduled event. + /// + public Optional PrivacyLevel { get; } + + /// + /// Indicates the time at which this event is scheduled to start. + /// + public Optional ScheduledStartTime { get; } + + /// + /// Indicates the time at which this event is scheduled to end. + /// + public Optional ScheduledEndTime { get; } + + /// + /// Description for this scheduled event. + /// + public Optional Description { get; } + + /// + /// The event type of this event. + /// + public Optional EntityType { get; } + + /// + /// The status of this scheduled event. To start or end an event, set this field to its respective state. + /// + public Optional Status { get; } + + /// + /// Image data representing the cover image of this scheduled event. + /// + public Optional Image { get; } + + /// + /// A definition for how often and at what dates this event should recur. + /// + public Optional RecurrenceRule { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/ICreateGuildSoundboardSoundPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/ICreateGuildSoundboardSoundPayload.cs new file mode 100644 index 0000000000..e8d1890289 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/ICreateGuildSoundboardSoundPayload.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST guilds/:guild-id/soundboard-sounds. +/// +public interface ICreateGuildSoundboardSoundPayload +{ + /// + /// The name of the soundboard sound, 2-32 characters long. + /// + public string Name { get; } + + /// + /// The sound data. This must be mp3 or ogg encoded. + /// + public InlineMediaData Sound { get; } + + /// + /// The volume of this soundboard sound, from 0 to 1. Defaults to 1. + /// + public Optional Volume { get; } + + /// + /// The snowflake identifier of the custom emoji associated with this soundboard sound. + /// + public Optional EmojiId { get; } + + /// + /// The unicode representation of the standard emoji associated with this soundboard sound. + /// + public Optional EmojiName { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/IModifyGuildSoundboardSoundPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/IModifyGuildSoundboardSoundPayload.cs new file mode 100644 index 0000000000..4dd078c997 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/IModifyGuildSoundboardSoundPayload.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH guilds/:guild-id/soundboard-sounds/:sound-id. +/// +public interface IModifyGuildSoundboardSoundPayload +{ + /// + /// The name of the soundboard sound, 2-32 characters long. + /// + public Optional Name { get; } + + /// + /// The volume of this soundboard sound, from 0 to 1. Defaults to 1. + /// + public Optional Volume { get; } + + /// + /// The snowflake identifier of the custom emoji associated with this soundboard sound. + /// + public Optional EmojiId { get; } + + /// + /// The unicode representation of the standard emoji associated with this soundboard sound. + /// + public Optional EmojiName { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/ISendSoundboardSoundPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/ISendSoundboardSoundPayload.cs new file mode 100644 index 0000000000..3c5756c360 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Soundboard/ISendSoundboardSoundPayload.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST channels/:channel-id/send-soundboard-sound. +/// +public interface ISendSoundboardSoundPayload +{ + /// + /// The snowflake identifier of the soundboard sound to play. + /// + public Snowflake SoundId { get; } + + /// + /// The snowflake identifier of the guild the soundboard sound originates from. + /// This is required to play sounds from different guilds to the present guild. + /// + public Optional SourceGuildId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/StageInstances/ICreateStageInstancePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/StageInstances/ICreateStageInstancePayload.cs new file mode 100644 index 0000000000..5437d39bfd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/StageInstances/ICreateStageInstancePayload.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /stage-instances. +/// +public interface ICreateStageInstancePayload +{ + /// + /// The snowflake identifier of the parent stage channel. + /// + public Snowflake ChannelId { get; } + + /// + /// The topic of the stage instance. + /// + public string Topic { get; } + + /// + /// The privacy level of the stage instance. + /// + public Optional PrivacyLevel { get; } + + /// + /// Indicates whether @everyone should be notified that a stage instance has started. + /// + public Optional SendStartNotification { get; } + + /// + /// The snowflake identifier of the scheduled event associated with this instance. + /// + public Optional GuildScheduledEventId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/StageInstances/IModifyStageInstancePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/StageInstances/IModifyStageInstancePayload.cs new file mode 100644 index 0000000000..dc00fa72a6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/StageInstances/IModifyStageInstancePayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /stage-instances/:channel-id. +/// +public interface IModifyStageInstancePayload +{ + /// + /// The new topic for this stage instance. + /// + public Optional Topic { get; } + + /// + /// The new privacy level of the current stage. + /// + public Optional PrivacyLevel { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Stickers/ICreateGuildStickerPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Stickers/ICreateGuildStickerPayload.cs new file mode 100644 index 0000000000..095a503b0f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Stickers/ICreateGuildStickerPayload.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Rpresents a payload to POST /guilds/:guild-id/stickers. +/// +public interface ICreateGuildStickerPayload +{ + /// + /// The name of this sticker. + /// + public string Name { get; } + + /// + /// The description for this sticker. + /// + public string Description { get; } + + /// + /// The autocomplete suggestion tags for this sticker. + /// + public string Tags { get; } + + /// + /// File contents of the sticker to upload. + /// + public AttachmentData File { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Stickers/IModifyGuildStickerPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Stickers/IModifyGuildStickerPayload.cs new file mode 100644 index 0000000000..7041363cf2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Stickers/IModifyGuildStickerPayload.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/stickers/:sticker-id. +/// +public interface IModifyGuildStickerPayload +{ + /// + /// The name of this sticker. + /// + public Optional Name { get; } + + /// + /// The description for this sticker. + /// + public Optional Description { get; } + + /// + /// The autocomplete suggestion tags for this sticker. + /// + public Optional Tags { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/ICreateDmPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/ICreateDmPayload.cs new file mode 100644 index 0000000000..52c83c80ba --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/ICreateDmPayload.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /users/@me/channels. +/// +public interface ICreateDmPayload +{ + /// + /// The snowflake identifier of the recipient of your DM. + /// + public Snowflake RecipientId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/ICreateGroupDmPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/ICreateGroupDmPayload.cs new file mode 100644 index 0000000000..160fb72d41 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/ICreateGroupDmPayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /users/@me/channels. +/// +public interface ICreateGroupDmPayload +{ + /// + /// The access tokens of users that have granted your app the gdm.join scope. + /// + public IReadOnlyList AccessTokens { get; } + + /// + /// The nicknames to initialize the users with. + /// + public IReadOnlyDictionary Nicks { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/IModifyCurrentUserPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/IModifyCurrentUserPayload.cs new file mode 100644 index 0000000000..2706e2fb4d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/IModifyCurrentUserPayload.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /users/@me. +/// +public interface IModifyCurrentUserPayload +{ + /// + /// The new username. If the current user is a bot user, it participates in discriminators, and + /// changing the username may cause the discriminator to be randomized if the current discriminator + /// is unavailable for the new username. + /// + public Optional Username { get; } + + /// + /// The new avatar for this user. + /// + public Optional Avatar { get; } + + /// + /// The new banner for this user. + /// + public Optional Banner { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/IUpdateCurrentUserApplicationRoleConnectionPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/IUpdateCurrentUserApplicationRoleConnectionPayload.cs new file mode 100644 index 0000000000..9cc9698324 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Users/IUpdateCurrentUserApplicationRoleConnectionPayload.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PUT /users/@me/applications/:application-id/role-connection. +/// +public interface IUpdateCurrentUserApplicationRoleConnectionPayload +{ + /// + /// The vanity name of the platform the bot is connecting. + /// + public Optional PlatformName { get; } + + /// + /// The username on the platform the bot is connecting. + /// + public Optional PlatformUsername { get; } + + /// + /// An object mapping application role connection metadata keys to their stringified value. + /// + public Optional Metadata { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Voice/IModifyCurrentUserVoiceStatePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Voice/IModifyCurrentUserVoiceStatePayload.cs new file mode 100644 index 0000000000..dcf3cefe39 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Voice/IModifyCurrentUserVoiceStatePayload.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/voice-states/@me. +/// +public interface IModifyCurrentUserVoiceStatePayload +{ + /// + /// The snowflake identifier of the channel this user is currently in. + /// + public Optional ChannelId { get; } + + /// + /// Toggles this user's suppression state. + /// + public Optional Suppress { get; } + + /// + /// Sets this user's speaking request in a stage channel. + /// + public Optional RequestToSpeakTimestamp { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Voice/IModifyUserVoiceStatePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Voice/IModifyUserVoiceStatePayload.cs new file mode 100644 index 0000000000..8c9debfdd3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Voice/IModifyUserVoiceStatePayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /guilds/:guild-id/voice-states/:user-id. +/// +public interface IModifyUserVoiceStatePayload +{ + /// + /// The snowflake identifier of the channel this user is currently in. + /// + public Snowflake ChannelId { get; } + + /// + /// Toggles this user's suppression state. + /// + public Optional Suppress { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/ICreateWebhookPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/ICreateWebhookPayload.cs new file mode 100644 index 0000000000..b2e16d4211 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/ICreateWebhookPayload.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /channels/:channel-id/webhooks. +/// +public interface ICreateWebhookPayload +{ + /// + /// The name of the webhook. + /// + /// + /// This follows several requirements:

+ /// + /// A webhook name can contain up to 80 characters, unlike usernames/nicknames which are limited to 32. + /// A webhook name is subject to all other requirements usernames and nicknames are subject to. + /// Webhook names cannot contain the substrings clyde and discord, case-insensitively. + /// + /// If the name does not fit all three requirements, it will be rejected and an error will be returned. + ///
+ public string Name { get; } + + /// + /// The default webhook avatar. This may be overridden by messages. + /// + public Optional Avatar { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IEditWebhookMessagePayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IEditWebhookMessagePayload.cs new file mode 100644 index 0000000000..f9d71c82f0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IEditWebhookMessagePayload.cs @@ -0,0 +1,53 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /webhooks/:webhook-id/:webhook-token/messages/:message-id. +/// +public interface IEditWebhookMessagePayload +{ + /// + /// The new message contents. + /// + public Optional Content { get; } + + /// + /// Up to 10 embeds attached to the message. + /// + public Optional?> Embeds { get; } + + /// + /// The flags to edit this message with. may be set but not removed, + /// may be set and removed, and all other flags cannot be set or removed. + /// + public Optional Flags { get; } + + /// + /// The new allowed mentions object for this message. + /// + public Optional AllowedMentions { get; } + + /// + /// The new components attached to this message. + /// + public Optional?> Components { get; } + + /// + /// The new files to be sent along with the edit. Note that all used files in this message must be passed here, + /// even if they were originally present. + /// + public IReadOnlyList? Files { get; } + + /// + /// Attachment descriptor objects for this message. + /// + public Optional?> Attachments { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IExecuteWebhookPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IExecuteWebhookPayload.cs new file mode 100644 index 0000000000..d0ef8f24b7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IExecuteWebhookPayload.cs @@ -0,0 +1,81 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to POST /webhooks/:webhook-id/:webhook-token. +/// +public interface IExecuteWebhookPayload +{ + /// + /// Message contents. + /// + public Optional Content { get; } + + /// + /// Overrides the default username of the webhook. + /// + public Optional Username { get; } + + /// + /// Overrides the default avatar of the webhook. + /// + public Optional AvatarUrl { get; } + + /// + /// True if this is a TTS message. + /// + public Optional Tts { get; } + + /// + /// Up to 10 embeds + /// + public Optional> Embeds { get; } + + /// + /// Allowed mentions object for this message. + /// + public Optional AllowedMentions { get; } + + /// + /// Components to include with this message. + /// + public Optional> Components { get; } + + /// + /// Attachment files to include with this message. + /// + public IReadOnlyList? Files { get; } + + /// + /// Attachment descriptor objects with filename and description. + /// + public Optional> Attachments { get; } + + /// + /// Message flags for this message. Only can be set. + /// + public Optional Flags { get; } + + /// + /// The name of the thread to create, if the webhook channel is a forum channel. + /// + public Optional ThreadName { get; } + + /// + /// The snowflake identifiers of tags to apply to the thread, if applicable. + /// + public Optional> AppliedTags { get; } + + /// + /// The poll object created with this message. + /// + public Optional Poll { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IModifyWebhookPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IModifyWebhookPayload.cs new file mode 100644 index 0000000000..d30c6a0316 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IModifyWebhookPayload.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /webhooks/:webhook-id. +/// +public interface IModifyWebhookPayload +{ + /// + /// The new default name of this webhook. + /// + public Optional Name { get; } + + /// + /// The new default webhook avatar image. + /// + public Optional Avatar { get; } + + /// + /// The snowflake identifier of the channel this webhook should be moved to. + /// + public Optional ChannelId { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IModifyWebhookWithTokenPayload.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IModifyWebhookWithTokenPayload.cs new file mode 100644 index 0000000000..ab77ef072f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads/Webhooks/IModifyWebhookWithTokenPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Payloads; + +/// +/// Represents a payload to PATCH /webhooks/:webhook-id/:webhook-token. +/// +public interface IModifyWebhookWithTokenPayload +{ + /// + /// The new default name of this webhook. + /// + public Optional Name { get; } + + /// + /// The new default webhook avatar image. + /// + public Optional Avatar { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ApplicationCommands/LocalizationQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ApplicationCommands/LocalizationQuery.cs new file mode 100644 index 0000000000..91401d953a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ApplicationCommands/LocalizationQuery.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains query parameters for application command endpoints that can include localizations +/// along with the command object. +/// +public readonly record struct LocalizationQuery +{ + /// + /// Indicates whether to include command localizations with the command object. + /// + public bool? WithLocalizations { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/AuditLogs/ListGuildAuditLogEntriesQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/AuditLogs/ListGuildAuditLogEntriesQuery.cs new file mode 100644 index 0000000000..72ca146cc1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/AuditLogs/ListGuildAuditLogEntriesQuery.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IAuditLogsRestAPI.ListGuildAuditLogEntriesAsync. +/// +public readonly record struct ListGuildAuditLogEntriesQuery : IPaginatedQuery +{ + /// + /// If specified, only list entries from this user ID. + /// + public Snowflake? UserId { get; init; } + + /// + /// If specified, only list entries of this audit log event type. + /// + public DiscordAuditLogEvent? ActionType { get; init; } + + /// + /// If specified, only list entries with an ID less than this ID. + /// + public Snowflake? Before { get; init; } + + /// + /// If specified, only list entries with an ID greater than this ID. + /// + public Snowflake? After { get; init; } + + /// + /// The maximum number of entries, between 1 and 100. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/GetThreadMemberQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/GetThreadMemberQuery.cs new file mode 100644 index 0000000000..e9845f21e1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/GetThreadMemberQuery.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for IChannelRestAPI.GetThreadMemberAsync. +/// +public readonly record struct GetThreadMemberQuery +{ + /// + /// Specifies whether the returned thread member object should contain guild member data. + /// + public bool? WithMember { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListArchivedThreadsQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListArchivedThreadsQuery.cs new file mode 100644 index 0000000000..90bf67fca6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListArchivedThreadsQuery.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for endpoints listing archived threads. +/// +public readonly record struct ListArchivedThreadsQuery +{ + /// + /// A timestamp to filter threads by: only threads archived before this timestamp will be returned. + /// + public DateTimeOffset? Before { get; init; } + + /// + /// The maximum number of threads to return from this request. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListJoinedPrivateArchivedThreadsQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListJoinedPrivateArchivedThreadsQuery.cs new file mode 100644 index 0000000000..319b066c89 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListJoinedPrivateArchivedThreadsQuery.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for listing joined private archived threads, specifically. +/// +public readonly record struct ListJoinedPrivateArchivedThreadsQuery +{ + /// + /// A snowflake to filter threads by: only threads archived before this timestamp will be returned. + /// + public Snowflake? Before { get; init; } + + /// + /// The maximum number of threads to return from this request. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListThreadMembersQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListThreadMembersQuery.cs new file mode 100644 index 0000000000..6e3403c1f0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Channels/ListThreadMembersQuery.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for IChannelRestAPI.ListThreadMembersAsync. +/// +public readonly record struct ListThreadMembersQuery +{ + /// + /// Specifies whether the returned thread member object should contain guild member data. + /// + public bool? WithMember { get; init; } + + /// + /// If specified, only request thread members with an ID greater than this ID. + /// + public Snowflake? After { get; init; } + + /// + /// The maximum number of entities for this request. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Entitlements/ListEntitlementsQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Entitlements/ListEntitlementsQuery.cs new file mode 100644 index 0000000000..31af180917 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Entitlements/ListEntitlementsQuery.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IEntitlementsRestAPI.ListEntitlementsAsync. +/// +public readonly record struct ListEntitlementsQuery : IPaginatedQuery +{ + /// + public Snowflake? Before { get; init; } + + /// + public Snowflake? After { get; init; } + + /// + public int? Limit { get; init; } + + /// + /// The snowflake identifier of the user to look up entitlements for. + /// + public Snowflake? UserId { get; init; } + + /// + /// A comma-delimited set of snowflakes of SKUs to check entitlements for. + /// + public string? SkuIds { get; init; } + + /// + /// The snowflake identifier of the guild to look up entitlements for. + /// + public Snowflake? GuildId { get; init; } + + /// + /// Specifies whether or not to include ended entitlements. + /// + public bool? ExcludeEnded { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ForwardsPaginatedQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ForwardsPaginatedQuery.cs new file mode 100644 index 0000000000..36e1f9bf9f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ForwardsPaginatedQuery.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +public readonly record struct ForwardsPaginatedQuery +{ + /// + /// If specified, only request entities with an ID greater than this ID. + /// + public Snowflake? After { get; init; } + + /// + /// The maximum number of entities to return from this request. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/CreateGuildBanQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/CreateGuildBanQuery.cs new file mode 100644 index 0000000000..2fd9916edc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/CreateGuildBanQuery.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +public readonly record struct CreateGuildBanQuery +{ + /// + /// Specifies how many seconds worth of message history should be deleted along with this ban. + /// This allows up to 7 days, or 604800 seconds. + /// + public int? DeleteMessageSeconds { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildPruneCountQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildPruneCountQuery.cs new file mode 100644 index 0000000000..6d72eb416f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildPruneCountQuery.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for IGuildRestAPI.GetGuildPruneCountAsync. +/// +public readonly record struct GetGuildPruneCountQuery +{ + /// + /// The days of inactivity to measure, from 0 to 30. + /// + public int? Days { get; init; } + + /// + /// A comma-separated list of snowflake identifiers of roles to include in the prune. + /// + /// + /// Any user with a subset of these roles will be considered for the prune. Any user with any role + /// not listed here will not be included in the count. + /// + public string? IncludeRoles { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildQuery.cs new file mode 100644 index 0000000000..10940e7106 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildQuery.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains query parameters for IGuildRestAPI.GetGuildAsync. +/// +public readonly record struct GetGuildQuery +{ + /// + /// Specifies whether the response should include online member counts. + /// + public bool? WithCounts { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildWidgetImageQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildWidgetImageQuery.cs new file mode 100644 index 0000000000..bc3269762a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/GetGuildWidgetImageQuery.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the request parameters to IGuildRestAPI.GetGuildWidgetImageAsync. +/// +public readonly record struct GetGuildWidgetImageQuery +{ + /// + /// The style of the widget image, one of 'shield' or 'banner1' through 'banner4'. + /// + public string? Style { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/SearchGuildMembersQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/SearchGuildMembersQuery.cs new file mode 100644 index 0000000000..f77a946067 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Guilds/SearchGuildMembersQuery.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IGuildRestAPI.SearchGuildMembersAsync. This request cannot +/// be paginated. +/// +public readonly record struct SearchGuildMembersQuery +{ + /// + /// The query string to match usernames and nicknames against. + /// + public required string Query { get; init; } + + /// + /// The amount of guild members to return. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/IPaginatedQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/IPaginatedQuery.cs new file mode 100644 index 0000000000..76052ec3c9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/IPaginatedQuery.cs @@ -0,0 +1,29 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Represents a base interface for all snowflake-paginated queries, for user convenience. +/// +// this is here despite not being technically necessary to accurately implement queries because +// as of now, it can't go anywhere else. we'll move this once possible, using explicit extensions +// acting as interface adapters. +public interface IPaginatedQuery +{ + /// + /// If specified, only request entities with an ID less than this ID. + /// + public Snowflake? Before { get; init; } + + /// + /// If specified, only request entities with an ID greater than this ID. + /// + public Snowflake? After { get; init; } + + /// + /// The maximum number of entities to return from this request. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Invites/GetInviteQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Invites/GetInviteQuery.cs new file mode 100644 index 0000000000..fa79debee5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Invites/GetInviteQuery.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains query parameters to IInviteRestAPI.GetInviteAsync. +/// +public readonly record struct GetInviteQuery +{ + /// + /// Specifies whether the returned invite object should include approximate member counts. + /// + public bool? WithCounts { get; init; } + + /// + /// Specifies whether the returned invite object should include the expiration date. + /// + public bool? WithExpiration { get; init; } + + /// + /// The snowflake identifier of the scheduled event to include with the invite. + /// + public Snowflake? GuildScheduledEventId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/GetChannelMessagesQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/GetChannelMessagesQuery.cs new file mode 100644 index 0000000000..4d832e9ca8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/GetChannelMessagesQuery.cs @@ -0,0 +1,34 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains query parameters for IChannelRestAPI.GetChannelMessagesAsync. +/// +public readonly record struct GetChannelMessagesQuery : IPaginatedQuery +{ + /// + /// If specified, request entities around this ID. + /// + /// + /// Mutually exclusive with and . + /// + public Snowflake? Around { get; init; } + + /// + /// + /// Mutually exclusive with and . + /// + public Snowflake? Before { get; init; } + + /// + /// + /// Mutually exclusive with and . + /// + public Snowflake? After { get; init; } + + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/GetReactionsQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/GetReactionsQuery.cs new file mode 100644 index 0000000000..d70620b1bf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/GetReactionsQuery.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +public readonly record struct GetReactionsQuery +{ + /// + /// The type of reactions to query for. + /// + public ReactionType? Type { get; init; } + + /// + /// If specified, only request entities with an ID greater than this ID. + /// + public Snowflake? After { get; init; } + + /// + /// The maximum number of entities to return from this request. + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/ReactionType.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/ReactionType.cs new file mode 100644 index 0000000000..0def512725 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Messages/ReactionType.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Enumerates reaction types to query for. +/// +public enum ReactionType +{ + /// + /// Standard reactions. + /// + Normal, + + /// + /// Super reactions. + /// + Burst +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/PaginatedQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/PaginatedQuery.cs new file mode 100644 index 0000000000..31438fd908 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/PaginatedQuery.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for all purely snowflake-paginated requests. +/// +public readonly record struct PaginatedQuery : IPaginatedQuery +{ + /// + public Snowflake? Before { get; init; } + + /// + public Snowflake? After { get; init; } + + /// + public int? Limit { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ScheduledEvents/GetScheduledEventUsersQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ScheduledEvents/GetScheduledEventUsersQuery.cs new file mode 100644 index 0000000000..d9b9b3c9d6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ScheduledEvents/GetScheduledEventUsersQuery.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IGuildScheduledEventRestAPI.GetScheduledEventUsersAsync. +/// +public readonly record struct GetScheduledEventUsersQuery : IPaginatedQuery +{ + /// + public Snowflake? Before { get; init; } + + /// + public Snowflake? After { get; init; } + + /// + public int? Limit { get; init; } + + /// + /// Specifies whether the response should include guild member data. + /// + public bool? WithMember { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ScheduledEvents/WithUserCountQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ScheduledEvents/WithUserCountQuery.cs new file mode 100644 index 0000000000..941e1df48e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/ScheduledEvents/WithUserCountQuery.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters for scheduled events that may be returned with user counts. +/// +public readonly record struct WithUserCountQuery +{ + /// + /// Specifies whether to include user counts in the returned scheduled events. + /// + public bool? WithUserCount { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Subscriptions/ListSkuSubscriptionsQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Subscriptions/ListSkuSubscriptionsQuery.cs new file mode 100644 index 0000000000..d96d462b0e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Subscriptions/ListSkuSubscriptionsQuery.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +public readonly record struct ListSkuSubscriptionsQuery : IPaginatedQuery +{ + + /// + public Snowflake? Before { get; init; } + + /// + public Snowflake? After { get; init; } + + /// + public int? Limit { get; init; } + + /// + /// Specifies a user ID for whom to query subscriptions. This is required except for OAuth2 queries. + /// + public Snowflake? UserId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Users/GetCurrentUserGuildsQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Users/GetCurrentUserGuildsQuery.cs new file mode 100644 index 0000000000..7449bba518 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Users/GetCurrentUserGuildsQuery.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IUserRestAPI.GetCurrentUserGuildsAsync. +/// +public readonly record struct GetCurrentUserGuildsQuery : IPaginatedQuery +{ + /// + public Snowflake? Before { get; init; } + + /// + public Snowflake? After { get; init; } + + /// + public int? Limit { get; init; } + + /// + /// Specifies whether the returned guild objects should include approximate member counts. + /// + public bool? WithCounts { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/EditWebhookMessageQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/EditWebhookMessageQuery.cs new file mode 100644 index 0000000000..9c74e9ea3d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/EditWebhookMessageQuery.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IWebhookRestAPI.EditWebhookMessageAsync. +/// +public readonly record struct EditWebhookMessageQuery +{ + + /// + /// Specifies a thread to edit the message in rather than directly in the parent channel. If the thread + /// is archived, this will automatically unarchive it. Only threads with the same parent channel as the + /// webhook can be passed. + /// + public Snowflake? ThreadId { get; init; } + + /// + /// Specifies whether this request will allow sending non-interactive components for non-application-owned webhooks. + /// Defaults to + /// + public bool? WithComponents { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/ExecuteWebhookQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/ExecuteWebhookQuery.cs new file mode 100644 index 0000000000..b9975cb1bb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/ExecuteWebhookQuery.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the query parameters to IWebhookRestAPI.ExecuteWebhookAsync. +/// +public readonly record struct ExecuteWebhookQuery +{ + /// + /// Specifies whether to wait for server confirmation. If this is set to true, a + /// object will be returned, if not, will be returned on success instead. + /// Defaults to . + /// + public bool? Wait { get; init; } + + /// + /// Specifies a thread to send the message to rather than directly to the parent channel. If the thread + /// is archived, this will automatically unarchive it. Only threads with the same parent channel as the + /// webhook can be passed. + /// + public Snowflake? ThreadId { get; init; } + + /// + /// Specifies whether this request will allow sending non-interactive components for non-application-owned webhooks. + /// Defaults to + /// + public bool? WithComponents { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/ThreadIdQuery.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/ThreadIdQuery.cs new file mode 100644 index 0000000000..6b3910bda9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Queries/Webhooks/ThreadIdQuery.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Queries; + +/// +/// Contains the ThreadId query parameter for webhook requests that can target a message in a thread. +/// +public readonly record struct ThreadIdQuery +{ + /// + /// Specifies a thread to search in rather than directly to the parent channel. If the thread is archived, + /// and this is an edit, this will automatically unarchive it. Only threads with the same parent channel + /// as the webhook can be passed. + /// + public Snowflake? ThreadId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/RequestBuilder.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/RequestBuilder.cs new file mode 100644 index 0000000000..018cb26217 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/RequestBuilder.cs @@ -0,0 +1,111 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA2227 + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest; + +/// +/// Provides an user-friendly way to build a rest request to Discord. +/// +public class RequestBuilder +{ + /// + /// The audit log reason for Discord, if applicable. + /// + public string? AuditLogReason { get; set; } + + /// + /// The payload to send to discord. + /// + public object? Payload { get; set; } + + /// + /// The route this request will take. + /// + public string? Route { get; set; } + + /// + /// Additional information to pass to the rest client. + /// + public IDictionary? AdditionalContext { get; set; } + + /// + /// Additional files to upload with this request. + /// + public IList? AdditionalFiles { get; set; } + + /// + /// Additional headers to add to this request. + /// + public IDictionary? Headers { get; set; } + + /// + /// Specifies the audit log reason for this request. + /// + /// The builder for chaining. + public RequestBuilder WithAuditLogReason(string? reason) + { + this.AuditLogReason = reason; + return this; + } + + /// + /// Attaches a payload to this request. + /// + /// The builder for chaining. + public RequestBuilder WithPayload(object payload) + { + this.Payload = payload; + return this; + } + + /// + /// Specifies the route this request will take. + /// + /// The builder for chaining. + public RequestBuilder WithRoute(string route) + { + this.Route = route; + return this; + } + + /// + /// Adds a field to the request context. + /// + /// The builder for chaining. + public RequestBuilder AddToContext(string key, object value) + { + this.AdditionalContext ??= new Dictionary(); + + this.AdditionalContext.Add(key, value); + return this; + } + + /// + /// Adds a file to attach to the request. + /// + /// The builder for chaining. + public RequestBuilder AddFile(AttachmentData file) + { + this.AdditionalFiles ??= []; + + this.AdditionalFiles.Add(file); + return this; + } + + /// + /// Adds a header to the request. + /// + /// The builder for chaining. + public RequestBuilder AddHeader(string key, string value) + { + this.Headers ??= new Dictionary(); + + this.Headers.Add(key, value); + return this; + } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/RequestInfo.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/RequestInfo.cs new file mode 100644 index 0000000000..f406d6c060 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/RequestInfo.cs @@ -0,0 +1,54 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.Internal.Abstractions.Rest; + +/// +/// Contains additional instructions on how to execute a request. +/// +public readonly record struct RequestInfo +{ + /// + /// The maximum time to wait for this request, in milliseconds. + /// + /// + /// If a ratelimit is triggered for this request and it cannot be retried in time, this request will fail + /// immediately. null specifies no timeout, which is the default. + /// + public int? Timeout { get; init; } + + /// + /// The maximum amount of retries to make for this request. + /// + /// + /// null falls back to the underlying rest clients discretion. + /// + public int? MaxRetries { get; init; } + + /// + /// A set of flags to specify retrying behaviour. + /// + /// + /// null falls back to the underlying rest clients discretion. + /// + public RetryMode? RetryMode { get; init; } + + /// + /// Specifies whether to skip updating the cache after making a request. + /// + public bool SkipUpdatingCache { get; init; } + + /// + /// Specifies whether to skip asking cache for whether it already has the requested information. + /// + public bool SkipCache { get; init; } + + /// + /// An asynchronous callback to execute when the ratelimit fails. + /// + public Func? RatelimitCallback { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/BeginGuildPruneResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/BeginGuildPruneResponse.cs new file mode 100644 index 0000000000..e102e8c66f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/BeginGuildPruneResponse.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from POST /guilds/:guild-id/prune +/// +public readonly record struct BeginGuildPruneResponse +{ + /// + /// The amount of pruned members, or if compute_prune_count was set to + /// . + /// + public int? Pruned { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/BulkGuildBanResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/BulkGuildBanResponse.cs new file mode 100644 index 0000000000..cfcfdac27b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/BulkGuildBanResponse.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from POST /guilds/:guild-id/bulk-ban. +/// +public readonly record struct BulkGuildBanResponse +{ + /// + /// The snowflakes of users that were successfully banned. + /// + public required IReadOnlyList BannedUsers { get; init; } + + /// + /// The snowflakes of users that could not be banned. + /// + public required IReadOnlyList FailedUsers { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/CreateApplicationCommandResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/CreateApplicationCommandResponse.cs new file mode 100644 index 0000000000..50ee22a31f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/CreateApplicationCommandResponse.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents the information returned by POST /applications/:application-id/commands. +/// +public readonly record struct CreateApplicationCommandResponse +{ + /// + /// Indicates whether this command was newly created or whether it already existed. + /// + public required bool IsNewlyCreated { get; init; } + + /// + /// The created command. + /// + public required IApplicationCommand CreatedCommand { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/GetAnswerVotersResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/GetAnswerVotersResponse.cs new file mode 100644 index 0000000000..14e69994b3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/GetAnswerVotersResponse.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from GET /channels/:channel-id/polls/:message-id/answers/:answer-id. +/// +public readonly record struct GetAnswerVotersResponse +{ + /// + /// The users who voted for this answer. + /// + public IReadOnlyList Users { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/GetGuildPruneCountResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/GetGuildPruneCountResponse.cs new file mode 100644 index 0000000000..118c568f15 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/GetGuildPruneCountResponse.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from GET /guilds/:guild-id/prune +/// +public readonly record struct GetGuildPruneCountResponse +{ + /// + /// The amount of members that would be pruned in this operation. + /// + public int Pruned { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListActiveGuildThreadsResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListActiveGuildThreadsResponse.cs new file mode 100644 index 0000000000..9fe84b2c68 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListActiveGuildThreadsResponse.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from GET /guilds/:guild-id/threads/active. +/// +public readonly record struct ListActiveGuildThreadsResponse +{ + /// + /// The active threads in this guild, sorted by their ID in descending order. + /// + public required IReadOnlyList Threads { get; init; } + + /// + /// The thread member objects corresponding to the thread objects. + /// + public required IReadOnlyList Members { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListApplicationEmojisResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListApplicationEmojisResponse.cs new file mode 100644 index 0000000000..b6098439ed --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListApplicationEmojisResponse.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from GET /applications/:application-id/emojis. +/// +public readonly record struct ListApplicationEmojisResponse +{ + public IReadOnlyList Items { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListArchivedThreadsResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListArchivedThreadsResponse.cs new file mode 100644 index 0000000000..4c27bf44f4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListArchivedThreadsResponse.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents the response received from fetching archived threads. +/// +public readonly record struct ListArchivedThreadsResponse +{ + /// + /// The archived threads. + /// + public IReadOnlyList Threads { get; } + + /// + /// The thread member objects for each returned thread the current user has joined. + /// + public IReadOnlyList Members { get; } + + /// + /// Indicates whether there are potentially additional threads that could be returned on a subsequent call. + /// + public bool HasMore { get; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListGuildSoundboardSoundsResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListGuildSoundboardSoundsResponse.cs new file mode 100644 index 0000000000..3e58d45619 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListGuildSoundboardSoundsResponse.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from GET guilds/:guild-id/soundboard-sounds. +/// +public readonly record struct ListGuildSoundboardSoundsResponse +{ + /// + /// The soundboard sounds present in the provided guild. + /// + public IReadOnlyList Items { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListStickerPacksResponse.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListStickerPacksResponse.cs new file mode 100644 index 0000000000..d73636333f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/Responses/ListStickerPacksResponse.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Abstractions.Rest.Responses; + +/// +/// Represents a response from GET /sticker-packs. +/// +public readonly record struct ListStickerPacksResponse +{ + /// + /// The sticker packs returned by the call. + /// + public required IReadOnlyList StickerPacks { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Abstractions.Rest/RetryMode.cs b/src/core/DSharpPlus.Internal.Abstractions.Rest/RetryMode.cs new file mode 100644 index 0000000000..77c29b25a6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Abstractions.Rest/RetryMode.cs @@ -0,0 +1,70 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Internal.Abstractions.Rest; + +/// +/// Specifies behaviour flags to apply to retrying a request. +/// +[Flags] +public enum RetryMode +{ + /// + /// No options, behave as the rest client default. + /// + None = 0, + + /// + /// Suppresses executing a configured . + /// + SuppressCallback = 1 << 1, + + /// + /// If any error is encountered, excluding a pre-emptive ratelimit, fail immediately. + /// + AlwaysFailExcludingPreemptive = 1 << 2, + + /// + /// If any error is encountered, including a pre-emptive ratelimit, fail immediately. + /// + AlwaysFail = 1 << 3, + + /// + /// Retry on 5xx errors. + /// + Retry5xx = 1 << 4, + + /// + /// Skips retrying on preemptive ratelimits. + /// + DoNotRetryPreemptiveRatelimit = 1 << 5, + + /// + /// Retry on discord-returned ratelimits, except global ratelimits. + /// + RetryDiscordRatelimit = 1 << 6, + + /// + /// Retry on global ratelimits. + /// + RetryGlobalRatelimit = 1 << 7, + + /// + /// Skip request validation. This should be used only with the greatest care. + /// + SkipValidation = 1 << 8, + + /// + /// Convenience combination for always retrying on ratelimits and 5xx errors. + /// + AlwaysRetry = Retry5xx | RetryDiscordRatelimit | RetryGlobalRatelimit, + + /// + /// Convenience combination for always retrying on non-global ratelimits and 5xx errors, as global ratelimits + /// can take untenably long times to complete. + /// + AlwaysRetryNonGlobal = Retry5xx | RetryDiscordRatelimit +} diff --git a/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommand.cs b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommand.cs new file mode 100644 index 0000000000..c45e932373 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommand.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommand : IApplicationCommand +{ + /// + public required Snowflake Id { get; init; } + + /// + public Optional Type { get; init; } + + /// + public required Snowflake ApplicationId { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public required string Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public required string Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public DiscordPermissions? DefaultMemberPermissions { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public Optional> IntegrationTypes { get; init; } + + /// + public Optional?> Contexts { get; init; } + + /// + public required Snowflake Version { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandOption.cs b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandOption.cs new file mode 100644 index 0000000000..bff9107100 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandOption.cs @@ -0,0 +1,58 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommandOption : IApplicationCommandOption +{ + /// + public required DiscordApplicationCommandOptionType Type { get; init; } + + /// + public required string Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public required string Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } + + /// + public Optional Required { get; init; } + + /// + public Optional> Choices { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional> ChannelTypes { get; init; } + + /// + public Optional> MinValue { get; init; } + + /// + public Optional> MaxValue { get; init; } + + /// + public Optional MinLength { get; init; } + + /// + public Optional MaxLength { get; init; } + + /// + public Optional Autocomplete { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandOptionChoice.cs b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandOptionChoice.cs new file mode 100644 index 0000000000..a3a9747a4e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandOptionChoice.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommandOptionChoice : IApplicationCommandOptionChoice +{ + /// + public required string Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public required OneOf Value { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandPermission.cs b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandPermission.cs new file mode 100644 index 0000000000..003518e08d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandPermission.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommandPermission : IApplicationCommandPermission +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordApplicationCommandPermissionType Type { get; init; } + + /// + public required bool Permission { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandPermissions.cs b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandPermissions.cs new file mode 100644 index 0000000000..0066c2f948 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/ApplicationCommandPermissions.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommandPermissions : IApplicationCommandPermissions +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake ApplicationId { get; init; } + + /// + public required Snowflake GuildId { get; init; } + + /// + public required IReadOnlyList Permissions { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/ApplicationCommands/PartialApplicationCommandPermissions.cs b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/PartialApplicationCommandPermissions.cs new file mode 100644 index 0000000000..0c0bd49695 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ApplicationCommands/PartialApplicationCommandPermissions.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialApplicationCommandPermissions : IPartialApplicationCommandPermissions +{ + /// + public Optional Id { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional> Permissions { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Applications/ActivityInstance.cs b/src/core/DSharpPlus.Internal.Models/Applications/ActivityInstance.cs new file mode 100644 index 0000000000..ada41c8c15 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Applications/ActivityInstance.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ActivityInstance : IActivityInstance +{ + /// + public required Snowflake ApplicationId { get; init; } + + /// + public required string InstanceId { get; init; } + + /// + public required Snowflake LaunchId { get; init; } + + /// + public required IActivityLocation Location { get; init; } + + /// + public required IReadOnlyList Users { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Applications/ActivityLocation.cs b/src/core/DSharpPlus.Internal.Models/Applications/ActivityLocation.cs new file mode 100644 index 0000000000..4bb9bac18f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Applications/ActivityLocation.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ActivityLocation : IActivityLocation +{ + /// + public required string Id { get; init; } + + /// + public required string Kind { get; init; } + + /// + public required Snowflake ChannelId { get; init; } + + /// + public Optional GuildId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Applications/Application.cs b/src/core/DSharpPlus.Internal.Models/Applications/Application.cs new file mode 100644 index 0000000000..b099ba05f2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Applications/Application.cs @@ -0,0 +1,98 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Application : IApplication +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? Icon { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional> RpcOrigins { get; init; } + + /// + public required bool BotPublic { get; init; } + + /// + public required bool BotRequireCodeGrant { get; init; } + + /// + public Optional Bot { get; init; } + + /// + public Optional TermsOfServiceUrl { get; init; } + + /// + public Optional PrivacyPolicyUrl { get; init; } + + /// + public Optional Owner { get; init; } + + /// + public required string VerifyKey { get; init; } + + /// + public ITeam? Team { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Guild { get; init; } + + /// + public Optional PrimarySkuId { get; init; } + + /// + public Optional Slug { get; init; } + + /// + public Optional CoverImage { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional ApproximateGuildCount { get; init; } + + /// + public Optional> RedirectUris { get; init; } + + /// + public Optional InteractionsEndpointUrl { get; init; } + + /// + public Optional RoleConnectionsVerificationUrl { get; init; } + + /// + public Optional> Tags { get; init; } + + /// + public Optional InstallParams { get; init; } + + /// + public Optional> IntegrationTypesConfig { get; init; } + + /// + public Optional CustomInstallUrl { get; init; } + + /// + public Optional ApproximateUserInstallCount { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Applications/ApplicationIntegrationTypeConfiguration.cs b/src/core/DSharpPlus.Internal.Models/Applications/ApplicationIntegrationTypeConfiguration.cs new file mode 100644 index 0000000000..2e280addbf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Applications/ApplicationIntegrationTypeConfiguration.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationIntegrationTypeConfiguration : IApplicationIntegrationTypeConfiguration +{ + /// + public Optional Oauth2InstallParams { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Applications/InstallParameters.cs b/src/core/DSharpPlus.Internal.Models/Applications/InstallParameters.cs new file mode 100644 index 0000000000..93b028aaeb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Applications/InstallParameters.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record InstallParameters : IInstallParameters +{ + /// + public required IReadOnlyList Scopes { get; init; } + + /// + public required DiscordPermissions Permissions { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Applications/PartialApplication.cs b/src/core/DSharpPlus.Internal.Models/Applications/PartialApplication.cs new file mode 100644 index 0000000000..c1edbc339c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Applications/PartialApplication.cs @@ -0,0 +1,98 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialApplication : IPartialApplication +{ + /// + public Optional Id { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional> RpcOrigins { get; init; } + + /// + public Optional BotPublic { get; init; } + + /// + public Optional BotRequireCodeGrant { get; init; } + + /// + public Optional Bot { get; init; } + + /// + public Optional TermsOfServiceUrl { get; init; } + + /// + public Optional PrivacyPolicyUrl { get; init; } + + /// + public Optional Owner { get; init; } + + /// + public Optional VerifyKey { get; init; } + + /// + public Optional Team { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Guild { get; init; } + + /// + public Optional PrimarySkuId { get; init; } + + /// + public Optional Slug { get; init; } + + /// + public Optional CoverImage { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional ApproximateGuildCount { get; init; } + + /// + public Optional> RedirectUris { get; init; } + + /// + public Optional InteractionsEndpointUrl { get; init; } + + /// + public Optional RoleConnectionsVerificationUrl { get; init; } + + /// + public Optional> Tags { get; init; } + + /// + public Optional InstallParams { get; init; } + + /// + public Optional> IntegrationTypesConfig { get; init; } + + /// + public Optional CustomInstallUrl { get; init; } + + /// + public Optional ApproximateUserInstallCount { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLog.cs b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLog.cs new file mode 100644 index 0000000000..7dd525dd57 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLog.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AuditLog : IAuditLog +{ + /// + public required IReadOnlyList ApplicationCommands { get; init; } + + /// + public required IReadOnlyList AuditLogEntries { get; init; } + + /// + public required IReadOnlyList AutoModerationRules { get; init; } + + /// + public required IReadOnlyList GuildScheduledEvents { get; init; } + + /// + public required IReadOnlyList Integrations { get; init; } + + /// + public required IReadOnlyList Threads { get; init; } + + /// + public required IReadOnlyList Users { get; init; } + + /// + public required IReadOnlyList Webhooks { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogChange.cs b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogChange.cs new file mode 100644 index 0000000000..851289f358 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogChange.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AuditLogChange : IAuditLogChange +{ + /// + public Optional NewValue { get; init; } + + /// + public Optional OldValue { get; init; } + + /// + public required string Key { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogEntry.cs b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogEntry.cs new file mode 100644 index 0000000000..1fa0fd9d5e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogEntry.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AuditLogEntry : IAuditLogEntry +{ + /// + public OneOf? TargetId { get; init; } + + /// + public Optional> Changes { get; init; } + + /// + public Snowflake? UserId { get; init; } + + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordAuditLogEvent ActionType { get; init; } + + /// + public Optional Options { get; init; } + + /// + public Optional Reason { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogEntryInfo.cs b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogEntryInfo.cs new file mode 100644 index 0000000000..0eccde3cd8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AuditLogs/AuditLogEntryInfo.cs @@ -0,0 +1,44 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AuditLogEntryInfo : IAuditLogEntryInfo +{ + /// + public Optional ApplicationId { get; init; } + + /// + public Optional AutoModerationRuleName { get; init; } + + /// + public Optional AutoModerationRuleTriggerType { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional Count { get; init; } + + /// + public Optional DeleteMemberDays { get; init; } + + /// + public Optional Id { get; init; } + + /// + public Optional MembersRemoved { get; init; } + + /// + public Optional MessageId { get; init; } + + /// + public Optional RoleName { get; init; } + + /// + public Optional Type { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationAction.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationAction.cs new file mode 100644 index 0000000000..81cd5b58c0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationAction.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AutoModerationAction : IAutoModerationAction +{ + /// + public required DiscordAutoModerationActionType Type { get; init; } + + /// + public Optional Metadata { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationActionMetadata.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationActionMetadata.cs new file mode 100644 index 0000000000..cd977e68c4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationActionMetadata.cs @@ -0,0 +1,12 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +/// Placeholder implementation of a marker interface. Please report spotting this to library developers. +/// +internal sealed record AutoModerationActionMetadata : IAutoModerationActionMetadata; diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationRule.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationRule.cs new file mode 100644 index 0000000000..e35f12a6cb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationRule.cs @@ -0,0 +1,47 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AutoModerationRule : IAutoModerationRule +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake GuildId { get; init; } + + /// + public required string Name { get; init; } + + /// + public required Snowflake CreatorId { get; init; } + + /// + public required DiscordAutoModerationEventType EventType { get; init; } + + /// + public required DiscordAutoModerationTriggerType TriggerType { get; init; } + + /// + public required IAutoModerationTriggerMetadata TriggerMetadata { get; init; } + + /// + public required IReadOnlyList Actions { get; init; } + + /// + public required bool Enabled { get; init; } + + /// + public required IReadOnlyList ExemptRoles { get; init; } + + /// + public required IReadOnlyList ExemptChannels { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationTriggerMetadata.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationTriggerMetadata.cs new file mode 100644 index 0000000000..fedb2b02be --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/AutoModerationTriggerMetadata.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AutoModerationTriggerMetadata : IAutoModerationTriggerMetadata +{ + /// + public Optional> KeywordFilter { get; init; } + + /// + public Optional> RegexPatterns { get; init; } + + /// + public Optional> Presets { get; init; } + + /// + public Optional> AllowList { get; init; } + + /// + public Optional MentionTotalLimit { get; init; } + + /// + public Optional MentionRaidProtectionEnabled { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/BlockMessageActionMetadata.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/BlockMessageActionMetadata.cs new file mode 100644 index 0000000000..6f7b3e4bd7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/BlockMessageActionMetadata.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record BlockMessageActionMetadata : IBlockMessageActionMetadata +{ + /// + public Optional CustomMessage { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/PartialAutoModerationRule.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/PartialAutoModerationRule.cs new file mode 100644 index 0000000000..774b8b65f9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/PartialAutoModerationRule.cs @@ -0,0 +1,47 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialAutoModerationRule : IPartialAutoModerationRule +{ + /// + public Optional Id { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional CreatorId { get; init; } + + /// + public Optional EventType { get; init; } + + /// + public Optional TriggerType { get; init; } + + /// + public Optional TriggerMetadata { get; init; } + + /// + public Optional> Actions { get; init; } + + /// + public Optional Enabled { get; init; } + + /// + public Optional> ExemptRoles { get; init; } + + /// + public Optional> ExemptChannels { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/SendAlertMessageActionMetadata.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/SendAlertMessageActionMetadata.cs new file mode 100644 index 0000000000..d13868210c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/SendAlertMessageActionMetadata.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record SendAlertMessageActionMetadata : ISendAlertMessageActionMetadata +{ + /// + public required Snowflake ChannelId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/AutoModeration/TimeoutActionMetadata.cs b/src/core/DSharpPlus.Internal.Models/AutoModeration/TimeoutActionMetadata.cs new file mode 100644 index 0000000000..19be154272 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/AutoModeration/TimeoutActionMetadata.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record TimeoutActionMetadata : ITimeoutActionMetadata +{ + /// + public required int DurationSeconds { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Channels/Channel.cs b/src/core/DSharpPlus.Internal.Models/Channels/Channel.cs new file mode 100644 index 0000000000..d561f36344 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/Channel.cs @@ -0,0 +1,120 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Channel : IChannel +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordChannelType Type { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional> PermissionOverwrites { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Topic { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public Optional LastMessageId { get; init; } + + /// + public Optional Bitrate { get; init; } + + /// + public Optional UserLimit { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public Optional> Recipients { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional OwnerId { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional Managed { get; init; } + + /// + public Optional ParentId { get; init; } + + /// + public Optional LastPinTimestamp { get; init; } + + /// + public Optional RtcRegion { get; init; } + + /// + public Optional VideoQualityMode { get; init; } + + /// + public Optional MessageCount { get; init; } + + /// + public Optional MemberCount { get; init; } + + /// + public Optional ThreadMetadata { get; init; } + + /// + public Optional Member { get; init; } + + /// + public Optional DefaultAutoArchiveDuration { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional TotalMessageSent { get; init; } + + /// + public Optional> AvailableTags { get; init; } + + /// + public Optional> AppliedTags { get; init; } + + /// + public Optional DefaultReactionEmoji { get; init; } + + /// + public Optional DefaultThreadRateLimitPerUser { get; init; } + + /// + public Optional DefaultSortOrder { get; init; } + + /// + public Optional DefaultForumLayout { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Channels/ChannelOverwrite.cs b/src/core/DSharpPlus.Internal.Models/Channels/ChannelOverwrite.cs new file mode 100644 index 0000000000..148266e5b7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/ChannelOverwrite.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ChannelOverwrite : IChannelOverwrite +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordChannelOverwriteType Type { get; init; } + + /// + public required DiscordPermissions Allow { get; init; } + + /// + public required DiscordPermissions Deny { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Channels/DefaultReaction.cs b/src/core/DSharpPlus.Internal.Models/Channels/DefaultReaction.cs new file mode 100644 index 0000000000..3bd156f0c0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/DefaultReaction.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record DefaultReaction : IDefaultReaction +{ + /// + public Snowflake? EmojiId { get; init; } + + /// + public string? EmojiName { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Channels/FollowedChannel.cs b/src/core/DSharpPlus.Internal.Models/Channels/FollowedChannel.cs new file mode 100644 index 0000000000..d6748dc27d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/FollowedChannel.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record FollowedChannel : IFollowedChannel +{ + /// + public required Snowflake ChannelId { get; init; } + + /// + public required Snowflake WebhookId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Channels/ForumTag.cs b/src/core/DSharpPlus.Internal.Models/Channels/ForumTag.cs new file mode 100644 index 0000000000..f093919ab6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/ForumTag.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ForumTag : IForumTag +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required bool Moderated { get; init; } + + /// + public Snowflake? EmojiId { get; init; } + + /// + public string? EmojiName { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Channels/PartialChannel.cs b/src/core/DSharpPlus.Internal.Models/Channels/PartialChannel.cs new file mode 100644 index 0000000000..041c876fa3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/PartialChannel.cs @@ -0,0 +1,120 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialChannel : IPartialChannel +{ + /// + public Optional Id { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional> PermissionOverwrites { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Topic { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public Optional LastMessageId { get; init; } + + /// + public Optional Bitrate { get; init; } + + /// + public Optional UserLimit { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public Optional> Recipients { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional OwnerId { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional Managed { get; init; } + + /// + public Optional ParentId { get; init; } + + /// + public Optional LastPinTimestamp { get; init; } + + /// + public Optional RtcRegion { get; init; } + + /// + public Optional VideoQualityMode { get; init; } + + /// + public Optional MessageCount { get; init; } + + /// + public Optional MemberCount { get; init; } + + /// + public Optional ThreadMetadata { get; init; } + + /// + public Optional Member { get; init; } + + /// + public Optional DefaultAutoArchiveDuration { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional TotalMessageSent { get; init; } + + /// + public Optional> AvailableTags { get; init; } + + /// + public Optional> AppliedTags { get; init; } + + /// + public Optional DefaultReactionEmoji { get; init; } + + /// + public Optional DefaultThreadRateLimitPerUser { get; init; } + + /// + public Optional DefaultSortOrder { get; init; } + + /// + public Optional DefaultForumLayout { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Channels/PartialChannelOverwrite.cs b/src/core/DSharpPlus.Internal.Models/Channels/PartialChannelOverwrite.cs new file mode 100644 index 0000000000..24bee8573d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/PartialChannelOverwrite.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialChannelOverwrite : IPartialChannelOverwrite +{ + /// + public Optional Id { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Allow { get; init; } + + /// + public Optional Deny { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Channels/ThreadMember.cs b/src/core/DSharpPlus.Internal.Models/Channels/ThreadMember.cs new file mode 100644 index 0000000000..fabc2a036d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/ThreadMember.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ThreadMember : IThreadMember +{ + /// + public Optional Id { get; init; } + + /// + public Optional UserId { get; init; } + + /// + public required DateTimeOffset JoinTimestamp { get; init; } + + /// + public required int Flags { get; init; } + + /// + public Optional Member { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Channels/ThreadMetadata.cs b/src/core/DSharpPlus.Internal.Models/Channels/ThreadMetadata.cs new file mode 100644 index 0000000000..47327b3197 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Channels/ThreadMetadata.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ThreadMetadata : IThreadMetadata +{ + /// + public required bool Archived { get; init; } + + /// + public required int AutoArchiveDuration { get; init; } + + /// + public required DateTimeOffset ArchiveTimestamp { get; init; } + + /// + public required bool Locked { get; init; } + + /// + public Optional Invitable { get; init; } + + /// + public Optional CreateTimestamp { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Components/ActionRowComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/ActionRowComponent.cs new file mode 100644 index 0000000000..89ac6c62d1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/ActionRowComponent.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ActionRowComponent : IActionRowComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required IReadOnlyList Components { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/ButtonComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/ButtonComponent.cs new file mode 100644 index 0000000000..08aedeb440 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/ButtonComponent.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ButtonComponent : IButtonComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required DiscordButtonStyle Style { get; init; } + + /// + public Optional Label { get; init; } + + /// + public Optional Emoji { get; init; } + + /// + public Optional CustomId { get; init; } + + /// + public Optional Url { get; init; } + + /// + public Optional Disabled { get; init; } + + /// + public Optional SkuId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/ChannelSelectComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/ChannelSelectComponent.cs new file mode 100644 index 0000000000..7f6c48e5fd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/ChannelSelectComponent.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ChannelSelectComponent : IChannelSelectComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public required IReadOnlyList ChannelTypes { get; init; } + + /// + public Optional Placeholder { get; init; } + + /// + public Optional> DefaultValues { get; init; } + + /// + public Optional MinValues { get; init; } + + /// + public Optional MaxValues { get; init; } + + /// + public Optional Disabled { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/Component.cs b/src/core/DSharpPlus.Internal.Models/Components/Component.cs new file mode 100644 index 0000000000..2b0979c066 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/Component.cs @@ -0,0 +1,12 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +/// Placeholder implementation of a marker interface. Please report spotting this to library developers. +/// +internal sealed record Component : IComponent; \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/ContainerComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/ContainerComponent.cs new file mode 100644 index 0000000000..716677e8d7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/ContainerComponent.cs @@ -0,0 +1,29 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ContainerComponent : IContainerComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required IReadOnlyList Components { get; init; } + + /// + public Optional AccentColor { get; init; } + + /// + public Optional Spoiler { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/DefaultSelectValue.cs b/src/core/DSharpPlus.Internal.Models/Components/DefaultSelectValue.cs new file mode 100644 index 0000000000..80a96fa1ad --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/DefaultSelectValue.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record DefaultSelectValue : IDefaultSelectValue +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Type { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/FileComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/FileComponent.cs new file mode 100644 index 0000000000..a0bfc44922 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/FileComponent.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record FileComponent : IFileComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required IUnfurledMediaItem File { get; init; } + + /// + public Optional Spoiler { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/MediaGalleryComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/MediaGalleryComponent.cs new file mode 100644 index 0000000000..9dd206628e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/MediaGalleryComponent.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MediaGalleryComponent : IMediaGalleryComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required IReadOnlyList Items { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/MediaGalleryItem.cs b/src/core/DSharpPlus.Internal.Models/Components/MediaGalleryItem.cs new file mode 100644 index 0000000000..b447dc9f56 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/MediaGalleryItem.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MediaGalleryItem : IMediaGalleryItem +{ + /// + public required IUnfurledMediaItem Media { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Spoiler { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/MentionableSelectComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/MentionableSelectComponent.cs new file mode 100644 index 0000000000..c0a55a356a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/MentionableSelectComponent.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MentionableSelectComponent : IMentionableSelectComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public Optional Placeholder { get; init; } + + /// + public Optional> DefaultValues { get; init; } + + /// + public Optional MinValues { get; init; } + + /// + public Optional MaxValues { get; init; } + + /// + public Optional Disabled { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/RoleSelectComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/RoleSelectComponent.cs new file mode 100644 index 0000000000..6927670b42 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/RoleSelectComponent.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record RoleSelectComponent : IRoleSelectComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public Optional Placeholder { get; init; } + + /// + public Optional> DefaultValues { get; init; } + + /// + public Optional MinValues { get; init; } + + /// + public Optional MaxValues { get; init; } + + /// + public Optional Disabled { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/SectionComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/SectionComponent.cs new file mode 100644 index 0000000000..adf699e900 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/SectionComponent.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record SectionComponent : ISectionComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required IReadOnlyList Components { get; init; } + + /// + public required IComponent Accessory { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/SelectOption.cs b/src/core/DSharpPlus.Internal.Models/Components/SelectOption.cs new file mode 100644 index 0000000000..344ea8fe03 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/SelectOption.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record SelectOption : ISelectOption +{ + /// + public required string Label { get; init; } + + /// + public required string Value { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Emoji { get; init; } + + /// + public Optional Default { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/SeparatorComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/SeparatorComponent.cs new file mode 100644 index 0000000000..dd6feb2e42 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/SeparatorComponent.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record SeparatorComponent : ISeparatorComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public Optional Divider { get; init; } + + /// + public Optional Spacing { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/StringSelectComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/StringSelectComponent.cs new file mode 100644 index 0000000000..2c288de0de --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/StringSelectComponent.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record StringSelectComponent : IStringSelectComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public required IReadOnlyList Options { get; init; } + + /// + public Optional Placeholder { get; init; } + + /// + public Optional MinValues { get; init; } + + /// + public Optional MaxValues { get; init; } + + /// + public Optional Disabled { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/TextDisplayComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/TextDisplayComponent.cs new file mode 100644 index 0000000000..2dc60a511a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/TextDisplayComponent.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record TextDisplayComponent : ITextDisplayComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string Content { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/TextInputComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/TextInputComponent.cs new file mode 100644 index 0000000000..474a70989e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/TextInputComponent.cs @@ -0,0 +1,42 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record TextInputComponent : ITextInputComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public required DiscordTextInputStyle Style { get; init; } + + /// + public required string Label { get; init; } + + /// + public Optional MinLength { get; init; } + + /// + public Optional MaxLength { get; init; } + + /// + public Optional Required { get; init; } + + /// + public Optional Value { get; init; } + + /// + public Optional Placeholder { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/ThumbnailComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/ThumbnailComponent.cs new file mode 100644 index 0000000000..ff49e9460b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/ThumbnailComponent.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ThumbnailComponent : IThumbnailComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required IUnfurledMediaItem Media { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Spoiler { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/UnfurledMediaItem.cs b/src/core/DSharpPlus.Internal.Models/Components/UnfurledMediaItem.cs new file mode 100644 index 0000000000..39a2e7a407 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/UnfurledMediaItem.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record UnfurledMediaItem : IUnfurledMediaItem +{ + /// + public required string Url { get; init; } + + /// + public Optional ProxyUrl { get; init; } + + /// + public Optional Height { get; init; } + + /// + public Optional Width { get; init; } + + /// + public Optional ContentType { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/UnknownComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/UnknownComponent.cs new file mode 100644 index 0000000000..497307fb4e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/UnknownComponent.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record UnknownComponent : IUnknownComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string RawPayload { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Components/UserSelectComponent.cs b/src/core/DSharpPlus.Internal.Models/Components/UserSelectComponent.cs new file mode 100644 index 0000000000..4be6c44c3d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Components/UserSelectComponent.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record UserSelectComponent : IUserSelectComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public Optional Id { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public Optional Placeholder { get; init; } + + /// + public Optional> DefaultValues { get; init; } + + /// + public Optional MinValues { get; init; } + + /// + public Optional MaxValues { get; init; } + + /// + public Optional Disabled { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/DSharpPlus.Internal.Models.csproj b/src/core/DSharpPlus.Internal.Models/DSharpPlus.Internal.Models.csproj new file mode 100644 index 0000000000..22c7b1fbd1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/DSharpPlus.Internal.Models.csproj @@ -0,0 +1,19 @@ + + + + $(_DSharpPlusInternalModelsVersion) + $(Description) This package implements the serialization models mirroring the Discord API. + Library + + $(NoWarn);CA1812;IDE0005 + + + + + + + + + + + diff --git a/src/core/DSharpPlus.Internal.Models/Emojis/Emoji.cs b/src/core/DSharpPlus.Internal.Models/Emojis/Emoji.cs new file mode 100644 index 0000000000..bb0542d2f8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Emojis/Emoji.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Emoji : IEmoji +{ + /// + public Snowflake? Id { get; init; } + + /// + public string? Name { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional User { get; init; } + + /// + public Optional RequireColons { get; init; } + + /// + public Optional Managed { get; init; } + + /// + public Optional Animated { get; init; } + + /// + public Optional Available { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Emojis/PartialEmoji.cs b/src/core/DSharpPlus.Internal.Models/Emojis/PartialEmoji.cs new file mode 100644 index 0000000000..4fb192ce1f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Emojis/PartialEmoji.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialEmoji : IPartialEmoji +{ + /// + public Optional Id { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional User { get; init; } + + /// + public Optional RequireColons { get; init; } + + /// + public Optional Managed { get; init; } + + /// + public Optional Animated { get; init; } + + /// + public Optional Available { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Entitlements/Entitlement.cs b/src/core/DSharpPlus.Internal.Models/Entitlements/Entitlement.cs new file mode 100644 index 0000000000..878eab1b3e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Entitlements/Entitlement.cs @@ -0,0 +1,44 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Entitlement : IEntitlement +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake SkuId { get; init; } + + /// + public Optional UserId { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public required Snowflake ApplicationId { get; init; } + + /// + public required DiscordEntitlementType Type { get; init; } + + /// + public required bool Deleted { get; init; } + + /// + public DateTimeOffset? StartsAt { get; init; } + + /// + public DateTimeOffset? EndsAt { get; init; } + + /// + public Optional Consumed { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Entitlements/PartialEntitlement.cs b/src/core/DSharpPlus.Internal.Models/Entitlements/PartialEntitlement.cs new file mode 100644 index 0000000000..9629d58219 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Entitlements/PartialEntitlement.cs @@ -0,0 +1,44 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialEntitlement : IPartialEntitlement +{ + /// + public required Snowflake Id { get; init; } + + /// + public Optional SkuId { get; init; } + + /// + public Optional UserId { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Deleted { get; init; } + + /// + public Optional StartsAt { get; init; } + + /// + public Optional EndsAt { get; init; } + + /// + public Optional Consumed { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.Registration.cs b/src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.Registration.cs new file mode 100644 index 0000000000..3eea272d8a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.Registration.cs @@ -0,0 +1,175 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Models; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Internal.Models.Extensions; + +partial class ServiceCollectionExtensions +{ + private static void RegisterSerialization(IServiceCollection services) + { + services.Configure + ( + options => + { + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + } + ); + } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.cs b/src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..f27d9574da --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using System.Text.Json; + +using DSharpPlus.Internal.Models.Serialization.Converters; +using DSharpPlus.Internal.Models.Serialization.Resolvers; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Internal.Models.Extensions; + +/// +/// Provides extensions on IServiceCollection to register our JSON serialization of Discord models. +/// +public static partial class ServiceCollectionExtensions +{ + /// + /// Registers converters for Discord's API models. + /// + /// The service collection to register into. + /// The name under which the serialization options should be accessible. + /// The same service collection for chaining. + public static IServiceCollection RegisterDiscordModelSerialization + ( + this IServiceCollection services, + string? name = "dsharpplus" + ) + { + services.Configure + ( + name, + options => + { + options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + + options.Converters.Add(new OptionalConverterFactory()); + options.Converters.Add(new SnowflakeConverter()); + options.Converters.Add(new OneOfConverterFactory()); + options.Converters.Add(new ImageDataConverter()); + + options.Converters.Add(new AuditLogChangeConverter()); + options.Converters.Add(new AutoModerationActionConverter()); + options.Converters.Add(new DiscordPermissionConverter()); + options.Converters.Add(new ComponentConverter()); + options.Converters.Add(new ApplicationIntegrationTypeKeyConverter()); + + options.TypeInfoResolverChain.Add(OptionalTypeInfoResolver.Default); + options.TypeInfoResolverChain.Add(NullBooleanTypeInfoResolver.Default); + // this needs to be below OptionalTypeInfoResolver so as to avoid the former overwriting this + options.TypeInfoResolverChain.Add(AttachmentDataTypeInfoResolver.Default); + } + ); + + RegisterSerialization(services); + + return services; + } +} diff --git a/src/core/DSharpPlus.Internal.Models/GuildTemplates/Template.cs b/src/core/DSharpPlus.Internal.Models/GuildTemplates/Template.cs new file mode 100644 index 0000000000..95c3c4732c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/GuildTemplates/Template.cs @@ -0,0 +1,46 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Template : ITemplate +{ + /// + public required string Code { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? Description { get; init; } + + /// + public required int UsageCount { get; init; } + + /// + public required Snowflake CreatorId { get; init; } + + /// + public required IUser Creator { get; init; } + + /// + public required DateTimeOffset CreatedAt { get; init; } + + /// + public required DateTimeOffset UpdatedAt { get; init; } + + /// + public required Snowflake SourceGuildId { get; init; } + + /// + public required IPartialGuild SerializedSourceGuild { get; init; } + + /// + public bool? IsDirty { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/Ban.cs b/src/core/DSharpPlus.Internal.Models/Guilds/Ban.cs new file mode 100644 index 0000000000..cd4694c540 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/Ban.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Ban : IBan +{ + /// + public string? Reason { get; init; } + + /// + public required IUser User { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/Guild.cs b/src/core/DSharpPlus.Internal.Models/Guilds/Guild.cs new file mode 100644 index 0000000000..68eaab0533 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/Guild.cs @@ -0,0 +1,143 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Guild : IGuild +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? Icon { get; init; } + + /// + public Optional IconHash { get; init; } + + /// + public string? Splash { get; init; } + + /// + public string? DiscoverySplash { get; init; } + + /// + public Optional Owner { get; init; } + + /// + public required Snowflake OwnerId { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Snowflake? AfkChannelId { get; init; } + + /// + public required int AfkTimeout { get; init; } + + /// + public Optional WidgetEnabled { get; init; } + + /// + public Optional WidgetChannelId { get; init; } + + /// + public required DiscordVerificationLevel VerificationLevel { get; init; } + + /// + public required DiscordMessageNotificationLevel DefaultMessageNotifications { get; init; } + + /// + public required DiscordExplicitContentFilterLevel ExplicitContentFilter { get; init; } + + /// + public required IReadOnlyList Roles { get; init; } + + /// + public required IReadOnlyList Emojis { get; init; } + + /// + public required IReadOnlyList Features { get; init; } + + /// + public required DiscordMfaLevel MfaLevel { get; init; } + + /// + public Snowflake? ApplicationId { get; init; } + + /// + public Snowflake? SystemChannelId { get; init; } + + /// + public required DiscordSystemChannelFlags SystemChannelFlags { get; init; } + + /// + public Snowflake? RulesChannelId { get; init; } + + /// + public Optional MaxPresences { get; init; } + + /// + public Optional MaxMembers { get; init; } + + /// + public string? VanityUrlCode { get; init; } + + /// + public string? Description { get; init; } + + /// + public string? Banner { get; init; } + + /// + public required int PremiumTier { get; init; } + + /// + public Optional PremiumSubscriptionCount { get; init; } + + /// + public required string PreferredLocale { get; init; } + + /// + public Snowflake? PublicUpdatesChannelId { get; init; } + + /// + public Optional MaxVideoChannelUsers { get; init; } + + /// + public Optional MaxStageVideoChannelUsers { get; init; } + + /// + public Optional ApproximateMemberCount { get; init; } + + /// + public Optional ApproximatePresenceCount { get; init; } + + /// + public Optional WelcomeScreen { get; init; } + + /// + public required DiscordNsfwLevel NsfwLevel { get; init; } + + /// + public Optional> Stickers { get; init; } + + /// + public required bool PremiumProgressBarEnabled { get; init; } + + /// + public Snowflake? SafetyAlertsChannelId { get; init; } + + /// + public IIncidentsData? IncidentsData { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/GuildMember.cs b/src/core/DSharpPlus.Internal.Models/Guilds/GuildMember.cs new file mode 100644 index 0000000000..476820816e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/GuildMember.cs @@ -0,0 +1,57 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record GuildMember : IGuildMember +{ + /// + public Optional User { get; init; } + + /// + public Optional Nick { get; init; } + + /// + public Optional Avatar { get; init; } + + /// + public Optional Banner { get; init; } + + /// + public required IReadOnlyList Roles { get; init; } + + /// + public required DateTimeOffset JoinedAt { get; init; } + + /// + public Optional PremiumSince { get; init; } + + /// + public required bool Deaf { get; init; } + + /// + public required bool Mute { get; init; } + + /// + public required DiscordGuildMemberFlags Flags { get; init; } + + /// + public Optional Pending { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional CommunicationDisabledUntil { get; init; } + + /// + public Optional AvatarDecorationData { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/GuildPreview.cs b/src/core/DSharpPlus.Internal.Models/Guilds/GuildPreview.cs new file mode 100644 index 0000000000..f6e8226d16 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/GuildPreview.cs @@ -0,0 +1,46 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record GuildPreview : IGuildPreview +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? Icon { get; init; } + + /// + public string? Splash { get; init; } + + /// + public string? DiscoverySplash { get; init; } + + /// + public required IReadOnlyList Emojis { get; init; } + + /// + public required IReadOnlyList Features { get; init; } + + /// + public required int ApproximateMemberCount { get; init; } + + /// + public required int ApproximatePresenceCount { get; init; } + + /// + public string? Description { get; init; } + + /// + public required IReadOnlyList Stickers { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/GuildWidget.cs b/src/core/DSharpPlus.Internal.Models/Guilds/GuildWidget.cs new file mode 100644 index 0000000000..42aeafbfe4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/GuildWidget.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record GuildWidget : IGuildWidget +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? InstantInvite { get; init; } + + /// + public required IReadOnlyList Channels { get; init; } + + /// + public required IReadOnlyList Members { get; init; } + + /// + public required int PresenceCount { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/GuildWidgetSettings.cs b/src/core/DSharpPlus.Internal.Models/Guilds/GuildWidgetSettings.cs new file mode 100644 index 0000000000..ba4ba8f80f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/GuildWidgetSettings.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record GuildWidgetSettings : IGuildWidgetSettings +{ + /// + public required bool Enabled { get; init; } + + /// + public Snowflake? ChannelId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/IncidentsData.cs b/src/core/DSharpPlus.Internal.Models/Guilds/IncidentsData.cs new file mode 100644 index 0000000000..bd0706c647 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/IncidentsData.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record IncidentsData : IIncidentsData +{ + /// + public DateTimeOffset? InvitesDisabledUntil { get; init; } + + /// + public DateTimeOffset? DmsDisabledUntil { get; init; } + + /// + public Optional DmSpamDetectedAt { get; init; } + + /// + public Optional RaidDetectedAt { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/Integration.cs b/src/core/DSharpPlus.Internal.Models/Guilds/Integration.cs new file mode 100644 index 0000000000..476db799dd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/Integration.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Integration : IIntegration +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required string Type { get; init; } + + /// + public required bool Enabled { get; init; } + + /// + public Optional Syncing { get; init; } + + /// + public Optional RoleId { get; init; } + + /// + public Optional EnableEmoticons { get; init; } + + /// + public Optional ExpireBehaviour { get; init; } + + /// + public Optional ExpireGracePeriod { get; init; } + + /// + public Optional User { get; init; } + + /// + public required IIntegrationAccount Account { get; init; } + + /// + public Optional SyncedAt { get; init; } + + /// + public Optional SubscriberCount { get; init; } + + /// + public Optional Revoked { get; init; } + + /// + public Optional Application { get; init; } + + /// + public Optional> Scopes { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/IntegrationAccount.cs b/src/core/DSharpPlus.Internal.Models/Guilds/IntegrationAccount.cs new file mode 100644 index 0000000000..e229d5eafb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/IntegrationAccount.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record IntegrationAccount : IIntegrationAccount +{ + /// + public required string Id { get; init; } + + /// + public required string Name { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/IntegrationApplication.cs b/src/core/DSharpPlus.Internal.Models/Guilds/IntegrationApplication.cs new file mode 100644 index 0000000000..c9b3d7bd6e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/IntegrationApplication.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record IntegrationApplication : IIntegrationApplication +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? Icon { get; init; } + + /// + public required string Description { get; init; } + + /// + public Optional Bot { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/Onboarding.cs b/src/core/DSharpPlus.Internal.Models/Guilds/Onboarding.cs new file mode 100644 index 0000000000..03ac3cea95 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/Onboarding.cs @@ -0,0 +1,29 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Onboarding : IOnboarding +{ + /// + public required Snowflake GuildId { get; init; } + + /// + public required IReadOnlyList Prompts { get; init; } + + /// + public required IReadOnlyList DefaultChannelIds { get; init; } + + /// + public required bool Enabled { get; init; } + + /// + public required DiscordGuildOnboardingPromptType Mode { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/OnboardingPrompt.cs b/src/core/DSharpPlus.Internal.Models/Guilds/OnboardingPrompt.cs new file mode 100644 index 0000000000..7124867ce4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/OnboardingPrompt.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record OnboardingPrompt : IOnboardingPrompt +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordGuildOnboardingPromptType Type { get; init; } + + /// + public required IReadOnlyList Options { get; init; } + + /// + public required string Title { get; init; } + + /// + public required bool SingleSelect { get; init; } + + /// + public required bool Required { get; init; } + + /// + public required bool InOnboarding { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/OnboardingPromptOption.cs b/src/core/DSharpPlus.Internal.Models/Guilds/OnboardingPromptOption.cs new file mode 100644 index 0000000000..dcb178d0bf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/OnboardingPromptOption.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record OnboardingPromptOption : IOnboardingPromptOption +{ + /// + public required Snowflake Id { get; init; } + + /// + public required IReadOnlyList ChannelIds { get; init; } + + /// + public required IReadOnlyList RoleIds { get; init; } + + /// + public Optional Emoji { get; init; } + + /// + public Optional EmojiId { get; init; } + + /// + public Optional EmojiName { get; init; } + + /// + public Optional EmojiAnimated { get; init; } + + /// + public required string Title { get; init; } + + /// + public string? Description { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/PartialGuild.cs b/src/core/DSharpPlus.Internal.Models/Guilds/PartialGuild.cs new file mode 100644 index 0000000000..e60323468b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/PartialGuild.cs @@ -0,0 +1,143 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialGuild : IPartialGuild +{ + /// + public Optional Id { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional IconHash { get; init; } + + /// + public Optional Splash { get; init; } + + /// + public Optional DiscoverySplash { get; init; } + + /// + public Optional Owner { get; init; } + + /// + public Optional OwnerId { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional AfkChannelId { get; init; } + + /// + public Optional AfkTimeout { get; init; } + + /// + public Optional WidgetEnabled { get; init; } + + /// + public Optional WidgetChannelId { get; init; } + + /// + public Optional VerificationLevel { get; init; } + + /// + public Optional DefaultMessageNotifications { get; init; } + + /// + public Optional ExplicitContentFilter { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional> Emojis { get; init; } + + /// + public Optional> Features { get; init; } + + /// + public Optional MfaLevel { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional SystemChannelId { get; init; } + + /// + public Optional SystemChannelFlags { get; init; } + + /// + public Optional RulesChannelId { get; init; } + + /// + public Optional MaxPresences { get; init; } + + /// + public Optional MaxMembers { get; init; } + + /// + public Optional VanityUrlCode { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Banner { get; init; } + + /// + public Optional PremiumTier { get; init; } + + /// + public Optional PremiumSubscriptionCount { get; init; } + + /// + public Optional PreferredLocale { get; init; } + + /// + public Optional PublicUpdatesChannelId { get; init; } + + /// + public Optional MaxVideoChannelUsers { get; init; } + + /// + public Optional MaxStageVideoChannelUsers { get; init; } + + /// + public Optional ApproximateMemberCount { get; init; } + + /// + public Optional ApproximatePresenceCount { get; init; } + + /// + public Optional WelcomeScreen { get; init; } + + /// + public Optional NsfwLevel { get; init; } + + /// + public Optional> Stickers { get; init; } + + /// + public Optional PremiumProgressBarEnabled { get; init; } + + /// + public Optional SafetyAlertsChannelId { get; init; } + + /// + public Optional IncidentsData { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/PartialGuildMember.cs b/src/core/DSharpPlus.Internal.Models/Guilds/PartialGuildMember.cs new file mode 100644 index 0000000000..e69a3b3338 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/PartialGuildMember.cs @@ -0,0 +1,57 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialGuildMember : IPartialGuildMember +{ + /// + public Optional User { get; init; } + + /// + public Optional Nick { get; init; } + + /// + public Optional Avatar { get; init; } + + /// + public Optional Banner { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional JoinedAt { get; init; } + + /// + public Optional PremiumSince { get; init; } + + /// + public Optional Deaf { get; init; } + + /// + public Optional Mute { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional Pending { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional CommunicationDisabledUntil { get; init; } + + /// + public Optional AvatarDecorationData { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/PartialIntegration.cs b/src/core/DSharpPlus.Internal.Models/Guilds/PartialIntegration.cs new file mode 100644 index 0000000000..bb27b3aca6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/PartialIntegration.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialIntegration : IPartialIntegration +{ + /// + public Optional Id { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Enabled { get; init; } + + /// + public Optional Syncing { get; init; } + + /// + public Optional RoleId { get; init; } + + /// + public Optional EnableEmoticons { get; init; } + + /// + public Optional ExpireBehaviour { get; init; } + + /// + public Optional ExpireGracePeriod { get; init; } + + /// + public Optional User { get; init; } + + /// + public Optional Account { get; init; } + + /// + public Optional SyncedAt { get; init; } + + /// + public Optional SubscriberCount { get; init; } + + /// + public Optional Revoked { get; init; } + + /// + public Optional Application { get; init; } + + /// + public Optional> Scopes { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/PartialRole.cs b/src/core/DSharpPlus.Internal.Models/Guilds/PartialRole.cs new file mode 100644 index 0000000000..9c3c6be1c7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/PartialRole.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialRole : IPartialRole +{ + /// + public Optional Id { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Color { get; init; } + + /// + public Optional Hoist { get; init; } + + /// + public Optional Hash { get; init; } + + /// + public Optional UnicodeEmoji { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional Managed { get; init; } + + /// + public Optional Mentionable { get; init; } + + /// + public Optional Tags { get; init; } + + /// + public Optional Flags { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/Role.cs b/src/core/DSharpPlus.Internal.Models/Guilds/Role.cs new file mode 100644 index 0000000000..c554e30469 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/Role.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Role : IRole +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required int Color { get; init; } + + /// + public required bool Hoist { get; init; } + + /// + public Optional Hash { get; init; } + + /// + public Optional UnicodeEmoji { get; init; } + + /// + public required int Position { get; init; } + + /// + public required DiscordPermissions Permissions { get; init; } + + /// + public required bool Managed { get; init; } + + /// + public required bool Mentionable { get; init; } + + /// + public Optional Tags { get; init; } + + /// + public required DiscordRoleFlags Flags { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/RoleTags.cs b/src/core/DSharpPlus.Internal.Models/Guilds/RoleTags.cs new file mode 100644 index 0000000000..1d9caff635 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/RoleTags.cs @@ -0,0 +1,29 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record RoleTags : IRoleTags +{ + /// + public Optional BotId { get; init; } + + /// + public Optional IntegrationId { get; init; } + + /// + public required bool PremiumSubscriber { get; init; } + + /// + public Optional SubscriptionListingId { get; init; } + + /// + public required bool AvailableForPurchase { get; init; } + + /// + public required bool GuildConnections { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/WelcomeScreen.cs b/src/core/DSharpPlus.Internal.Models/Guilds/WelcomeScreen.cs new file mode 100644 index 0000000000..d119e123d4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/WelcomeScreen.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record WelcomeScreen : IWelcomeScreen +{ + /// + public string? Description { get; init; } + + /// + public required IReadOnlyList WelcomeChannels { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Guilds/WelcomeScreenChannel.cs b/src/core/DSharpPlus.Internal.Models/Guilds/WelcomeScreenChannel.cs new file mode 100644 index 0000000000..2a0995c87e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Guilds/WelcomeScreenChannel.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record WelcomeScreenChannel : IWelcomeScreenChannel +{ + /// + public required Snowflake ChannelId { get; init; } + + /// + public required string Description { get; init; } + + /// + public Snowflake? EmojiId { get; init; } + + /// + public string? EmojiName { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/ApplicationCommandInteractionData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/ApplicationCommandInteractionData.cs new file mode 100644 index 0000000000..1cab8a21d8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/ApplicationCommandInteractionData.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommandInteractionData : IApplicationCommandInteractionData +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required DiscordApplicationCommandType Type { get; init; } + + /// + public Optional Resolved { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional TargetId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/ApplicationCommandInteractionDataOption.cs b/src/core/DSharpPlus.Internal.Models/Interactions/ApplicationCommandInteractionDataOption.cs new file mode 100644 index 0000000000..52aaddad88 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/ApplicationCommandInteractionDataOption.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationCommandInteractionDataOption : IApplicationCommandInteractionDataOption +{ + /// + public required string Name { get; init; } + + /// + public required DiscordApplicationCommandOptionType Type { get; init; } + + /// + public Optional> Value { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional Focused { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/AutocompleteCallbackData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/AutocompleteCallbackData.cs new file mode 100644 index 0000000000..73d90ff561 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/AutocompleteCallbackData.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AutocompleteCallbackData : IAutocompleteCallbackData +{ + /// + public required IReadOnlyList Choices { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/Interaction.cs b/src/core/DSharpPlus.Internal.Models/Interactions/Interaction.cs new file mode 100644 index 0000000000..83d3891a51 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/Interaction.cs @@ -0,0 +1,76 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Interaction : IInteraction +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake ApplicationId { get; init; } + + /// + public required DiscordInteractionType Type { get; init; } + + /// + public Optional> Data { get; init; } + + /// + public Optional Guild { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Channel { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional Member { get; init; } + + /// + public Optional User { get; init; } + + /// + public required string Token { get; init; } + + /// + public required int Version { get; init; } + + /// + public Optional Message { get; init; } + + /// + public Optional AppPermissions { get; init; } + + /// + public Optional Locale { get; init; } + + /// + public Optional GuildLocale { get; init; } + + /// + public required IReadOnlyList Entitlements { get; init; } + + /// + public required IReadOnlyDictionary AuthorizingIntegrationOwners { get; init; } + + /// + public Optional Context { get; init; } + + /// + public required int AttachmentSizeLimit { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/InteractionResponse.cs b/src/core/DSharpPlus.Internal.Models/Interactions/InteractionResponse.cs new file mode 100644 index 0000000000..9d6879ab99 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/InteractionResponse.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record InteractionResponse : IInteractionResponse +{ + /// + public required DiscordInteractionCallbackType Type { get; init; } + + /// + public Optional> Data { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/MessageCallbackData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/MessageCallbackData.cs new file mode 100644 index 0000000000..bb9c96d125 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/MessageCallbackData.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageCallbackData : IMessageCallbackData +{ + /// + public Optional Tts { get; init; } + + /// + public Optional Content { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Poll { get; init; } + + /// + public IReadOnlyList? Files { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/MessageComponentInteractionData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/MessageComponentInteractionData.cs new file mode 100644 index 0000000000..eeaf44f197 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/MessageComponentInteractionData.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageComponentInteractionData : IMessageComponentInteractionData +{ + /// + public required string CustomId { get; init; } + + /// + public required DiscordMessageComponentType ComponentType { get; init; } + + /// + public Optional> Values { get; init; } + + /// + public Optional Resolved { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/ModalCallbackData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/ModalCallbackData.cs new file mode 100644 index 0000000000..6d00440964 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/ModalCallbackData.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ModalCallbackData : IModalCallbackData +{ + /// + public required string CustomId { get; init; } + + /// + public required string Title { get; init; } + + /// + public required IReadOnlyList Components { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/ModalInteractionData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/ModalInteractionData.cs new file mode 100644 index 0000000000..3a7b2302e1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/ModalInteractionData.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ModalInteractionData : IModalInteractionData +{ + /// + public required string CustomId { get; init; } + + /// + public required IReadOnlyList Components { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Interactions/ResolvedData.cs b/src/core/DSharpPlus.Internal.Models/Interactions/ResolvedData.cs new file mode 100644 index 0000000000..2b57cb7e4a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Interactions/ResolvedData.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ResolvedData : IResolvedData +{ + /// + public Optional> Users { get; init; } + + /// + public Optional> Members { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional> Channels { get; init; } + + /// + public Optional> Messages { get; init; } + + /// + public Optional> Attachments { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Invites/Invite.cs b/src/core/DSharpPlus.Internal.Models/Invites/Invite.cs new file mode 100644 index 0000000000..c35068fca2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Invites/Invite.cs @@ -0,0 +1,65 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Invite : IInvite +{ + /// + public required DiscordInviteType Type { get; init; } + + /// + public required string Code { get; init; } + + /// + public Optional Guild { get; init; } + + /// + public IPartialChannel? Channel { get; init; } + + /// + public Optional Inviter { get; init; } + + /// + public Optional TargetType { get; init; } + + /// + public Optional TargetUser { get; init; } + + /// + public Optional TargetApplication { get; init; } + + /// + public Optional ApproximatePresenceCount { get; init; } + + /// + public Optional ApproximateMemberCount { get; init; } + + /// + public Optional ExpiresAt { get; init; } + + /// + public Optional GuildScheduledEvent { get; init; } + + /// + public Optional Uses { get; init; } + + /// + public Optional MaxUses { get; init; } + + /// + public Optional MaxAge { get; init; } + + /// + public Optional Temporary { get; init; } + + /// + public Optional CreatedAt { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Invites/PartialInvite.cs b/src/core/DSharpPlus.Internal.Models/Invites/PartialInvite.cs new file mode 100644 index 0000000000..49ee583b37 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Invites/PartialInvite.cs @@ -0,0 +1,65 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialInvite : IPartialInvite +{ + /// + public Optional Type { get; init; } + + /// + public Optional Code { get; init; } + + /// + public Optional Guild { get; init; } + + /// + public Optional Channel { get; init; } + + /// + public Optional Inviter { get; init; } + + /// + public Optional TargetType { get; init; } + + /// + public Optional TargetUser { get; init; } + + /// + public Optional TargetApplication { get; init; } + + /// + public Optional ApproximatePresenceCount { get; init; } + + /// + public Optional ApproximateMemberCount { get; init; } + + /// + public Optional ExpiresAt { get; init; } + + /// + public Optional GuildScheduledEvent { get; init; } + + /// + public Optional Uses { get; init; } + + /// + public Optional MaxUses { get; init; } + + /// + public Optional MaxAge { get; init; } + + /// + public Optional Temporary { get; init; } + + /// + public Optional CreatedAt { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/AllowedMentions.cs b/src/core/DSharpPlus.Internal.Models/Messages/AllowedMentions.cs new file mode 100644 index 0000000000..39325b0bda --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/AllowedMentions.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AllowedMentions : IAllowedMentions +{ + /// + public Optional> Parse { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional> Users { get; init; } + + /// + public Optional RepliedUser { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/Attachment.cs b/src/core/DSharpPlus.Internal.Models/Messages/Attachment.cs new file mode 100644 index 0000000000..0862c949c5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/Attachment.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Attachment : IAttachment +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Filename { get; init; } + + /// + public Optional Title { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional ContentType { get; init; } + + /// + public required int Size { get; init; } + + /// + public required string Url { get; init; } + + /// + public required string ProxyUrl { get; init; } + + /// + public Optional Height { get; init; } + + /// + public Optional Width { get; init; } + + /// + public Optional Ephemeral { get; init; } + + /// + public Optional DurationSecs { get; init; } + + /// + public Optional> Waveform { get; init; } + + /// + public Optional Flags { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/ChannelMention.cs b/src/core/DSharpPlus.Internal.Models/Messages/ChannelMention.cs new file mode 100644 index 0000000000..cd2e870b7f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/ChannelMention.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ChannelMention : IChannelMention +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake GuildId { get; init; } + + /// + public required DiscordChannelType Type { get; init; } + + /// + public required string Name { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/Embed.cs b/src/core/DSharpPlus.Internal.Models/Messages/Embed.cs new file mode 100644 index 0000000000..54f3ac7732 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/Embed.cs @@ -0,0 +1,53 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Embed : IEmbed +{ + /// + public Optional Title { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Url { get; init; } + + /// + public Optional Timestamp { get; init; } + + /// + public Optional Color { get; init; } + + /// + public Optional Footer { get; init; } + + /// + public Optional Image { get; init; } + + /// + public Optional Thumbnail { get; init; } + + /// + public Optional Video { get; init; } + + /// + public Optional Provider { get; init; } + + /// + public Optional Author { get; init; } + + /// + public Optional> Fields { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedAuthor.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedAuthor.cs new file mode 100644 index 0000000000..a70c637dcb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedAuthor.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedAuthor : IEmbedAuthor +{ + /// + public required string Name { get; init; } + + /// + public Optional Url { get; init; } + + /// + public Optional IconUrl { get; init; } + + /// + public Optional ProxyIconUrl { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedField.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedField.cs new file mode 100644 index 0000000000..dfba94145a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedField.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedField : IEmbedField +{ + /// + public required string Name { get; init; } + + /// + public required string Value { get; init; } + + /// + public Optional Inline { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedFooter.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedFooter.cs new file mode 100644 index 0000000000..9386af213f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedFooter.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedFooter : IEmbedFooter +{ + /// + public required string Text { get; init; } + + /// + public Optional IconUrl { get; init; } + + /// + public Optional ProxyIconUrl { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedImage.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedImage.cs new file mode 100644 index 0000000000..5e2f47bd72 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedImage.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedImage : IEmbedImage +{ + /// + public required string Url { get; init; } + + /// + public Optional ProxyUrl { get; init; } + + /// + public Optional Height { get; init; } + + /// + public Optional Width { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedProvider.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedProvider.cs new file mode 100644 index 0000000000..531c8908a5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedProvider.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedProvider : IEmbedProvider +{ + /// + public Optional Name { get; init; } + + /// + public Optional Url { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedThumbnail.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedThumbnail.cs new file mode 100644 index 0000000000..26db818a6f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedThumbnail.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedThumbnail : IEmbedThumbnail +{ + /// + public required string Url { get; init; } + + /// + public Optional ProxyUrl { get; init; } + + /// + public Optional Height { get; init; } + + /// + public Optional Width { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/EmbedVideo.cs b/src/core/DSharpPlus.Internal.Models/Messages/EmbedVideo.cs new file mode 100644 index 0000000000..f1df768180 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/EmbedVideo.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record EmbedVideo : IEmbedVideo +{ + /// + public Optional Url { get; init; } + + /// + public Optional ProxyUrl { get; init; } + + /// + public Optional Height { get; init; } + + /// + public Optional Width { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/Message.cs b/src/core/DSharpPlus.Internal.Models/Messages/Message.cs new file mode 100644 index 0000000000..baf3b1273c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/Message.cs @@ -0,0 +1,117 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Message : IMessage +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake ChannelId { get; init; } + + /// + public required IUser Author { get; init; } + + /// + public required string Content { get; init; } + + /// + public required DateTimeOffset Timestamp { get; init; } + + /// + public DateTimeOffset? EditedTimestamp { get; init; } + + /// + public required bool Tts { get; init; } + + /// + public required bool MentionEveryone { get; init; } + + /// + public required IReadOnlyList Mentions { get; init; } + + /// + public required IReadOnlyList MentionRoles { get; init; } + + /// + public Optional> MentionChannels { get; init; } + + /// + public required IReadOnlyList Attachments { get; init; } + + /// + public required IReadOnlyList Embeds { get; init; } + + /// + public Optional> Reactions { get; init; } + + /// + public Optional Nonce { get; init; } + + /// + public required bool Pinned { get; init; } + + /// + public Optional WebhookId { get; init; } + + /// + public required DiscordMessageType Type { get; init; } + + /// + public Optional Activity { get; init; } + + /// + public Optional Application { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional MessageReference { get; init; } + + /// + public Optional> MessageSnapshots { get; init; } + + /// + public Optional ReferencedMessage { get; init; } + + /// + public Optional InteractionMetadata { get; init; } + + /// + public Optional Thread { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional StickerItems { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional RoleSubscriptionData { get; init; } + + /// + public Optional Resolved { get; init; } + + /// + public Optional Poll { get; init; } + + /// + public Optional Call { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/MessageActivity.cs b/src/core/DSharpPlus.Internal.Models/Messages/MessageActivity.cs new file mode 100644 index 0000000000..9051e346e9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/MessageActivity.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageActivity : IMessageActivity +{ + /// + public required DiscordMessageActivityType Type { get; init; } + + /// + public Optional PartyId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/MessageCall.cs b/src/core/DSharpPlus.Internal.Models/Messages/MessageCall.cs new file mode 100644 index 0000000000..b6102772da --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/MessageCall.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageCall : IMessageCall +{ + /// + public required IReadOnlyList Participants { get; init; } + + /// + public Optional EndedTimestamp { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/MessageInteractionMetadata.cs b/src/core/DSharpPlus.Internal.Models/Messages/MessageInteractionMetadata.cs new file mode 100644 index 0000000000..9b26135319 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/MessageInteractionMetadata.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageInteractionMetadata : IMessageInteractionMetadata +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordInteractionType Type { get; init; } + + /// + public required IUser User { get; init; } + + /// + public required IReadOnlyDictionary AuthorizingIntegrationOwners { get; init; } + + /// + public Optional OriginalResponseMessageId { get; init; } + + /// + public Optional InteractedMessageId { get; init; } + + /// + public Optional TriggeringInteractionMetadata { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/MessageReference.cs b/src/core/DSharpPlus.Internal.Models/Messages/MessageReference.cs new file mode 100644 index 0000000000..c7493b4db8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/MessageReference.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageReference : IMessageReference +{ + /// + public Optional Type { get; init; } + + /// + public Optional MessageId { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional FailIfNotExists { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/MessageSnapshot.cs b/src/core/DSharpPlus.Internal.Models/Messages/MessageSnapshot.cs new file mode 100644 index 0000000000..0bcd7985cf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/MessageSnapshot.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageSnapshot : IMessageSnapshot +{ + /// + public required IMessageSnapshotContent Message { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/MessageSnapshotContent.cs b/src/core/DSharpPlus.Internal.Models/Messages/MessageSnapshotContent.cs new file mode 100644 index 0000000000..8d4c7bc6bb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/MessageSnapshotContent.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record MessageSnapshotContent : IMessageSnapshotContent +{ + /// + public Optional Content { get; init; } + + /// + public Optional Timestamp { get; init; } + + /// + public Optional EditedTimestamp { get; init; } + + /// + public Optional> Mentions { get; init; } + + /// + public Optional> MentionRoles { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional StickerItems { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/PartialAttachment.cs b/src/core/DSharpPlus.Internal.Models/Messages/PartialAttachment.cs new file mode 100644 index 0000000000..ccc19f85b7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/PartialAttachment.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialAttachment : IPartialAttachment +{ + /// + public Optional Id { get; init; } + + /// + public Optional Filename { get; init; } + + /// + public Optional Title { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional ContentType { get; init; } + + /// + public Optional Size { get; init; } + + /// + public Optional Url { get; init; } + + /// + public Optional ProxyUrl { get; init; } + + /// + public Optional Height { get; init; } + + /// + public Optional Width { get; init; } + + /// + public Optional Ephemeral { get; init; } + + /// + public Optional DurationSecs { get; init; } + + /// + public Optional> Waveform { get; init; } + + /// + public Optional Flags { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/PartialMessage.cs b/src/core/DSharpPlus.Internal.Models/Messages/PartialMessage.cs new file mode 100644 index 0000000000..c5be6c7bef --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/PartialMessage.cs @@ -0,0 +1,117 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialMessage : IPartialMessage +{ + /// + public required Snowflake Id { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional Author { get; init; } + + /// + public Optional Content { get; init; } + + /// + public Optional Timestamp { get; init; } + + /// + public Optional EditedTimestamp { get; init; } + + /// + public Optional Tts { get; init; } + + /// + public Optional MentionEveryone { get; init; } + + /// + public Optional> Mentions { get; init; } + + /// + public Optional> MentionRoles { get; init; } + + /// + public Optional> MentionChannels { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional> Reactions { get; init; } + + /// + public Optional Nonce { get; init; } + + /// + public Optional Pinned { get; init; } + + /// + public Optional WebhookId { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Activity { get; init; } + + /// + public Optional Application { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional MessageReference { get; init; } + + /// + public Optional> MessageSnapshots { get; init; } + + /// + public Optional ReferencedMessage { get; init; } + + /// + public Optional InteractionMetadata { get; init; } + + /// + public Optional Thread { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional StickerItems { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional RoleSubscriptionData { get; init; } + + /// + public Optional Resolved { get; init; } + + /// + public Optional Poll { get; init; } + + /// + public Optional Call { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/Reaction.cs b/src/core/DSharpPlus.Internal.Models/Messages/Reaction.cs new file mode 100644 index 0000000000..d6de702260 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/Reaction.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Reaction : IReaction +{ + /// + public required int Count { get; init; } + + /// + public required IReactionCountDetails CountDetails { get; init; } + + /// + public required bool Me { get; init; } + + /// + public required bool MeBurst { get; init; } + + /// + public required IPartialEmoji Emoji { get; init; } + + /// + public required IReadOnlyList BurstColors { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/ReactionCountDetails.cs b/src/core/DSharpPlus.Internal.Models/Messages/ReactionCountDetails.cs new file mode 100644 index 0000000000..7f40e8877d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/ReactionCountDetails.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ReactionCountDetails : IReactionCountDetails +{ + /// + public required int Burst { get; init; } + + /// + public required int Normal { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Messages/RoleSubscriptionData.cs b/src/core/DSharpPlus.Internal.Models/Messages/RoleSubscriptionData.cs new file mode 100644 index 0000000000..8673710cec --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Messages/RoleSubscriptionData.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record RoleSubscriptionData : IRoleSubscriptionData +{ + /// + public required Snowflake RoleSubscriptionListingId { get; init; } + + /// + public required string TierName { get; init; } + + /// + public required int TotalMonthsSubscribed { get; init; } + + /// + public required bool IsRenewal { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Polls/CreatePoll.cs b/src/core/DSharpPlus.Internal.Models/Polls/CreatePoll.cs new file mode 100644 index 0000000000..6478175a76 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Polls/CreatePoll.cs @@ -0,0 +1,29 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record CreatePoll : ICreatePoll +{ + /// + public required IPollMedia Question { get; init; } + + /// + public required IReadOnlyList Answers { get; init; } + + /// + public Optional Duration { get; init; } + + /// + public Optional AllowMultiselect { get; init; } + + /// + public Optional LayoutType { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Polls/Poll.cs b/src/core/DSharpPlus.Internal.Models/Polls/Poll.cs new file mode 100644 index 0000000000..a122a5e087 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Polls/Poll.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Poll : IPoll +{ + /// + public required IPollMedia Question { get; init; } + + /// + public required IReadOnlyList Answers { get; init; } + + /// + public required DateTimeOffset Expiry { get; init; } + + /// + public required bool AllowMultiselect { get; init; } + + /// + public Optional LayoutType { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Polls/PollAnswer.cs b/src/core/DSharpPlus.Internal.Models/Polls/PollAnswer.cs new file mode 100644 index 0000000000..14b2bff419 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Polls/PollAnswer.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PollAnswer : IPollAnswer +{ + /// + public required int AnswerId { get; init; } + + /// + public required IPollMedia PollMedia { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Polls/PollAnswerCount.cs b/src/core/DSharpPlus.Internal.Models/Polls/PollAnswerCount.cs new file mode 100644 index 0000000000..a4e4f19cdd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Polls/PollAnswerCount.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PollAnswerCount : IPollAnswerCount +{ + /// + public required int Id { get; init; } + + /// + public required int Count { get; init; } + + /// + public required bool MeVoted { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Polls/PollMedia.cs b/src/core/DSharpPlus.Internal.Models/Polls/PollMedia.cs new file mode 100644 index 0000000000..14f313e153 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Polls/PollMedia.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PollMedia : IPollMedia +{ + /// + public Optional Text { get; init; } + + /// + public Optional Emoji { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Polls/PollResults.cs b/src/core/DSharpPlus.Internal.Models/Polls/PollResults.cs new file mode 100644 index 0000000000..92a20f8f29 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Polls/PollResults.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PollResults : IPollResults +{ + /// + public required bool IsFinalized { get; init; } + + /// + public required IReadOnlyList AnswerCounts { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/RoleConnections/RoleConnectionMetadata.cs b/src/core/DSharpPlus.Internal.Models/RoleConnections/RoleConnectionMetadata.cs new file mode 100644 index 0000000000..ecbc246636 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/RoleConnections/RoleConnectionMetadata.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record RoleConnectionMetadata : IRoleConnectionMetadata +{ + /// + public required DiscordRoleConnectionMetadataType Type { get; init; } + + /// + public required string Key { get; init; } + + /// + public required string Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public required string Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ScheduledEvents/PartialScheduledEvent.cs b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/PartialScheduledEvent.cs new file mode 100644 index 0000000000..24d7dacc7c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/PartialScheduledEvent.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialScheduledEvent : IPartialScheduledEvent +{ + /// + public Optional Id { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional CreatorId { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional ScheduledStartTime { get; init; } + + /// + public Optional PrivacyLevel { get; init; } + + /// + public Optional Status { get; init; } + + /// + public Optional EntityType { get; init; } + + /// + public Optional Creator { get; init; } + + /// + public Optional UserCount { get; init; } + + /// + public Optional Image { get; init; } + + /// + public Optional RecurrenceRule { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEvent.cs b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEvent.cs new file mode 100644 index 0000000000..1f07522c52 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEvent.cs @@ -0,0 +1,56 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ScheduledEvent : IScheduledEvent +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake GuildId { get; init; } + + /// + public required Snowflake ChannelId { get; init; } + + /// + public Optional CreatorId { get; init; } + + /// + public required string Name { get; init; } + + /// + public Optional Description { get; init; } + + /// + public required DateTimeOffset ScheduledStartTime { get; init; } + + /// + public required DiscordScheduledEventPrivacyLevel PrivacyLevel { get; init; } + + /// + public required DiscordScheduledEventStatus Status { get; init; } + + /// + public required DiscordScheduledEventType EntityType { get; init; } + + /// + public Optional Creator { get; init; } + + /// + public Optional UserCount { get; init; } + + /// + public Optional Image { get; init; } + + /// + public IScheduledEventRecurrenceRule? RecurrenceRule { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventMetadata.cs b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventMetadata.cs new file mode 100644 index 0000000000..473ddd4743 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventMetadata.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ScheduledEventMetadata : IScheduledEventMetadata +{ + /// + public Optional Location { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceDay.cs b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceDay.cs new file mode 100644 index 0000000000..7228288fd2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceDay.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ScheduledEventRecurrenceDay : IScheduledEventRecurrenceDay +{ + /// + public required int N { get; init; } + + /// + public required DiscordScheduledEventRecurrenceWeekday Day { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceRule.cs b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceRule.cs new file mode 100644 index 0000000000..d8bfa9fab8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventRecurrenceRule.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ScheduledEventRecurrenceRule : IScheduledEventRecurrenceRule +{ + /// + public required DateTimeOffset Start { get; init; } + + /// + public DateTimeOffset? End { get; init; } + + /// + public required DiscordScheduledEventRecurrenceFrequency Frequency { get; init; } + + /// + public required int Interval { get; init; } + + /// + public IReadOnlyList? ByWeekday { get; init; } + + /// + public IReadOnlyList? ByNWeekday { get; init; } + + /// + public IReadOnlyList? ByMonth { get; init; } + + /// + public IReadOnlyList? ByMonthDay { get; init; } + + /// + public IReadOnlyList? ByYearDay { get; init; } + + /// + public int? Count { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventUser.cs b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventUser.cs new file mode 100644 index 0000000000..41bcd9d5fe --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/ScheduledEvents/ScheduledEventUser.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ScheduledEventUser : IScheduledEventUser +{ + /// + public required Snowflake GuildScheduledEventId { get; init; } + + /// + public required IUser User { get; init; } + + /// + public Optional Member { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ApplicationIntegrationTypeKeyConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ApplicationIntegrationTypeKeyConverter.cs new file mode 100644 index 0000000000..1883d0acf9 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ApplicationIntegrationTypeKeyConverter.cs @@ -0,0 +1,72 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Enables to be used as a dictionary key. +/// +public class ApplicationIntegrationTypeKeyConverter : JsonConverter +{ + // short-circuit read and write + + /// + public override DiscordApplicationIntegrationType Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + => JsonSerializer.Deserialize(ref reader, options); + + /// + public override void Write + ( + Utf8JsonWriter writer, + DiscordApplicationIntegrationType value, + JsonSerializerOptions options + ) + => writer.WriteNumberValue((int)value); + + // what we actually want to override is Read/WriteAsPropertyName + + /// + public override DiscordApplicationIntegrationType ReadAsPropertyName + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType is not JsonTokenType.PropertyName) + { + throw new JsonException("Expected a property name."); + } + + string? name = reader.GetString(); + + return !int.TryParse(name, CultureInfo.InvariantCulture, out int value) + ? throw new JsonException("Expected an integer key.") + : (DiscordApplicationIntegrationType)value; + } + + /// + public override void WriteAsPropertyName + ( + Utf8JsonWriter writer, + + [DisallowNull] + DiscordApplicationIntegrationType value, + JsonSerializerOptions options + ) + => writer.WritePropertyName(((int)value).ToString(CultureInfo.InvariantCulture)); +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/AuditLogChangeConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/AuditLogChangeConverter.cs new file mode 100644 index 0000000000..be30f56c56 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/AuditLogChangeConverter.cs @@ -0,0 +1,87 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Provides conversion for s. +/// +public class AuditLogChangeConverter : JsonConverter +{ + /// + public override IAuditLogChange? Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("There was no JSON object found."); + } + + if + ( + !JsonDocument.TryParseValue(ref reader, out JsonDocument? document) + || !document.RootElement.TryGetProperty("key", out JsonElement propertyKey) + ) + { + throw new JsonException("The provided JSON object was malformed."); + } + + string key = propertyKey.GetString()!; + + Optional newProperty = document.RootElement.TryGetProperty("new_value", out JsonElement value) + ? JsonSerializer.Serialize(value, options) + : new Optional(); + + Optional oldProperty = document.RootElement.TryGetProperty("old_value", out JsonElement oldValue) + ? JsonSerializer.Serialize(oldValue, options) + : new Optional(); + + document.Dispose(); + + return new AuditLogChange + { + Key = key, + NewValue = newProperty, + OldValue = oldProperty + }; + } + + /// + public override void Write + ( + Utf8JsonWriter writer, + IAuditLogChange value, + JsonSerializerOptions options + ) + { + writer.WriteStartObject(); + + writer.WritePropertyName("key"); + writer.WriteStringValue(value.Key); + + if (value.NewValue.TryGetValue(out string? newValue)) + { + writer.WritePropertyName("new_value"); + writer.WriteStringValue(newValue); + } + + if (value.OldValue.TryGetValue(out string? oldValue)) + { + writer.WritePropertyName("old_value"); + writer.WriteStringValue(oldValue); + } + + writer.WriteEndObject(); + } +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/AutoModerationActionConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/AutoModerationActionConverter.cs new file mode 100644 index 0000000000..41dc744a2f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/AutoModerationActionConverter.cs @@ -0,0 +1,122 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Provides conversion for . +/// +public class AutoModerationActionConverter : JsonConverter +{ + /// + public override IAutoModerationAction? Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("There was no JSON object found."); + } + + if + ( + !JsonDocument.TryParseValue(ref reader, out JsonDocument? document) + || document.RootElement.TryGetProperty("type", out JsonElement typeElement) + || !typeElement.TryGetInt32(out int type) + ) + { + throw new JsonException("The provided JSON object was malformed."); + } + + Optional data; + + // these three types have associated metadata, deserialize accordingly + if + ( + (DiscordAutoModerationActionType)type is DiscordAutoModerationActionType.BlockMessage + or DiscordAutoModerationActionType.SendAlertMessage + or DiscordAutoModerationActionType.Timeout + && document.RootElement.TryGetProperty("metadata", out JsonElement metadata) + ) + { +#pragma warning disable IDE0072 + data = (DiscordAutoModerationActionType)type switch + { + DiscordAutoModerationActionType.BlockMessage + => new(metadata.Deserialize(options)!), + DiscordAutoModerationActionType.SendAlertMessage + => new(metadata.Deserialize(options)!), + DiscordAutoModerationActionType.Timeout + => new(metadata.Deserialize(options)!), + _ => Optional.None + }; +#pragma warning restore IDE0072 + } + // everyone else doesn't have metadata, good job, we made it through + else + { + data = Optional.None; + } + + document.Dispose(); + + return new AutoModerationAction + { + Type = (DiscordAutoModerationActionType)type, + Metadata = data + }; + } + + /// + public override void Write + ( + Utf8JsonWriter writer, + IAutoModerationAction value, + JsonSerializerOptions options + ) + { + writer.WriteStartObject(); + + writer.WritePropertyName("type"); + writer.WriteNumberValue((int)value.Type); + + if (!value.Metadata.TryGetValue(out IAutoModerationActionMetadata? metadata)) + { + writer.WriteEndObject(); + return; + } + + writer.WritePropertyName("metadata"); + + switch (metadata) + { + case IBlockMessageActionMetadata block: + JsonSerializer.Serialize(writer, block, options); + break; + + case ISendAlertMessageActionMetadata alert: + JsonSerializer.Serialize(writer, alert, options); + break; + + case ITimeoutActionMetadata timeout: + JsonSerializer.Serialize(writer, timeout, options); + break; + + default: + break; + } + + writer.WriteEndObject(); + } +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ComponentConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ComponentConverter.cs new file mode 100644 index 0000000000..1545cc48b3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ComponentConverter.cs @@ -0,0 +1,71 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +public class ComponentConverter : JsonConverter +{ + /// + public override IComponent? Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("There was no JSON object found."); + } + + if + ( + !JsonDocument.TryParseValue(ref reader, out JsonDocument? document) + || !document.RootElement.TryGetProperty("type", out JsonElement property) + || !property.TryGetInt32(out int type) + ) + { + throw new JsonException("The provided JSON object was malformed."); + } + + IComponent? component = (DiscordMessageComponentType)type switch + { + DiscordMessageComponentType.ActionRow => document.Deserialize(options), + DiscordMessageComponentType.Button => document.Deserialize(options), + DiscordMessageComponentType.StringSelect => document.Deserialize(options), + DiscordMessageComponentType.TextInput => document.Deserialize(options), + DiscordMessageComponentType.UserSelect => document.Deserialize(options), + DiscordMessageComponentType.RoleSelect => document.Deserialize(options), + DiscordMessageComponentType.MentionableSelect => document.Deserialize(options), + DiscordMessageComponentType.ChannelSelect => document.Deserialize(options), + DiscordMessageComponentType.Section => document.Deserialize(options), + DiscordMessageComponentType.TextDisplay => document.Deserialize(options), + DiscordMessageComponentType.Thumbnail => document.Deserialize(options), + DiscordMessageComponentType.MediaGallery => document.Deserialize(options), + DiscordMessageComponentType.File => document.Deserialize(options), + DiscordMessageComponentType.Separator => document.Deserialize(options), + DiscordMessageComponentType.Container => document.Deserialize(options), + + _ => new UnknownComponent + { + Type = (DiscordMessageComponentType)type, + RawPayload = JsonSerializer.Serialize(document) + } + }; + + document.Dispose(); + return component; + } + + /// + public override void Write(Utf8JsonWriter writer, IComponent value, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, (object)value, options); +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/DiscordPermissionConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/DiscordPermissionConverter.cs new file mode 100644 index 0000000000..dca010b60a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/DiscordPermissionConverter.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0072 + +using System; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Serialization; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Enables serializing and deserializing Discord permissions. +/// +public class DiscordPermissionConverter : JsonConverter +{ + /// + public override DiscordPermissions Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + return reader.TokenType switch + { + JsonTokenType.String => !BigInteger.TryParse(reader.GetString()!, out BigInteger permissions) + ? throw new JsonException("The provided permission value could not be parsed.") + : new DiscordPermissions(permissions), + + JsonTokenType.Number => new DiscordPermissions(new BigInteger(reader.GetUInt64())), + _ => throw new JsonException("The provided permission value was provided in an unrecognized format.") + }; + } + + /// + public override void Write + ( + Utf8JsonWriter writer, + DiscordPermissions value, + JsonSerializerOptions options + ) + => writer.WriteStringValue(value.ToString()); +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ImageDataConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ImageDataConverter.cs new file mode 100644 index 0000000000..ed6e403032 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/ImageDataConverter.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using CommunityToolkit.HighPerformance.Buffers; + +namespace DSharpPlus.Serialization; + +public sealed class ImageDataConverter : JsonConverter +{ + /// + /// Deserializing image data is unsupported. + /// + public override InlineMediaData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotSupportedException(); + + public override void Write + ( + Utf8JsonWriter writer, + InlineMediaData value, + JsonSerializerOptions options + ) + { + using ArrayPoolBufferWriter bufferWriter = new(65536); + + value.WriteTo(bufferWriter); + + writer.WriteStringValue(bufferWriter.WrittenSpan); + } +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/NullBooleanConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/NullBooleanConverter.cs new file mode 100644 index 0000000000..90bc4efb6c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/NullBooleanConverter.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Provides serialization for discord's optional null booleans, see +/// . +/// +/// +/// This needs to be applied to every null boolean property individually. +/// +public class NullBooleanJsonConverter : JsonConverter +{ + // if the token type is False or True we have an actual boolean on our hands and should read it + // appropriately. if not, we judge by the existence of the token (which is what discord sends). + public override bool Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + bool? value = JsonSerializer.Deserialize(ref reader, options); + + // null is a 'true' value. + return value != false; + } + + // slightly off, but since we can't ever actually send this to discord we don't need to deal + // with any of this. we'll serialize it as a correct boolean so it can be read correctly if the + // end user uses our models for serialization. + public override void Write + ( + Utf8JsonWriter writer, + bool value, + JsonSerializerOptions options + ) + => writer.WriteBooleanValue(value); +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OneOfConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OneOfConverter.cs new file mode 100644 index 0000000000..811177d391 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OneOfConverter.cs @@ -0,0 +1,189 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1810 + +using System; +using System.Collections; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +using OneOf; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Provides a mechanicsm for serializing and deserializing objects. +/// +public sealed class OneOfConverter : JsonConverter + where TUnion : IOneOf +{ + // using type handles turns out to be marginally faster than Type, but it doesn't fundamentally matter + // and this might well change in future .NET versions, in which case this code should use Type for ease + // of reading + // at some point i'd also like to kill the MethodInfo, but today is not that day + private static readonly FrozenDictionary constructionMethods; + + // we cache ordered types by the jsontokentype + private static readonly FrozenDictionary> orderedUnion; + + // this cctor is *extremely* expensive and creates quite a lot of cached data. we might want to break this + // up a bit more, or use specialized converters for smaller OneOfs that don't need all this elaborate + // ordering ceremony because they only have a select few valid orders anyways. + static OneOfConverter() + { + Type[] unionTypes = typeof(TUnion).GetGenericArguments(); + + // order of type priority: + // 1. snowflake + // 2. integer primitives (is assignable to INumber but not to IFloatingPoint, is struct) + // 3. float primitives (assignable to INumber and IFloatingPoint, is struct) + // 4. other types that aren't models + // 5. models + IEnumerable baselineOrderedUnionTypes = unionTypes + .OrderByDescending(t => t == typeof(Snowflake) || t == typeof(Snowflake?)) + .ThenByDescending + ( + t => TestAssignableToGenericMathInterface(t, typeof(INumber<>)) + ) + .ThenBy + ( + t => TestAssignableToGenericMathInterface(t, typeof(IFloatingPoint<>)) + ) + .ThenBy(t => t.FullName!.StartsWith("DSharpPlus", StringComparison.InvariantCulture)); + + // construction methods + Dictionary methods = []; + + for (int i = 0; i < unionTypes.Length; i++) + { + MethodInfo method = typeof(TUnion).GetMethod($"FromT{i}")!; + + methods.Add(unionTypes[i].TypeHandle.Value, method); + } + + constructionMethods = methods.ToFrozenDictionary(); + + // priority + Dictionary> priorities = new() + { + // our baseline is already optimized for numbers + [JsonTokenType.Number] = baselineOrderedUnionTypes.ToFrozenSet() + }; + + // nullability + priorities.Add + ( + JsonTokenType.Null, + baselineOrderedUnionTypes.OrderByDescending + ( + type => (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) || !type.IsValueType + ) + .ToFrozenSet() + ); + + // booleans + FrozenSet booleanPriority = baselineOrderedUnionTypes.OrderByDescending + ( + type => type == typeof(bool) || type == typeof(bool?) + ) + .ToFrozenSet(); + + priorities.Add(JsonTokenType.True, booleanPriority); + priorities.Add(JsonTokenType.False, booleanPriority); + + // string - snowflakes also use strings, so we should prioritize accordingly + priorities.Add + ( + JsonTokenType.String, + baselineOrderedUnionTypes.OrderByDescending + ( + type => type == typeof(string) || type == typeof(Snowflake) + ) + .ToFrozenSet() + ); + + // collections + priorities.Add + ( + JsonTokenType.StartArray, + // we can make our life easier here by seeing whether we're assignable to non-generic IEnumerable + baselineOrderedUnionTypes.OrderByDescending(type => type.IsAssignableTo(typeof(IEnumerable))) + .ToFrozenSet() + ); + + // start object + priorities.Add + ( + JsonTokenType.StartObject, + baselineOrderedUnionTypes.OrderByDescending(type => !type.IsPrimitive) + .ThenByDescending(type => type != typeof(Snowflake) && type != typeof(Snowflake?)) + .ThenByDescending(type => type.IsGenericType && type.GetGenericTypeDefinition() != typeof(Nullable<>)) + .ToFrozenSet() + ); + + orderedUnion = priorities.ToFrozenDictionary(); + } + + private static bool TestAssignableToGenericMathInterface + ( + Type type, + Type @interface + ) + { + if (!type.IsValueType) + { + return false; + } + + try + { + if (type.IsAssignableTo(@interface.MakeGenericType(type))) + { + return true; + } + } + catch (ArgumentException) + { + return false; + } + + return false; + } + + public override TUnion? Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + foreach (Type type in orderedUnion[reader.TokenType]) + { + object value; + + try + { + value = JsonSerializer.Deserialize(ref reader, type, options)!; + } + // it's tragic we have to eat an exception here, but, try again + catch (JsonException) + { + continue; + } + + return (TUnion)constructionMethods[type.TypeHandle.Value].Invoke(null, [value])!; + } + + throw new JsonException("The value could not be parsed into the given union."); + } + + public override void Write(Utf8JsonWriter writer, TUnion value, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, value.Value, typeof(TUnion).GetGenericArguments()[value.Index], options); +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OneOfConverterFactory.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OneOfConverterFactory.cs new file mode 100644 index 0000000000..3718c14acc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OneOfConverterFactory.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +using OneOf; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// Provides a factory for OneOf converters. +/// +public sealed class OneOfConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + => typeToConvert.IsGenericType && typeToConvert.IsAssignableTo(typeof(IOneOf)); + + public override JsonConverter? CreateConverter + ( + Type typeToConvert, + JsonSerializerOptions options + ) + { + Type concreteConverter = typeof(OneOfConverter<>).MakeGenericType(typeToConvert); + + return (JsonConverter)Activator.CreateInstance(concreteConverter)!; + } +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OptionalConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OptionalConverter.cs new file mode 100644 index 0000000000..2f001c4cf1 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OptionalConverter.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// A converter for . +/// +public sealed class OptionalConverter : JsonConverter> +{ + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => new(JsonSerializer.Deserialize(ref reader, options)!); + + public override void Write + ( + Utf8JsonWriter writer, + Optional value, + JsonSerializerOptions options + ) + { + if (!value.HasValue) + { + throw new ArgumentException("Serializing an empty optional is not allowed."); + } + + JsonSerializer.Serialize(writer, value.Value, options); + } +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OptionalConverterFactory.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OptionalConverterFactory.cs new file mode 100644 index 0000000000..f0210b12a8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/OptionalConverterFactory.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +/// +/// A converter factory for . +/// +public class OptionalConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + => typeToConvert.IsConstructedGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Optional<>); + + public override JsonConverter? CreateConverter + ( + Type typeToConvert, + JsonSerializerOptions options + ) + { + return (JsonConverter)typeof(OptionalConverter<>) + .MakeGenericType(typeToConvert.GetGenericArguments()) + .GetConstructor(Type.EmptyTypes)! + .Invoke(null)!; + } +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Converters/SnowflakeConverter.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/SnowflakeConverter.cs new file mode 100644 index 0000000000..fc4988dbd4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Converters/SnowflakeConverter.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0072 + +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DSharpPlus.Internal.Models.Serialization.Converters; + +public sealed class SnowflakeConverter : JsonConverter +{ + public override Snowflake Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => new(reader.GetInt64()), + JsonTokenType.String => new(long.Parse(reader.GetString()!, CultureInfo.InvariantCulture)), + _ => throw new JsonException("The present payload could not be parsed as a snowflake.") + }; + } + + public override void Write(Utf8JsonWriter writer, Snowflake value, JsonSerializerOptions options) + => writer.WriteNumberValue(value.Value); +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/AttachmentDataTypeInfoResolver.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/AttachmentDataTypeInfoResolver.cs new file mode 100644 index 0000000000..e1f9442135 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/AttachmentDataTypeInfoResolver.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; + +namespace DSharpPlus.Internal.Models.Serialization.Resolvers; + +public static class AttachmentDataTypeInfoResolver +{ + public static IJsonTypeInfoResolver Default { get; } = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + (type) => + { + foreach (JsonPropertyInfo property in type.Properties) + { + if + ( + property.PropertyType == typeof(AttachmentData) + || property.PropertyType == typeof(AttachmentData?) + || property.PropertyType == typeof(Optional) + || property.PropertyType == typeof(Optional) + || property.PropertyType == typeof(IReadOnlyList) + ) + { + property.ShouldSerialize = (_, _) => false; + } + } + } + } + }; +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/NullBooleanTypeInfoResolver.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/NullBooleanTypeInfoResolver.cs new file mode 100644 index 0000000000..8f90dbb6fc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/NullBooleanTypeInfoResolver.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Text.Json.Serialization.Metadata; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Models.Serialization.Converters; + +namespace DSharpPlus.Internal.Models.Serialization.Resolvers; + +public static class NullBooleanTypeInfoResolver +{ + public static IJsonTypeInfoResolver Default { get; } = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + (type) => + { + if (type.Type != typeof(IRoleTags)) + { + return; + } + + foreach (JsonPropertyInfo property in type.Properties) + { + if (property.PropertyType == typeof(bool)) + { + property.IsRequired = false; + property.CustomConverter = new NullBooleanJsonConverter(); + } + } + } + } + }; +} diff --git a/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/OptionalTypeInfoResolver.cs b/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/OptionalTypeInfoResolver.cs new file mode 100644 index 0000000000..1f9b4665fe --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Serialization/Resolvers/OptionalTypeInfoResolver.cs @@ -0,0 +1,50 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization.Metadata; + +namespace DSharpPlus.Internal.Models.Serialization.Resolvers; + +/// +/// Provides a mechanism for resolving serialization of . +/// +public static class OptionalTypeInfoResolver +{ + public static IJsonTypeInfoResolver Default { get; } = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + (type) => + { + foreach (JsonPropertyInfo property in type.Properties) + { + if (property.PropertyType.IsConstructedGenericType && + property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>)) + { + property.ShouldSerialize = Unsafe.As> + ( + typeof(OptionalTypeInfoResolver) + .GetMethod + ( + nameof(ShouldIgnoreOptional), + BindingFlags.NonPublic | BindingFlags.Static + )! + .MakeGenericMethod + ( + property.PropertyType.GetGenericArguments()[0]! + ) + .CreateDelegate>() + ); + } + } + } + } + }; + + private static bool ShouldIgnoreOptional(object _, object? value) + => Unsafe.Unbox>(value!).HasValue; +} diff --git a/src/core/DSharpPlus.Internal.Models/Skus/Sku.cs b/src/core/DSharpPlus.Internal.Models/Skus/Sku.cs new file mode 100644 index 0000000000..77c21ab64f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Skus/Sku.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Sku : ISku +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordSkuType Type { get; init; } + + /// + public required Snowflake ApplicationId { get; init; } + + /// + public required string Name { get; init; } + + /// + public required string Slug { get; init; } + + /// + public required DiscordSkuFlags Flags { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Soundboard/PartialSoundboardSound.cs b/src/core/DSharpPlus.Internal.Models/Soundboard/PartialSoundboardSound.cs new file mode 100644 index 0000000000..c3d9bad26b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Soundboard/PartialSoundboardSound.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialSoundboardSound : IPartialSoundboardSound +{ + /// + public required Snowflake SoundId { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Volume { get; init; } + + /// + public Optional EmojiId { get; init; } + + /// + public Optional EmojiName { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional Available { get; init; } + + /// + public Optional User { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Soundboard/SoundboardSound.cs b/src/core/DSharpPlus.Internal.Models/Soundboard/SoundboardSound.cs new file mode 100644 index 0000000000..47b78e29c5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Soundboard/SoundboardSound.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record SoundboardSound : ISoundboardSound +{ + /// + public required Snowflake SoundId { get; init; } + + /// + public required string Name { get; init; } + + /// + public required double Volume { get; init; } + + /// + public Snowflake? EmojiId { get; init; } + + /// + public string? EmojiName { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public required bool Available { get; init; } + + /// + public Optional User { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/StageInstances/PartialStageInstance.cs b/src/core/DSharpPlus.Internal.Models/StageInstances/PartialStageInstance.cs new file mode 100644 index 0000000000..f442e7cc7e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/StageInstances/PartialStageInstance.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialStageInstance : IPartialStageInstance +{ + /// + public Optional Id { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional Topic { get; init; } + + /// + public Optional PrivacyLevel { get; init; } + + /// + public Optional GuildScheduledEventId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/StageInstances/StageInstance.cs b/src/core/DSharpPlus.Internal.Models/StageInstances/StageInstance.cs new file mode 100644 index 0000000000..88d587261a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/StageInstances/StageInstance.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record StageInstance : IStageInstance +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake GuildId { get; init; } + + /// + public required Snowflake ChannelId { get; init; } + + /// + public required string Topic { get; init; } + + /// + public required DiscordStagePrivacyLevel PrivacyLevel { get; init; } + + /// + public Snowflake? GuildScheduledEventId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Stickers/PartialSticker.cs b/src/core/DSharpPlus.Internal.Models/Stickers/PartialSticker.cs new file mode 100644 index 0000000000..7ed8636836 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Stickers/PartialSticker.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialSticker : IPartialSticker +{ + /// + public Optional Id { get; init; } + + /// + public Optional PackId { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Tags { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional FormatType { get; init; } + + /// + public Optional Available { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional User { get; init; } + + /// + public Optional SortValue { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Stickers/Sticker.cs b/src/core/DSharpPlus.Internal.Models/Stickers/Sticker.cs new file mode 100644 index 0000000000..3385397e26 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Stickers/Sticker.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Sticker : ISticker +{ + /// + public required Snowflake Id { get; init; } + + /// + public Optional PackId { get; init; } + + /// + public required string Name { get; init; } + + /// + public string? Description { get; init; } + + /// + public required string Tags { get; init; } + + /// + public required DiscordStickerType Type { get; init; } + + /// + public required DiscordStickerFormatType FormatType { get; init; } + + /// + public Optional Available { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional User { get; init; } + + /// + public Optional SortValue { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Stickers/StickerItem.cs b/src/core/DSharpPlus.Internal.Models/Stickers/StickerItem.cs new file mode 100644 index 0000000000..5156d4be0a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Stickers/StickerItem.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record StickerItem : IStickerItem +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required DiscordStickerFormatType FormatType { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Stickers/StickerPack.cs b/src/core/DSharpPlus.Internal.Models/Stickers/StickerPack.cs new file mode 100644 index 0000000000..f0691e757f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Stickers/StickerPack.cs @@ -0,0 +1,34 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record StickerPack : IStickerPack +{ + /// + public required Snowflake Id { get; init; } + + /// + public required IReadOnlyList Stickers { get; init; } + + /// + public required string Name { get; init; } + + /// + public required Snowflake SkuId { get; init; } + + /// + public Optional CoverStickerId { get; init; } + + /// + public required string Description { get; init; } + + /// + public Optional BannerAssetId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Subscriptions/Subscription.cs b/src/core/DSharpPlus.Internal.Models/Subscriptions/Subscription.cs new file mode 100644 index 0000000000..917b65c9eb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Subscriptions/Subscription.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Subscription : ISubscription +{ + /// + public required Snowflake Id { get; init; } + + /// + public required Snowflake UserId { get; init; } + + /// + public required IReadOnlyList SkuIds { get; init; } + + /// + public required IReadOnlyList EntitlementIds { get; init; } + + /// + public IReadOnlyList? RenewalSkuIds { get; init; } + + /// + public required DateTimeOffset CurrentPeriodStart { get; init; } + + /// + public required DateTimeOffset CurrentPeriodEnd { get; init; } + + /// + public required DiscordSubscriptionStatus Status { get; init; } + + /// + public DateTimeOffset? CanceledAt { get; init; } + + /// + public Optional Country { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Teams/Team.cs b/src/core/DSharpPlus.Internal.Models/Teams/Team.cs new file mode 100644 index 0000000000..53212b7327 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Teams/Team.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Team : ITeam +{ + /// + public string? Icon { get; init; } + + /// + public required Snowflake Id { get; init; } + + /// + public required IReadOnlyList Members { get; init; } + + /// + public required string Name { get; init; } + + /// + public required Snowflake OwnerUserId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Teams/TeamMember.cs b/src/core/DSharpPlus.Internal.Models/Teams/TeamMember.cs new file mode 100644 index 0000000000..cfdcc8dc34 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Teams/TeamMember.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record TeamMember : ITeamMember +{ + /// + public required DiscordTeamMembershipState MembershipState { get; init; } + + /// + public required IReadOnlyList Permissions { get; init; } + + /// + public required Snowflake TeamId { get; init; } + + /// + public required IPartialUser User { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Users/ApplicationRoleConnection.cs b/src/core/DSharpPlus.Internal.Models/Users/ApplicationRoleConnection.cs new file mode 100644 index 0000000000..23c597e0fb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Users/ApplicationRoleConnection.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record ApplicationRoleConnection : IApplicationRoleConnection +{ + /// + public string? PlatformName { get; init; } + + /// + public string? PlatformUsername { get; init; } + + /// + public required IReadOnlyDictionary Metadata { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Users/AvatarDecorationData.cs b/src/core/DSharpPlus.Internal.Models/Users/AvatarDecorationData.cs new file mode 100644 index 0000000000..d78256c8b4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Users/AvatarDecorationData.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record AvatarDecorationData : IAvatarDecorationData +{ + /// + public required string Asset { get; init; } + + /// + public required Snowflake SkuId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Users/Connection.cs b/src/core/DSharpPlus.Internal.Models/Users/Connection.cs new file mode 100644 index 0000000000..9935f9893f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Users/Connection.cs @@ -0,0 +1,44 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Connection : IConnection +{ + /// + public required string Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required string Type { get; init; } + + /// + public Optional Revoked { get; init; } + + /// + public Optional> Integrations { get; init; } + + /// + public required bool Verified { get; init; } + + /// + public required bool FriendSync { get; init; } + + /// + public required bool ShowActivity { get; init; } + + /// + public required bool TwoWayLink { get; init; } + + /// + public required DiscordConnectionVisibility Visibility { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Users/PartialUser.cs b/src/core/DSharpPlus.Internal.Models/Users/PartialUser.cs new file mode 100644 index 0000000000..91bccf6103 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Users/PartialUser.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialUser : IPartialUser +{ + /// + public Optional Id { get; init; } + + /// + public Optional Username { get; init; } + + /// + public Optional Discriminator { get; init; } + + /// + public Optional GlobalName { get; init; } + + /// + public Optional Avatar { get; init; } + + /// + public Optional Bot { get; init; } + + /// + public Optional System { get; init; } + + /// + public Optional MfaEnabled { get; init; } + + /// + public Optional Banner { get; init; } + + /// + public Optional AccentColor { get; init; } + + /// + public Optional Locale { get; init; } + + /// + public Optional Verified { get; init; } + + /// + public Optional Email { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional PremiumType { get; init; } + + /// + public Optional PublicFlags { get; init; } + + /// + public Optional AvatarDecorationData { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Users/User.cs b/src/core/DSharpPlus.Internal.Models/Users/User.cs new file mode 100644 index 0000000000..d0cd9b7258 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Users/User.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record User : IUser +{ + /// + public required Snowflake Id { get; init; } + + /// + public required string Username { get; init; } + + /// + public required string Discriminator { get; init; } + + /// + public string? GlobalName { get; init; } + + /// + public string? Avatar { get; init; } + + /// + public Optional Bot { get; init; } + + /// + public Optional System { get; init; } + + /// + public Optional MfaEnabled { get; init; } + + /// + public Optional Banner { get; init; } + + /// + public Optional AccentColor { get; init; } + + /// + public Optional Locale { get; init; } + + /// + public Optional Verified { get; init; } + + /// + public Optional Email { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional PremiumType { get; init; } + + /// + public Optional PublicFlags { get; init; } + + /// + public Optional AvatarDecorationData { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Models/Voice/VoiceRegion.cs b/src/core/DSharpPlus.Internal.Models/Voice/VoiceRegion.cs new file mode 100644 index 0000000000..e3ce616747 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Voice/VoiceRegion.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record VoiceRegion : IVoiceRegion +{ + /// + public required string Id { get; init; } + + /// + public required string Name { get; init; } + + /// + public required bool Optimal { get; init; } + + /// + public required bool Deprecated { get; init; } + + /// + public required bool Custom { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Voice/VoiceState.cs b/src/core/DSharpPlus.Internal.Models/Voice/VoiceState.cs new file mode 100644 index 0000000000..39d5abc9a4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Voice/VoiceState.cs @@ -0,0 +1,52 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record VoiceState : IVoiceState +{ + /// + public Optional GuildId { get; init; } + + /// + public Snowflake? ChannelId { get; init; } + + /// + public required Snowflake UserId { get; init; } + + /// + public Optional Member { get; init; } + + /// + public required string SessionId { get; init; } + + /// + public required bool Deaf { get; init; } + + /// + public required bool Mute { get; init; } + + /// + public required bool SelfDeaf { get; init; } + + /// + public required bool SelfMute { get; init; } + + /// + public Optional SelfStream { get; init; } + + /// + public required bool SelfVideo { get; init; } + + /// + public required bool Suppress { get; init; } + + /// + public DateTimeOffset? RequestToSpeakTimestamp { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Webhooks/PartialWebhook.cs b/src/core/DSharpPlus.Internal.Models/Webhooks/PartialWebhook.cs new file mode 100644 index 0000000000..3ee5f39c54 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Webhooks/PartialWebhook.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record PartialWebhook : IPartialWebhook +{ + /// + public Optional Id { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional User { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional Avatar { get; init; } + + /// + public Optional Token { get; init; } + + /// + public Optional ApplicationId { get; init; } + + /// + public Optional SourceGuild { get; init; } + + /// + public Optional SourceChannel { get; init; } + + /// + public Optional Url { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Models/Webhooks/Webhook.cs b/src/core/DSharpPlus.Internal.Models/Webhooks/Webhook.cs new file mode 100644 index 0000000000..ac592031e0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Models/Webhooks/Webhook.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Internal.Models; + +/// +public sealed record Webhook : IWebhook +{ + /// + public required Snowflake Id { get; init; } + + /// + public required DiscordWebhookType Type { get; init; } + + /// + public Optional GuildId { get; init; } + + /// + public Snowflake? ChannelId { get; init; } + + /// + public Optional User { get; init; } + + /// + public string? Name { get; init; } + + /// + public string? Avatar { get; init; } + + /// + public Optional Token { get; init; } + + /// + public Snowflake? ApplicationId { get; init; } + + /// + public Optional SourceGuild { get; init; } + + /// + public Optional SourceChannel { get; init; } + + /// + public Optional Url { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/ApplicationCommandsRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/ApplicationCommandsRestAPI.cs new file mode 100644 index 0000000000..5f306c2c68 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/ApplicationCommandsRestAPI.cs @@ -0,0 +1,492 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; +using DSharpPlus.Serialization; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class ApplicationCommandsRestAPI +( + IRestClient restClient, + SerializationService serializationService +) + : IApplicationCommandsRestAPI +{ + /// + public async ValueTask>> BulkOverwriteGlobalApplicationCommandsAsync + ( + Snowflake applicationId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + foreach (ICreateGlobalApplicationCommandPayload command in payload) + { + if (command.Name.Length is > 32 or < 1) + { + return new ValidationError("The name of an application command must be between 1 and 32 characters."); + } + + if (command.Name.Length is > 100 or < 1) + { + return new ValidationError("The description of an application command must be between 1 and 100 characters."); + } + + if (command.Options.HasValue && command.Type != DiscordApplicationCommandType.ChatInput) + { + return new ValidationError("Only chat input commands can have options and subcommands."); + } + + if (command.Options.HasValue && command.Options.Value!.Count > 25) + { + return new ValidationError("An application command can only have up to 25 options and subcommands."); + } + } + + if (payload.Count > 100) + { + return new ValidationError("An application can only have up to 100 global commands."); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Put, + $"applications/{applicationId}/commands", + b => b.WithPayload(payload) + .WithRoute("PUT applications/:application-id/commands"), + info, + ct + ); + } + + /// + public async ValueTask>> BulkOverwriteGuildApplicationCommandsAsync + ( + Snowflake applicationId, + Snowflake guildId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + foreach (ICreateGuildApplicationCommandPayload command in payload) + { + if (command.Name.Length is > 32 or < 1) + { + return new ValidationError("The name of an application command must be between 1 and 32 characters."); + } + + if (command.Name.Length is > 100 or < 1) + { + return new ValidationError("The description of an application command must be between 1 and 100 characters."); + } + + if (command.Options.HasValue && command.Type != DiscordApplicationCommandType.ChatInput) + { + return new ValidationError("Only chat input commands can have options and subcommands."); + } + + if (command.Options.HasValue && command.Options.Value!.Count > 25) + { + return new ValidationError("An application command can only have up to 25 options and subcommands."); + } + } + + if (payload.Count > 100) + { + return new ValidationError("An application can only have up to 100 global commands."); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Put, + $"applications/{applicationId}/guilds/{guildId}/commands", + b => b.WithPayload(payload) + .WithRoute("PUT applications/:application-id/guilds/:guild-id/commands"), + info, + ct + ); + } + + /// + public async ValueTask> CreateGlobalApplicationCommandAsync + ( + Snowflake applicationId, + ICreateGlobalApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length is > 32 or < 1) + { + return new ValidationError("The name of an application command must be between 1 and 32 characters."); + } + + if (payload.Name.Length is > 100 or < 1) + { + return new ValidationError("The description of an application command must be between 1 and 100 characters."); + } + + if (payload.Options.HasValue && payload.Type != DiscordApplicationCommandType.ChatInput) + { + return new ValidationError("Only chat input commands can have options and subcommands."); + } + + if (payload.Options.HasValue && payload.Options.Value!.Count > 25) + { + return new ValidationError("An application command can only have up to 25 options and subcommands."); + } + + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"applications/{applicationId}/commands", + b => b.WithPayload(payload) + .WithRoute("POST applications/:application-id/commands"), + info, + ct + ); + + if (!response.IsSuccess) + { + return Result.FromError(response.Error); + } + + return new CreateApplicationCommandResponse + { + CreatedCommand = serializationService.DeserializeModel + ( + await response.Value.Content.ReadAsByteArrayAsync(ct) + ), + IsNewlyCreated = response.Value.StatusCode == HttpStatusCode.Created + }; + } + + /// + public async ValueTask> CreateGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + ICreateGuildApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length is > 32 or < 1) + { + return new ValidationError("The name of an application command must be between 1 and 32 characters."); + } + + if (payload.Name.Length is > 100 or < 1) + { + return new ValidationError("The description of an application command must be between 1 and 100 characters."); + } + + if (payload.Options.HasValue && payload.Type != DiscordApplicationCommandType.ChatInput) + { + return new ValidationError("Only chat input commands can have options and subcommands."); + } + + if (payload.Options.HasValue && payload.Options.Value!.Count > 25) + { + return new ValidationError("An application command can only have up to 25 options and subcommands."); + } + + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"applications/{applicationId}/guilds/{guildId}/commands", + b => b.WithPayload(payload) + .WithRoute("POST applications/:application-id/guilds/:guild-id/commands"), + info, + ct + ); + + if (!response.IsSuccess) + { + return Result.FromError(response.Error); + } + + return new CreateApplicationCommandResponse + { + CreatedCommand = serializationService.DeserializeModel + ( + await response.Value.Content.ReadAsByteArrayAsync(ct) + ), + IsNewlyCreated = response.Value.StatusCode == HttpStatusCode.Created + }; + } + + /// + public async ValueTask DeleteGlobalApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"applications/{applicationId}/commands/{commandId}", + b => b.WithRoute("DELETE applications/:application-id/commands/:command-id"), + info, + ct + ); + } + + /// + public async ValueTask DeleteGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"applications/{applicationId}/guilds/{guildId}/commands/{commandId}", + b => b.WithRoute("DELETE applications/:application-id/guilds/:guild-id/commands/:command-id"), + info, + ct + ); + } + + /// + public async ValueTask> EditGlobalApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake commandId, + IEditGlobalApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length is > 32 or < 1) + { + return new ValidationError("The name of an application command must be between 1 and 32 characters."); + } + + if (payload.Name.HasValue && payload.Name.Value.Length is > 100 or < 1) + { + return new ValidationError("The description of an application command must be between 1 and 100 characters."); + } + + if (payload.Options.HasValue && payload.Type != DiscordApplicationCommandType.ChatInput) + { + return new ValidationError("Only chat input commands can have options and subcommands."); + } + + if (payload.Options.HasValue && payload.Options.Value.Count > 25) + { + return new ValidationError("An application command can only have up to 25 options and subcommands."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"applications/{applicationId}/commands/{commandId}", + b => b.WithPayload(payload) + .WithRoute("PATCH applications/:application-id/commands/:command-id"), + info, + ct + ); + } + + /// + public async ValueTask> EditGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + IEditGuildApplicationCommandPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length is > 32 or < 1) + { + return new ValidationError("The name of an application command must be between 1 and 32 characters."); + } + + if (payload.Name.HasValue && payload.Name.Value.Length is > 100 or < 1) + { + return new ValidationError("The description of an application command must be between 1 and 100 characters."); + } + + if (payload.Options.HasValue && payload.Type != DiscordApplicationCommandType.ChatInput) + { + return new ValidationError("Only chat input commands can have options and subcommands."); + } + + if (payload.Options.HasValue && payload.Options.Value.Count > 25) + { + return new ValidationError("An application command can only have up to 25 options and subcommands."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"applications/{applicationId}/guild/{guildId}/commands/{commandId}", + b => b.WithPayload(payload) + .WithRoute("PATCH applications/:application-id/guilds/:guild-id/commands/:command-id"), + info, + ct + ); + } + + /// + public async ValueTask> GetApplicationCommandPermissionsAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"applications/{applicationId}/guild/{guildId}/commands/{commandId}/permissions", + b => b.WithRoute("GET applications/:application-id/guilds/:guild-id/commands/:command-id/permissions"), + info, + ct + ); + } + + /// + public async ValueTask> GetGlobalApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"applications/{applicationId}/commands/{commandId}", + b => b.WithRoute("GET applications/:application-id/commands/:command-id"), + info, + ct + ); + } + + /// + public async ValueTask>> GetGlobalApplicationCommandsAsync + ( + Snowflake applicationId, + LocalizationQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"applications/{applicationId}/commands"); + + if (query.WithLocalizations is not null) + { + _ = builder.AddParameter("with_localizations", query.WithLocalizations.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithRoute("GET applications/:application-id/commands"), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildApplicationCommandAsync + ( + Snowflake applicationId, + Snowflake guildId, + Snowflake commandId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"applications/{applicationId}/guilds/{guildId}/commands/{commandId}", + b => b.WithRoute("GET applications/:application-id/guilds/:guild-id/commands/:command-id"), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildApplicationCommandPermissionsAsync + ( + Snowflake applicationId, + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"applications/{applicationId}/guild/{guildId}/commands/permissions", + b => b.WithRoute("GET applications/:application-id/guilds/:guild-id/commands/permissions"), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildApplicationCommandsAsync + ( + Snowflake applicationId, + Snowflake guildId, + LocalizationQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"applications/{applicationId}/guilds/{guildId}/commands"); + + if (query.WithLocalizations is not null) + { + _ = builder.AddParameter("with_localizations", query.WithLocalizations.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithRoute("GET applications/:application-id/guilds/:guild-id/commands"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/ApplicationRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/ApplicationRestAPI.cs new file mode 100644 index 0000000000..3e61573e56 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/ApplicationRestAPI.cs @@ -0,0 +1,70 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class ApplicationRestAPI(IRestClient restClient) + : IApplicationRestAPI +{ + /// + public async ValueTask> EditCurrentApplicationAsync + ( + IEditCurrentApplicationPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Tags.TryGetNonNullValue(out IReadOnlyList? value)) + { + if (value.Count > 5) + { + return new ValidationError("An application can only have up to five tags."); + } + + if (value.Any(tag => tag.Length > 20)) + { + return new ValidationError("Tags of an application cannot exceed 20 characters."); + } + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"applications/@me", + b => b.WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> GetCurrentApplicationAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + method: HttpMethod.Get, + path: $"applications/@me", + info: info, + ct: ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/AuditLogsRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/AuditLogsRestAPI.cs new file mode 100644 index 0000000000..d4113c16f6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/AuditLogsRestAPI.cs @@ -0,0 +1,79 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class AuditLogsRestAPI +( + IRestClient restClient +) + : IAuditLogsRestAPI +{ + /// + public async ValueTask> ListGuildAuditLogEntriesAsync + ( + Snowflake guildId, + ListGuildAuditLogEntriesQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 1000)) + { + return new ValidationError("The limit of entries to return must be between 1 and 1000."); + } + + QueryBuilder builder = new($"guilds/{guildId}/audit-logs"); + + if (query.ActionType is not null) + { + int value = (int)query.ActionType.Value; + _ = builder.AddParameter("action_type", value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.UserId is not null) + { + _ = builder.AddParameter("user_id", query.UserId.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/audit-logs"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/AutoModerationRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/AutoModerationRestAPI.cs new file mode 100644 index 0000000000..8bf5073ac2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/AutoModerationRestAPI.cs @@ -0,0 +1,151 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class AutoModerationRestAPI(IRestClient restClient) + : IAutoModerationRestAPI +{ + /// + public async ValueTask> CreateAutoModerationRuleAsync + ( + Snowflake guildId, + ICreateAutoModerationRulePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.ExemptRoles.HasValue && payload.ExemptRoles.Value.Count > 20) + { + return new ValidationError("Only up to 20 roles can be exempted from an automod rule."); + } + + if (payload.ExemptChannels.HasValue && payload.ExemptChannels.Value.Count > 50) + { + return new ValidationError("Ibkt yp to 50 channels can be exempted from an automod rule."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/auto-moderation/rules", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteAutoModerationRuleAsync + ( + Snowflake guildId, + Snowflake ruleId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/auto-moderation/rules/{ruleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/auto-moderation/rules/:rule-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> GetAutoModerationRuleAsync + ( + Snowflake guildId, + Snowflake ruleId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/auto-moderation/rules/{ruleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/auto-moderation/rules/:rule-id"), + info, + ct + ); + } + + /// + public async ValueTask>> ListAutoModerationRulesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/auto-moderation/rules", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> ModifyAutoModerationRuleAsync + ( + Snowflake guildId, + Snowflake ruleId, + IModifyAutoModerationRulePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.ExemptRoles.HasValue && payload.ExemptRoles.Value.Count > 20) + { + return new ValidationError("Only up to 20 roles can be exempted from an automod rule."); + } + + if (payload.ExemptChannels.HasValue && payload.ExemptChannels.Value.Count > 50) + { + return new ValidationError("Only up to 50 channels can be exempted from an automod rule."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/auto-moderation/rules/{ruleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PATCH guilds/{guildId}/auto-moderation/rules/:rule-id") + .WithAuditLogReason(reason), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/ChannelRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/ChannelRestAPI.cs new file mode 100644 index 0000000000..f0bb13071f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/ChannelRestAPI.cs @@ -0,0 +1,897 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class ChannelRestAPI(IRestClient restClient) + : IChannelRestAPI +{ + /// + public async ValueTask AddThreadMemberAsync + ( + Snowflake threadId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"channels/{threadId}/thread-members/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, threadId) + .WithRoute($"PUT channels/{threadId}/thread-members/:user-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> CreateChannelInviteAsync + ( + Snowflake channelId, + ICreateChannelInvitePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.MaxAge.HasValue && payload.MaxAge.Value is < 0 or > 604800) + { + return new ValidationError + ( + "The maximum age of an invite must be between 0 and 604800 seconds. A maximum age of 0 makes it never expire." + ); + } + + if (payload.MaxUses.HasValue && payload.MaxUses.Value is < 0 or > 100) + { + return new ValidationError + ( + "The maximum use count of an invite must be between 0 and 100. A value of 0 makes it unlimited." + ); + } + + if (payload.TargetType.TryGetNonNullValue(out DiscordInviteTargetType type)) + { + if (type == DiscordInviteTargetType.Stream && !payload.TargetUserId.HasValue) + { + return new ValidationError("A target type of Stream requires a streaming user as target."); + } + + if (type == DiscordInviteTargetType.EmbeddedApplication && !payload.TargetApplicationId.HasValue) + { + return new ValidationError("A target type of EmbeddedApplication requires an application as target."); + } + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/invites", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> DeleteChannelAsync + ( + Snowflake channelId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteChannelPermissionAsync + ( + Snowflake channelId, + Snowflake overwriteId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/permissions/{overwriteId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask EditChannelPermissionsAsync + ( + Snowflake channelId, + Snowflake overwriteId, + IEditChannelPermissionsPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"channels/{channelId}/permissions/{overwriteId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"PUT channels/{channelId}/permissions/:overwrite-id") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> FollowAnnouncementChannelAsync + ( + Snowflake channelId, + IFollowAnnouncementChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/followers", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> GetChannelAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"channels/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask>> GetChannelInvitesAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"channels/{channelId}/invites", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask>> GetPinnedMessagesAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"channels/{channelId}/pins", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask> GetThreadMemberAsync + ( + Snowflake threadId, + Snowflake userId, + GetThreadMemberQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"channels/{threadId}/thread-members/{userId}"); + + if (query.WithMember is not null) + { + _ = builder.AddParameter("after", query.WithMember.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, threadId) + .WithRoute($"GET channels/{threadId}/thread-members/:user-id"), + info, + ct + ); + } + + /// + public async ValueTask GroupDMAddRecipientAsync + ( + Snowflake channelId, + Snowflake userId, + IGroupDMAddRecipientPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"channels/{channelId}/recipients/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"PUT channels/{channelId}/recipients/:user-id") + .WithPayload(payload), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask GroupDMRemoveRecipientAsync + ( + Snowflake channelId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/recipients/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/recipients/:user-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask JoinThreadAsync + ( + Snowflake threadId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"channels/{threadId}/recipients/@me", + b => b.WithSimpleRoute(TopLevelResource.Channel, threadId), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask LeaveThreadAsync + ( + Snowflake threadId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{threadId}/recipients/@me", + b => b.WithSimpleRoute(TopLevelResource.Channel, threadId), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> ListJoinedPrivateArchivedThreadsAsync + ( + Snowflake channelId, + ListJoinedPrivateArchivedThreadsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit of threads to return must be between 1 and 100."); + } + + QueryBuilder builder = new($"channels/{channelId}/users/@me/threads/archived/private"); + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask> ListPrivateArchivedThreadsAsync + ( + Snowflake channelId, + ListArchivedThreadsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit of threads to return must be between 1 and 100."); + } + + QueryBuilder builder = new($"channels/{channelId}/users/@me/threads/archived/private"); + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString("o", CultureInfo.InvariantCulture)); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask> ListPublicArchivedThreadsAsync + ( + Snowflake channelId, + ListArchivedThreadsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit of threads to return must be between 1 and 100."); + } + + QueryBuilder builder = new($"channels/{channelId}/threads/archived/public"); + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString("o", CultureInfo.InvariantCulture)); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask>> ListThreadMembersAsync + ( + Snowflake threadId, + ListThreadMembersQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit of threads to return must be between 1 and 100."); + } + + QueryBuilder builder = new($"channels/{threadId}/thread-members"); + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + if (query.WithMember is not null) + { + _ = builder.AddParameter("with_member", query.WithMember.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, threadId), + info, + ct + ); + } + + /// + public async ValueTask> ModifyChannelAsync + ( + Snowflake channelId, + IModifyGroupDMPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length > 100) + { + return new ValidationError("The name of a channel cannot exceed 100 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"channels/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> ModifyChannelAsync + ( + Snowflake channelId, + IModifyGuildChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length > 100) + { + return new ValidationError("The name of a channel cannot exceed 100 characters."); + } + + if (payload.RateLimitPerUser.HasValue && payload.RateLimitPerUser.Value is < 0 or > 21600) + { + return new ValidationError + ( + "The slowmode (rate limit per user) in a channel must be between 0 and 6 hours, or 21600 seconds." + ); + } + + if (payload.Bitrate.TryGetNonNullValue(out int? value) && value < 8000) + { + return new ValidationError("The bitrate of a voice channel cannot be below 8000."); + } + + if (payload.UserLimit.TryGetNonNullValue(out int? userLimit)) + { + if (payload.Type.HasValue && payload.Type == DiscordChannelType.GuildVoice && userLimit > 99) + { + return new ValidationError("The user limit of a voice channel cannot exceed 99."); + } + + if (userLimit > 10000) + { + return new ValidationError("The user limit of a stage channel cannot exceed 10,000."); + } + } + + if (payload.DefaultThreadRateLimitPerUser.HasValue && payload.DefaultThreadRateLimitPerUser.Value is < 0 or > 21600) + { + return new ValidationError + ( + "The default slowmode (rate limit per user) for threads must be between 0 and 6 hours, or 21600 seconds." + ); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"channels/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyChannelAsync + ( + Snowflake channelId, + IModifyThreadChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length > 100) + { + return new ValidationError("The name of a channel cannot exceed 100 characters."); + } + + if (payload.RateLimitPerUser.HasValue && payload.RateLimitPerUser.Value is < 0 or > 21600) + { + return new ValidationError + ( + "The slowmode (rate limit per user) in a channel must be between 0 and 6 hours, or 21600 seconds." + ); + } + + if (payload.AutoArchiveDuration.HasValue && !(payload.AutoArchiveDuration.Value is 60 or 1440 or 4320 or 10080)) + { + return new ValidationError + ( + "The auto-archive duration of a thread must be either 60, 1440, 4320 or 10080 minutes." + ); + } + + if (payload.AppliedTags.HasValue && payload.AppliedTags.Value.Count > 5) + { + return new ValidationError("A thread can only have up to five tags applied."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"channels/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask PinMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"channels/{channelId}/pins/{messageId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithAuditLogReason(reason) + .WithRoute($"PUT channels/{channelId}/pins/:message-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask RemoveThreadMemberAsync + ( + Snowflake threadId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{threadId}/thread-members/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, threadId) + .WithRoute($"DELETE channels/{threadId}/thread-members/:user-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> StartThreadFromMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + IStartThreadFromMessagePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length > 100) + { + return new ValidationError("The name of a channel cannot exceed 100 characters."); + } + + if (payload.RateLimitPerUser.HasValue && payload.RateLimitPerUser.Value is < 0 or > 21600) + { + return new ValidationError + ( + "The slowmode (rate limit per user) in a channel must be between 0 and 6 hours, or 21600 seconds." + ); + } + + if (payload.AutoArchiveDuration.HasValue && !(payload.AutoArchiveDuration.Value is 60 or 1440 or 4320 or 10080)) + { + return new ValidationError + ( + "The auto-archive duration of a thread must be either 60, 1440, 4320 or 10080 minutes." + ); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/messages/{messageId}/threads", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"POST channels/{channelId}/messages/:message-id/threads") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> StartThreadInForumOrMediaChannelAsync + ( + Snowflake channelId, + IStartThreadInForumOrMediaChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length > 100) + { + return new ValidationError("The name of a channel cannot exceed 100 characters."); + } + + if (payload.RateLimitPerUser.HasValue && payload.RateLimitPerUser.Value is < 0 or > 21600) + { + return new ValidationError + ( + "The slowmode (rate limit per user) in a channel must be between 0 and 6 hours, or 21600 seconds." + ); + } + + if (payload.AutoArchiveDuration.HasValue && !(payload.AutoArchiveDuration.Value is 60 or 1440 or 4320 or 10080)) + { + return new ValidationError + ( + "The auto-archive duration of a thread must be either 60, 1440, 4320 or 10080 minutes." + ); + } + + if (payload.AppliedTags.HasValue && payload.AppliedTags.Value.Count > 5) + { + return new ValidationError("A thread can only have up to five tags applied to it."); + } + + if + ( + !(payload.Message.Content.HasValue + || payload.Message.Embeds.HasValue + || payload.Message.StickerIds.HasValue + || payload.Message.Components.HasValue + || payload.Files is not null) + ) + { + return new ValidationError + ( + "At least one of Content, Embeds, StickerIds, Components or Files must be sent." + ); + } + + if (payload.Message.Content.HasValue && payload.Message.Content.Value.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (payload.Message.Embeds.HasValue && payload.Message.Embeds.Value.Count > 10) + { + return new ValidationError("Only up to 10 embeds can be sent with a message."); + } + + if (payload.Message.StickerIds.HasValue && payload.Message.StickerIds.Value.Count > 3) + { + return new ValidationError("Only up to 3 stickers can be sent with a message."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/threads", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> StartThreadWithoutMessageAsync + ( + Snowflake channelId, + IStartThreadWithoutMessagePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length > 100) + { + return new ValidationError("The name of a channel cannot exceed 100 characters."); + } + + if (payload.RateLimitPerUser.HasValue && payload.RateLimitPerUser.Value is < 0 or > 21600) + { + return new ValidationError + ( + "The slowmode (rate limit per user) in a channel must be between 0 and 6 hours, or 21600 seconds." + ); + } + + if (payload.AutoArchiveDuration.HasValue && !(payload.AutoArchiveDuration.Value is 60 or 1440 or 4320 or 10080)) + { + return new ValidationError + ( + "The auto-archive duration of a thread must be either 60, 1440, 4320 or 10080 minutes." + ); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/threads", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask TriggerTypingIndicatorAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/typing", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask UnpinMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/pins/{messageId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/pins/:message-id"), + info, + ct + ); + + return (Result)response; + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/EmojiRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/EmojiRestAPI.cs new file mode 100644 index 0000000000..551a1c39b3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/EmojiRestAPI.cs @@ -0,0 +1,228 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +// as per https://discord.com/developers/docs/topics/rate-limits, emojis are handled separately from other +// ratelimits. we therefore never count them towards the simple guild limit, and instead specify them as +// 'other' -> emojis/:guild-id so that their erratic behaviour doesn't mess with the rest of our ratelimits. +// +// application emojis kinda muddy the water here, but we'll just assume they're well-behaved for lack of actual docs. + +/// +public sealed class EmojiRestAPI(IRestClient restClient) + : IEmojiRestAPI +{ + /// + public async ValueTask> CreateApplicationEmojiAsync + ( + Snowflake applicationId, + ICreateApplicationEmojiPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"applications/{applicationId}/emojis", + b => b.WithRoute($"POST applications/:application-id/emojis") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> CreateGuildEmojiAsync + ( + Snowflake guildId, + ICreateGuildEmojiPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/emojis", + b => b.WithRoute($"POST emojis/{guildId}") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteApplicationEmojiAsync + ( + Snowflake applicationId, + Snowflake emojiId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"applications/{applicationId}/emojis/{emojiId}", + b => b.WithRoute($"DELETE applications/:application-id/emojis/:emoji-id"), + info, + ct + ); + } + + /// + public async ValueTask DeleteGuildEmojiAsync + ( + Snowflake guildId, + Snowflake emojiId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/emojis/{emojiId}", + b => b.WithRoute($"DELETE emojis/{guildId}") + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> GetApplicationEmojiAsync + ( + Snowflake applicationId, + Snowflake emojiId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"applications/{applicationId}/emojis/{emojiId}", + b => b.WithRoute($"GET applications/:application-id/emojis/:emoji-id"), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildEmojiAsync + ( + Snowflake guildId, + Snowflake emojiId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/emojis/{emojiId}", + b => b.WithRoute($"GET emojis/{guildId}"), + info, + ct + ); + } + + /// + public async ValueTask> ListApplicationEmojisAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"applications/{applicationId}/emojis", + b => b.WithRoute($"GET applications/:application-id/emojis"), + info, + ct + ); + } + + /// + public async ValueTask>> ListGuildEmojisAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/emojis", + b => b.WithRoute($"GET emojis/{guildId}"), + info, + ct + ); + } + + /// + public async ValueTask> ModifyApplicationEmojiAsync + ( + Snowflake applicationId, + Snowflake emojiId, + IModifyApplicationEmojiPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"applications/{applicationId}/emojis/{emojiId}", + b => b.WithRoute($"PATCH applications/:application-id/emojis/:emoji-id") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildEmojiAsync + ( + Snowflake guildId, + Snowflake emojiId, + IModifyGuildEmojiPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/emojis/{emojiId}", + b => b.WithRoute($"PATCH emojis/{guildId}") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/EntitlementsRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/EntitlementsRestAPI.cs new file mode 100644 index 0000000000..2846970e8f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/EntitlementsRestAPI.cs @@ -0,0 +1,142 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class EntitlementsRestAPI(IRestClient restClient) + : IEntitlementsRestAPI +{ + /// + public async ValueTask ConsumeEntitlementAsync + ( + Snowflake applicationId, + Snowflake entitlementId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"applications/{applicationId}/entitlements/{entitlementId}/consume", + info: info, + ct: ct + ); + } + + /// + public async ValueTask> CreateTestEntitlementAsync + ( + Snowflake applicationId, + ICreateTestEntitlementPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"applications/{applicationId}/entitlements", + b => b.WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask DeleteTestEntitlementAsync + ( + Snowflake applicationId, + Snowflake entitlementId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"applications/{applicationId}/entitlements/{entitlementId}", + b => b.WithRoute($"DELETE applications/:application-id/entitlements/:entitlement-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask>> ListEntitlementsAsync + ( + Snowflake applicationId, + ListEntitlementsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit of entitlements to list must be between 1 and 100."); + } + + QueryBuilder builder = new($"applications/{applicationId}/entitlements"); + + if (query.UserId is not null) + { + _ = builder.AddParameter("user_id", query.UserId.Value.ToString()); + } + + if (query.SkuIds is not null) + { + _ = builder.AddParameter("sku_ids", query.SkuIds); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.GuildId is not null) + { + _ = builder.AddParameter("guild_id", query.GuildId.Value.ToString()); + } + + if (query.ExcludeEnded is not null) + { + _ = builder.AddParameter("exclude_ended", query.ExcludeEnded.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync> + ( + method: HttpMethod.Get, + path: builder.Build(), + info: info, + ct: ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/GuildRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/GuildRestAPI.cs new file mode 100644 index 0000000000..592fd9c65b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/GuildRestAPI.cs @@ -0,0 +1,1125 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class GuildRestAPI(IRestClient restClient) + : IGuildRestAPI +{ + /// + public async ValueTask> AddGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + IAddGuildMemberPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"guilds/{guildId}/members/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PUT guilds/{guildId}/members/:user-id"), + info, + ct + ); + } + + /// + public async ValueTask AddGuildMemberRoleAsync + ( + Snowflake guildId, + Snowflake userId, + Snowflake roleId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"guilds/{guildId}/members/{userId}/roles/{roleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PUT guilds/{guildId}/members/:user-id/roles/:role-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask CreateGuildBanAsync + ( + Snowflake guildId, + Snowflake userId, + CreateGuildBanQuery query = default, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/bans/{userId}"); + + if (query.DeleteMessageSeconds is not null) + { + _ = builder.AddParameter("delete_message_seconds", query.DeleteMessageSeconds.Value.ToString(CultureInfo.InvariantCulture)); + } + + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PUT guilds/{guildId}/bans/:user-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> BeginGuildPruneAsync + ( + Snowflake guildId, + IBeginGuildPrunePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Days is < 1 or > 30) + { + return new ValidationError("The number of days to prune must be between 1 and 30."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/prune", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> CreateGuildChannelAsync + ( + Snowflake guildId, + ICreateGuildChannelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length is < 1 or > 100) + { + return new ValidationError("The channel name must be between 1 and 100 characters in length."); + } + + if (payload.Topic.TryGetNonNullValue(out string? topic) && topic.Length > 1024) + { + return new ValidationError("The channel topic must not exceed 1024 characters in length."); + } + + if (payload.Bitrate.TryGetNonNullValue(out int? bitrate) && bitrate < 8000) + { + return new ValidationError("The bitrate of a voice channel must not be below 8000 bits."); + } + + if (payload.RateLimitPerUser.TryGetNonNullValue(out int? slowmode) && slowmode is < 0 or > 21600) + { + return new ValidationError("The slowmode (rate limit per user) of a channel must be between 0 and 21600 seconds."); + } + + if (payload.DefaultThreadRateLimitPerUser.TryGetNonNullValue(out int? threadSlowmode) && threadSlowmode is < 0 or > 21600) + { + return new ValidationError + ( + "The slowmode (rate limit per user) of threads created in channel must be between 0 and 21600 seconds." + ); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/channels", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> CreateGuildRoleAsync + ( + Snowflake guildId, + ICreateGuildRolePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length > 100) + { + return new ValidationError("A role name cannot exceed 100 characters in length."); + } + + if (payload.Color.HasValue && payload.Color.Value is < 0 or > 0xFFFFFF) + { + return new ValidationError("The role color must be a valid RGB color code."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/roles", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteGuildAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + method: HttpMethod.Delete, + path: $"guilds/{guildId}", + info: info, + ct: ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteGuildIntegrationAsync + ( + Snowflake guildId, + Snowflake integrationId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/integrations/{integrationId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/integrations/:integration-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteGuildRoleAsync + ( + Snowflake guildId, + Snowflake roleId, + string? reason, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/roles/{roleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/roles/:role-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> GetGuildAsync + ( + Snowflake guildId, + GetGuildQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}"); + + if (query.WithCounts.HasValue) + { + _ = builder.AddParameter("with_counts", query.WithCounts.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync + ( + method: HttpMethod.Get, + path: builder.Build(), + info: info, + ct: ct + ); + } + + /// + public async ValueTask> GetGuildBanAsync + ( + Snowflake guildId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/bans/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/bans/:user-id"), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildBansAsync + ( + Snowflake guildId, + PaginatedQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/bans"); + + if (query.Limit.HasValue) + { + if (query.Limit.Value is < 1 or > 1000) + { + return new ValidationError("The amount of bans to request must be between 1 and 1000."); + } + + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Before.HasValue) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + + if (query.After.HasValue) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildChannelsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/channels", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildIntegrationsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/integrations", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildInvitesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/invites", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/members/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/members/:user-id"), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildOnboardingAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/onboarding", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildPreviewAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/preview", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildPruneCountAsync + ( + Snowflake guildId, + GetGuildPruneCountQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/prune"); + + if (query.Days.HasValue) + { + if (query.Days is < 1 or > 30) + { + return new ValidationError("The number of days to prune must be between 1 and 30."); + } + + _ = builder.AddParameter("days", query.Days.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.IncludeRoles is not null) + { + _ = builder.AddParameter("include_roles", query.IncludeRoles); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildRolesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/roles", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildVanityUrlAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/vanity-url", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildVoiceRegionsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/regions", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildWelcomeScreenAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/welcome-screen", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildWidgetAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/widget.json", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildWidgetImageAsync + ( + Snowflake guildId, + GetGuildWidgetImageQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/widget.png", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + + return response.Map(message => message.Content.ReadAsStream()); + } + + /// + public async ValueTask> GetGuildWidgetSettingsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/widget", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> ListActiveGuildThreadsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/threads/active", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask>> ListGuildMembersAsync + ( + Snowflake guildId, + ForwardsPaginatedQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/members"); + + if (query.Limit is not null) + { + if (query.Limit is < 1 or > 1000) + { + return new ValidationError("The amount of members to query at once must be between 1 and 1000."); + } + + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> ModifyCurrentMemberAsync + ( + Snowflake guildId, + IModifyCurrentMemberPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/members/@me", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildAsync + ( + Snowflake guildId, + IModifyGuildPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length is < 2 or > 100) + { + return new ValidationError("The name of a guild must be between 2 and 100 characters long."); + } + + if (payload.AfkTimeout.HasValue && payload.AfkTimeout.Value is not (60 or 300 or 900 or 1800 or 3600)) + { + return new ValidationError("The AFK timeout of a guild must be either 60, 300, 900, 1800 or 3600 seconds."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask ModifyGuildChannelPositionsAsync + ( + Snowflake guildId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/channels", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> ModifyGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + IModifyGuildMemberPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Nickname.TryGetNonNullValue(out string? nick) && nick.Length > 32) + { + return new ValidationError("Nicknames of guild members cannot be longer than 32 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/members/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PATCH guilds/{guildId}/members/:user-id") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildMFALevelAsync + ( + Snowflake guildId, + IModifyGuildMfaLevelPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/mfa", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildOnboardingAsync + ( + Snowflake guildId, + IModifyGuildOnboardingPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/onboarding", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildRoleAsync + ( + Snowflake guildId, + Snowflake roleId, + IModifyGuildRolePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.TryGetNonNullValue(out string? name) && name.Length > 100) + { + return new ValidationError("The name of a role cannot exceed 100 characters in length."); + } + + if (payload.Color.TryGetNonNullValue(out int? value) && value is < 0 or > 0xFFFFFF) + { + return new ValidationError("The color of a role must be a valid RGB color code."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/roles/{roleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PATCH guilds/{guildId}/roles/:role-id") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask>> ModifyGuildRolePositionsAsync + ( + Snowflake guildId, + IReadOnlyList payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Patch, + $"guilds/{guildId}/roles", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildWelcomeScreenAsync + ( + Snowflake guildId, + IModifyGuildWelcomeScreenPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/welcome-screen", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildWidgetAsync + ( + Snowflake guildId, + IGuildWidgetSettings settings, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/widget", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(settings) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask RemoveGuildBanAsync + ( + Snowflake guildId, + Snowflake userId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/bans/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/bans/:user-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask RemoveGuildMemberAsync + ( + Snowflake guildId, + Snowflake userId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/members/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/members/:user-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask RemoveGuildMemberRoleAsync + ( + Snowflake guildId, + Snowflake userId, + Snowflake roleId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/members/{userId}/roles/{roleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/members/:user-id/roles/:role-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask>> SearchGuildMembersAsync + ( + Snowflake guildId, + SearchGuildMembersQuery query, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/members/search"); + + _ = builder.AddParameter("query", query.Query); + + if (query.Limit is not null) + { + if (query.Limit is < 1 or > 1000) + { + return new ValidationError("The amount of members to request must be between 1 and 1000."); + } + + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/members/search", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> BulkGuildBanAsync + ( + Snowflake guildId, + IBulkGuildBanPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.DeleteMessageSeconds is { HasValue: true, Value: < 0 or > 604800 }) + { + return new ValidationError("The seconds to delete messages from must be between 0 and 604800, or 7 days."); + } + + if (payload.UserIds.Count > 200) + { + return new ValidationError("Up to 200 users may be bulk-banned at once."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/bulk-ban", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildRoleAsync + ( + Snowflake guildId, + Snowflake roleId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/roles/{roleId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/roles/:role-id"), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildIncidentActionsAsync + ( + Snowflake guildId, + IModifyGuildIncidentActionsPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"guilds/{guildId}/incident-actions", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/GuildScheduledEventRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/GuildScheduledEventRestAPI.cs new file mode 100644 index 0000000000..1c47e7663d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/GuildScheduledEventRestAPI.cs @@ -0,0 +1,195 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class GuildScheduledEventRestAPI(IRestClient restClient) + : IGuildScheduledEventRestAPI +{ + /// + public async ValueTask> CreateGuildScheduledEventAsync + ( + Snowflake guildId, + ICreateGuildScheduledEventPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/scheduled-events", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteScheduledEventAsync + ( + Snowflake guildId, + Snowflake eventId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/scheduled-events/{eventId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/scheduled-events/:event-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> GetScheduledEventAsync + ( + Snowflake guildId, + Snowflake eventId, + WithUserCountQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/scheduled-events/{eventId}"); + + if (query.WithUserCount is not null) + { + _ = builder.AddParameter("with_user_count", query.WithUserCount.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/scheduled-events/:event-id"), + info, + ct + ); + } + + /// + public async ValueTask>> GetScheduledEventUsersAsync + ( + Snowflake guildId, + Snowflake eventId, + GetScheduledEventUsersQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/scheduled-events/{eventId}/users"); + + if (query.Limit is not null) + { + if (query.Limit is < 1 or > 100) + { + return new ValidationError("The amount of scheduled event users to request must be between 1 and 100."); + } + + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.WithMember is not null) + { + _ = builder.AddParameter("with_member", query.WithMember.Value.ToString().ToLowerInvariant()); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/scheduled-events/:event-id/users"), + info, + ct + ); + } + + /// + public async ValueTask>> ListScheduledEventsForGuildAsync + ( + Snowflake guildId, + WithUserCountQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"guilds/{guildId}/scheduled-events"); + + if (query.WithUserCount is not null) + { + _ = builder.AddParameter("with_user_count", query.WithUserCount.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> ModifyScheduledEventAsync + ( + Snowflake guildId, + Snowflake eventId, + IModifyGuildScheduledEventPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/scheduled-events/{eventId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"POST guilds/{guildId}/scheduled-events/:event-id") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/GuildTemplateRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/GuildTemplateRestAPI.cs new file mode 100644 index 0000000000..efe75b29bf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/GuildTemplateRestAPI.cs @@ -0,0 +1,188 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class GuildTemplateRestAPI(IRestClient restClient) + : IGuildTemplateRestAPI +{ + /// + public async ValueTask> CreateGuildFromGuildTemplateAsync + ( + string templateCode, + ICreateGuildFromGuildTemplatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length is < 2 or > 100) + { + return new ValidationError("The length of a guild name must be between 2 and 100 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/templates/{templateCode}", + b => b.WithRoute($"POST guilds/templates/:template-code") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> CreateGuildTemplateAsync + ( + Snowflake guildId, + ICreateGuildTemplatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length > 100) + { + return new ValidationError("The length of a template name must be between 2 and 100 characters."); + } + + if (payload.Description.TryGetNonNullValue(out string? description) && description.Length > 120) + { + return new ValidationError("The description of a template name must not exceed 120 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/templates", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> DeleteGuildTemplateAsync + ( + Snowflake guildId, + string templateCode, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/templates/{templateCode}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/templates/:template-code"), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildTemplateAsync + ( + string templateCode, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/templates/{templateCode}", + b => b.WithRoute($"POST guilds/templates/:template-code"), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildTemplatesAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/templates", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildTemplateAsync + ( + Snowflake guildId, + string templateCode, + IModifyGuildTemplatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length > 100) + { + return new ValidationError("The length of a template name must be between 2 and 100 characters."); + } + + if (payload.Description.TryGetNonNullValue(out string? description) && description.Length > 120) + { + return new ValidationError("The description of a template name must not exceed 120 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/templates/{templateCode}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PATCH guilds/{guildId}/templates/:template-code") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> SyncGuildTemplateAsync + ( + Snowflake guildId, + string templateCode, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"guilds/{guildId}/templates/{templateCode}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PUT guilds/{guildId}/templates/:template-code"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/InteractionRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/InteractionRestAPI.cs new file mode 100644 index 0000000000..d4c19419cc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/InteractionRestAPI.cs @@ -0,0 +1,313 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class InteractionRestAPI(IRestClient restClient) + : IInteractionRestAPI +{ + /// + public async ValueTask> CreateFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + ICreateFollowupMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (!(payload.Content.HasValue || payload.Embeds.HasValue || payload.Components.HasValue || payload.Files is not null)) + { + return new ValidationError + ( + "At least one of Content, Embeds, Components or Files must be sent." + ); + } + + if (payload.Content.HasValue && payload.Content.Value.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (payload.Embeds.HasValue && payload.Embeds.Value.Count > 10) + { + return new ValidationError("Only up to 10 embeds can be sent with a message."); + } + + if (payload.Poll.TryGetNonNullValue(out ICreatePoll? poll)) + { + if (poll.Question.Text is { HasValue: true, Value.Length: > 300 }) + { + return new ValidationError("The poll must specify a question that cannot exceed 300 characters."); + } + + if (poll.Answers.Any(answer => answer.PollMedia.Text is { HasValue: true, Value.Length: > 55 })) + { + return new ValidationError("The answer text of a poll cannot exceed 55 characters."); + } + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"webhooks/{applicationId}/{interactionToken}", + b => b.WithRoute($"POST webhooks/:application-id/{interactionToken}") + .AsInteractionRequest() + .WithPayload(payload), + info, + ct + ); + } + + /// + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + public async ValueTask CreateInteractionResponseAsync + ( + Snowflake interactionId, + string interactionToken, + IInteractionResponse payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Type is DiscordInteractionCallbackType.ChannelMessageWithSource or DiscordInteractionCallbackType.UpdateMessage) + { + IMessageCallbackData message = payload.Data.Value.AsT1; + + if (!(message.Content.HasValue || message.Embeds.HasValue || message.Components.HasValue || message.Files is not null)) + { + return new ValidationError + ( + "At least one of Content, Embeds, Components or Files must be sent." + ); + } + + if (message.Content.HasValue && message.Content.Value.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (message.Embeds.HasValue && message.Embeds.Value.Count > 10) + { + return new ValidationError("Only up to 10 embeds can be sent with a message."); + } + + if (message.Poll.TryGetNonNullValue(out ICreatePoll? poll)) + { + if (poll.Question.Text is { HasValue: true, Value.Length: > 300 }) + { + return new ValidationError("The poll must specify a question that cannot exceed 300 characters."); + } + + if (poll.Answers.Any(answer => answer.PollMedia.Text is { HasValue: true, Value.Length: > 55 })) + { + return new ValidationError("The answer text of a poll cannot exceed 55 characters."); + } + } + } + else if (payload.Type is DiscordInteractionCallbackType.Modal) + { + IModalCallbackData modal = payload.Data.Value.AsT2; + + if (modal.CustomId.Length > 100) + { + return new ValidationError("The length of the custom ID of a modal cannot exceed 100 characters."); + } + + if (modal.Title.Length > 45) + { + return new ValidationError("The length of the modal title cannot exceed 45 characters."); + } + + if (modal.Components.Count > 5) + { + return new ValidationError("A modal does not support more than five components."); + } + } + + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"interactions/{interactionId}/{interactionToken}/callback", + b => b.WithSimpleRoute(TopLevelResource.Webhook, interactionId) + .WithRoute($"POST interactions/{interactionId}/{interactionToken}/callback") + .AsExempt() + .WithPayload(payload), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"webhooks/{applicationId}/{interactionToken}/messages/{messageId}", + b => b.WithRoute($"DELETE webhooks/:application-id/{interactionToken}/messages/:message-id") + .AsInteractionRequest(), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteInteractionResponseAsync + ( + Snowflake applicationId, + string interactionToken, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"webhooks/{applicationId}/{interactionToken}/messages/@original", + b => b.WithRoute($"webhooks/:application-id/{interactionToken}/messages/@original") + .AsInteractionRequest(), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> EditFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + IEditFollowupMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Content.TryGetNonNullValue(out string? content) && content.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (payload.Embeds.TryGetNonNullValue(out IReadOnlyList? embeds) && embeds.Count > 10) + { + return new ValidationError("Only up to 10 embeds can be sent with a message."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"webhooks/{applicationId}/{interactionToken}/messages/{messageId}", + b => b.WithRoute($"PATCH webhooks/:application-id/{interactionToken}/messages/:message-id") + .AsInteractionRequest() + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> EditInteractionResponseAsync + ( + Snowflake applicationId, + string interactionToken, + IEditInteractionResponsePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Content.TryGetNonNullValue(out string? content) && content.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (payload.Embeds.TryGetNonNullValue(out IReadOnlyList? embeds) && embeds.Count > 10) + { + return new ValidationError("Only up to 10 embeds can be sent with a message."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"webhooks/{applicationId}/{interactionToken}/messages/@original", + b => b.WithRoute($"PATCH webhooks/:application-id/{interactionToken}/messages/@original") + .AsInteractionRequest() + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> GetFollowupMessageAsync + ( + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"webhooks/{applicationId}/{interactionToken}/messages/{messageId}", + b => b.WithRoute($"GET webhooks/:application-id/{interactionToken}/messages/{messageId}") + .AsInteractionRequest(), + info, + ct + ); + } + + /// + public async ValueTask> GetInteractionResponseAsync + ( + Snowflake applicationId, + string interactionToken, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"webhooks/{applicationId}/{interactionToken}/messages/@original", + b => b.WithRoute($"GET webhooks/:application-id/{interactionToken}/messages/@original") + .AsInteractionRequest(), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/InviteRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/InviteRestAPI.cs new file mode 100644 index 0000000000..a941c45f42 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/InviteRestAPI.cs @@ -0,0 +1,76 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class InviteRestAPI(IRestClient restClient) + : IInviteRestAPI +{ + /// + public async ValueTask> DeleteInviteAsync + ( + string inviteCode, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"invites/{inviteCode}", + b => b.WithRoute($"DELETE invites/:invite-code") + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> GetInviteAsync + ( + string inviteCode, + GetInviteQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"invites/{inviteCode}"); + + if (query.WithCounts is not null) + { + _ = builder.AddParameter("with_counts", query.WithCounts.Value.ToString().ToLowerInvariant()); + } + + if (query.WithExpiration is not null) + { + _ = builder.AddParameter("with_expiration", query.WithExpiration.Value.ToString().ToLowerInvariant()); + } + + if (query.GuildScheduledEventId is not null) + { + _ = builder.AddParameter("guild_scheduled_event_id", query.GuildScheduledEventId.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithRoute($"GET invites/:invite-code"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/MessageRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/MessageRestAPI.cs new file mode 100644 index 0000000000..1332e76dad --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/MessageRestAPI.cs @@ -0,0 +1,416 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class MessageRestAPI(IRestClient restClient) + : IMessageRestAPI +{ + /// + public async ValueTask BulkDeleteMessagesAsync + ( + Snowflake channelId, + IBulkDeleteMessagesPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/messages/bulk-delete", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> CreateMessageAsync + ( + Snowflake channelId, + ICreateMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if + ( + !(payload.Content.HasValue || payload.Embeds.HasValue || payload.StickerIds.HasValue + || payload.Components.HasValue || payload.Files is not null) + ) + { + return new ValidationError + ( + "At least one of Content, Embeds, StickerIds, Components or Files must be sent." + ); + } + + if (payload.Content.HasValue && payload.Content.Value.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (payload.Nonce.HasValue && payload.Nonce.Value.Length > 25) + { + return new ValidationError("The nonce of a message cannot exceed 25 characters."); + } + + if (payload.Embeds.HasValue && payload.Embeds.Value.Count > 10) + { + return new ValidationError("Only up to 10 embeds can be sent with a message."); + } + + if (payload.StickerIds.HasValue && payload.StickerIds.Value.Count > 3) + { + return new ValidationError("Only up to 3 stickers can be sent with a message."); + } + + if (payload.Poll.TryGetNonNullValue(out ICreatePoll? poll)) + { + if (poll.Question.Text is { HasValue: true, Value.Length: > 300 }) + { + return new ValidationError("The poll must specify a question that cannot exceed 300 characters."); + } + + if (poll.Answers.Any(answer => answer.PollMedia.Text is { HasValue: true, Value.Length: > 55 })) + { + return new ValidationError("The answer text of a poll cannot exceed 55 characters."); + } + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/messages", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask CreateReactionAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"PUT channels/{channelId}/messages/:message-id/reactions/:emoji/@me"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> CrosspostMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/messages/{messageId}/crosspost", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"POST channels/{channelId}/messages/:message-id/crosspost"), + info, + ct + ); + } + + /// + public async ValueTask DeleteAllReactionsAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/messages/{messageId}/reactions", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/messages/:message-id/reactions"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteAllReactionsForEmojiAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/messages/:message-id/reactions/:emoji"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/messages/{messageId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/messages/:message-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteOwnReactionAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/@me", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/messages/:message-id/reactions/:emoji/@me"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteUserReactionAsync + ( + Snowflake channelId, + Snowflake messageId, + Snowflake userId, + string emoji, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"channels/{channelId}/messages/{messageId}/reactions/{emoji}/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE channels/{channelId}/messages/:message-id/reactions/:emoji/:user-id"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> EditMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + IEditMessagePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Content.TryGetNonNullValue(out string? content) && content.Length > 2000) + { + return new ValidationError("The content of a message cannot exceed 2000 characters."); + } + + if (payload.Embeds.TryGetNonNullValue(out IReadOnlyList? embeds) && embeds.Count > 10) + { + return new ValidationError("A message can only have up to 10 embeds."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"channels/{channelId}/messages/{messageId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"PATCH channels/{channelId}/messages/:message-id") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> GetChannelMessageAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"channels/{channelId}/messages/{messageId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"GET channels/{channelId}/messages/:message-id"), + info, + ct + ); + } + + /// + public async ValueTask>> GetChannelMessagesAsync + ( + Snowflake channelId, + GetChannelMessagesQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit for messages to request at once must be between 1 and 100."); + } + + QueryBuilder builder = new($"channels/{channelId}/messages"); + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Around is not null) + { + _ = builder.AddParameter("around", query.Around.Value.ToString()); + } + else if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + else if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask>> GetReactionsAsync + ( + Snowflake channelId, + Snowflake messageId, + string emoji, + GetReactionsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is not null and (< 1 or > 100)) + { + return new ValidationError("The limit of reactions to request must be between 1 and 100."); + } + + QueryBuilder builder = new($"channels/{channelId}/messages/{messageId}/reactions/{emoji}"); + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + if (query.Limit is not null) + { + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Type is not null) + { + _ = builder.AddParameter("type", ((int)query.Type.Value).ToString(CultureInfo.InvariantCulture)); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"GET channels/{channelId}/messages/:message-id/reactions/:emoji"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/PollRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/PollRestAPI.cs new file mode 100644 index 0000000000..193e039d61 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/PollRestAPI.cs @@ -0,0 +1,83 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class PollRestAPI(IRestClient restClient) + : IPollRestAPI +{ + /// + public async ValueTask> EndPollAsync + ( + Snowflake channelId, + Snowflake messageId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/polls/{messageId}/expire", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"POST channels/{channelId}/polls/:message-id/expire"), + info, + ct + ); + } + + /// + public async ValueTask> GetAnswerVotersAsync + ( + Snowflake channelId, + Snowflake messageId, + int answerId, + ForwardsPaginatedQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"channels/{channelId}/polls/{messageId}/answers/{answerId}"); + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + if (query.Limit is not null) + { + if (query.Limit is < 1 or > 100) + { + return new ValidationError("The provided limit must be between 1 and 100."); + } + + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"POST channels/{channelId}/polls/:message-id/answers/:answer-id"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/RoleConnectionsRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/RoleConnectionsRestAPI.cs new file mode 100644 index 0000000000..cc8d9e518c --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/RoleConnectionsRestAPI.cs @@ -0,0 +1,66 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class RoleConnectionsRestAPI(IRestClient restClient) + : IRoleConnectionsRestAPI +{ + /// + public async ValueTask>> GetRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"applications/{applicationId}/role-connections/metadata", + b => b.WithRoute("GET applications/:application-id/role-connections/metadata"), + info, + ct + ); + } + + /// + public async ValueTask>> UpdateRoleConnectionMetadataRecordsAsync + ( + Snowflake applicationId, + IReadOnlyList payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Count > 5) + { + return new ValidationError("An application can have up to five role connection metadata records."); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Put, + $"applications/{applicationId}/role-connections/metadata", + b => b.WithRoute("PUT applications/:application-id/role-connections/metadata") + .WithPayload(payload), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/SkusRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/SkusRestAPI.cs new file mode 100644 index 0000000000..d1e28cdd08 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/SkusRestAPI.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class SkusRestAPI(IRestClient restClient) + : ISkusRestAPI +{ + /// + public async ValueTask>> ListSkusAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"applications/{applicationId}/skus", + b => b.WithRoute("GET applications/:application-id/skus"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/SoundboardRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/SoundboardRestAPI.cs new file mode 100644 index 0000000000..a1acdc4b5f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/SoundboardRestAPI.cs @@ -0,0 +1,197 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class SoundboardRestAPI(IRestClient restClient) + : ISoundboardRestAPI +{ + /// + public async ValueTask> CreateGuildSoundboardSoundAsync + ( + Snowflake guildId, + ICreateGuildSoundboardSoundPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length is < 2 or > 32) + { + return new ValidationError("The sound name must be between 2 and 32 characters."); + } + + if (payload.Volume.TryGetNonNullValue(out double? value) && value is < 0.0 or > 1.0) + { + return new ValidationError("The sound volume must be between 0.0 and 1.0."); + } + + if (payload.EmojiId.TryGetNonNullValue(out _) && payload.EmojiName.TryGetNonNullValue(out _)) + { + return new ValidationError("A sound must not have a custom emoji and an unicode emoji defined at the same time."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/soundboard-sounds", + b => b.WithPayload(payload) + .WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteGuildSoundboardSoundAsync + ( + Snowflake guildId, + Snowflake soundId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/soundboard-sounds/{soundId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"POST guilds/{guildId}/soundboard-sounds/:sound-id") + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> GetGuildSoundboardSoundAsync + ( + Snowflake guildId, + Snowflake soundId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/soundboard-sounds/{soundId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/soundboard-sounds/:sound-id"), + info, + ct + ); + } + + /// + public async ValueTask>> ListDefaultSoundboardSoundsAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + "soundboard-default-sounds", + info: info, + ct: ct + ); + } + + /// + public async ValueTask> ListGuildSoundboardSoundsAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/soundboard-sounds/", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildSoundboardSoundAsync + ( + Snowflake guildId, + Snowflake soundId, + IModifyGuildSoundboardSoundPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.TryGetNonNullValue(out string? name) && name.Length is < 2 or > 32) + { + return new ValidationError("The sound name must be between 2 and 32 characters."); + } + + if (payload.Volume.TryGetNonNullValue(out double? value) && value is < 0.0 or > 1.0) + { + return new ValidationError("The sound volume must be between 0.0 and 1.0."); + } + + if (payload.EmojiId.TryGetNonNullValue(out _) && payload.EmojiName.TryGetNonNullValue(out _)) + { + return new ValidationError("A sound must not have a custom emoji and an unicode emoji defined at the same time."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/soundboard-sounds", + b => b.WithPayload(payload) + .WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask SendSoundboardSoundAsync + ( + Snowflake channelId, + ISendSoundboardSoundPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/send-soundboard-sound", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithPayload(payload), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/StageInstanceRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/StageInstanceRestAPI.cs new file mode 100644 index 0000000000..c0a526db2d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/StageInstanceRestAPI.cs @@ -0,0 +1,119 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class StageInstanceRestAPI(IRestClient restClient) + : IStageInstanceRestAPI +{ + /// + public async ValueTask> CreateStageInstanceAsync + ( + ICreateStageInstancePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Topic.Length > 120) + { + return new ValidationError("The length of a stage topic cannot exceed 120 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"stage-instances", + b => b.WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteStageInstanceAsync + ( + Snowflake channelId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"stage-instances/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"DELETE stage-instances/{channelId}") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> GetStageInstanceAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"stage-instances/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"GET stage-instances/{channelId}"), + info, + ct + ); + } + + /// + public async ValueTask> ModifyStageInstanceAsync + ( + Snowflake channelId, + IModifyStageInstancePayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Topic.HasValue && payload.Topic.Value.Length > 120) + { + return new ValidationError("The length of a stage topic cannot exceed 120 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"stage-instances/{channelId}", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithRoute($"PATCH stage-instances/{channelId}") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/StickerRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/StickerRestAPI.cs new file mode 100644 index 0000000000..cadca2a27d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/StickerRestAPI.cs @@ -0,0 +1,223 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Responses; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class StickerRestAPI(IRestClient restClient) + : IStickerRestAPI +{ + /// + public async ValueTask> CreateGuildStickerAsync + ( + Snowflake guildId, + ICreateGuildStickerPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length is < 2 or > 30) + { + return new ValidationError("Sticker names must be between 2 and 30 characters in length."); + } + + if (!string.IsNullOrWhiteSpace(payload.Description) && payload.Description.Length is < 2 or > 100) + { + return new ValidationError("Sticker descriptions must be either empty or between 2 and 100 characters in length."); + } + + if (payload.Tags.Length > 200) + { + return new ValidationError("Sticker tags must not exceed 200 characters in length."); + } + + return await restClient.ExecuteMultipartPayloadRequestAsync + ( + HttpMethod.Post, + $"guilds/{guildId}/stickers", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteGuildStickerAsync + ( + Snowflake guildId, + Snowflake stickerId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"guilds/{guildId}/stickers/{stickerId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE guilds/{guildId}/stickers/:sticker-id") + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> GetGuildStickerAsync + ( + Snowflake guildId, + Snowflake stickerId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/stickers/{stickerId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/stickers/:sticker-id"), + info, + ct + ); + } + + /// + public async ValueTask> GetStickerAsync + ( + Snowflake stickerId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"stickers/{stickerId}", + b => b.WithRoute("GET stickers/:sticker-id"), + info, + ct + ); + } + + /// + public async ValueTask> GetStickerPackAsync + ( + Snowflake packId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"sticker-packs/{packId}", + b => b.WithRoute("GET sticker-packs/:pack-id"), + info, + ct + ); + } + + /// + public async ValueTask>> ListGuildStickersAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/stickers", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/stickers"), + info, + ct + ); + } + + /// + public async ValueTask> ListStickerPacksAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + "sticker-packs", + b => b.WithRoute("GET sticker-packs"), + info, + ct + ); + } + + /// + public async ValueTask> ModifyGuildStickerAsync + ( + Snowflake guildId, + Snowflake stickerId, + IModifyGuildStickerPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue && payload.Name.Value.Length is < 2 or > 30) + { + return new ValidationError("Sticker names must be between 2 and 30 characters in length."); + } + + if + ( + payload.Description.TryGetNonNullValue(out string? description) + && !string.IsNullOrWhiteSpace(description) + && description.Length is < 2 or > 100 + ) + { + return new ValidationError("Sticker descriptions must be either empty or between 2 and 100 characters in length."); + } + + if (payload.Tags.HasValue && payload.Tags.Value.Length > 200) + { + return new ValidationError("Sticker tags must not exceed 200 characters in length."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/stickers/{stickerId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PATCH guilds/{guildId}/stickers/:sticker-id") + .WithPayload(payload) + .WithAuditLogReason(reason), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/SubscriptionRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/SubscriptionRestAPI.cs new file mode 100644 index 0000000000..819e42b981 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/SubscriptionRestAPI.cs @@ -0,0 +1,90 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class SubscriptionRestAPI(IRestClient restClient) + : ISubscriptionRestAPI +{ + /// + public async ValueTask> GetSkuSubscriptionAsync + ( + Snowflake skuId, + Snowflake subscriptionId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteMultipartPayloadRequestAsync + ( + HttpMethod.Get, + $"skus/{skuId}/subscriptions/{subscriptionId}", + b => b.WithRoute("GET skus/:sku-id/subscriptions/:subscription-id"), + info, + ct + ); + } + + /// + public async ValueTask>> ListSkuSubscriptionsAsync + ( + Snowflake skuId, + ListSkuSubscriptionsQuery query, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (query.Limit is < 1 or > 100) + { + return new ValidationError("The limit of subscriptions to query must be between 1 and 100."); + } + + QueryBuilder queryBuilder = new($"skus/{skuId}/subscriptions"); + + if (query.Before is Snowflake before) + { + _ = queryBuilder.AddParameter("before", before.ToString()); + } + + if (query.After is Snowflake after) + { + _ = queryBuilder.AddParameter("after", after.ToString()); + } + + if (query.Limit is int limit) + { + _ = queryBuilder.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } + + if (query.UserId is Snowflake userId) + { + _ = queryBuilder.AddParameter("user_id", userId.ToString()); + } + + return await restClient.ExecuteMultipartPayloadRequestAsync> + ( + HttpMethod.Get, + queryBuilder.Build(), + b => b.WithRoute("GET skus/:sku-id/subscriptions"), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/UserRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/UserRestAPI.cs new file mode 100644 index 0000000000..7db4abed6e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/UserRestAPI.cs @@ -0,0 +1,269 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class UserRestAPI(IRestClient restClient) + : IUserRestAPI +{ + /// + public async ValueTask> CreateDmAsync + ( + ICreateDmPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + "users/@me/channels", + b => b.WithRoute($"POST users/@me/channels") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> CreateGroupDmAsync + ( + ICreateGroupDmPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + "users/@me/channels", + b => b.WithRoute($"POST users/@me/channels") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> GetCurrentUserApplicationRoleConnectionAsync + ( + Snowflake applicationId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"users/@me/applications/{applicationId}/role-connection", + b => b.WithRoute($"GET users/@me/applications/:application-id/role-connection"), + info, + ct + ); + } + + /// + public async ValueTask> GetCurrentUserAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + "users/@me", + b => b.WithRoute($"GET users/@me"), + info, + ct + ); + } + + /// + public async ValueTask>> GetCurrentUserConnectionsAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + "users/@me/connections", + b => b.WithRoute("GET users/@me/connections"), + info, + ct + ); + } + + /// + public async ValueTask> GetCurrentUserGuildMemberAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"users/@me/guilds/{guildId}/member", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET users/@me/guilds/{guildId}/member"), + info, + ct + ); + } + + /// + public async ValueTask>> GetCurrentUserGuildsAsync + ( + GetCurrentUserGuildsQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new("users/@me/guilds"); + + if (query.Limit is not null) + { + if (query.Limit.Value is < 1 or > 200) + { + return new ValidationError("The limit of guilds to request must be between 1 and 200."); + } + + _ = builder.AddParameter("limit", query.Limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (query.Before is not null) + { + _ = builder.AddParameter("before", query.Before.Value.ToString()); + } + + if (query.After is not null) + { + _ = builder.AddParameter("after", query.After.Value.ToString()); + } + + if (query.WithCounts is not null) + { + _ = builder.AddParameter("with_counts", query.WithCounts.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + builder.Build(), + b => b.WithRoute("GET users/@me/guilds"), + info, + ct + ); + } + + /// + public async ValueTask> GetUserAsync + ( + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"users/{userId}", + b => b.WithRoute("GET users/:user-id"), + info, + ct + ); + } + + /// + public async ValueTask LeaveGuildAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"users/@me/guilds/{guildId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"DELETE users/@me/guilds/{guildId}"), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> ModifyCurrentUserAsync + ( + IModifyCurrentUserPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + "users/@me", + b => b.WithRoute("PATCH users/@me") + .WithPayload(payload), + info, + ct + ); + } + + /// + public async ValueTask> UpdateCurrentUserApplicationRoleConnectionAsync + ( + Snowflake applicationId, + IUpdateCurrentUserApplicationRoleConnectionPayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.PlatformName.HasValue && payload.PlatformName.Value.Length > 50) + { + return new ValidationError("The platform name of a role connection cannot exceed 50 characters."); + } + + if (payload.PlatformUsername.HasValue && payload.PlatformUsername.Value.Length > 100) + { + return new ValidationError("The platform username of a role connection cannot exceed 100 characters."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Put, + $"users/@me/applications/{applicationId}/role-connection", + b => b.WithRoute("PUT users/@me/applications/:application-id/role-connection") + .WithPayload(payload), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/VoiceRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/VoiceRestAPI.cs new file mode 100644 index 0000000000..8becbef7ae --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/VoiceRestAPI.cs @@ -0,0 +1,123 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class VoiceRestAPI(IRestClient restClient) + : IVoiceRestAPI +{ + /// + public async ValueTask> GetCurrentUserVoiceStateAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/voice-states/@me", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetUserVoiceStateAsync + ( + Snowflake guildId, + Snowflake userId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"guilds/{guildId}/voice-states/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"GET guilds/{guildId}/voice-states/:user-id"), + info, + ct + ); + } + + /// + public async ValueTask>> ListVoiceRegionsAsync + ( + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"voice/regions", + b => b.WithRoute("GET voice/regions"), + info, + ct + ); + } + + /// + public async ValueTask ModifyCurrentUserVoiceStateAsync + ( + Snowflake guildId, + IModifyCurrentUserVoiceStatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/voice-states/@me", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithPayload(payload), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask ModifyUserVoiceStateAsync + ( + Snowflake guildId, + Snowflake userId, + IModifyUserVoiceStatePayload payload, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"guilds/{guildId}/voice-states/{userId}", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId) + .WithRoute($"PATCH guilds/{guildId}/voice-states/:user-id") + .WithPayload(payload), + info, + ct + ); + + return (Result)response; + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/API/WebhookRestAPI.cs b/src/core/DSharpPlus.Internal.Rest/API/WebhookRestAPI.cs new file mode 100644 index 0000000000..326e8dca65 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/API/WebhookRestAPI.cs @@ -0,0 +1,465 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Abstractions.Rest.Queries; +using DSharpPlus.Internal.Rest.Ratelimiting; +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.API; + +/// +public sealed class WebhookRestAPI(IRestClient restClient) + : IWebhookRestAPI +{ + /// + public async ValueTask> CreateWebhookAsync + ( + Snowflake channelId, + ICreateWebhookPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.Length > 80) + { + return new ValidationError("A webhook name cannot exceed 80 characters."); + } + + if + ( + payload.Name.Contains("discord", StringComparison.InvariantCultureIgnoreCase) + || payload.Name.Contains("clyde", StringComparison.InvariantCultureIgnoreCase) + ) + { + return new ValidationError("A webhook name cannot contain the substrings \"clyde\" or \"discord\"."); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Post, + $"channels/{channelId}/webhooks", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask DeleteWebhookAsync + ( + Snowflake webhookId, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"webhooks/{webhookId}", + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithAuditLogReason(reason), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteWebhookMessageAsync + ( + Snowflake webhookId, + string webhookToken, + Snowflake messageId, + ThreadIdQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"webhooks/{webhookId}/{webhookToken}/messages/{messageId}"); + + if (query.ThreadId is not null) + { + _ = builder.AddParameter("thread_id", query.ThreadId.Value.ToString()); + } + + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + builder.ToString(), + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithRoute($"DELETE webhooks/{webhookId}/:webhook-token/messages/:message-id") + .AsWebhookRequest(), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask DeleteWebhookWithTokenAsync + ( + Snowflake webhookId, + string webhookToken, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Delete, + $"webhooks/{webhookId}/{webhookToken}", + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithRoute($"DELETE webhooks/{webhookId}/:webhook-token") + .WithAuditLogReason(reason) + .AsWebhookRequest(), + info, + ct + ); + + return (Result)response; + } + + /// + public async ValueTask> EditWebhookMessageAsync + ( + Snowflake webhookId, + string webhookToken, + Snowflake messageId, + IEditWebhookMessagePayload payload, + EditWebhookMessageQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Content.TryGetNonNullValue(out string? content) && content.Length > 2000) + { + return new ValidationError("A webhook message cannot exceed 2000 characters in length."); + } + + if (payload.Embeds.TryGetNonNullValue(out IReadOnlyList? embeds) && embeds.Count > 10) + { + return new ValidationError("A webhook message cannot contain more than 10 embeds."); + } + + if (payload.Components.TryGetNonNullValue(out IReadOnlyList? components) && components.Count > 5) + { + return new ValidationError("A webhook message cannot contain more than 5 action rows."); + } + + QueryBuilder builder = new($"webhooks/{webhookId}/{webhookToken}/messages/{messageId}"); + + if (query.ThreadId is not null) + { + _ = builder.AddParameter("thread_id", query.ThreadId.Value.ToString()); + } + + if (query.WithComponents is not null) + { + _ = builder.AddParameter("with_components", query.WithComponents.Value.ToString().ToLowerInvariant()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithRoute($"PATCH webhooks/{webhookId}/:webhook-token/messages/:message-id") + .WithPayload(payload) + .AsWebhookRequest(), + info, + ct + ); + } + + /// + public async ValueTask> ExecuteWebhookAsync + ( + Snowflake webhookId, + string webhookToken, + IExecuteWebhookPayload payload, + ExecuteWebhookQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Content.TryGetNonNullValue(out string? content) && content.Length > 2000) + { + return new ValidationError("A webhook message cannot exceed 2000 characters in length."); + } + + if (payload.Embeds.TryGetNonNullValue(out IReadOnlyList? embeds) && embeds.Count > 10) + { + return new ValidationError("A webhook message cannot contain more than 10 embeds."); + } + + if (payload.Components.TryGetNonNullValue(out IReadOnlyList? components) && components.Count > 5) + { + return new ValidationError("A webhook message cannot contain more than 5 action rows."); + } + + if (payload.Poll.TryGetNonNullValue(out ICreatePoll? poll)) + { + if (poll.Question.Text is { HasValue: true, Value.Length: > 300 }) + { + return new ValidationError("The poll must specify a question that cannot exceed 300 characters."); + } + + if (poll.Answers.Any(answer => answer.PollMedia.Text is { HasValue: true, Value.Length: > 55 })) + { + return new ValidationError("The answer text of a poll cannot exceed 55 characters."); + } + } + + QueryBuilder builder = new($"webhooks/{webhookId}/{webhookToken}"); + + if (query.ThreadId is not null) + { + _ = builder.AddParameter("thread_id", query.ThreadId.Value.ToString()); + } + + if (query.Wait is not null) + { + _ = builder.AddParameter("wait", query.Wait.Value.ToString().ToLowerInvariant()); + } + + if (query.WithComponents is not null) + { + _ = builder.AddParameter("with_components", query.WithComponents.Value.ToString().ToLowerInvariant()); + } + + if (query.Wait == true) + { +#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithRoute($"PATCH webhooks/{webhookId}/:webhook-token/messages/:message-id") + .WithPayload(payload) + .AsWebhookRequest(), + info, + ct + ); +#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. + } + else + { + Result response = await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithRoute($"PATCH webhooks/{webhookId}/:webhook-token/messages/:message-id") + .WithPayload(payload) + .AsWebhookRequest(), + info, + ct + ); + + return response.Map(message => null); + } + } + + /// + public async ValueTask>> GetChannelWebhooksAsync + ( + Snowflake channelId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"channels/{channelId}/webhooks", + b => b.WithSimpleRoute(TopLevelResource.Channel, channelId), + info, + ct + ); + } + + /// + public async ValueTask>> GetGuildWebhooksAsync + ( + Snowflake guildId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync> + ( + HttpMethod.Get, + $"guilds/{guildId}/webhooks", + b => b.WithSimpleRoute(TopLevelResource.Guild, guildId), + info, + ct + ); + } + + /// + public async ValueTask> GetWebhookAsync + ( + Snowflake webhookId, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"webhooks/{webhookId}", + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId), + info, + ct + ); + } + + /// + public async ValueTask> GetWebhookMessageAsync + ( + Snowflake webhookId, + string webhookToken, + Snowflake messageId, + ThreadIdQuery query = default, + RequestInfo info = default, + CancellationToken ct = default + ) + { + QueryBuilder builder = new($"webhooks/{webhookId}/{webhookToken}/messages/{messageId}"); + + if (query.ThreadId is not null) + { + _ = builder.AddParameter("thread_id", query.ThreadId.Value.ToString()); + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + builder.Build(), + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithRoute($"PATCH webhooks/{webhookId}/:webhook-token/messages/:message-id"), + info, + ct + ); + } + + /// + public async ValueTask> GetWebhookWithTokenAsync + ( + Snowflake webhookId, + string webhookToken, + RequestInfo info = default, + CancellationToken ct = default + ) + { + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Get, + $"webhooks/{webhookId}/{webhookToken}", + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .AsWebhookRequest(), + info, + ct + ); + } + + /// + public async ValueTask> ModifyWebhookAsync + ( + Snowflake webhookId, + IModifyWebhookPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue) + { + if (payload.Name.Value.Length > 80) + { + return new ValidationError("A webhook name cannot exceed 80 characters."); + } + + if + ( + payload.Name.Value.Contains("discord", StringComparison.InvariantCultureIgnoreCase) + || payload.Name.Value.Contains("clyde", StringComparison.InvariantCultureIgnoreCase) + ) + { + return new ValidationError("A webhook name cannot contain the substrings \"clyde\" or \"discord\"."); + } + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"webhooks/{webhookId}", + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithAuditLogReason(reason), + info, + ct + ); + } + + /// + public async ValueTask> ModifyWebhookWithTokenAsync + ( + Snowflake webhookId, + string webhookToken, + IModifyWebhookWithTokenPayload payload, + string? reason = null, + RequestInfo info = default, + CancellationToken ct = default + ) + { + if (payload.Name.HasValue) + { + if (payload.Name.Value.Length > 80) + { + return new ValidationError("A webhook name cannot exceed 80 characters."); + } + + if + ( + payload.Name.Value.Contains("discord", StringComparison.InvariantCultureIgnoreCase) + || payload.Name.Value.Contains("clyde", StringComparison.InvariantCultureIgnoreCase) + ) + { + return new ValidationError("A webhook name cannot contain the substrings \"clyde\" or \"discord\"."); + } + } + + return await restClient.ExecuteRequestAsync + ( + HttpMethod.Patch, + $"webhooks/{webhookId}/{webhookToken}", + b => b.WithSimpleRoute(TopLevelResource.Webhook, webhookId) + .WithAuditLogReason(reason) + .AsWebhookRequest(), + info, + ct + ); + } +} diff --git a/src/core/DSharpPlus.Internal.Rest/DSharpPlus.Internal.Rest.csproj b/src/core/DSharpPlus.Internal.Rest/DSharpPlus.Internal.Rest.csproj new file mode 100644 index 0000000000..6d413c4121 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/DSharpPlus.Internal.Rest.csproj @@ -0,0 +1,28 @@ + + + + $(_DSharpPlusInternalRestVersion) + + + + + + + + + + + + + + + + + + + + <_Parameter1>DSharpPlus.Internal.Rest.Tests + + + + diff --git a/src/core/DSharpPlus.Internal.Rest/Extensions/ServiceCollectionExtensions.Registration.cs b/src/core/DSharpPlus.Internal.Rest/Extensions/ServiceCollectionExtensions.Registration.cs new file mode 100644 index 0000000000..b0d0045c8a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Extensions/ServiceCollectionExtensions.Registration.cs @@ -0,0 +1,94 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; +using DSharpPlus.Internal.Rest.Payloads; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Internal.Rest.Extensions; + +partial class ServiceCollectionExtensions +{ + private static void RegisterSerialization(IServiceCollection services) + { + services.Configure + ( + options => + { + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + options.AddModel(); + } + ); + } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/CreateGlobalApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/CreateGlobalApplicationCommandPayload.cs new file mode 100644 index 0000000000..50d4d08f7f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/CreateGlobalApplicationCommandPayload.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGlobalApplicationCommandPayload : ICreateGlobalApplicationCommandPayload +{ + /// + public required string Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional DefaultMemberPermissions { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public required IReadOnlyList IntegrationTypes { get; init; } + + /// + public required IReadOnlyList Contexts { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/CreateGuildApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/CreateGuildApplicationCommandPayload.cs new file mode 100644 index 0000000000..411ee34778 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/CreateGuildApplicationCommandPayload.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildApplicationCommandPayload : ICreateGuildApplicationCommandPayload +{ + /// + public required string Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } + + /// + public Optional DefaultMemberPermissions { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Nsfw { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/EditGlobalApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/EditGlobalApplicationCommandPayload.cs new file mode 100644 index 0000000000..b406a2d6dd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/EditGlobalApplicationCommandPayload.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditGlobalApplicationCommandPayload : IEditGlobalApplicationCommandPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional DefaultMemberPermissions { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public required IReadOnlyList IntegrationTypes { get; init; } + + /// + public required IReadOnlyList Contexts { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/EditGuildApplicationCommandPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/EditGuildApplicationCommandPayload.cs new file mode 100644 index 0000000000..b02f6c9155 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/ApplicationCommands/EditGuildApplicationCommandPayload.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditGuildApplicationCommandPayload : IEditGuildApplicationCommandPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional?> NameLocalizations { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional?> DescriptionLocalizations { get; init; } + + /// + public Optional> Options { get; init; } + + /// + public Optional DefaultMemberPermissions { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Nsfw { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Applications/EditCurrentApplicationPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Applications/EditCurrentApplicationPayload.cs new file mode 100644 index 0000000000..ab4be9fae3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Applications/EditCurrentApplicationPayload.cs @@ -0,0 +1,42 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditCurrentApplicationPayload : IEditCurrentApplicationPayload +{ + /// + public Optional CustomInstallUrl { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional RoleConnectionsVerificationUrl { get; init; } + + /// + public Optional InstallParams { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional CoverImage { get; init; } + + /// + public Optional InteractionsEndpointUrl { get; init; } + + /// + public Optional> Tags { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/AutoModeration/CreateAutoModerationRulePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/AutoModeration/CreateAutoModerationRulePayload.cs new file mode 100644 index 0000000000..d6738d0184 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/AutoModeration/CreateAutoModerationRulePayload.cs @@ -0,0 +1,39 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateAutoModerationRulePayload : ICreateAutoModerationRulePayload +{ + /// + public required string Name { get; init; } + + /// + public required DiscordAutoModerationEventType EventType { get; init; } + + /// + public required DiscordAutoModerationTriggerType TriggerType { get; init; } + + /// + public Optional TriggerMetadata { get; init; } + + /// + public required IReadOnlyList Actions { get; init; } + + /// + public Optional Enabled { get; init; } + + /// + public Optional> ExemptRoles { get; init; } + + /// + public Optional> ExemptChannels { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/AutoModeration/ModifyAutoModerationRulePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/AutoModeration/ModifyAutoModerationRulePayload.cs new file mode 100644 index 0000000000..e5614894e6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/AutoModeration/ModifyAutoModerationRulePayload.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyAutoModerationRulePayload : IModifyAutoModerationRulePayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional EventType { get; init; } + + /// + public Optional TriggerMetadata { get; init; } + + /// + public Optional> Actions { get; init; } + + /// + public Optional Enabled { get; init; } + + /// + public Optional> ExemptRoles { get; init; } + + /// + public Optional> ExemptChannels { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/CreateChannelInvitePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/CreateChannelInvitePayload.cs new file mode 100644 index 0000000000..e2471e9e80 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/CreateChannelInvitePayload.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateChannelInvitePayload : ICreateChannelInvitePayload +{ + /// + public Optional MaxAge { get; init; } + + /// + public Optional MaxUses { get; init; } + + /// + public Optional Temporary { get; init; } + + /// + public Optional Unique { get; init; } + + /// + public Optional TargetType { get; init; } + + /// + public Optional TargetUserId { get; init; } + + /// + public Optional TargetApplicationId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/EditChannelPermissionsPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/EditChannelPermissionsPayload.cs new file mode 100644 index 0000000000..1dfab8efd5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/EditChannelPermissionsPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditChannelPermissionsPayload : IEditChannelPermissionsPayload +{ + /// + public required DiscordChannelOverwriteType Type { get; init; } + + /// + public Optional Allow { get; init; } + + /// + public Optional Deny { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/FollowAnnouncementChannelPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/FollowAnnouncementChannelPayload.cs new file mode 100644 index 0000000000..2906fb6a72 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/FollowAnnouncementChannelPayload.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record FollowAnnouncementChannelPayload : IFollowAnnouncementChannelPayload +{ + /// + public required Snowflake WebhookChannelId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ForumAndMediaThreadMessage.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ForumAndMediaThreadMessage.cs new file mode 100644 index 0000000000..cfd9b2bbc8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ForumAndMediaThreadMessage.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ForumAndMediaThreadMessage : IForumAndMediaThreadMessage +{ + /// + public Optional Content { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional> StickerIds { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Flags { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/GroupDMAddRecipientPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/GroupDMAddRecipientPayload.cs new file mode 100644 index 0000000000..481bf65031 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/GroupDMAddRecipientPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record GroupDMAddRecipientPayload : IGroupDMAddRecipientPayload +{ + /// + public required string AccessToken { get; init; } + + /// + public required string Nick { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyGroupDMPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyGroupDMPayload.cs new file mode 100644 index 0000000000..2a58fd64fd --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyGroupDMPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGroupDMPayload : IModifyGroupDMPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Icon { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyGuildChannelPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyGuildChannelPayload.cs new file mode 100644 index 0000000000..29f48bbbb8 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyGuildChannelPayload.cs @@ -0,0 +1,72 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildChannelPayload : IModifyGuildChannelPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional Topic { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public Optional Bitrate { get; init; } + + /// + public Optional UserLimit { get; init; } + + /// + public Optional?> PermissionOverwrites { get; init; } + + /// + public Optional ParentId { get; init; } + + /// + public Optional RtcRegion { get; init; } + + /// + public Optional VideoQualityMode { get; init; } + + /// + public Optional DefaultAutoArchiveDuration { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional> AvailableTags { get; init; } + + /// + public Optional DefaultReactionEmoji { get; init; } + + /// + public Optional DefaultThreadRateLimitPerUser { get; init; } + + /// + public Optional DefaultSortOrder { get; init; } + + /// + public Optional DefaultForumLayout { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyThreadChannelPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyThreadChannelPayload.cs new file mode 100644 index 0000000000..fbc487d386 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/ModifyThreadChannelPayload.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyThreadChannelPayload : IModifyThreadChannelPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Archived { get; init; } + + /// + public Optional AutoArchiveDuration { get; init; } + + /// + public Optional Locked { get; init; } + + /// + public Optional Invitable { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional> AppliedTags { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadFromMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadFromMessagePayload.cs new file mode 100644 index 0000000000..6d949afd19 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadFromMessagePayload.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record StartThreadFromMessagePayload : IStartThreadFromMessagePayload +{ + /// + public required string Name { get; init; } + + /// + public Optional AutoArchiveDuration { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadInForumOrMediaChannelPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadInForumOrMediaChannelPayload.cs new file mode 100644 index 0000000000..c9a7a1d289 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadInForumOrMediaChannelPayload.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record StartThreadInForumOrMediaChannelPayload : IStartThreadInForumOrMediaChannelPayload +{ + /// + public required string Name { get; init; } + + /// + public Optional AutoArchiveDuration { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public required IForumAndMediaThreadMessage Message { get; init; } + + /// + public Optional> AppliedTags { get; init; } + + /// + public IReadOnlyList? Files { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadWithoutMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadWithoutMessagePayload.cs new file mode 100644 index 0000000000..f776b31819 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Channels/StartThreadWithoutMessagePayload.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record StartThreadWithoutMessagePayload : IStartThreadWithoutMessagePayload +{ + /// + public required string Name { get; init; } + + /// + public Optional AutoArchiveDuration { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public required DiscordChannelType Type { get; init; } + + /// + public Optional Invitable { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/CreateApplicationEmojiPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/CreateApplicationEmojiPayload.cs new file mode 100644 index 0000000000..bc1dd2a503 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/CreateApplicationEmojiPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateApplicationEmojiPayload : ICreateApplicationEmojiPayload +{ + /// + public required string Name { get; init; } + + /// + public required InlineMediaData Image { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/CreateGuildEmojiPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/CreateGuildEmojiPayload.cs new file mode 100644 index 0000000000..fdb1efe990 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/CreateGuildEmojiPayload.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildEmojiPayload : ICreateGuildEmojiPayload +{ + /// + public required string Name { get; init; } + + /// + public required InlineMediaData Image { get; init; } + + /// + public Optional> Roles { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/ModifyApplicationEmojiPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/ModifyApplicationEmojiPayload.cs new file mode 100644 index 0000000000..c961501d61 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/ModifyApplicationEmojiPayload.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyApplicationEmojiPayload : IModifyApplicationEmojiPayload +{ + /// + public required string Name { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/ModifyGuildEmojiPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/ModifyGuildEmojiPayload.cs new file mode 100644 index 0000000000..d360660d5d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Emojis/ModifyGuildEmojiPayload.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildEmojiPayload : IModifyGuildEmojiPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional> Roles { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Entitlements/CreateTestEntitlementPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Entitlements/CreateTestEntitlementPayload.cs new file mode 100644 index 0000000000..ea48d30b9f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Entitlements/CreateTestEntitlementPayload.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateTestEntitlementPayload : ICreateTestEntitlementPayload +{ + /// + public required Snowflake SkuId { get; init; } + + /// + public required Snowflake OwnerId { get; init; } + + /// + public required DiscordEntitlementOwnerType OwnerType { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/CreateGuildFromGuildTemplatePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/CreateGuildFromGuildTemplatePayload.cs new file mode 100644 index 0000000000..a47cbfd8be --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/CreateGuildFromGuildTemplatePayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildFromGuildTemplatePayload : ICreateGuildFromGuildTemplatePayload +{ + /// + public required string Name { get; init; } + + /// + public Optional Icon { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/CreateGuildTemplatePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/CreateGuildTemplatePayload.cs new file mode 100644 index 0000000000..d10b497d37 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/CreateGuildTemplatePayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildTemplatePayload : ICreateGuildTemplatePayload +{ + /// + public required string Name { get; init; } + + /// + public Optional Description { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/ModifyGuildTemplatePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/ModifyGuildTemplatePayload.cs new file mode 100644 index 0000000000..f802264bec --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/GuildTemplates/ModifyGuildTemplatePayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildTemplatePayload : IModifyGuildTemplatePayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Description { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/AddGuildMemberPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/AddGuildMemberPayload.cs new file mode 100644 index 0000000000..80302eea4a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/AddGuildMemberPayload.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record AddGuildMemberPayload : IAddGuildMemberPayload +{ + /// + public required string AccessToken { get; init; } + + /// + public Optional Nickname { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional Mute { get; init; } + + /// + public Optional Deaf { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/BeginGuildPrunePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/BeginGuildPrunePayload.cs new file mode 100644 index 0000000000..62c548da28 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/BeginGuildPrunePayload.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record BeginGuildPrunePayload : IBeginGuildPrunePayload +{ + /// + public int? Days { get; init; } + + /// + public string? IncludeRoles { get; init; } + + /// + public bool? ComputeCount { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/BulkGuildBanPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/BulkGuildBanPayload.cs new file mode 100644 index 0000000000..9d3fb59eea --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/BulkGuildBanPayload.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record BulkGuildBanPayload : IBulkGuildBanPayload +{ + /// + public required IReadOnlyList UserIds { get; init; } + + /// + public Optional DeleteMessageSeconds { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildChannelPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildChannelPayload.cs new file mode 100644 index 0000000000..abeb172fd2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildChannelPayload.cs @@ -0,0 +1,69 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildChannelPayload : ICreateGuildChannelPayload +{ + /// + public required string Name { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Topic { get; init; } + + /// + public Optional Bitrate { get; init; } + + /// + public Optional UserLimit { get; init; } + + /// + public Optional RateLimitPerUser { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional?> PermissionOverwrites { get; init; } + + /// + public Optional ParentId { get; init; } + + /// + public Optional Nsfw { get; init; } + + /// + public Optional RtcRegion { get; init; } + + /// + public Optional VideoQualityMode { get; init; } + + /// + public Optional DefaultAutoArchiveDuration { get; init; } + + /// + public Optional DefaultReactionEmoji { get; init; } + + /// + public Optional?> AvailableTags { get; init; } + + /// + public Optional DefaultSortOrder { get; init; } + + /// + public Optional DefaultForumLayout { get; init; } + + /// + public Optional DefaultThreadRateLimitPerUser { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildPayload.cs new file mode 100644 index 0000000000..385b982370 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildPayload.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildPayload : ICreateGuildPayload +{ + /// + public required string Name { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional VerificationLevel { get; init; } + + /// + public Optional DefaultMessageNotifications { get; init; } + + /// + public Optional ExplicitContentFilter { get; init; } + + /// + public Optional> Roles { get; init; } + + /// + public Optional> Channels { get; init; } + + /// + public Optional AfkChannelId { get; init; } + + /// + public Optional AfkTimeout { get; init; } + + /// + public Optional SystemChannelId { get; init; } + + /// + public Optional SystemChannelFlags { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildRolePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildRolePayload.cs new file mode 100644 index 0000000000..045d445716 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/CreateGuildRolePayload.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildRolePayload : ICreateGuildRolePayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional Color { get; init; } + + /// + public Optional Hoist { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional UnicodeEmoji { get; init; } + + /// + public Optional Mentionable { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyCurrentMemberPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyCurrentMemberPayload.cs new file mode 100644 index 0000000000..9217d9c9eb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyCurrentMemberPayload.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyCurrentMemberPayload : IModifyCurrentMemberPayload +{ + /// + public Optional Nick { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildChannelPositionsPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildChannelPositionsPayload.cs new file mode 100644 index 0000000000..1b4d5dd551 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildChannelPositionsPayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildChannelPositionsPayload : IModifyGuildChannelPositionsPayload +{ + /// + public required Snowflake ChannelId { get; init; } + + /// + public Optional Position { get; init; } + + /// + public Optional LockPermissions { get; init; } + + /// + public Optional ParentChannelId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildIncidentActionsPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildIncidentActionsPayload.cs new file mode 100644 index 0000000000..6f606bdcbf --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildIncidentActionsPayload.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildIncidentActionsPayload : IModifyGuildIncidentActionsPayload +{ + /// + public Optional InvitesDisabledUntil { get; init; } + + /// + public Optional DmsDisabledUntil { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildMemberPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildMemberPayload.cs new file mode 100644 index 0000000000..9692272dfb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildMemberPayload.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildMemberPayload : IModifyGuildMemberPayload +{ + /// + public Optional Nickname { get; init; } + + /// + public Optional?> Roles { get; init; } + + /// + public Optional Mute { get; init; } + + /// + public Optional Deaf { get; init; } + + /// + public Optional ChannelId { get; init; } + + /// + public Optional CommunicationDisabledUntil { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildMfaLevelPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildMfaLevelPayload.cs new file mode 100644 index 0000000000..a5c4fb2d9d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildMfaLevelPayload.cs @@ -0,0 +1,15 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildMfaLevelPayload : IModifyGuildMfaLevelPayload +{ + /// + public required DiscordMfaLevel Level { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildOnboardingPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildOnboardingPayload.cs new file mode 100644 index 0000000000..03b75258ea --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildOnboardingPayload.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildOnboardingPayload : IModifyGuildOnboardingPayload +{ + /// + public required IReadOnlyList Prompts { get; init; } + + /// + public required IReadOnlyList DefaultChannelIds { get; init; } + + /// + public required bool Enabled { get; init; } + + /// + public required DiscordGuildOnboardingMode Mode { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildPayload.cs new file mode 100644 index 0000000000..d3ecc90225 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildPayload.cs @@ -0,0 +1,74 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildPayload : IModifyGuildPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional VerificationLevel { get; init; } + + /// + public Optional DefaultMessageNotifications { get; init; } + + /// + public Optional ExplicitContentFilter { get; init; } + + /// + public Optional AfkChannelId { get; init; } + + /// + public Optional AfkTimeout { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional OwnerId { get; init; } + + /// + public Optional Splash { get; init; } + + /// + public Optional DiscoverySplash { get; init; } + + /// + public Optional Banner { get; init; } + + /// + public Optional SystemChannelId { get; init; } + + /// + public Optional SystemChannelFlags { get; init; } + + /// + public Optional RulesChannelId { get; init; } + + /// + public Optional PublicUpdatesChannelId { get; init; } + + /// + public Optional PreferredLocale { get; init; } + + /// + public Optional> Features { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional PremiumProgressBarEnabled { get; init; } + + /// + public Optional SafetyAlertsChannelId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildRolePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildRolePayload.cs new file mode 100644 index 0000000000..eea0f47f9f --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildRolePayload.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildRolePayload : IModifyGuildRolePayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Permissions { get; init; } + + /// + public Optional Color { get; init; } + + /// + public Optional Hoist { get; init; } + + /// + public Optional Icon { get; init; } + + /// + public Optional UnicodeEmoji { get; init; } + + /// + public Optional Mentionable { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildRolePositionsPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildRolePositionsPayload.cs new file mode 100644 index 0000000000..e454455230 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildRolePositionsPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildRolePositionsPayload : IModifyGuildRolePositionsPayload +{ + /// + public required Snowflake Id { get; init; } + + /// + public Optional Position { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildWelcomeScreenPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildWelcomeScreenPayload.cs new file mode 100644 index 0000000000..925a170b4d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Guilds/ModifyGuildWelcomeScreenPayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildWelcomeScreenPayload : IModifyGuildWelcomeScreenPayload +{ + /// + public Optional Enabled { get; init; } + + /// + public Optional?> WelcomeChannels { get; init; } + + /// + public Optional Description { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/CreateFollowupMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/CreateFollowupMessagePayload.cs new file mode 100644 index 0000000000..8b16a90ea4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/CreateFollowupMessagePayload.cs @@ -0,0 +1,42 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateFollowupMessagePayload : ICreateFollowupMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional Tts { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional Poll { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditFollowupMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditFollowupMessagePayload.cs new file mode 100644 index 0000000000..183f0ad2e2 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditFollowupMessagePayload.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditFollowupMessagePayload : IEditFollowupMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public Optional?> Attachments { get; init; } + + /// + public IReadOnlyList? Files { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditInteractionResponsePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditInteractionResponsePayload.cs new file mode 100644 index 0000000000..180701af7e --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Interactions/EditInteractionResponsePayload.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditInteractionResponsePayload : IEditInteractionResponsePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public Optional?> Attachments { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional Poll { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/BulkDeleteMessagesPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/BulkDeleteMessagesPayload.cs new file mode 100644 index 0000000000..af1c9237cb --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/BulkDeleteMessagesPayload.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record BulkDeleteMessagesPayload : IBulkDeleteMessagesPayload +{ + /// + public required IReadOnlyList Messages { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/CreateMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/CreateMessagePayload.cs new file mode 100644 index 0000000000..95dc0cdc27 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/CreateMessagePayload.cs @@ -0,0 +1,54 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateMessagePayload : ICreateMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional Nonce { get; init; } + + /// + public Optional Tts { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional MessageReference { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional> StickerIds { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional EnforceNonce { get; init; } + + /// + public Optional Poll { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/EditMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/EditMessagePayload.cs new file mode 100644 index 0000000000..95b4ca4841 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Messages/EditMessagePayload.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditMessagePayload : IEditMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional?> Attachments { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/ScheduledEvents/CreateGuildScheduledEventPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/ScheduledEvents/CreateGuildScheduledEventPayload.cs new file mode 100644 index 0000000000..0fe9970a0b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/ScheduledEvents/CreateGuildScheduledEventPayload.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildScheduledEventPayload : ICreateGuildScheduledEventPayload +{ + /// + public Optional ChannelId { get; init; } + + /// + public Optional EntityMetadata { get; init; } + + /// + public required string Name { get; init; } + + /// + public required DiscordScheduledEventPrivacyLevel PrivacyLevel { get; init; } + + /// + public required DateTimeOffset ScheduledStartTime { get; init; } + + /// + public Optional ScheduledEndTime { get; init; } + + /// + public Optional Description { get; init; } + + /// + public required DiscordScheduledEventType EntityType { get; init; } + + /// + public Optional Image { get; init; } + + /// + public Optional RecurrenceRule { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/ScheduledEvents/ModifyGuildScheduledEventPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/ScheduledEvents/ModifyGuildScheduledEventPayload.cs new file mode 100644 index 0000000000..412eda735b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/ScheduledEvents/ModifyGuildScheduledEventPayload.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildScheduledEventPayload : IModifyGuildScheduledEventPayload +{ + /// + public Optional ChannelId { get; init; } + + /// + public Optional EntityMetadata { get; init; } + + /// + public Optional Name { get; init; } + + /// + public Optional PrivacyLevel { get; init; } + + /// + public Optional ScheduledStartTime { get; init; } + + /// + public Optional ScheduledEndTime { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional EntityType { get; init; } + + /// + public Optional Status { get; init; } + + /// + public Optional Image { get; init; } + + /// + public Optional RecurrenceRule { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/CreateGuildSoundboardSoundPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/CreateGuildSoundboardSoundPayload.cs new file mode 100644 index 0000000000..7e309fded7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/CreateGuildSoundboardSoundPayload.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildSoundboardSoundPayload : ICreateGuildSoundboardSoundPayload +{ + /// + public required string Name { get; init; } + + /// + public required InlineMediaData Sound { get; init; } + + /// + public Optional Volume { get; init; } + + /// + public Optional EmojiId { get; init; } + + /// + public Optional EmojiName { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/ModifyGuildSoundboardSoundPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/ModifyGuildSoundboardSoundPayload.cs new file mode 100644 index 0000000000..98ab221515 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/ModifyGuildSoundboardSoundPayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildSoundboardSoundPayload : IModifyGuildSoundboardSoundPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Volume { get; init; } + + /// + public Optional EmojiId { get; init; } + + /// + public Optional EmojiName { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/SendSoundboardSoundPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/SendSoundboardSoundPayload.cs new file mode 100644 index 0000000000..ae44451297 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Soundboard/SendSoundboardSoundPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record SendSoundboardSoundPayload : ISendSoundboardSoundPayload +{ + /// + public required Snowflake SoundId { get; init; } + + /// + public Optional SourceGuildId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/StageInstances/CreateStageInstancePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/StageInstances/CreateStageInstancePayload.cs new file mode 100644 index 0000000000..918eb113a5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/StageInstances/CreateStageInstancePayload.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateStageInstancePayload : ICreateStageInstancePayload +{ + /// + public required Snowflake ChannelId { get; init; } + + /// + public required string Topic { get; init; } + + /// + public Optional PrivacyLevel { get; init; } + + /// + public Optional SendStartNotification { get; init; } + + /// + public Optional GuildScheduledEventId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/StageInstances/ModifyStageInstancePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/StageInstances/ModifyStageInstancePayload.cs new file mode 100644 index 0000000000..d49d997fa0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/StageInstances/ModifyStageInstancePayload.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyStageInstancePayload : IModifyStageInstancePayload +{ + /// + public Optional Topic { get; init; } + + /// + public Optional PrivacyLevel { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Stickers/CreateGuildStickerPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Stickers/CreateGuildStickerPayload.cs new file mode 100644 index 0000000000..cd1039b969 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Stickers/CreateGuildStickerPayload.cs @@ -0,0 +1,23 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGuildStickerPayload : ICreateGuildStickerPayload +{ + /// + public required string Name { get; init; } + + /// + public required string Description { get; init; } + + /// + public required string Tags { get; init; } + + /// + public required AttachmentData File { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Stickers/ModifyGuildStickerPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Stickers/ModifyGuildStickerPayload.cs new file mode 100644 index 0000000000..cc3e6942d0 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Stickers/ModifyGuildStickerPayload.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyGuildStickerPayload : IModifyGuildStickerPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Tags { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Users/CreateDmPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/CreateDmPayload.cs new file mode 100644 index 0000000000..dd25507054 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/CreateDmPayload.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateDmPayload : ICreateDmPayload +{ + /// + public required Snowflake RecipientId { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Users/CreateGroupDmPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/CreateGroupDmPayload.cs new file mode 100644 index 0000000000..b13986d780 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/CreateGroupDmPayload.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateGroupDmPayload : ICreateGroupDmPayload +{ + /// + public required IReadOnlyList AccessTokens { get; init; } + + /// + public required IReadOnlyDictionary Nicks { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Users/ModifyCurrentUserPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/ModifyCurrentUserPayload.cs new file mode 100644 index 0000000000..f0adf1c6a5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/ModifyCurrentUserPayload.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyCurrentUserPayload : IModifyCurrentUserPayload +{ + /// + public Optional Username { get; init; } + + /// + public Optional Avatar { get; init; } + + /// + public Optional Banner { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Users/UpdateCurrentUserApplicationRoleConnectionPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/UpdateCurrentUserApplicationRoleConnectionPayload.cs new file mode 100644 index 0000000000..8abee779e3 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Users/UpdateCurrentUserApplicationRoleConnectionPayload.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record UpdateCurrentUserApplicationRoleConnectionPayload : IUpdateCurrentUserApplicationRoleConnectionPayload +{ + /// + public Optional PlatformName { get; init; } + + /// + public Optional PlatformUsername { get; init; } + + /// + public Optional Metadata { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Voice/ModifyCurrentUserVoiceStatePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Voice/ModifyCurrentUserVoiceStatePayload.cs new file mode 100644 index 0000000000..8599d8dfb6 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Voice/ModifyCurrentUserVoiceStatePayload.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyCurrentUserVoiceStatePayload : IModifyCurrentUserVoiceStatePayload +{ + /// + public Optional ChannelId { get; init; } + + /// + public Optional Suppress { get; init; } + + /// + public Optional RequestToSpeakTimestamp { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Voice/ModifyUserVoiceStatePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Voice/ModifyUserVoiceStatePayload.cs new file mode 100644 index 0000000000..aaf8c9c3f4 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Voice/ModifyUserVoiceStatePayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyUserVoiceStatePayload : IModifyUserVoiceStatePayload +{ + /// + public required Snowflake ChannelId { get; init; } + + /// + public Optional Suppress { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/CreateWebhookPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/CreateWebhookPayload.cs new file mode 100644 index 0000000000..a9ca335f57 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/CreateWebhookPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record CreateWebhookPayload : ICreateWebhookPayload +{ + /// + public required string Name { get; init; } + + /// + public Optional Avatar { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/EditWebhookMessagePayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/EditWebhookMessagePayload.cs new file mode 100644 index 0000000000..2a7acc0ccc --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/EditWebhookMessagePayload.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record EditWebhookMessagePayload : IEditWebhookMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional?> Attachments { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ExecuteWebhookPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ExecuteWebhookPayload.cs new file mode 100644 index 0000000000..8754612707 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ExecuteWebhookPayload.cs @@ -0,0 +1,54 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ExecuteWebhookPayload : IExecuteWebhookPayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional Username { get; init; } + + /// + public Optional AvatarUrl { get; init; } + + /// + public Optional Tts { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional ThreadName { get; init; } + + /// + public Optional> AppliedTags { get; init; } + + /// + public Optional Poll { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ModifyWebhookPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ModifyWebhookPayload.cs new file mode 100644 index 0000000000..f292d1a108 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ModifyWebhookPayload.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyWebhookPayload : IModifyWebhookPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Avatar { get; init; } + + /// + public Optional ChannelId { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ModifyWebhookWithTokenPayload.cs b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ModifyWebhookWithTokenPayload.cs new file mode 100644 index 0000000000..b7cfd2950a --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Payloads/Webhooks/ModifyWebhookWithTokenPayload.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Internal.Rest.Payloads; + +/// +public sealed record ModifyWebhookWithTokenPayload : IModifyWebhookWithTokenPayload +{ + /// + public Optional Name { get; init; } + + /// + public Optional Avatar { get; init; } +} \ No newline at end of file diff --git a/src/core/DSharpPlus.Internal.Rest/QueryBuilder.cs b/src/core/DSharpPlus.Internal.Rest/QueryBuilder.cs new file mode 100644 index 0000000000..0ac4546b36 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/QueryBuilder.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Linq; + +using Bundles; + +namespace DSharpPlus.Internal.Rest; + +/// +/// Constructs a new query from the specified parameters. +/// +internal record struct QueryBuilder(string RootUri) +{ + public DictionarySlim Parameters { get; } = new(); + + public readonly QueryBuilder AddParameter(string key, string value) + { + ref string parameter = ref this.Parameters.GetOrAddValueRef(key); + parameter = value; + return this; + } + + public readonly string Build() + => this.RootUri + string.Join("&", this.Parameters.Select(e => Uri.EscapeDataString(e.Key) + '=' + Uri.EscapeDataString(e.Value))); +} diff --git a/src/core/DSharpPlus.Internal.Rest/Ratelimiting/IRatelimitRegistry.cs b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/IRatelimitRegistry.cs new file mode 100644 index 0000000000..ea5d8b36b7 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/IRatelimitRegistry.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Net.Http; + +using DSharpPlus.Results; + +namespace DSharpPlus.Internal.Rest.Ratelimiting; + +/// +/// Represents a library hook for modifying ratelimiting behaviour. This is called into by rest-internal code, +/// and third-party rest implementations may not behave identically. +/// +public interface IRatelimitRegistry +{ + /// + /// Checks whether this request should be allowed to proceed. This should be considering as enqueuing a request + /// as far as is concerned. + /// + public Result CheckRatelimit(HttpRequestMessage request); + + /// + /// Updates the ratelimits encountered from the given request. + /// + public Result UpdateRatelimit(HttpRequestMessage request, HttpResponseMessage response); + + /// + /// Cancels a request reservation made in . + /// + public Result CancelRequest(HttpRequestMessage request); +} diff --git a/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitBucket.cs b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitBucket.cs new file mode 100644 index 0000000000..e580009fa5 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitBucket.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; + +namespace DSharpPlus.Internal.Rest.Ratelimiting; + +/// +/// Represents a full ratelimiting bucket. +/// +// this is a class because we use a ConditionalWeakTable to hold it, but there's room for improvement here. +internal sealed class RatelimitBucket +{ + public required float Expiry { get => this.expiry; set => this.expiry = value; } + private float expiry; + + public required int Limit { get => this.limit; set => this.limit = value; } + private int limit; + + public required int Remaining { get => this.remaining; set => this.remaining = value; } + private int remaining; + + public required int Reserved { get => this.reserved; set => this.reserved = value; } + private int reserved; + + public void UpdateFromResponse(float expiry, int limit, int remaining) + { + _ = Interlocked.Exchange(ref this.expiry, expiry); + _ = Interlocked.Exchange(ref this.limit, limit); + _ = Interlocked.Exchange(ref this.remaining, remaining); + _ = Interlocked.Decrement(ref this.reserved); + } + + public void Reserve() => Interlocked.Increment(ref this.reserved); + public void CancelReservation() => Interlocked.Decrement(ref this.reserved); +} diff --git a/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitOptions.cs b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitOptions.cs new file mode 100644 index 0000000000..7542077bce --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitOptions.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading; + +namespace DSharpPlus.Internal.Rest.Ratelimiting; + +/// +/// Contains options ot configure ratelimiting behaviour. +/// +public sealed class RatelimitOptions +{ + /// + /// Indicates to the library whether to use a separate thread to clean up old ratelimits. + /// + public bool UseSeparateCleanupThread { get; set; } = true; + + /// + /// Specifies the interval in milliseconds at which ratelimits are cleaned up. + /// + public int CleanupInterval { get; set; } = 10000; + + /// + /// Gets a cancellation token for ratelimit cleanup. + /// + public CancellationToken Token { get; set; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitRegistry.cs b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitRegistry.cs new file mode 100644 index 0000000000..ad5294a046 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/RatelimitRegistry.cs @@ -0,0 +1,346 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1812, CA2008 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Results; +using DSharpPlus.Results.Errors; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +using NonBlocking; + +using Polly; + +namespace DSharpPlus.Internal.Rest.Ratelimiting; + +/// +/// Contains and manages ratelimits as far as known. +/// +public sealed class RatelimitRegistry : IRatelimitRegistry +{ + private readonly ConcurrentDictionary webhook429s = new(); + private readonly ConcurrentDictionary route429s = new(); + private readonly ConcurrentDictionary hashes = new(); + private readonly ConditionalWeakTable ratelimitBuckets = []; + private readonly ILogger logger; + + private readonly double timeReferencePoint; + + private bool dirty; + + public RatelimitRegistry + ( + ILogger logger, + IOptions options + ) + { + this.logger = logger; + + // ExecuteSynchronously ensures we never leave that thread. + TaskFactory factory = options.Value.UseSeparateCleanupThread + ? new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.ExecuteSynchronously) + : Task.Factory; + + _ = factory.StartNew(() => this.CleanupSimpleRatelimitsAsync(options.Value.CleanupInterval, options.Value.Token)); + this.timeReferencePoint = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + private async ValueTask CleanupSimpleRatelimitsAsync(int interval, CancellationToken ct) + { + using PeriodicTimer timer = new(TimeSpan.FromMilliseconds(interval)); + + // 2x the interval, rounded down + int decay = interval / 500; + int counter = 0; + + startingCleanupLoop(this.logger, interval, decay, null); + + while (await timer.WaitForNextTickAsync(ct)) + { + if (!this.dirty) + { +#pragma warning disable CA1848 // there's no performance improvement to be gained from not having any placeholders. + this.logger.LogTrace("There was no simple ratelimit added since last cleanup, skipping."); + continue; +#pragma warning restore CA1848 + } + + double currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + initiatingCleanupMessage(this.logger, "webhook", this.webhook429s.Count, null); + + // we iterate on a snapshot. this leads to rare race conditions if a bucket is updated while we're iterating, + // but since we only remove considerably outdated buckets that have already reset a while ago, the worst that + // can happen is that we delete the first response, the second will then be correctly limited - unless the bucket + // is extremely long-lived and we hit this condition on every request. this is unlikely enough to assume it + // doesn't happen, i believe. + foreach (KeyValuePair pair in this.webhook429s) + { + if (currentTime > pair.Value + this.timeReferencePoint) + { + _ = this.webhook429s.Remove(pair.Key, out _); + } + } + + completedCleanupMessage(this.logger, "webhook", this.webhook429s.Count, null); + initiatingCleanupMessage(this.logger, "route", this.route429s.Count, null); + + foreach (KeyValuePair pair in this.route429s) + { + if (currentTime > pair.Value + this.timeReferencePoint) + { + _ = this.route429s.Remove(pair.Key, out _); + } + } + + completedCleanupMessage(this.logger, "route", this.route429s.Count, null); + + // only clean buckets every ten iterations. this can be changed later + if (counter >= 10) + { + initiatingHashCleanupMessage(this.logger, this.hashes.Count, null); + + foreach (KeyValuePair pair in this.hashes) + { + if (this.ratelimitBuckets.TryGetValue(pair.Key, out RatelimitBucket? bucket)) + { + if (currentTime > bucket.Expiry + this.timeReferencePoint) + { + // since we use a CWT, the bucket will automatically die once all routes holding on to it are dead + _ = this.hashes.Remove(pair.Key, out _); + } + } + } + + completedHashCleanupMessage(this.logger, this.hashes.Count, null); + counter = 0; + } + + this.dirty = false; + counter++; + } + } + + /// + public Result CheckRatelimit(HttpRequestMessage request) + { + Context? context = request.GetPolicyExecutionContext(); + double currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + string route = context is not null && context.TryGetValue("route", out object rawRoute) && rawRoute is string contextRoute + ? contextRoute + : $"{request.Method.ToString().ToUpperInvariant()} {request.RequestUri}"; + + // we know the route - easy enough + if (this.hashes.TryGetValue(route, out string? hash) && this.ratelimitBuckets.TryGetValue(hash, out RatelimitBucket? bucket)) + { + bool success = bucket.Remaining - bucket.Reserved > 0; + + if (!success) + { + rejectedRatelimit(this.logger, request.RequestUri!.AbsoluteUri, "conclusively", null); + } + + bucket.Reserve(); + return success; + } + + // what if we don't know this route? check whether we know related routes, answer heuristically based on that + int index = route.IndexOf(' ', StringComparison.Ordinal); + StringSegment segment = new(route, index + 1, route.Length - index - 1); + + if (this.route429s.TryGetValue(segment, out float last429Expiry)) + { + bool success = currentTime > this.timeReferencePoint + last429Expiry; + + if (!success) + { + rejectedRatelimit(this.logger, request.RequestUri!.AbsoluteUri, "speculatively based on route ratelimits", null); + } + + return success; + } + + // we don't know related routes yet. if it's a webhook route, we check the simple route. for other kinds of routes we + // can impl heuristics here too, but not yet + if + ( + context is not null + && context.TryGetValue("simple-route", out object rawSimpleRoute) + && rawSimpleRoute is SimpleRatelimitRoute simpleRoute + && simpleRoute.Resource == TopLevelResource.Webhook + ) + { + if (this.webhook429s.TryGetValue(simpleRoute.Id, out last429Expiry)) + { + bool success = currentTime > this.timeReferencePoint + last429Expiry; + + if (!success) + { + rejectedRatelimit(this.logger, request.RequestUri!.AbsoluteUri, "speculatively based on webhook ratelimits", null); + } + + return success; + } + } + + return true; + } + + /// + public Result UpdateRatelimit(HttpRequestMessage request, HttpResponseMessage response) + { + // extract the reset time, immediately bail if we couldn't find one + HttpResponseHeaders headers = response.Headers; + double reset = 0; + + if + ( + !headers.TryGetValues("X-RateLimit-Reset", out IEnumerable? rawReset) + && !double.TryParse(rawReset?.SingleOrDefault(), out reset) + ) + { + return new ArgumentError("response.Headers", "Failed to parse an X-RateLimit-Reset header from the response."); + } + + Context? context = request.GetPolicyExecutionContext(); + + string route = context is not null && context.TryGetValue("route", out object rawRoute) && rawRoute is string contextRoute + ? contextRoute + : $"{request.Method.ToString().ToUpperInvariant()} {request.RequestUri}"; + + // if we have a ratelimit, try to encode the result into the preemptive handlers if necessary + if (response.StatusCode == HttpStatusCode.TooManyRequests) + { + if + ( + context is not null + && context.TryGetValue("simple-route", out object rawSimpleRoute) + && rawSimpleRoute is SimpleRatelimitRoute simpleRoute + && simpleRoute.Resource == TopLevelResource.Webhook + ) + { + this.webhook429s[simpleRoute.Id] = (float)(reset - this.timeReferencePoint); + } + else + { + // what if we don't know this route? check whether we know related routes, answer heuristically based on that + int index = route.IndexOf(' ', StringComparison.Ordinal); + StringSegment segment = new(route, index + 1, route.Length - index - 1); + + this.route429s[segment] = (float)(reset - this.timeReferencePoint); + } + } + + // update the bucket, first by extracting the other relevant information... + if (!headers.TryGetValues("X-RateLimit-Bucket", out IEnumerable? rawBucket)) + { + return new ArgumentError("response.Headers", "Failed to parse an X-RateLimit-Bucket header from the response."); + } + + string hash = rawBucket.Single(); + short limit = 0, remaining = 0; + + if + ( + !headers.TryGetValues("X-RateLimit-Limit", out IEnumerable? rawLimit) + && !short.TryParse(rawLimit?.SingleOrDefault(), out limit) + ) + { + return new ArgumentError("response.Headers", "Failed to parse an X-RateLimit-Limit header from the response."); + } + + if + ( + !headers.TryGetValues("X-RateLimit-Remaining", out IEnumerable? rawRemaining) + && !short.TryParse(rawRemaining?.SingleOrDefault(), out remaining) + ) + { + return new ArgumentError("response.Headers", "Failed to parse an X-RateLimit-Remaining header from the response."); + } + + this.dirty = true; + + // ... and then by updating the values we know. + this.hashes[route] = hash; + RatelimitBucket bucket = this.ratelimitBuckets.GetOrCreateValue(hash); + + bucket.UpdateFromResponse((float)(reset - this.timeReferencePoint), limit, remaining); + + return default; + } + + public Result CancelRequest(HttpRequestMessage request) + { + Context? context = request.GetPolicyExecutionContext(); + + string route = context is not null && context.TryGetValue("route", out object rawRoute) && rawRoute is string contextRoute + ? contextRoute + : $"{request.Method.ToString().ToUpperInvariant()} {request.RequestUri}"; + + if (this.hashes.TryGetValue(route, out string? hash) && this.ratelimitBuckets.TryGetValue(hash, out RatelimitBucket? bucket)) + { + bucket.CancelReservation(); + } + + return default; + } + + // logging delegates defined down here + + private static readonly Action initiatingCleanupMessage = LoggerMessage.Define + ( + LogLevel.Trace, + default, + "Initiating {Kind}-based heuristic cleanup, current size: {Size}." + ); + + private static readonly Action completedCleanupMessage = LoggerMessage.Define + ( + LogLevel.Trace, + default, + "Completed {Kind}-based heuristic cleanup, new size: {Size}." + ); + + private static readonly Action initiatingHashCleanupMessage = LoggerMessage.Define + ( + LogLevel.Trace, + default, + "Initiating ratelimit hash cleanup, current size: {Size}." + ); + + private static readonly Action completedHashCleanupMessage = LoggerMessage.Define + ( + LogLevel.Trace, + default, + "Completed ratelimit hash cleanup, new size: {Size}." + ); + + private static readonly Action startingCleanupLoop = LoggerMessage.Define + ( + LogLevel.Debug, + default, + "Starting heuristic cleanup loop with an interval of {Interval}ms and a bucket decay of {Decay}s." + ); + + private static readonly Action rejectedRatelimit = LoggerMessage.Define + ( + LogLevel.Debug, + default, + "Rejected request to {Url}, {Status}." + ); +} diff --git a/src/core/DSharpPlus.Internal.Rest/Ratelimiting/SimpleRatelimitRoute.cs b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/SimpleRatelimitRoute.cs new file mode 100644 index 0000000000..c4e7d60a11 --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/SimpleRatelimitRoute.cs @@ -0,0 +1,15 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Rest.Ratelimiting; + +/// +/// Specifies a route likely defined by its resource and a snowflake. +/// +internal readonly record struct SimpleRatelimitRoute +{ + public required TopLevelResource Resource { get; init; } + + public required Snowflake Id { get; init; } +} diff --git a/src/core/DSharpPlus.Internal.Rest/Ratelimiting/TopLevelResource.cs b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/TopLevelResource.cs new file mode 100644 index 0000000000..a70e57138d --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/Ratelimiting/TopLevelResource.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Internal.Rest.Ratelimiting; + +/// +/// Specifies the top-level resources for requests. +/// +internal enum TopLevelResource +{ + Channel, + Guild, + Webhook, + Other +} diff --git a/src/core/DSharpPlus.Internal.Rest/RequestBuilderExtensions.cs b/src/core/DSharpPlus.Internal.Rest/RequestBuilderExtensions.cs new file mode 100644 index 0000000000..1cac07378b --- /dev/null +++ b/src/core/DSharpPlus.Internal.Rest/RequestBuilderExtensions.cs @@ -0,0 +1,46 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Rest.Ratelimiting; + +namespace DSharpPlus.Internal.Rest; + +internal static class RequestBuilderExtensions +{ + public static RequestBuilder WithSimpleRoute(this RequestBuilder request, TopLevelResource resource, Snowflake id) + { + request.AddToContext + ( + "simple-route", + new SimpleRatelimitRoute + { + Id = id, + Resource = resource + } + ); + + return request; + } + + public static RequestBuilder AsExempt(this RequestBuilder request, bool isExempt = true) + { + request.AddToContext("is-exempt", isExempt); + return request; + } + + public static RequestBuilder AsWebhookRequest(this RequestBuilder request, bool isWebhookRequest = true) + { + request.AddToContext("is-webhook-request", isWebhookRequest); + return request; + } + + public static RequestBuilder AsInteractionRequest(this RequestBuilder request, bool isInteractionRequest = true) + { + request.AddToContext("is-interaction-request", isInteractionRequest); + return request; + } +} diff --git a/src/core/DSharpPlus.Shared/.editorconfig b/src/core/DSharpPlus.Shared/.editorconfig new file mode 100644 index 0000000000..3e37f7b31f --- /dev/null +++ b/src/core/DSharpPlus.Shared/.editorconfig @@ -0,0 +1,2 @@ +# don't force namespaces matching folder paths +dotnet_style_namespace_match_folder = false \ No newline at end of file diff --git a/src/core/DSharpPlus.Shared/AttachmentData.cs b/src/core/DSharpPlus.Shared/AttachmentData.cs new file mode 100644 index 0000000000..1e2bd20d54 --- /dev/null +++ b/src/core/DSharpPlus.Shared/AttachmentData.cs @@ -0,0 +1,224 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Threading.Tasks; + +namespace DSharpPlus; + +/// +/// Represents a file sent to Discord as a message attachment. +/// +public readonly record struct AttachmentData +{ + private readonly Stream stream; + + /// + /// The media type of this attachment. Defaults to empty, in which case it will be interpreted according to + /// the file extension as provided by . + /// + public string? MediaType { get; init; } + + /// + /// The filename of this attachment. + /// + public required string Filename { get; init; } + + /// + /// Specifies whether to encode the attachment as base64. + /// + public bool ConvertToBase64 { get; init; } + + /// + /// Creates a new instance of this structure from the provided stream. + /// + /// A stream containing the attachment. + /// The name of this file to upload. + /// The media type of this file. + /// Whether to upload the attachment as base64. + public AttachmentData + ( + Stream stream, + string filename, + string? mediaType = null, + bool base64 = false + ) + { + this.stream = stream; + this.MediaType = mediaType; + this.Filename = filename; + this.ConvertToBase64 = base64; + } + + /// + /// Creates a new instance of this structure from the provided pipe. + /// + /// A reader to the pipe. + /// The name of this file to upload. + /// The media type of this file. + /// Whether to upload the attachment as base64. + public AttachmentData + ( + PipeReader reader, + string filename, + string? mediaType = null, + bool base64 = false + ) + : this + ( + reader.AsStream(), + filename, + mediaType, + base64 + ) + { + + } + + public async ValueTask GetStreamAsync() + { + if (this.ConvertToBase64) + { + Pipe pipe = new(); + await this.WriteToPipeAsBase64Async(pipe.Writer); + + return pipe.Reader.AsStream(); + } + + return this.stream; + } + + /// + /// Writes the base64 data to the specified PipeWriter. + /// + private readonly async ValueTask WriteToPipeAsBase64Async(PipeWriter writer) + { + const int readSegmentLength = 12288; + const int writeSegmentLength = 16384; + + PipeReader reader = PipeReader.Create + ( + this.stream, + new StreamPipeReaderOptions + ( + bufferSize: 12288, + leaveOpen: true, + useZeroByteReads: true + ) + ); + + byte[] readBuffer = ArrayPool.Shared.Rent(readSegmentLength); + byte[] writeBuffer = ArrayPool.Shared.Rent(writeSegmentLength); + + int readRollover = 0; + + while (true) + { + ReadResult result = await reader.ReadAsync(); + + if (result.IsCanceled) + { + break; + } + + ProcessResult(result, reader); + + if (result.IsCompleted) + { + break; + } + } + + ArrayPool.Shared.Return(readBuffer); + ArrayPool.Shared.Return(writeBuffer); + + void ProcessResult(ReadResult result, PipeReader reader) + { + SequenceReader sequence = new(result.Buffer); + + scoped Span readSpan = readBuffer.AsSpan()[..readSegmentLength]; + scoped Span writeSpan = writeBuffer.AsSpan()[..writeSegmentLength]; + + while (!sequence.End) + { + if (readRollover != 0) + { + if (sequence.Remaining + readRollover >= readSegmentLength) + { + if (!sequence.TryCopyTo(readSpan[readRollover..])) + { + Trace.Assert(false); + } + + sequence.Advance(readSegmentLength - readRollover); + readRollover = 0; + } + else + { + if (!sequence.TryCopyTo(readSpan.Slice(readRollover, (int)sequence.Remaining))) + { + Trace.Assert(false); + } + + readRollover += (int)sequence.Remaining; + sequence.Advance((int)sequence.Remaining); + break; + } + } + else if (sequence.Remaining >= readSegmentLength) + { + if (!sequence.TryCopyTo(readSpan)) + { + Trace.Assert(false); + } + + sequence.Advance(readSegmentLength); + } + else + { + if (!sequence.TryCopyTo(readSpan[..(int)sequence.Remaining])) + { + Trace.Assert(false); + } + + readRollover += (int)sequence.Remaining; + sequence.AdvanceToEnd(); + break; + } + + OperationStatus status = Base64.EncodeToUtf8(readSpan, writeSpan, out int consumed, out int written, false); + + Debug.Assert + ( + consumed != readSegmentLength || written != writeSegmentLength, + "Buffer management error while converting to base64. Aborting." + ); + + Debug.Assert(status == OperationStatus.Done); + + writer.Write(writeSpan[..written]); + } + + if (result.IsCompleted) + { + OperationStatus status = Base64.EncodeToUtf8(readSpan[..readRollover], writeSpan, out int _, out int written, true); + + Debug.Assert(status == OperationStatus.Done); + + writer.Write(writeSpan[..written]); + + writer.Complete(); + reader.Complete(); + return; + } + + reader.AdvanceTo(result.Buffer.End); + } + } +} diff --git a/src/core/DSharpPlus.Shared/DSharpPlus.Shared.csproj b/src/core/DSharpPlus.Shared/DSharpPlus.Shared.csproj new file mode 100644 index 0000000000..cd32215efa --- /dev/null +++ b/src/core/DSharpPlus.Shared/DSharpPlus.Shared.csproj @@ -0,0 +1,28 @@ + + + + $(_DSharpPlusReleaseVersion) + $(Description) This package contains shared types between public-facing and internal implementation packages. + Library + true + $(NoWarn);CA1008 + + DSharpPlus + + + + + + + + + + + + + + + + + + diff --git a/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandOptionType.cs b/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandOptionType.cs new file mode 100644 index 0000000000..ead585cccc --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandOptionType.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the valid types of an application command option. +/// +public enum DiscordApplicationCommandOptionType +{ + /// + /// This option signifies a subcommand of the parent command. + /// + SubCommand = 1, + + /// + /// This option signifies a group of subcommands of the parent command. + /// + SubCommandGroup, + + String, + + /// + /// Any integer between -2^53 and +2^53. + /// + Integer, + + Boolean, + + User, + + /// + /// By default, this includes all channel types + categories. + /// + Channel, + + Role, + + /// + /// This includes roles and users. + /// + Mentionable, + + /// + /// Any double-precision floating-point number between -2^53 and +2^53. + /// + Number, + + Attachment +} diff --git a/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandPermissionType.cs b/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandPermissionType.cs new file mode 100644 index 0000000000..30f1dad338 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandPermissionType.cs @@ -0,0 +1,15 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Lists the different targets for an application command permission override. +/// +public enum DiscordApplicationCommandPermissionType +{ + Role = 1, + User, + Channel +} diff --git a/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandType.cs b/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandType.cs new file mode 100644 index 0000000000..e3ab0c0520 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ApplicationCommands/DiscordApplicationCommandType.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the possible application command types. +/// +public enum DiscordApplicationCommandType +{ + /// + /// Slash commands, text based commands that show up in the chat box. + /// + ChatInput = 1, + + /// + /// User context menu based commands. + /// + User, + + /// + /// Message context menu based commands. + /// + Message +} diff --git a/src/core/DSharpPlus.Shared/Entities/Applications/DiscordApplicationFlags.cs b/src/core/DSharpPlus.Shared/Entities/Applications/DiscordApplicationFlags.cs new file mode 100644 index 0000000000..df89c38895 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Applications/DiscordApplicationFlags.cs @@ -0,0 +1,67 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Enumerates flags for applications and thereby bots. +/// +[Flags] +public enum DiscordApplicationFlags +{ + /// + /// Indicates whether an application uses the auto moderation API. + /// + ApplicationAutoModerationRuleCreateBadge = 1 << 6, + + /// + /// The intent required for bots in 100 or more servers to receive presence update events. + /// + GatewayPresence = 1 << 12, + + /// + /// The intent required for bots in less than 100 servers to receive presence update events. + /// Unlike , this does not require staff approval. + /// + GatewayPresenceLimited = 1 << 13, + + /// + /// The intent required for bots in 100 or more servers to receive guild member related events. + /// + GatewayGuildMembers = 1 << 14, + + /// + /// The intent required for bots in less than 100 servers to receive guild member related events. + /// Unlike , this does not require staff approval. + /// + GatewayGuildMembersLimited = 1 << 15, + + /// + /// Indicates unusual growth of an app that prevents verification. + /// + VerificationPendingGuildLimit = 1 << 16, + + /// + /// Indicates whether this app is embedded into the Discord client. + /// + Embedded = 1 << 17, + + /// + /// The intent required for bots in 100 or more servers to receive message content. + /// + GatewayMessageContent = 1 << 18, + + /// + /// The intent required for bots in less than 100 servers to receive message content. + /// Unlike , this does not require staff approval. + /// + GatewayMessageContentLimited = 1 << 19, + + /// + /// Indicates whether this application has registered global application commands. + /// + ApplicationCommandBadge = 1 << 23 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Applications/DiscordApplicationIntegrationType.cs b/src/core/DSharpPlus.Shared/Entities/Applications/DiscordApplicationIntegrationType.cs new file mode 100644 index 0000000000..07f200055e --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Applications/DiscordApplicationIntegrationType.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies where an application can be installed. +/// +public enum DiscordApplicationIntegrationType +{ + /// + /// Specifies that this application can be installed to guilds. + /// + GuildInstall, + + /// + /// Specifies that this application can be installed to users. + /// + UserInstall +} diff --git a/src/core/DSharpPlus.Shared/Entities/AuditLogs/DiscordAuditLogEvent.cs b/src/core/DSharpPlus.Shared/Entities/AuditLogs/DiscordAuditLogEvent.cs new file mode 100644 index 0000000000..2332cb3bc6 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/AuditLogs/DiscordAuditLogEvent.cs @@ -0,0 +1,467 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the visible types of audit log events; and lists the metadata provided to each event. +/// +public enum DiscordAuditLogEvent +{ + /// + /// The server settings were updated. + /// + /// + /// Metadata is provided for a guild object. + /// + GuildUpdated = 1, + + /// + /// A channel was created. + /// + /// + /// Metadata is provided for a channel object. + /// + ChannelCreated = 10, + + /// + /// A channel was updated. + /// + /// + /// Metadata is provided for a channel object. + /// + ChannelUpdated = 11, + + /// + /// A channel was deleted. + /// + /// + /// Metadata is provided for a channel object. + /// + ChannelDeleted = 12, + + /// + /// A permission overwrite was added to a channel. + /// + /// + /// Metadata is provided for a channel overwrite object. + /// + ChannelOverwriteCreated = 13, + + /// + /// A permission overwrite in a channel was updated. + /// + /// + /// Metadata is provided for a channel overwrite object. + /// + ChannelOverwriteUpdated = 14, + + /// + /// A permission overwrite in a channel was deleted. + /// + /// + /// Metadata is provided for a channel overwrite object. + /// + ChannelOverwriteDeleted = 15, + + /// + /// A member was kicked from the server. + /// + /// + /// No metadata is provided. + /// + MemberKicked = 20, + + /// + /// Members were pruned from the server. + /// + /// + /// No metadata is provided. + /// + MemberPruned = 21, + + /// + /// A member was banned from the server. + /// + /// + /// No metadata is provided. + /// + MemberBanned = 22, + + /// + /// A member was unbanned from the server. + /// + /// + /// No metadata is provided. + /// + MemberUnbanned = 23, + + /// + /// A server member was updated. + /// + /// + /// Metadata is provided for a member object. + /// + MemberUpdated = 24, + + /// + /// A role was granted to or removed from a server member. + /// + /// + /// Metadata is provided for a role object. + /// + MemberRoleUpdated = 25, + + /// + /// A server member was moved to a different voice channel. + /// + /// + /// No metadata is provided. + /// + MemberMoved = 26, + + /// + /// A server member was disconnected from a voice channel. + /// + /// + /// No metadata is provided. + /// + MemberDisconnected = 27, + + /// + /// A bot user was added to the server. + /// + /// + /// No metadata is provided. + /// + BotAdded = 28, + + /// + /// A role was created. + /// + /// + /// Metadata is provided for a role object. + /// + RoleCreated = 30, + + /// + /// A role was updated. + /// + /// + /// Metadata is provided for a role object. + /// + RoleUpdated = 31, + + /// + /// A role was deleted. + /// + /// + /// Metadata is provided for a role object. + /// + RoleDeleted = 32, + + /// + /// An invite was created. + /// + /// + /// Metadata is provided for an invite object. + /// + InviteCreated = 40, + + /// + /// An invite was updated. + /// + /// + /// Metadata is provided for an invite object. + /// + InviteUpdated = 41, + + /// + /// An invite was deleted. + /// + /// + /// Metadata is provided for an invite object. + /// + InviteDeleted = 42, + + /// + /// A webhook was created. + /// + /// + /// Metadata is provided for a webhook object. + /// + WebhookCreated = 50, + + /// + /// A webhook was updated. + /// + /// + /// Metadata is provided for a webhook object. + /// + WebhookUpdated = 51, + + /// + /// A webhook was deleted. + /// + /// + /// Metadata is provided for a webhook object. + /// + WebhookDeleted = 52, + + /// + /// An emoji was created. + /// + /// + /// Metadata is provided for an emoji object. + /// + EmojiCreated = 60, + + /// + /// An emoji was updated. + /// + /// + /// Metadata is provided for an emoji object. + /// + EmojiUpdated = 61, + + /// + /// An emoji was deleted. + /// + /// + /// Metadata is provided for an emoji object. + /// + EmojiDeleted = 62, + + /// + /// A message was deleted. + /// + /// + /// No metadata is provided. + /// + MessageDeleted = 72, + + /// + /// Multiple messages were bulk-deleted. + /// + /// + /// No metadata is provided. + /// + MessageBulkDeleted = 73, + + /// + /// A message was pinned to a channel. + /// + /// + /// No metadata is provided. + /// + MessagePinned = 74, + + /// + /// A message was unpinned from a channel. + /// + /// + /// No metadata is provided. + /// + MessageUnpinned = 75, + + /// + /// An integration was added to a server. + /// + /// + /// Metadata is provided for an integration object. + /// + IntegrationCreated = 80, + + /// + /// An integration within a server was updated. + /// + /// + /// Metadata is provided for an integration object. + /// + IntegrationUpdated = 81, + + /// + /// An integration was deleted from a server. + /// + /// + /// Metadata is provided for an integration object. + /// + IntegrationDeleted = 82, + + /// + /// A stage channel went live. + /// + /// + /// Metadata is provided for a stage instance object. + /// + StageInstanceCreated = 83, + + /// + /// A live stage channel was updated. + /// + /// + /// Metadata is provided for a stage instance object. + /// + StageInstanceUpdated = 84, + + /// + /// A stage instance ended. + /// + /// + /// Metadata is provided for a stage instance object. + /// + StageInstanceDeleted = 85, + + /// + /// A sticker was created. + /// + /// + /// Metadata is provided for a sticker object. + /// + StickerCreated = 90, + + /// + /// A sticker was updated. + /// + /// + /// Metadata is provided for a sticker object. + /// + StickerUpdated = 91, + + /// + /// A sticker was deleted. + /// + /// + /// Metadata is provided for a sticker object. + /// + StickerDeleted = 92, + + /// + /// A scheduled event was created. + /// + /// + /// Metadata is provided for a scheduled event object. + /// + ScheduledEventCreated = 100, + + /// + /// A scheduled event was updated. + /// + /// + /// Metadata is provided for a scheduled event object. + /// + ScheduledEventUpdated = 101, + + /// + /// A scheduled event was cancelled. + /// + /// + /// Metadata is provided for a scheduled event object. + /// + ScheduledEventDeleted = 102, + + /// + /// A thread was created in a channel. + /// + /// + /// Metadata is provided for a channel object. + /// + ThreadCreated = 110, + + /// + /// A thread was updated. + /// + /// + /// Metadata is provided for a channel object. + /// + ThreadUpdated = 111, + + /// + /// A thread was deleted. + /// + /// + /// Metadata is provided for a channel object. + /// + ThreadDeleted = 112, + + /// + /// An application command's permissions were updated. + /// + /// + /// Metadata is provided for an application command permissions object. + /// + ApplicationCommandPermissionsUpdated = 121, + + /// + /// A soundboard sound was created. + /// + /// + /// Metadata is provided for a soundboard sound object. + /// + SoundboardSoundCreated = 130, + + /// + /// A soundboard sound was updated. + /// + /// + /// Metadata is provided for a soundboard sound object. + /// + SoundboardSoundUpdated = 131, + + /// + /// A soundboard sound was deleted. + /// + /// + /// Metadata is provided for a soundboard sound object. + /// + SoundboardSoundDeleted = 132, + + /// + /// An auto moderation rule was created. + /// + /// + /// Metadata is provided for an auto moderation rule object. + /// + AutoModerationRuleCreated = 140, + + /// + /// An auto moderation rule was updated. + /// + /// + /// Metadata is provided for an auto moderation rule object. + /// + AutoModerationRuleUpdated = 141, + + /// + /// An auto moderation rule was deleted. + /// + /// + /// Metadata is provided for an auto moderation rule object. + /// + AutoModerationRuleDeleted = 142, + + /// + /// A message was blocked by the discord automod. + /// + /// + /// No metadata is provided. + /// + AutoModerationMessageBlocked = 143, + + /// + /// A message was flagged and alerted to by the discord automod. + /// + /// + /// No metadata is provided. + /// + AutoModerationFlaggedToChannel = 144, + + /// + /// A member was timed out by the discord automod. + /// + /// + /// No metadata is provided. + /// + AutoModerationUserCommunicationDisabled = 145 +} diff --git a/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationActionType.cs b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationActionType.cs new file mode 100644 index 0000000000..9427ac2131 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationActionType.cs @@ -0,0 +1,34 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates different actions to take when a message is filtered by the automod. +/// +public enum DiscordAutoModerationActionType +{ + /// + /// Prevents the message from being sent. An explanation as to why it was blocked may be specified + /// and shown to members whenever their message is blocked. + /// + BlockMessage = 1, + + /// + /// Logs the message to a specified channel. + /// + SendAlertMessage, + + /// + /// Causes an user to be timed out for a specified duration. This can only be applied to rules of types + /// and + /// . + /// + Timeout, + + /// + /// Prevents a member from using text, voice or other interactions. + /// + BlockMemberInteraction +} diff --git a/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationEventType.cs b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationEventType.cs new file mode 100644 index 0000000000..9e598d8322 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationEventType.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Indicates on what events a rule should be checked. +/// +public enum DiscordAutoModerationEventType +{ + /// + /// Fired when a member sends or edits a message. + /// + MessageSend = 1, + + /// + /// Fired when a member edits their profile. + /// + MemberUpdate +} diff --git a/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationPresetType.cs b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationPresetType.cs new file mode 100644 index 0000000000..abc0b11200 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationPresetType.cs @@ -0,0 +1,27 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the internal keyword presets for auto-moderation rules of type +/// . +/// +public enum DiscordAutoModerationPresetType +{ + /// + /// Contains words that may be considered swearing, cursing or other profanity. + /// + Profanity = 1, + + /// + /// Contains words that may refer to sexually explicit behaviour or activity. + /// + SexualContent, + + /// + /// Contains words that may be considered hate speech. + /// + Slurs +} diff --git a/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationTriggerType.cs b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationTriggerType.cs new file mode 100644 index 0000000000..04fe5d10cb --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/AutoModeration/DiscordAutoModerationTriggerType.cs @@ -0,0 +1,37 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents different automod triggers. +/// +public enum DiscordAutoModerationTriggerType +{ + /// + /// Check whether the message content contains words from a user-defined list of keywords. + /// There can be up to six such rules in a guild. + /// + Keyword = 1, + + /// + /// Check whether the message content represents generic spam. + /// + Spam = 3, + + /// + /// Check whether the message content contains words from internally defined wordsets. + /// + KeywordPreset, + + /// + /// Check whether the message content contains more unique mentions than allowed. + /// + MentionSpam, + + /// + /// Check whether the member profile contains words from an user-defined list of keyword. + /// + MemberProfile +} diff --git a/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelFlags.cs b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelFlags.cs new file mode 100644 index 0000000000..8e7a94ca83 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelFlags.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Channel flags applied to the containing channel as bit fields. +/// +[Flags] +public enum DiscordChannelFlags +{ + None, + + /// + /// Indicates whether this is a thread channel pinned to the top of its parent forum channel. + /// + Pinned = 1 << 1, + + /// + /// Indicates whether this is a forum channel which requires tags to be specified when creating + /// a thread inside. + /// + RequireTag = 1 << 4, + + /// + /// Indicates to the client to hide the embedded media download options. This is available only for + /// media channels. + /// + HideMediaDownloadOptions = 1 << 15 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelOverwriteType.cs b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelOverwriteType.cs new file mode 100644 index 0000000000..cd3403519c --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelOverwriteType.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates different targets for channel overwrites. +/// +public enum DiscordChannelOverwriteType +{ + Role, + User +} diff --git a/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelType.cs b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelType.cs new file mode 100644 index 0000000000..d4ee64bdbd --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordChannelType.cs @@ -0,0 +1,76 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the valid types for a channel. +/// +public enum DiscordChannelType +{ + /// + /// A text channel within a guild. + /// + GuildText, + + /// + /// A direct message between users. + /// + Dm, + + /// + /// A voice channel within a guild. + /// + GuildVoice, + + /// + /// A direct message between multiple users. + /// + GroupDm, + + /// + /// A category channel within a guild that contains up to 50 channels. + /// + GuildCategory, + + /// + /// A channel that users can follow and crosspost into their own guilds. + /// + GuildAnnouncement, + + /// + /// A thread channel within an announcement channel. + /// + AnnouncementThread = 10, + + /// + /// A public thread channel in a text or forum channel. + /// + PublicThread, + + /// + /// A private thread channel in a text channel. + /// + PrivateThread, + + /// + /// A stage channel. + /// + GuildStageVoice, + + /// + /// A list of servers in a hub. + /// + GuildDirectory, + + /// + /// A channel that can only contain public threads. + /// + GuildForum, + + /// + /// A channel that can only contain threads, similar to . + /// + GuildMedia +} diff --git a/src/core/DSharpPlus.Shared/Entities/Channels/DiscordForumLayoutType.cs b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordForumLayoutType.cs new file mode 100644 index 0000000000..6bbef8339c --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordForumLayoutType.cs @@ -0,0 +1,15 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the different forum layouts permitted. +/// +public enum DiscordForumLayoutType +{ + NotSet, + ListView, + GalleryView +} diff --git a/src/core/DSharpPlus.Shared/Entities/Channels/DiscordForumSortOrder.cs b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordForumSortOrder.cs new file mode 100644 index 0000000000..111060b3a7 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordForumSortOrder.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the different sorting orders for forum posts. +/// +public enum DiscordForumSortOrder +{ + LatestActivity, + CreationDate +} diff --git a/src/core/DSharpPlus.Shared/Entities/Channels/DiscordVideoQualityMode.cs b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordVideoQualityMode.cs new file mode 100644 index 0000000000..a673a66e7b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Channels/DiscordVideoQualityMode.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Lists different video quality modes +/// +public enum DiscordVideoQualityMode +{ + /// + /// Discord chooses the quality for optimal performance. + /// + Auto = 1, + + /// + /// 720p + /// + Full +} diff --git a/src/core/DSharpPlus.Shared/Entities/Components/DiscordButtonStyle.cs b/src/core/DSharpPlus.Shared/Entities/Components/DiscordButtonStyle.cs new file mode 100644 index 0000000000..1e08fa4d8f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Components/DiscordButtonStyle.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates different display styles for buttons. +/// +public enum DiscordButtonStyle +{ + /// + /// A discord-blurple button. + /// + Primary = 1, + + /// + /// A gray button. + /// + Secondary, + + /// + /// A green button. + /// + Success, + + /// + /// A red button. + /// + Danger, + + /// + /// A gray button, navigating to an URL. + /// + Link, + + /// + /// A discord-blurple button directing an user to purchase an SKU. + /// + Premium +} diff --git a/src/core/DSharpPlus.Shared/Entities/Components/DiscordMessageComponentType.cs b/src/core/DSharpPlus.Shared/Entities/Components/DiscordMessageComponentType.cs new file mode 100644 index 0000000000..2baa453f4b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Components/DiscordMessageComponentType.cs @@ -0,0 +1,86 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the different types of message components. +/// +public enum DiscordMessageComponentType +{ + /// + /// A container for other components: up to five button or one non-button component. + /// + ActionRow = 1, + + /// + /// A clickable button component. + /// + Button, + + /// + /// A select menu for picking from application-defined text options. + /// + StringSelect, + + /// + /// A text input field. + /// + TextInput, + + /// + /// A select menu for picking from users. + /// + UserSelect, + + /// + /// A select menu for picking from roles. + /// + RoleSelect, + + /// + /// A select menu for picking from mentionable entities (users and roles). + /// + MentionableSelect, + + /// + /// A select menu for picking from channels. + /// + ChannelSelect, + + /// + /// A container to display text alongside an accessory component (button or thumbnail). + /// + Section, + + /// + /// A component containing markdown text. + /// + TextDisplay, + + /// + /// A small image that can be used as an accessory for a . + /// + Thumbnail, + + /// + /// A component displaying media items. + /// + MediaGallery, + + /// + /// A component displaying an attached file. + /// + File, + + /// + /// A component to add vertical padding and/or display a vertical line between other components. + /// + Separator, + + /// + /// A container that visually groups components, akin to an embed. + /// + Container = 17 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Components/DiscordSeparatorSpacingSize.cs b/src/core/DSharpPlus.Shared/Entities/Components/DiscordSeparatorSpacingSize.cs new file mode 100644 index 0000000000..c0d1b0c5eb --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Components/DiscordSeparatorSpacingSize.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the spacing size for a separator. +/// +public enum DiscordSeparatorSpacingSize +{ + /// + /// 17px, or about one line of text, worth of space. + /// + Small = 1, + + /// + /// 33px, or about two lines of text, worth of space. + /// + Large +} diff --git a/src/core/DSharpPlus.Shared/Entities/Components/DiscordTextInputStyle.cs b/src/core/DSharpPlus.Shared/Entities/Components/DiscordTextInputStyle.cs new file mode 100644 index 0000000000..759fb7c5d2 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Components/DiscordTextInputStyle.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents input styles for text input fields. +/// +public enum DiscordTextInputStyle +{ + /// + /// Single-line input. + /// + Short = 1, + + /// + /// Multi-line input. + /// + Paragraph +} diff --git a/DSharpPlus/Entities/DiscordPermission.cs b/src/core/DSharpPlus.Shared/Entities/DiscordPermission.cs similarity index 87% rename from DSharpPlus/Entities/DiscordPermission.cs rename to src/core/DSharpPlus.Shared/Entities/DiscordPermission.cs index 1e171e2450..7962972e10 100644 --- a/DSharpPlus/Entities/DiscordPermission.cs +++ b/src/core/DSharpPlus.Shared/Entities/DiscordPermission.cs @@ -1,3 +1,9 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1027 + using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; @@ -86,7 +92,7 @@ public enum DiscordPermission /// Allows members to send text-to-speech messages. /// [Display(Name = "Send Text-to-speech Messages")] - SendTtsMessages = 12, + SendTTSMessages = 12, /// /// Allows members to delete other's messages. @@ -191,7 +197,7 @@ public enum DiscordPermission ManageWebhooks = 29, /// - /// Allows members to manage guild emojis, stickers and soundboard sounds. + /// Allows members to manage guild emojis and stickers. /// [Display(Name = "Manage Guild Expressions")] ManageGuildExpressions = 30, @@ -266,25 +272,7 @@ public enum DiscordPermission /// Allows members to use the soundboard in a voice channel. /// [Display(Name = "Use Soundboard")] - UseSoundboard = 42, - - /// - /// Allows members to create emojis, stickers and soundboard sounds, as well as editing and deleting those they created. - /// - [Display(Name = "Create Guild Expressions")] - CreateGuildExpressions = 43, - - /// - /// Allows members to create scheduled events, as well as editing and deleting those they created. - /// - [Display(Name = "Create Scheduled Events")] - CreateEvents = 44, - - /// - /// Allows members to play soundboard sounds from other guilds. - /// - [Display(Name = "Use External Soundboard Sounds")] - UseExternalSounds = 45, + UseSoundboard = 42, /// /// Allows members to send voice messages. @@ -302,18 +290,5 @@ public enum DiscordPermission /// Allows members to use external, user-installable apps. /// [Display(Name = "Use External Apps")] - UseExternalApps = 50, - - /// - /// Allows members to pin and unpin messages. - /// - [Display(Name = "Pin Messages")] - PinMessages = 51, - - /// - /// Allows members to bypass slowmode. - /// - [Display(Name = "Bypass Slowmode")] - BypassSlowmode = 52 + UseExternalApps = 50 } - diff --git a/DSharpPlus/Entities/DiscordPermissions.Arithmetics.cs b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Arithmetics.cs similarity index 87% rename from DSharpPlus/Entities/DiscordPermissions.Arithmetics.cs rename to src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Arithmetics.cs index f79e7e4906..d132fa0db1 100644 --- a/DSharpPlus/Entities/DiscordPermissions.Arithmetics.cs +++ b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Arithmetics.cs @@ -1,4 +1,8 @@ -#pragma warning disable IDE0040 +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable SYSLIB5001 using System; using System.Runtime.Intrinsics; @@ -207,4 +211,32 @@ public void Remove(params ReadOnlySpan permissions) return new(result); } -} + + /// + /// Adds the provided permission sets together. + /// + /// + /// This is semantically equivalent to repeatedly calling Add, just, faster. + /// + public static DiscordPermissions Combine(params ReadOnlySpan permissions) + { + Span result = stackalloc byte[ContainerByteCount]; + + for (int i = 0; i < ContainerByteCount; i += 16) + { + Vector128 v1 = Vector128.Zero; + + foreach (DiscordPermissions set in permissions) + { + ReadOnlySpan other = set.AsSpan; + Vector128 v2 = Vector128.LoadUnsafe(in other[i]); + + v1 = Vector128.BitwiseOr(v1, v2); + } + + v1.StoreUnsafe(ref result[i]); + } + + return new(result); + } +} diff --git a/DSharpPlus/Entities/DiscordPermissions.Enumeration.cs b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Enumeration.cs similarity index 85% rename from DSharpPlus/Entities/DiscordPermissions.Enumeration.cs rename to src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Enumeration.cs index 7a737e9f4c..770f2dbbd3 100644 --- a/DSharpPlus/Entities/DiscordPermissions.Enumeration.cs +++ b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Enumeration.cs @@ -1,9 +1,15 @@ -#pragma warning disable IDE0040 +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1034 +#pragma warning disable CA1815 using System.Collections; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; + using CommunityToolkit.HighPerformance.Helpers; namespace DSharpPlus.Entities; @@ -17,8 +23,8 @@ partial struct DiscordPermissions public readonly DiscordPermissionEnumerator GetEnumerator() => new(this.data); // implementations for IEnumerable, we'd like to not box by default - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Represents an enumerator for permission fields within a permission set. @@ -30,9 +36,7 @@ public struct DiscordPermissionEnumerator : IEnumerator internal DiscordPermissionEnumerator(DiscordPermissionContainer data) => this.data = data; -#pragma warning disable DSP0009 public readonly DiscordPermission Current => (DiscordPermission)(this.block << 5) + this.bit; -#pragma warning restore DSP0009 readonly object IEnumerator.Current => this.Current; @@ -84,4 +88,4 @@ public readonly void Dispose() } } -} +} diff --git a/DSharpPlus/Entities/DiscordPermissions.Utility.cs b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Utility.cs similarity index 75% rename from DSharpPlus/Entities/DiscordPermissions.Utility.cs rename to src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Utility.cs index 1296ac70b2..1336ea1fd6 100644 --- a/DSharpPlus/Entities/DiscordPermissions.Utility.cs +++ b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.Utility.cs @@ -1,44 +1,46 @@ -#pragma warning disable IDE0040 - -using System; - -namespace DSharpPlus.Entities; - -partial struct DiscordPermissions -{ - /// - /// Toggles the specified permission between states. - /// - public readonly DiscordPermissions Toggle(DiscordPermission permission) - => this ^ permission; - - /// - /// Toggles all of the specified permissions between states. - /// - public readonly DiscordPermissions Toggle(params ReadOnlySpan permissions) - => this ^ new DiscordPermissions(permissions); - - /// - /// Returns whether the specified permission is set explicitly. - /// - public readonly bool HasFlag(DiscordPermission flag) - => GetFlag((int)flag); - - /// - /// Returns whether the specified permission is granted, either directly or through Administrator permissions. - /// - public readonly bool HasPermission(DiscordPermission permission) - => HasFlag(DiscordPermission.Administrator) || HasFlag(permission); - - /// - /// Returns whether any of the specified permissions are granted, either directly or through Administrator permissions. - /// - public readonly bool HasAnyPermission(DiscordPermissions permissions) - => HasFlag(DiscordPermission.Administrator) || (this & permissions) != None; - - /// - /// Returns whether all of the specified permissions are granted, either directly or through Administrator permissions. - /// - public readonly bool HasAllPermissions(DiscordPermissions expected) - => HasFlag(DiscordPermission.Administrator) || (this & expected) == expected; -} +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +partial struct DiscordPermissions +{ + /// + /// Toggles the specified permission between states. + /// + public readonly DiscordPermissions Toggle(DiscordPermission permission) + => this ^ permission; + + /// + /// Toggles all of the specified permissions between states. + /// + public readonly DiscordPermissions Toggle(params ReadOnlySpan permissions) + => this ^ new DiscordPermissions(permissions); + + /// + /// Returns whether the specified permission is set explicitly. + /// + public readonly bool HasFlag(DiscordPermission flag) + => GetFlag((int)flag); + + /// + /// Returns whether the specified permission is granted, either directly or through Administrator permissions. + /// + public readonly bool HasPermission(DiscordPermission permission) + => this.HasFlag(DiscordPermission.Administrator) || this.HasFlag(permission); + + /// + /// Returns whether any of the specified permissions are granted, either directly or through Administrator permissions. + /// + public readonly bool HasAnyPermission(DiscordPermissions permissions) + => this.HasFlag(DiscordPermission.Administrator) || (this & permissions) != None; + + /// + /// Returns whether all of the specified permissions are granted, either directly or through Administrator permissions. + /// + public readonly bool HasAllPermissions(DiscordPermissions expected) + => this.HasFlag(DiscordPermission.Administrator) || (this & expected) == expected; +} diff --git a/DSharpPlus/Entities/DiscordPermissions.cs b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.cs similarity index 94% rename from DSharpPlus/Entities/DiscordPermissions.cs rename to src/core/DSharpPlus.Shared/Entities/DiscordPermissions.cs index fcbc884485..d58621d81f 100644 --- a/DSharpPlus/Entities/DiscordPermissions.cs +++ b/src/core/DSharpPlus.Shared/Entities/DiscordPermissions.cs @@ -1,329 +1,331 @@ -#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))] -[DebuggerDisplay("{ToString(\"name\")}")] -public partial struct DiscordPermissions - : IEquatable, IEnumerable -{ - // 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 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(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 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 readonly ReadOnlySpan AsSpan - => MemoryMarshal.Cast(this.data); - - private readonly bool GetFlag(int index) - => this.data.HasFlag(index); - - /// - /// Determines whether this Discord permission set is equal to the provided object. - /// - public override readonly 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 readonly bool Equals(DiscordPermissions other) - => ((ReadOnlySpan)this.data).SequenceEqual(other.data); - - /// - /// Returns a string representation of this permission set. - /// - public override readonly 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.
- /// - name:format: This prints each flag by name according to the specified format. The string {permission} must be contained to mark the position of the flag.
- /// - anything else will print the integer value contained in this instance. - /// - public readonly 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 this) - { - 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 readonly int GetHashCode() - => HashCode.Combine(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); - } - } -} +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable SYSLIB5001 + +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 HashCode = CommunityToolkit.HighPerformance.Helpers.HashCode; + +namespace DSharpPlus.Entities; + +/// +/// Represents a set of Discord permissions. +/// +/// +/// This type expects to be zero-initialized. Using this type in [SkipLocalsInit] contexts may be dangerous. +/// +[DebuggerDisplay("{ToString(\"name\")}")] +public partial struct DiscordPermissions + : IEquatable, IEnumerable +{ + // 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 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(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 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 readonly ReadOnlySpan AsSpan + => MemoryMarshal.Cast(this.data); + + private readonly bool GetFlag(int index) + => this.data.HasFlag(index); + + /// + /// Determines whether this Discord permission set is equal to the provided object. + /// + public override readonly bool Equals([NotNullWhen(true)] object? obj) + => obj is DiscordPermissions permissions && this.Equals(permissions); + + /// + /// Determines whether this Discord permission set is equal to the provided Discord permission set. + /// + public readonly bool Equals(DiscordPermissions other) + => ((ReadOnlySpan)this.data).SequenceEqual(other.data); + + /// + /// Returns a string representation of this permission set. + /// + public override readonly string ToString() => this.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.
+ /// - name:format: This prints each flag by name according to the specified format. The string {permission} must be contained to mark the position of the flag.
+ /// - anything else will print the integer value contained in this instance. + /// + public readonly 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 this) + { + 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 readonly int GetHashCode() + => HashCode.Combine(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/src/core/DSharpPlus.Shared/Entities/Entitlements/DiscordEntitlementOwnerType.cs b/src/core/DSharpPlus.Shared/Entities/Entitlements/DiscordEntitlementOwnerType.cs new file mode 100644 index 0000000000..0e0b8a536c --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Entitlements/DiscordEntitlementOwnerType.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the owners of an entitlement. +/// +public enum DiscordEntitlementOwnerType +{ + Guild = 1, + User +} diff --git a/src/core/DSharpPlus.Shared/Entities/Entitlements/DiscordEntitlementType.cs b/src/core/DSharpPlus.Shared/Entities/Entitlements/DiscordEntitlementType.cs new file mode 100644 index 0000000000..254bad0b28 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Entitlements/DiscordEntitlementType.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// The type of an entitlement. +/// +public enum DiscordEntitlementType +{ + /// + /// The entitlement was purchased by the user. + /// + Purchase = 1, + + /// + /// The entitlement was given for a Nitro subscription. + /// + PremiumSubscription, + + /// + /// The entitlement was a gift from the developer. + /// + DeveloperGift, + + /// + /// The entitlement was purchased by a developer in test mode. + /// + TestModePurchase, + + /// + /// The entitlement was granted when the SKU was free. + /// + FreePurchase, + + /// + /// The entitlement was gifted by another user. + /// + UserGift, + + /// + /// The entitlement was claimed by the user as a Nitro subscriber. + /// + PremiumPurchase, + + /// + /// This entitlement was purchased as an app subscription. + /// + ApplicationSubscription +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordExplicitContentFilterLevel.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordExplicitContentFilterLevel.cs new file mode 100644 index 0000000000..3f1143f4f8 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordExplicitContentFilterLevel.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the severity of the explicit content filter in the given guild. +/// +public enum DiscordExplicitContentFilterLevel +{ + /// + /// Media content will not be scanned. + /// + Disabled, + + /// + /// Media content sent by members without roles will be scanned. + /// + MembersWithoutRoles, + + /// + /// Media content sent by all members will be scanned. + /// + AllMembers +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildMemberFlags.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildMemberFlags.cs new file mode 100644 index 0000000000..37efa0152d --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildMemberFlags.cs @@ -0,0 +1,62 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents bitwise guild member flags. +/// +[Flags] +public enum DiscordGuildMemberFlags +{ + None = 0, + + /// + /// Indicates that this member has left and rejoined this guild. + /// + DidRejoin = 1 << 0, + + /// + /// Indicates that this member has completed guild onboarding. + /// + CompletedOnboarding = 1 << 1, + + /// + /// Indicates that this member is exempt from verification requirements. This flag can be set + /// by bots. + /// + BypassesVerification = 1 << 2, + + /// + /// Indicates that this member has started the guild onboarding process. + /// + StartedOnboarding = 1 << 3, + + /// + /// Indicates that this member is a guest and can only access the voice channel they were invited to. + /// + IsGuest = 1 << 4, + + /// + /// Indicates that this member has started the 'new member' actions in the server guide. + /// + StartedServerGuide = 1 << 5, + + /// + /// Indicates that this member has completed the 'new member' actions in the server guide. + /// + CompletedServerGuide = 1 << 6, + + /// + /// Indicates that this member was quarantined by automod for their username, global name or nickname. + /// + AutomodQuarantinedUsername = 1 << 7, + + /// + /// Indicates that this member has acknowledged and dismissed the DM settings upsell. + /// + DmSettingsUpsellAcknowledged = 1 << 9 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildOnboardingMode.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildOnboardingMode.cs new file mode 100644 index 0000000000..fa19a9dbea --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildOnboardingMode.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Defines the criteria used to satisfy Onboarding constraints required for enabling. +/// +[Flags] +public enum DiscordGuildOnboardingMode +{ + /// + /// Counts only default channels towards constraints. + /// + OnboardingDefault, + + /// + /// Counts default channels and questions towards constraints. + /// + OnboardingAdvanced +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildOnboardingPromptType.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildOnboardingPromptType.cs new file mode 100644 index 0000000000..eeac34104a --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordGuildOnboardingPromptType.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the type of a guild onboarding prompt +/// +public enum DiscordGuildOnboardingPromptType +{ + MultipleChoice, + Dropdown +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordIntegrationExpirationBehaviour.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordIntegrationExpirationBehaviour.cs new file mode 100644 index 0000000000..e97b1a15ce --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordIntegrationExpirationBehaviour.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies how an integration should act in the event of a subscription expiring. +/// +public enum DiscordIntegrationExpirationBehaviour +{ + RemoveRole, + Kick +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordMessageNotificationLevel.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordMessageNotificationLevel.cs new file mode 100644 index 0000000000..8805e6e85e --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordMessageNotificationLevel.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// The default notification settings for a guild. +/// +public enum DiscordMessageNotificationLevel +{ + /// + /// Members will, by default, receive notifications for all messages. + /// + AllMessages, + + /// + /// Members will, by default, receive notifications only for messages mentioning them specifically. + /// + OnlyMentions +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordMfaLevel.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordMfaLevel.cs new file mode 100644 index 0000000000..a588fc0fd3 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordMfaLevel.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Indicates multi-factor-auth (MFA) requirements for moderation actions. +/// +public enum DiscordMfaLevel +{ + /// + /// This guild has no MFA requirement for moderation actions. + /// + None, + + /// + /// This guild has a 2FA requirement for moderation actions. + /// + Elevated +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordNsfwLevel.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordNsfwLevel.cs new file mode 100644 index 0000000000..1f2ba0b876 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordNsfwLevel.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates NSFW levels for a certain guild. +/// +public enum DiscordNsfwLevel +{ + Default, + Explicit, + Safe, + AgeRestricted +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordRoleFlags.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordRoleFlags.cs new file mode 100644 index 0000000000..f9788c4e1f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordRoleFlags.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents flags for a role. +/// +[Flags] +public enum DiscordRoleFlags +{ + /// + /// Indicates that this role can be selected by members in an onboarding prompt. + /// + InPrompt = 1 << 0 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordSystemChannelFlags.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordSystemChannelFlags.cs new file mode 100644 index 0000000000..b8667afa7b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordSystemChannelFlags.cs @@ -0,0 +1,46 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents additional settings for system channels. +/// +[Flags] +public enum DiscordSystemChannelFlags +{ + None = 0, + + /// + /// Disables member join messages in this channel. + /// + SuppressJoinNotifications = 1 << 0, + + /// + /// Disables server boost notifications in this channel. + /// + SuppressPremiumSubscriptions = 1 << 1, + + /// + /// Disables server setup tips in this channnel. + /// + SuppressGuildReminderNotifications = 1 << 2, + + /// + /// Disables the sticker reply buttons on member join messages. + /// + SuppressJoinNotificationReplies = 1 << 3, + + /// + /// Disables role subscription purchase and renewal notifications in this channel. + /// + SuppressRoleSubscriptionPurchaseNotifications = 1 << 4, + + /// + /// Disables the sticker reply buttons on role subscription purchase and renewal messages. + /// + SuppressRoleSubscriptionPurchaseNotificationReplies = 1 << 5 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordVerificationLevel.cs b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordVerificationLevel.cs new file mode 100644 index 0000000000..e5700df29f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Guilds/DiscordVerificationLevel.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the levels of verification restriction required to speak in a guild. +/// +public enum DiscordVerificationLevel +{ + /// + /// Unrestricted. + /// + None, + + /// + /// The user must have a verified email. + /// + Low, + + /// + /// This user must be registered on Discord for longer than five minutes. + /// + Medium, + + /// + /// This user must have been a member of this server for longer than ten minutes. + /// + High, + + /// + /// This user must have a verified phone number. + /// + VeryHigh +} diff --git a/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionCallbackType.cs b/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionCallbackType.cs new file mode 100644 index 0000000000..70e1af63df --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionCallbackType.cs @@ -0,0 +1,49 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the different ways of responding to different interactions. +/// +public enum DiscordInteractionCallbackType +{ + /// + /// Acknowledges a . + /// + Pong = 1, + + /// + /// Responds to an interaction by sending a message. + /// + ChannelMessageWithSource = 4, + + /// + /// Acknowledges an interaction and allows the bot to edit a response in later; the user + /// sees a loading state. + /// + DeferredChannelMessageWithSource, + + /// + /// Acknowledges a component interaction and allows the bot to edit a response in later; + /// the user does not see a loading state. + /// + DeferredUpdateMessage, + + /// + /// Responds to a component interaction by editing the message the component was attached to. + /// + UpdateMessage, + + /// + /// Responds to an autocomplete interaction by suggesting choices. + /// + ApplicationCommandAutocompleteResult, + + /// + /// Responds to an interaction with a pop-up modal. This cannot be sent in response to a modal + /// submission or a ping. + /// + Modal +} diff --git a/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionContextType.cs b/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionContextType.cs new file mode 100644 index 0000000000..5079f82d34 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionContextType.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies contexts where an interaction can be used or was triggered from. +/// +public enum DiscordInteractionContextType +{ + /// + /// This interaction can be used or was used within a guild. + /// + Guild, + + /// + /// This interaction can be used or was used in a direct message with the bot. + /// + BotDm, + + /// + /// This interaction can be used or was used in a DM or group DM without the bot. + /// + PrivateChannel +} diff --git a/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionType.cs b/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionType.cs new file mode 100644 index 0000000000..e2b1e311a8 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Interactions/DiscordInteractionType.cs @@ -0,0 +1,17 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the types of inbound interactions. +/// +public enum DiscordInteractionType +{ + Ping = 1, + ApplicationCommand, + MessageComponent, + ApplicationCommandAutocomplete, + ModalSubmit +} diff --git a/src/core/DSharpPlus.Shared/Entities/Invites/DiscordInviteTargetType.cs b/src/core/DSharpPlus.Shared/Entities/Invites/DiscordInviteTargetType.cs new file mode 100644 index 0000000000..2264428fd8 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Invites/DiscordInviteTargetType.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies where this invite points to, if not a channel. +/// +public enum DiscordInviteTargetType +{ + Stream = 1, + EmbeddedApplication +} diff --git a/src/core/DSharpPlus.Shared/Entities/Invites/DiscordInviteType.cs b/src/core/DSharpPlus.Shared/Entities/Invites/DiscordInviteType.cs new file mode 100644 index 0000000000..637b3d4669 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Invites/DiscordInviteType.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the type of an invite. +/// +public enum DiscordInviteType +{ + /// + /// An invite to a guild. + /// + Guild, + + /// + /// An invite to a group DM. + /// + GroupDm, + + /// + /// A friend invite. + /// + Friend +} diff --git a/src/core/DSharpPlus.Shared/Entities/Messages/DiscordAttachmentFlags.cs b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordAttachmentFlags.cs new file mode 100644 index 0000000000..f9f925c5c2 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordAttachmentFlags.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents flags for an attachment. +/// +[Flags] +public enum DiscordAttachmentFlags +{ + /// + /// Indicates that this attachment has been edited using the remix feature on mobile. + /// + IsRemix = 1 << 2 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageActivityType.cs b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageActivityType.cs new file mode 100644 index 0000000000..548517c5ea --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageActivityType.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates valid activity kinds to be encoded in a message. +/// +public enum DiscordMessageActivityType +{ + Join = 1, + Spectate, + Listen, + JoinRequest = 5 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageFlags.cs b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageFlags.cs new file mode 100644 index 0000000000..47844e8d60 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageFlags.cs @@ -0,0 +1,81 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Enumerates additional flags applied to messages. +/// +[Flags] +public enum DiscordMessageFlags +{ + None = 0, + + /// + /// This message has been published to following channels. + /// + Crossposted = 1 << 0, + + /// + /// This message originated from a message in another channel this channel is following. + /// + IsCrosspost = 1 << 1, + + /// + /// Indicates whether embeds on this message are to be displayed or not. + /// + SuppressEmbeds = 1 << 2, + + /// + /// Indicates that this is a crossposted message whose source has been deleted. + /// + SourceMessageDeleted = 1 << 3, + + /// + /// Indicates that this is a message originating from the urgent messaging system. + /// + Urgent = 1 << 4, + + /// + /// Indicates that this message has an associated thread with the same identifier as this message. + /// + HasThread = 1 << 5, + + /// + /// Indicates that this message is only visible to the user who invoked the interaction. + /// + Ephemeral = 1 << 6, + + /// + /// Indicates that this message is an interaction response and that the bot is 'thinking'. + /// + Loading = 1 << 7, + + /// + /// This message failed to mention some roles and add their members to the thread. + /// + FailedToEnforceSomeRolesInThread = 1 << 8, + + /// + /// This message will not trigger push and desktop notifications. + /// + SuppressNotifications = 1 << 12, + + /// + /// This message is a voice message. + /// + IsVoiceMessage = 1 << 13, + + /// + /// This message contains a message snapshot, via forwarding. + /// + HasSnapshot = 1 << 14, + + /// + /// This message contains layout components and does not contain content, embeds, polls or stickers. + /// + EnableLayoutComponents = 1 << 15, +} diff --git a/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageReferenceType.cs b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageReferenceType.cs new file mode 100644 index 0000000000..b7bd6e2a7c --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageReferenceType.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the type of a message reference. +/// +public enum DiscordMessageReferenceType +{ + /// + /// A standard reference used by replies. + /// + Default, + + /// + /// A reference used to point to a message at a certain point in time. + /// + Forward +} diff --git a/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageType.cs b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageType.cs new file mode 100644 index 0000000000..ef2a104af7 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Messages/DiscordMessageType.cs @@ -0,0 +1,48 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1027 // this is not a flags enum. + +namespace DSharpPlus.Entities; + +public enum DiscordMessageType +{ + Default, + RecipientAdd, + RecipientRemove, + Call, + ChannelNameChange, + ChannelIconChange, + ChannelPinnedMessage, + UserJoin, + GuildBoost, + GuildBoostTier1, + GuildBoostTier2, + GuildBoostTier3, + ChannelFollowAdd, + GuildDiscoveryDisqualified = 14, + GuildDiscoveryRequalified, + GuildDiscoveryGracePeriodInitialWarning, + GuildDiscoveryGracePeriodFinalWarning, + ThreadCreated, + Reply, + ChatInputCommand, + ThreadStarterMessage, + GuildInviteReminder, + ContextMenuCommand, + AutoModerationAction, + RoleSubscriptionPurchase, + InteractionPremiumUpsell, + StageStart, + StageEnd, + StageSpeaker, + StageTopic = 31, + GuildApplicationPremiumSubscription, + GuildIncidentAlertModeEnabled = 36, + GuildIncidentAlertModeDisabled, + GuildIncidentReportRaid, + GuildIncidentReportFalseAlarm, + PurchaseNotification = 44, + PollResult = 46 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Polls/DiscordPollLayoutType.cs b/src/core/DSharpPlus.Shared/Entities/Polls/DiscordPollLayoutType.cs new file mode 100644 index 0000000000..5315df7d4f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Polls/DiscordPollLayoutType.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the available layout types for polls. +/// +public enum DiscordPollLayoutType +{ + /// + /// "The, uhm, default layout type." - Discord. + /// + Default = 1 +} diff --git a/src/core/DSharpPlus.Shared/Entities/RoleConnections/DiscordRoleConnectionMetadataType.cs b/src/core/DSharpPlus.Shared/Entities/RoleConnections/DiscordRoleConnectionMetadataType.cs new file mode 100644 index 0000000000..7a545b5ec7 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/RoleConnections/DiscordRoleConnectionMetadataType.cs @@ -0,0 +1,51 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// The specific kinds of metadata comparisons that can be made. +/// +public enum DiscordRoleConnectionMetadataType +{ + /// + /// The metadata integer is less than or equal to the guild's configured value. + /// + IntegerLessThanOrEqual = 1, + + /// + /// The metadata integer is greater than or equal to the guild's configured value. + /// + IntegerGreaterThanOrEqual, + + /// + /// The metadata integer is equal to the guild's configured value. + /// + IntegerEqual, + + /// + /// The metadata integer is not equal to the guild's configured value. + /// + IntegerNotEqual, + + /// + /// The metadata date/time object is less than or equal to the guild's configured value. + /// + DateTimeLessThanOrEqual, + + /// + /// The metadata date/time object is greater than or equal to the guild's configured value. + /// + DateTimeGreaterThanOrEqual, + + /// + /// The metadata boolean is equal to the guild's configured value. + /// + BooleanEqual, + + /// + /// The metadata boolean is not equal to the guild's configured value. + /// + BooleanNotEqual +} diff --git a/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventPrivacyLevel.cs b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventPrivacyLevel.cs new file mode 100644 index 0000000000..f7a9de9c43 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventPrivacyLevel.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies who this event is accessible to. +/// +public enum DiscordScheduledEventPrivacyLevel +{ + /// + /// This event is only accessible to guild members. + /// + GuildOnly = 2 +} diff --git a/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceFrequency.cs b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceFrequency.cs new file mode 100644 index 0000000000..12ed55f278 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceFrequency.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the frequency of a recurrence rule. +/// +public enum DiscordScheduledEventRecurrenceFrequency +{ + Yearly, + Monthly, + Weekly, + Daily +} diff --git a/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceMonth.cs b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceMonth.cs new file mode 100644 index 0000000000..222ab44a6b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceMonth.cs @@ -0,0 +1,24 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies months to recur on. +/// +public enum DiscordScheduledEventRecurrenceMonth +{ + January = 1, + February, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December +} diff --git a/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceWeekday.cs b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceWeekday.cs new file mode 100644 index 0000000000..231709d00d --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventRecurrenceWeekday.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies weekdays to recur on. +/// +public enum DiscordScheduledEventRecurrenceWeekday +{ + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday +} diff --git a/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventStatus.cs b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventStatus.cs new file mode 100644 index 0000000000..da88644692 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventStatus.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the valid states of a scheduled event. +/// +/// +/// Once the status is set to or , it can no longer +/// be updated. +/// +public enum DiscordScheduledEventStatus +{ + Scheduled = 1, + Active, + Completed, + Canceled +} diff --git a/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventType.cs b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventType.cs new file mode 100644 index 0000000000..6dca6c706f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/ScheduledEvents/DiscordScheduledEventType.cs @@ -0,0 +1,15 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the different entity types for events; influencing what fields are present on the parent +/// object. +/// +public enum DiscordScheduledEventType +{ + StageInstance = 1, + Voice +} diff --git a/src/core/DSharpPlus.Shared/Entities/Skus/DiscordSkuFlags.cs b/src/core/DSharpPlus.Shared/Entities/Skus/DiscordSkuFlags.cs new file mode 100644 index 0000000000..f59cede176 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Skus/DiscordSkuFlags.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents additional flags for a given SKU. +/// +[Flags] +public enum DiscordSkuFlags +{ + /// + /// This SKU is available for purchase. + /// + Available = 1 << 2, + + /// + /// A subscription purchased by a user and applied to a single server. Everyone in that server gets access + /// to the given SKU. + /// + GuildSubscription = 1 << 7, + + /// + /// A subscription purchased by a user for themselves. They get access to the given SKU in every server. + /// + UserSubscription = 1 << 8 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Skus/DiscordSkuType.cs b/src/core/DSharpPlus.Shared/Entities/Skus/DiscordSkuType.cs new file mode 100644 index 0000000000..646aac127f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Skus/DiscordSkuType.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the type of a given SKU. +/// +public enum DiscordSkuType +{ + /// + /// Represents a permanent one-time purchase. + /// + Durable = 2, + + /// + /// Represents a consumable one-time purchase. + /// + Consumable = 3, + + /// + /// Represents a recurring subscription. + /// + Subscription = 5, + + /// + /// A system-generated group for each subscription SKU created. + /// + SubscriptionGroup = 6 +} diff --git a/src/core/DSharpPlus.Shared/Entities/StageInstances/DiscordStagePrivacyLevel.cs b/src/core/DSharpPlus.Shared/Entities/StageInstances/DiscordStagePrivacyLevel.cs new file mode 100644 index 0000000000..92693c647a --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/StageInstances/DiscordStagePrivacyLevel.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Indicates to whom this stage instance is visible. +/// +public enum DiscordStagePrivacyLevel +{ + /// + /// This stage instance is only visible to guild members. + /// + GuildOnly = 2 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Stickers/DiscordStickerFormatType.cs b/src/core/DSharpPlus.Shared/Entities/Stickers/DiscordStickerFormatType.cs new file mode 100644 index 0000000000..a843b5a422 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Stickers/DiscordStickerFormatType.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates different sticker file format types. +/// +public enum DiscordStickerFormatType +{ + Png = 1, + Apng, + Lottie, + Gif +} diff --git a/src/core/DSharpPlus.Shared/Entities/Stickers/DiscordStickerType.cs b/src/core/DSharpPlus.Shared/Entities/Stickers/DiscordStickerType.cs new file mode 100644 index 0000000000..e48f1282f7 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Stickers/DiscordStickerType.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies whether the given sticker belongs to a guild or is a default sticker. +/// +public enum DiscordStickerType +{ + /// + /// Indicates an official sticker in a sticker pack; a nitro sticker or a part of + /// a removed purchasable pack. + /// + Standard = 1, + + /// + /// Indicates a sticker uploaded to a guild. + /// + Guild +} diff --git a/src/core/DSharpPlus.Shared/Entities/Subscriptions/DiscordSubscriptionStatus.cs b/src/core/DSharpPlus.Shared/Entities/Subscriptions/DiscordSubscriptionStatus.cs new file mode 100644 index 0000000000..b2f80c083b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Subscriptions/DiscordSubscriptionStatus.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Indicates the status of a subscription object. +/// +public enum DiscordSubscriptionStatus +{ + /// + /// Specifies that the subscription is active and will renew. + /// + Active, + + /// + /// Specifies that the subscription is active and will not renew. + /// + Ending, + + /// + /// Specifies that the subscription is inactive. + /// + Inactive +} diff --git a/src/core/DSharpPlus.Shared/Entities/Teams/DiscordTeamMembershipState.cs b/src/core/DSharpPlus.Shared/Entities/Teams/DiscordTeamMembershipState.cs new file mode 100644 index 0000000000..4466fa7e02 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Teams/DiscordTeamMembershipState.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the membership state of an user in a team. +/// +public enum DiscordTeamMembershipState +{ + Invited = 1, + Accepted +} diff --git a/src/core/DSharpPlus.Shared/Entities/Users/DiscordConnectionVisibility.cs b/src/core/DSharpPlus.Shared/Entities/Users/DiscordConnectionVisibility.cs new file mode 100644 index 0000000000..fb8326cea0 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Users/DiscordConnectionVisibility.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Represents the visibility of a user profile connection. +/// +public enum DiscordConnectionVisibility +{ + /// + /// Visible to only the user themself. + /// + None, + + /// + /// Visible to everyone. + /// + Everyone +} diff --git a/src/core/DSharpPlus.Shared/Entities/Users/DiscordPremiumType.cs b/src/core/DSharpPlus.Shared/Entities/Users/DiscordPremiumType.cs new file mode 100644 index 0000000000..407d0055f4 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Users/DiscordPremiumType.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Enumerates the valid nitro levels an user has. +/// +public enum DiscordPremiumType +{ + None, + NitroClassic, + Nitro, + NitroBasic +} diff --git a/src/core/DSharpPlus.Shared/Entities/Users/DiscordUserFlags.cs b/src/core/DSharpPlus.Shared/Entities/Users/DiscordUserFlags.cs new file mode 100644 index 0000000000..eb2e0402f1 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Users/DiscordUserFlags.cs @@ -0,0 +1,95 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Entities; + +/// +/// Enumerates all exposed and documented user flags. +/// +[Flags] +public enum DiscordUserFlags +{ + /// + /// None. + /// + None = 0, + + /// + /// Profile badge indicating this user is a Discord employee. + /// + DiscordEmployee = 1 << 0, + + /// + /// Profile badge indicating this user owns a Partnered server. + /// + PartneredServerOwner = 1 << 1, + + /// + /// Profile badge indicating this user has attended a real-world HypeSquad event. + /// + HypeSquadEvents = 1 << 2, + + /// + /// First of two badges for bug hunters. Discord doesn't tell us any further information. + /// + BugHunterLevel1 = 1 << 3, + + /// + /// Profile badge indicating this user is a member of the HypeSquad House of Bravery. + /// + HouseBravery = 1 << 6, + + /// + /// Profile badge indicating this user is a member of the Hypesquad House of Brilliance. + /// + HouseBrilliance = 1 << 7, + + /// + /// Profile badge indicating this user is a member of the HypeSquad House of Balance. + /// + HouseBalance = 1 << 8, + + /// + /// Profile badge indicating this user has purchased Nitro before 10/10/2018. + /// + EarlySupporter = 1 << 9, + + /// + /// Profile badge indicating... what, exactly? Discord doesn't tell us. + /// + TeamUser = 1 << 10, + + /// + /// Bug hunter badge, Level 2 + /// + BugHunterLevel2 = 1 << 14, + + /// + /// Profile badge indicating this bot is a verified bot. + /// + VerifiedBot = 1 << 16, + + /// + /// Profile badge indicating this user has developed a bot which obtained verification before Discord + /// stopped giving out the badge. + /// + EarlyVerifiedBotDeveloper = 1 << 17, + + /// + /// Profile badge indicating this user has passed Discord's Moderator Exam. + /// + DiscordCertifiedModerator = 1 << 18, + + /// + /// Bot that uses only HTTP interactions and is thus shown in the online member list. + /// + BotHttpInteractions = 1 << 19, + + /// + /// Profile badge indicating that this user meets the requirements for Discord's active developer badge. + /// + ActiveDeveloper = 1 << 22 +} diff --git a/src/core/DSharpPlus.Shared/Entities/Webhooks/DiscordWebhookType.cs b/src/core/DSharpPlus.Shared/Entities/Webhooks/DiscordWebhookType.cs new file mode 100644 index 0000000000..8ecf45c79b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/Webhooks/DiscordWebhookType.cs @@ -0,0 +1,26 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Entities; + +/// +/// Specifies the different types of webhooks. +/// +public enum DiscordWebhookType +{ + /// + /// Incoming webhooks can post messages to channels with a generated token. + /// + Incoming = 1, + + /// + /// Channel Follower webhooks are internal webhooks used with channel following to cross-post messages. + /// + ChannelFollower, + + /// + /// Application webhooks are webhooks used with interactions. + /// + Application +} diff --git a/src/core/DSharpPlus.Shared/Entities/readme.md b/src/core/DSharpPlus.Shared/Entities/readme.md new file mode 100644 index 0000000000..bf2ec7b8f0 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Entities/readme.md @@ -0,0 +1,2 @@ +Enums located here live in the namespace DSharpPlus.Entities. This is because we reuse them later +in the public-facing library for our entities, and want to keep them in the same namespace. \ No newline at end of file diff --git a/src/core/DSharpPlus.Shared/IOptional.cs b/src/core/DSharpPlus.Shared/IOptional.cs new file mode 100644 index 0000000000..9e71fa30a6 --- /dev/null +++ b/src/core/DSharpPlus.Shared/IOptional.cs @@ -0,0 +1,16 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus; + +/// +/// Defines the principal working of an Optional type. +/// +public interface IOptional +{ + /// + /// Indicates whether this optional is logically defined. + /// + public bool HasValue { get; } +} diff --git a/src/core/DSharpPlus.Shared/InlineMediaData.cs b/src/core/DSharpPlus.Shared/InlineMediaData.cs new file mode 100644 index 0000000000..9d8fc9a543 --- /dev/null +++ b/src/core/DSharpPlus.Shared/InlineMediaData.cs @@ -0,0 +1,135 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0072 + +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; + +using CommunityToolkit.HighPerformance.Buffers; + +namespace DSharpPlus; + +/// +/// Represents an image sent to Discord as part of JSON payloads. +/// +public readonly record struct InlineMediaData +{ + private static ReadOnlySpan PngString => "data:image/png;base64,"u8; + private static ReadOnlySpan JpegString => "data:image/jpeg;base64,"u8; + private static ReadOnlySpan GifString => "data:image/gif;base64,"u8; + private static ReadOnlySpan WebpString => "data:image/webp;base64,"u8; + private static ReadOnlySpan AvifString => "data:image/avif;base64,"u8; + private static ReadOnlySpan OggString => "data:audio/ogg;base64,"u8; + private static ReadOnlySpan Mp3String => "data:audio/mpeg;base64,"u8; + private static ReadOnlySpan AutoString => "data:image/auto;base64,"u8; + + private readonly Stream reader; + private readonly MediaFormat format; + + /// + /// Creates a new instance of this struct from the provided stream. + /// + /// The Stream to convert to base64. + /// The format of this image. + public InlineMediaData(Stream reader, MediaFormat format) + { + this.reader = reader; + this.format = format; + } + + /// + /// Creates a new instance of this struct from the provided pipe. + /// + /// The pipe to conver to base64. + /// The format of this image. + public InlineMediaData(PipeReader reader, MediaFormat format) : this(reader.AsStream(), format) + { + + } + + /// + /// Creates a new instance of this struct from the provided buffer. + /// + /// The buffer to convert to base64. + /// The format of this image. + public InlineMediaData(ReadOnlySequence data, MediaFormat format) : this(PipeReader.Create(data), format) + { + + } + + /// + /// Writes the base64 data to the specified array pool buffer writer. + /// + public readonly void WriteTo(ArrayPoolBufferWriter writer) + { + // chosen because a StreamPipeReader buffers to 4096 + const int readSegmentLength = 12288; + const int writeSegmentLength = 16384; + + writer.Write + ( + this.format switch + { + MediaFormat.Png => PngString, + MediaFormat.Gif => GifString, + MediaFormat.Jpeg => JpegString, + MediaFormat.WebP => WebpString, + MediaFormat.Avif => AvifString, + MediaFormat.Ogg => OggString, + MediaFormat.Mp3 => Mp3String, + _ => AutoString + } + ); + + byte[] readBuffer = ArrayPool.Shared.Rent(readSegmentLength); + byte[] writeBuffer = ArrayPool.Shared.Rent(writeSegmentLength); + + scoped Span readSpan = readBuffer.AsSpan()[..readSegmentLength]; + scoped Span writeSpan = writeBuffer.AsSpan()[..writeSegmentLength]; + + int readRollover = 0; + + while (true) + { + int read = this.reader.Read(readSpan[readRollover..]); + int currentLength = read + readRollover; + + if (read == 0) + { + break; + } + + OperationStatus status = Base64.EncodeToUtf8 + ( + bytes: readSpan[..currentLength], + utf8: writeSpan, + bytesConsumed: out int consumed, + bytesWritten: out int written, + isFinalBlock: false + ); + + Debug.Assert(status is OperationStatus.Done or OperationStatus.NeedMoreData); + Debug.Assert(read - consumed < 3); + + writer.Write(writeSpan[..written]); + + readSpan[consumed..currentLength].CopyTo(readSpan[0..]); + readRollover = currentLength - consumed; + } + + OperationStatus lastStatus = Base64.EncodeToUtf8(readSpan[..readRollover], writeSpan, out int _, out int lastWritten); + + Debug.Assert(lastStatus == OperationStatus.Done); + + writer.Write(writeSpan[..lastWritten]); + + ArrayPool.Shared.Return(readBuffer); + ArrayPool.Shared.Return(writeBuffer); + } +} diff --git a/src/core/DSharpPlus.Shared/MediaFormat.cs b/src/core/DSharpPlus.Shared/MediaFormat.cs new file mode 100644 index 0000000000..cee3478ca6 --- /dev/null +++ b/src/core/DSharpPlus.Shared/MediaFormat.cs @@ -0,0 +1,21 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus; + +/// +/// Represents formats recognized and handled by . +/// +public enum MediaFormat +{ + Png, + Gif, + Jpeg, + WebP, + Avif, + Ogg, + Mp3, + Auto, + Unknown, +} diff --git a/src/core/DSharpPlus.Shared/Optional`1.cs b/src/core/DSharpPlus.Shared/Optional`1.cs new file mode 100644 index 0000000000..452bda8a18 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Optional`1.cs @@ -0,0 +1,135 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1000 + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace DSharpPlus; + +/// +/// Represents a logical container for the presence of a value in the context of Discord's REST API. +/// +/// The type of the enclosed value. +public readonly record struct Optional : IOptional +{ + private readonly T? value; + + /// + public bool HasValue { get; } + + /// + /// Retrieves the underlying value, if present. + /// + public T Value + { + get + { + if (!this.HasValue) + { + ThrowHelper.ThrowOptionalNoValuePresent(); + } + + return this.value!; + } + } + + public Optional(T value) + { + this.HasValue = true; + this.value = value; + } + + public static Optional None => default; + + /// + /// Returns the contained value if one is present, or throws the given exception if none is present. + /// + public readonly T Expect(Func exception) + { + if (!this.HasValue) + { + ThrowHelper.ThrowFunc(exception); + } + + return this.value!; + } + + /// + /// Returns the contained value if present, or the provided value if not present. + /// + public readonly T Or(T value) + => this.HasValue ? this.value! : value; + + /// + /// Returns the contained value if present, or the default value for this type if not present. + /// + public readonly T? OrDefault() => this.value; + + /// + /// Transforms the given optional to an optional of if it has a value, + /// returning an empty optional if there was no value present. + /// + public readonly Optional Map(Func transformation) + { + return this.HasValue + ? new Optional(transformation(this.value!)) + : Optional.None; + } + + /// + /// Transforms the value of the given optional to , returning + /// if there was no value present. + /// + public readonly TOther MapOr(Func transformation, TOther value) + { + return this.HasValue + ? transformation(this.value!) + : value; + } + + /// + /// Returns a value indicating whether is set. + /// + /// The value of this optional. This may still be null if the set value was null. + public readonly bool TryGetValue + ( + [MaybeNullWhen(false)] + out T value + ) + { + value = this.value; + return this.HasValue; + } + + /// + /// Returns a value indicating whether is set and not null. + /// + /// The value of this optional. + [MemberNotNullWhen(true, nameof(value))] + public readonly bool TryGetNonNullValue + ( + [MaybeNullWhen(false)] + [NotNullWhen(true)] + out T value + ) + { + value = this.value; + return this.value is not null && this.HasValue; + } + + /// + /// Returns a string representing the present optional instance. + /// + public override string ToString() + { + return this.HasValue + ? $"Optional {{ {this.value} }}" + : "Optional { no value }"; + } + + public static implicit operator Optional(T value) + => new(value); +} diff --git a/src/core/DSharpPlus.Shared/Results/Errors/ArgumentError.cs b/src/core/DSharpPlus.Shared/Results/Errors/ArgumentError.cs new file mode 100644 index 0000000000..0c17002245 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Errors/ArgumentError.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Results.Errors; + +/// +/// An error returned when a provided argument was invalid. +/// +public record ArgumentError : ExceptionError +{ + /// + /// The name of the invalid argument. + /// + public string ArgumentName { get; private protected set; } + + /// + /// Creates a new with the specified message and an unspecified argument name. + /// + public ArgumentError(string message) : base(message) + => this.ArgumentName = "Unspecified."; + + /// + /// Creates a new with the specified message and argument name. + /// + public ArgumentError(string message, string argumentName) : base(message) + => this.ArgumentName = argumentName; + + /// + /// Creates a new from the specified exception. + /// + public ArgumentError(Exception exception) : base(exception) + { + this.Message = exception.Message; + this.ArgumentName = exception is ArgumentException { ParamName: { } argument } ? argument : "Unspecified."; + } + + public override Exception ToException() => new ArgumentException(this.Message, this.ArgumentName); +} diff --git a/src/core/DSharpPlus.Shared/Results/Errors/ArgumentNullError.cs b/src/core/DSharpPlus.Shared/Results/Errors/ArgumentNullError.cs new file mode 100644 index 0000000000..aae42f9982 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Errors/ArgumentNullError.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Results.Errors; + +/// +/// An error returned when a provided argument was null. +/// +public record ArgumentNullError : ArgumentError +{ + /// + /// Creates a new with the default message and specified argument name. + /// + public ArgumentNullError(string argumentName) : base("The provided value must not be null", argumentName) + { + + } + + /// + /// Creates a new with the specified message and argument name. + /// + public ArgumentNullError(string message, string argumentName) : base(message, argumentName) + { + + } + + /// + /// Creates a new from the specified exception. + /// + public ArgumentNullError(Exception exception) : base(exception) + { + this.Message = exception.Message; + this.ArgumentName = exception is ArgumentNullException { ParamName: { } argument } ? argument : "Unspecified."; + } + + public override Exception ToException() => new ArgumentNullException(this.Message, this.ArgumentName); +} diff --git a/src/core/DSharpPlus.Shared/Results/Errors/ArgumentOutOfRangeError.cs b/src/core/DSharpPlus.Shared/Results/Errors/ArgumentOutOfRangeError.cs new file mode 100644 index 0000000000..6cfcc51123 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Errors/ArgumentOutOfRangeError.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Results.Errors; + +/// +/// An error returned when a provided argument was null. +/// +public record ArgumentOutOfRangeError : ArgumentError +{ + /// + /// Creates a new with the specified message and an unspecified argument name. + /// + public ArgumentOutOfRangeError(string message) : base(message) + { + + } + + /// + /// Creates a new with the specified message and argument name. + /// + public ArgumentOutOfRangeError(string message, string argumentName) : base(message, argumentName) + { + + } + + /// + /// Creates a new from the specified exception. + /// + public ArgumentOutOfRangeError(Exception exception) : base(exception) + { + this.Message = exception.Message; + this.ArgumentName = exception is ArgumentOutOfRangeException { ParamName: { } argument } ? argument : "Unspecified."; + } + + public override Exception ToException() => new ArgumentOutOfRangeException(this.Message, this.ArgumentName); +} diff --git a/src/core/DSharpPlus.Shared/Results/Errors/Error.cs b/src/core/DSharpPlus.Shared/Results/Errors/Error.cs new file mode 100644 index 0000000000..848d460249 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Errors/Error.cs @@ -0,0 +1,19 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Results.Errors; + +/// +/// Represents an error that has occurred during the operation described by the Result containing this error. +/// +public abstract record Error +{ + /// + /// The human-readable error message. + /// + public string Message { get; init; } + + protected Error(string message) + => this.Message = message; +} diff --git a/src/core/DSharpPlus.Shared/Results/Errors/ExceptionError.cs b/src/core/DSharpPlus.Shared/Results/Errors/ExceptionError.cs new file mode 100644 index 0000000000..3832143123 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Errors/ExceptionError.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA2201 + +using System; + +namespace DSharpPlus.Results.Errors; + +/// +/// Provides a base class for errors that can map to an exception. +/// +public abstract record ExceptionError : Error +{ + /// + /// Converts this error into a throwable exception. + /// + public virtual Exception ToException() => new(this.Message); + + protected ExceptionError(string message) : base(message) + { + + } + + /// + /// Override this constructor to provide a way to be constructible from an exception. + /// + protected ExceptionError(Exception exception) : base(exception.Message) + { + + } +} diff --git a/src/core/DSharpPlus.Shared/Results/Errors/InvalidOperationError.cs b/src/core/DSharpPlus.Shared/Results/Errors/InvalidOperationError.cs new file mode 100644 index 0000000000..e101a34d5f --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Errors/InvalidOperationError.cs @@ -0,0 +1,28 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.Results.Errors; + +/// +/// An error indicating an invalid operation. +/// +public record InvalidOperationError : ExceptionError +{ + /// + /// Creates a new with the specified message. + /// + public InvalidOperationError(string message) : base(message) + { + } + + /// + /// Creates a new from the specified exception. + /// + public InvalidOperationError(Exception exception) : base(exception) + => this.Message = exception.Message; + + public override Exception ToException() => new InvalidOperationException(this.Message); +} diff --git a/src/core/DSharpPlus.Shared/Results/ExceptionServices/ExceptionMarshaller.cs b/src/core/DSharpPlus.Shared/Results/ExceptionServices/ExceptionMarshaller.cs new file mode 100644 index 0000000000..06422a60f5 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/ExceptionServices/ExceptionMarshaller.cs @@ -0,0 +1,113 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System; + +using DSharpPlus.Results.Errors; + +namespace DSharpPlus.Results.ExceptionServices; + +/// +/// Provides a way to marshal results to exceptions, if erroneous. Exceptions and result may not be able to be round-tripped. +/// +public static class ExceptionMarshaller +{ + /// + /// Creates an exception from the specified result error. + /// + public static Exception MarshalResultErrorToException(Error error) + { + // if we previously failed to marshal this into an error, just get the original exception + if (error is MarshalError marshalError) + { + return marshalError.Exception; + } + + // if this is freely convertible, just do that + if (error is ExceptionError exceptionError) + { + return exceptionError.ToException(); + } + + return new MarshalException(error); + } + + /// + /// Creates a result error from the specified exception. + /// + public static Error MarshalExceptionToResultError(Exception exception) + { + if (exception is MarshalException marshalException) + { + return marshalException.Error; + } + + // see if we can find a matching error in a sufficiently similarly named namespace, + // ie DSharpPlus.Exceptions.DiscordException -> DSharpPlus.Errors.DiscordError + if (Type.GetType(exception.GetType().FullName!.Replace("Exception", "Error", StringComparison.Ordinal)) is Type candidate) + { + if (candidate.IsAssignableTo(typeof(ExceptionError))) + { + return (Error)candidate.GetConstructor([typeof(Exception)])!.Invoke([exception]); + } + } + + // try replacing the namespace with DSharpPlus.Results.Errors, too + if + ( + Type.GetType + ( + $"DSharpPlus.Results.Errors.{exception.GetType().Name!.Replace("Exception", "Error", StringComparison.Ordinal)}" + ) is Type secondCandidate + ) + { + if (secondCandidate.IsAssignableTo(typeof(ExceptionError))) + { + return (Error)secondCandidate.GetConstructor([typeof(Exception)])!.Invoke([exception]); + } + } + + return new MarshalError(exception); + } + + /// + /// Creates an exception from the specified result, if erroneous. + /// + public static Exception? MarshalResultToException(Result result) + { + if (result.IsSuccess) + { + return null; + } + + return MarshalResultErrorToException(result.Error); + } + + /// + /// Creates an exception from the specified generic result, if erroneous. + /// + public static Exception? MarshalResultToException(Result result) + { + if (result.IsSuccess) + { + return null; + } + + return MarshalResultErrorToException(result.Error); + } + + /// + /// Creates an erroneous result from the specified exception. + /// + public static Result MarshalExceptionToResult(Exception exception) + => new(MarshalExceptionToResultError(exception)); + + /// + /// Creates an erroneous generic result from the specified exception. + /// + public static Result MarshalExceptionToResult(Exception exception) + => new(MarshalExceptionToResultError(exception)); +} diff --git a/src/core/DSharpPlus.Shared/Results/ExceptionServices/MarshalError.cs b/src/core/DSharpPlus.Shared/Results/ExceptionServices/MarshalError.cs new file mode 100644 index 0000000000..5c5c2114c8 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/ExceptionServices/MarshalError.cs @@ -0,0 +1,25 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics.CodeAnalysis; + +using DSharpPlus.Results.Errors; + +namespace DSharpPlus.Results.ExceptionServices; + +/// +/// Used by the result-exception marshaller when failing to find a matching result error. +/// +public sealed record MarshalError : Error +{ + /// + /// Gets the exception that failed to marshal. + /// + public required Exception Exception { get; init; } + + [SetsRequiredMembers] + public MarshalError(Exception exception) : base("Failed to find a suitable result type for the exception.") + => this.Exception = exception; +} diff --git a/src/core/DSharpPlus.Shared/Results/ExceptionServices/MarshalException.cs b/src/core/DSharpPlus.Shared/Results/ExceptionServices/MarshalException.cs new file mode 100644 index 0000000000..c4e8d6cd38 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/ExceptionServices/MarshalException.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1032 + +using System; +using System.Diagnostics.CodeAnalysis; + +using DSharpPlus.Results.Errors; + +namespace DSharpPlus.Results.ExceptionServices; + +[method: SetsRequiredMembers] +public sealed class MarshalException(Error underlying) + : Exception("Failed to find a matching exception type for this result error.") +{ + /// + /// Gets the result error this was originally + /// + public required Error Error { get; init; } = underlying; +} diff --git a/src/core/DSharpPlus.Shared/Results/Result.cs b/src/core/DSharpPlus.Shared/Results/Result.cs new file mode 100644 index 0000000000..ea08a03d28 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Result.cs @@ -0,0 +1,76 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +using DSharpPlus.Results.Errors; +using DSharpPlus.Results.ExceptionServices; + +namespace DSharpPlus.Results; + +/// +/// Represents the success or failure of an operation, and optionally the error returned. +/// +public readonly record struct Result +{ + public static Result Success => default; + + /// + /// The error this operation returned, if applicable. + /// + public Error? Error { get; private init; } + + /// + /// Indicates whether this operation was successful. + /// + [MemberNotNullWhen(false, nameof(Error))] + public bool IsSuccess => this.Error is null; + + /// + /// Constructs a new result from the specified failure case. + /// + /// + public Result(Error error) + => this.Error = error; + + public static implicit operator Result(Error error) + => new(error); + + public static implicit operator bool(Result result) + => result.IsSuccess; + + /// + /// Throws the result error as an exception, if applicable. + /// + [DebuggerHidden] + [StackTraceHidden] + public void Expect() + { + if (!this.IsSuccess) + { + throw ExceptionMarshaller.MarshalResultErrorToException(this.Error); + } + } + + /// + /// Throws the result error as an exception according to the provided transform, if applicable. + /// + [DebuggerHidden] + [StackTraceHidden] + public void Expect(Func transform) + { + if (!this.IsSuccess) + { + throw transform(this.Error); + } + } + + /// + /// Transforms the result error according to the provided function, returning either a successful result or the transformed result. + /// + public Result MapError(Func transform) + => this.IsSuccess ? this : new(transform(this.Error)); +} diff --git a/src/core/DSharpPlus.Shared/Results/Result`1.cs b/src/core/DSharpPlus.Shared/Results/Result`1.cs new file mode 100644 index 0000000000..59c4df0caa --- /dev/null +++ b/src/core/DSharpPlus.Shared/Results/Result`1.cs @@ -0,0 +1,158 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +using DSharpPlus.Results.Errors; +using DSharpPlus.Results.ExceptionServices; + +namespace DSharpPlus.Results; + +/// +/// Represents the success or failure of an operation, and the error or value returned as applicable. +/// +public readonly record struct Result +{ + /// + /// The value this operation returned, if applicable. + /// + [AllowNull] + public TValue Value { get; private init; } + + /// + /// The error this operation returned, if applicable. + /// + public Error? Error { get; private init; } + + /// + /// Indicates whether this operation was successful. + /// + [MemberNotNullWhen(false, nameof(Error))] + public bool IsSuccess => this.Error is null; + + /// + /// Constructs a new result from the specified failure case. + /// + public Result(Error error) + => this.Error = error; + + /// + /// Constructs a new successful result from the specified value. + /// + public Result(TValue value) + => this.Value = value; + + public static implicit operator Result(Error error) + => new(error); + + public static implicit operator Result(TValue value) + => new(value); + + public static implicit operator bool(Result result) + => result.IsSuccess; + + public static implicit operator Result(Result result) + => result.IsSuccess ? Result.Success : new(result.Error); + + /// + /// Throws the result error as an exception, if applicable. + /// + [DebuggerHidden] + [StackTraceHidden] + public void Expect() + { + if (!this.IsSuccess) + { + throw ExceptionMarshaller.MarshalResultErrorToException(this.Error); + } + } + + /// + /// Throws the result error as an exception according to the provided transform, if applicable. + /// + [DebuggerHidden] + [StackTraceHidden] + public void Expect(Func transform) + { + if (!this.IsSuccess) + { + throw transform(this.Error); + } + } + + /// + /// Unwraps the result, throwing an exception if unsuccessful. + /// + public TValue Unwrap() + { + this.Expect(); + return this.Value; + } + + /// + /// Unwraps the result, returning a provided default value if unsuccessful. + /// + public TValue UnwrapOr(TValue defaultValue) + => this.IsSuccess ? this.Value : defaultValue; + + /// + /// Unwraps the result, returning the default value if unsuccessful. + /// + public TValue? UnwrapOrDefault() + => this.IsSuccess ? this.Value : default; + + /// + /// Unwraps the result, returning the result of evaluating the provided function if unsuccessful. + /// + public TValue UnwrapOrElse(Func, TValue> fallback) + => this.IsSuccess ? this.Value : fallback(this); + + /// + /// Transforms the result value according to the provided function, returning either the transformed result or a failed result. + /// + public Result Map(Func transform) + => this.IsSuccess ? new(transform(this.Value)) : new(this.Error); + + /// + /// Transforms the result error according to the provided function, returning either a successful result or the transformed result. + /// + public Result MapError(Func transform) + => this.IsSuccess ? this : new(transform(this.Error)); + + /// + /// Transforms the result value according to the provided function, returning either the transformed result or the provided default value. + /// + public Result MapOr(Func transform, TResult fallback) + => this.IsSuccess ? new(transform(this.Value)) : new(fallback); + + /// + /// Transforms the result value according to the provided function, returning either the transformed result or a default value. + /// + public Result MapOrDefault(Func transform) + => this.IsSuccess ? new(transform(this.Value)) : new(default(TResult)); + + /// + /// Transforms the result value and error according to the provided functions. + /// + public Result MapOrElse(Func transformValue, Func transformError) + => this.IsSuccess ? new(transformValue(this.Value)) : new(transformError(this.Error)); + +#pragma warning disable CA1000 + + /// + /// Creates a new failed result from the specified error. + /// + public static Result FromError(Error error) => new(error); + + /// + /// Creates a new successful result from the specified value. + /// + /// + /// + public static Result FromSuccess(TValue value) => new(value); + +#pragma warning restore CA1000 +} diff --git a/src/core/DSharpPlus.Shared/RuntimeServices/RuntimeFeatures/RequiresRestRequestLoggingAttribute.cs b/src/core/DSharpPlus.Shared/RuntimeServices/RuntimeFeatures/RequiresRestRequestLoggingAttribute.cs new file mode 100644 index 0000000000..333092bc08 --- /dev/null +++ b/src/core/DSharpPlus.Shared/RuntimeServices/RuntimeFeatures/RequiresRestRequestLoggingAttribute.cs @@ -0,0 +1,15 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus.RuntimeServices.RuntimeFeatures; + +/// +/// This attribute causes code to be trimmed or ignored at JIT time conditional on the feature switch +/// DSharpPlus.DisableRestRequestLogging. +/// +/// +[AttributeUsage(AttributeTargets.All, Inherited = true)] +public sealed class RequiresRestRequestLoggingAttribute : Attribute; diff --git a/src/core/DSharpPlus.Shared/RuntimeServices/RuntimeFeatures/RestRequestLogging.cs b/src/core/DSharpPlus.Shared/RuntimeServices/RuntimeFeatures/RestRequestLogging.cs new file mode 100644 index 0000000000..9ae1b78d01 --- /dev/null +++ b/src/core/DSharpPlus.Shared/RuntimeServices/RuntimeFeatures/RestRequestLogging.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace DSharpPlus.RuntimeServices.RuntimeFeatures; + +/// +/// Contains the runtime feature switch for logging rest request data. +/// +public static class RestRequestLogging +{ + /// + /// Indicates to the library whether rest request contents should be logged. This feature switch can be controlled + /// from csproj files, where it will enable trimming the relevant code, and from runtimeconfig.json. + /// + /// + /// Enabling this switch will have catastrophic consequences for debugging issues related to rest requests and should + /// only ever be considered if there is concrete evidence to back it up.

+ /// This switch, if enabled in the project file, will enable trimming all related code away:
+ /// + /// ]]> + /// + ///
+ [FeatureSwitchDefinition("DSharpPlus.DisableRestRequestLogging")] + [FeatureGuard(typeof(RequiresRestRequestLoggingAttribute))] + public static bool IsEnabled + => !AppContext.TryGetSwitch("DSharpPlus.DisableRestRequestLogging", out bool enabled) || !enabled; +} diff --git a/src/core/DSharpPlus.Shared/RuntimeServices/TextWriters/IndentedArrayPoolUtf16TextWriter.cs b/src/core/DSharpPlus.Shared/RuntimeServices/TextWriters/IndentedArrayPoolUtf16TextWriter.cs new file mode 100644 index 0000000000..bfe737b7f7 --- /dev/null +++ b/src/core/DSharpPlus.Shared/RuntimeServices/TextWriters/IndentedArrayPoolUtf16TextWriter.cs @@ -0,0 +1,159 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1001 // yes, that type absolutely is disposable. + +using System; +using System.Buffers; + +using CommunityToolkit.HighPerformance.Buffers; + +namespace DSharpPlus.RuntimeServices.TextWriters; + +/// +/// Provides a simple and efficient way to write indented UTF-16 text using pooled buffers. +/// +public record struct IndentedArrayPoolUtf16TextWriter : IDisposable +{ + private static ReadOnlySpan SingleIndentation => " "; + private static ReadOnlySpan DoubleIndentation => " "; + private static ReadOnlySpan TripleIndentation => " "; + private static ReadOnlySpan QuadrupleIndentation => " "; + private static ReadOnlySpan DefaultNewline => "\r\n"; + + private int currentIndentation; + + private readonly ArrayPoolBufferWriter writer; + + public IndentedArrayPoolUtf16TextWriter() + => this.writer = new(); + + /// + /// Increases the indentation of this text writer by one level. + /// + public void IncreaseIndentation() => this.currentIndentation++; + + /// + /// Decreases the indentation of this text writer by one level. + /// + public void DecreaseIndentation() => this.currentIndentation--; + + /// + /// Writes the provided text to the writer, without appending a newline. + /// + /// The text to append to this text writer. + /// + /// Indicates whether this is literal text and thus whether each individual line should be properly indented. Defaults + /// to non-literal text where each line will be indented relative to the current writer indentation. + /// + public readonly void Write(ReadOnlySpan text, bool literal = false) + { + if (literal) + { + this.WriteCurrentIndentation(); + this.writer.Write(text); + return; + } + + while (text.Length > 0) + { + int newlineIndex = text.IndexOf(DefaultNewline); + + if (newlineIndex < 0) + { + this.WriteCurrentIndentation(); + this.writer.Write(text); + break; + } + else + { + ReadOnlySpan line = text[..newlineIndex]; + + if (!line.IsEmpty) + { + this.WriteCurrentIndentation(); + this.writer.Write(line); + } + + this.WriteLine(); + text = text[newlineIndex..]; + text = text[text.IndexOfAnyExcept("\r\n")..]; + } + } + } + + /// + /// Writes the provided text to the writer, appending a newline at the end. + /// + /// The text to append to this text writer. + /// + /// Indicates whether this is literal text and thus whether each individual line should be properly indented. Defaults + /// to non-literal text where each line will be indented relative to the current writer indentation. + /// + public readonly void WriteLine(ReadOnlySpan text, bool literal = false) + { + this.Write(text, literal); + this.writer.Write(DefaultNewline); + } + + /// + /// Writes a newline to the writer. + /// + public readonly void WriteLine() + => this.writer.Write(DefaultNewline); + + /// + /// Ascertains that the writer ends on a newline and not any other character. + /// + public readonly void EnsureEndsInNewline() + { + if (this.writer.WrittenSpan is [.., '\n']) + { + return; + } + + this.writer.Write(DefaultNewline); + } + + /// + /// Writes the current indentation to the writer using the cached indentation values. + /// + private readonly void WriteCurrentIndentation() + { + int quadrupleIndentationLevels = this.currentIndentation / 4; + + for (int i = 0; i < quadrupleIndentationLevels; i++) + { + this.writer.Write(QuadrupleIndentation); + } + + switch (this.currentIndentation % 4) + { + case 3: + this.writer.Write(TripleIndentation); + break; + + case 2: + this.writer.Write(DoubleIndentation); + break; + + case 1: + this.writer.Write(SingleIndentation); + break; + + default: + break; + } + } + + /// + /// Builds the current text writer into a string. Note that this operation is computationally expensive. + /// + /// + public override readonly string ToString() + => new(this.writer.WrittenSpan); + + /// + public readonly void Dispose() => this.writer.Dispose(); +} diff --git a/src/core/DSharpPlus.Shared/Serialization/ISerializationBackend.cs b/src/core/DSharpPlus.Shared/Serialization/ISerializationBackend.cs new file mode 100644 index 0000000000..0b94a6a340 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Serialization/ISerializationBackend.cs @@ -0,0 +1,40 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using CommunityToolkit.HighPerformance.Buffers; + +namespace DSharpPlus.Serialization; + +/// +/// Represents a serialization backend managed by . +/// +public interface ISerializationBackend +{ + /// + /// A unique ID for this backend, used to distinguish if multiple different backends are in use at once. + /// + public static abstract string Id { get; } + + /// + /// Serializes a given serialization model to the given writer. + /// + /// + /// This method serializes the library data models specifically, and may not exhibit correct behaviour + /// for other types. + /// + public void SerializeModel(TModel model, ArrayPoolBufferWriter target) + where TModel : notnull; + + /// + /// Deserializes a serialization model from the provided data. + /// + /// + /// This method deserializes the library data models specifically, and may not exhibit correct behaviour + /// for other types. + /// + public TModel DeserializeModel(ReadOnlySpan data) + where TModel : notnull; +} diff --git a/src/core/DSharpPlus.Shared/Serialization/RedirectingConverter.cs b/src/core/DSharpPlus.Shared/Serialization/RedirectingConverter.cs new file mode 100644 index 0000000000..124e3db57d --- /dev/null +++ b/src/core/DSharpPlus.Shared/Serialization/RedirectingConverter.cs @@ -0,0 +1,46 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1812 + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace DSharpPlus.Serialization; + +internal sealed class RedirectingConverter : JsonConverter + where TModel : TInterface +{ + public override TInterface? Read + ( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + if (!options.TryGetTypeInfo(typeof(TModel), out JsonTypeInfo? typeInfo)) + { + typeInfo = options.GetTypeInfo(typeof(TInterface)); + } + + return (TInterface?)JsonSerializer.Deserialize(ref reader, typeInfo); + } + + public override void Write + ( + Utf8JsonWriter writer, + TInterface value, + JsonSerializerOptions options + ) + { + if (!options.TryGetTypeInfo(value!.GetType(), out JsonTypeInfo? typeInfo)) + { + typeInfo = options.GetTypeInfo(typeof(TInterface)); + } + + JsonSerializer.Serialize(writer, value, typeInfo); + } +} diff --git a/src/core/DSharpPlus.Shared/Serialization/SerializationOptions.cs b/src/core/DSharpPlus.Shared/Serialization/SerializationOptions.cs new file mode 100644 index 0000000000..e954951bf2 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Serialization/SerializationOptions.cs @@ -0,0 +1,69 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Serialization; + +/// +/// Contains information about which library component uses which serialization format and how models +/// are bound to their definitions. +/// +public sealed class SerializationOptions +{ + public const string Json = "json"; + + /// + /// Well-known formats: + /// + /// "json", on top of System.Text.Json. + /// "etf", on top of ETFKit, not installed by default. + /// + /// + internal Dictionary Formats { get; } = new(4) + { + [typeof(SystemTextJsonFormatMarker)] = "json" + }; + + internal Dictionary InterfacesToConcrete { get; } = new(512); + + internal Dictionary BackendImplementations { get; } = new(2) + { + ["json"] = typeof(SystemTextJsonSerializationBackend) + }; + + public void RegisterBackendImplementation() + where T : ISerializationBackend + => this.BackendImplementations[T.Id] = typeof(T); + + /// + /// Specifies the concrete type used to deserialize an interface. + /// + public void AddModel() + where TInterface : notnull + where TModel : notnull, TInterface + => this.InterfacesToConcrete[typeof(TInterface)] = typeof(TModel); + + /// + /// Removes an interface to concrete type entry. + /// + public void RemoveModel() + where TInterface : notnull + => this.InterfacesToConcrete.Remove(typeof(TInterface)); + + /// + /// Specifies the format to use for a given library component. + /// + /// The interface associated with this component. + public void SetFormat(string format = "json") + => this.Formats[typeof(TComponent)] = format; + + /// + /// Clears the format associated with the given library component. + /// + /// The interface associated with this component. + public void ClearFormat() + => this.Formats.Remove(typeof(TComponent)); +} diff --git a/src/core/DSharpPlus.Shared/Serialization/SerializationService.cs b/src/core/DSharpPlus.Shared/Serialization/SerializationService.cs new file mode 100644 index 0000000000..8c319ba377 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Serialization/SerializationService.cs @@ -0,0 +1,86 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics.CodeAnalysis; + +using CommunityToolkit.HighPerformance.Buffers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DSharpPlus.Serialization; + +/// +/// Handles serializing and deserializing models between different formats as necessary. +/// +[RequiresDynamicCode("Serialization in DSharpPlus presently depends on unreferenced and dynamic code.")] +[RequiresUnreferencedCode("Serialization in DSharpPlus presently depends on unreferenced and dynamic code.")] +public sealed partial class SerializationService +( + IOptions options, + IServiceProvider services, + ILogger> logger +) +{ + /// + public TModel DeserializeModel(ReadOnlySpan data) + where TModel : notnull + { + ISerializationBackend backendImpl; + + if + ( + options.Value.Formats.TryGetValue(typeof(T), out string? name) + && options.Value.BackendImplementations.TryGetValue(name, out Type? backendType) + ) + { + backendImpl = (ISerializationBackend)services.GetRequiredService(backendType); + } + else + { + LogFormatFallback(logger, typeof(T)); + backendImpl = services.GetRequiredService(); + } + + return backendImpl.DeserializeModel(data); + } + + /// + public void SerializeModel + ( + TModel model, + ArrayPoolBufferWriter target + ) + where TModel : notnull + { + ISerializationBackend backendImpl; + + if + ( + options.Value.Formats.TryGetValue(typeof(T), out string? name) + && options.Value.BackendImplementations.TryGetValue(name, out Type? backendType) + ) + { + backendImpl = (ISerializationBackend)services.GetRequiredService(backendType); + } + else + { + LogFormatFallback(logger, typeof(T)); + backendImpl = services.GetRequiredService(); + } + + backendImpl.SerializeModel(model, target); + } + + [LoggerMessage( + Level = LogLevel.Warning, + Message = "No serialization format for library component {Component} found, falling back to \"json\"." + )] + private static partial void LogFormatFallback + ( + ILogger logger, + Type component + ); +} diff --git a/src/core/DSharpPlus.Shared/Serialization/SystemTextJsonFormatMarker.cs b/src/core/DSharpPlus.Shared/Serialization/SystemTextJsonFormatMarker.cs new file mode 100644 index 0000000000..898800f253 --- /dev/null +++ b/src/core/DSharpPlus.Shared/Serialization/SystemTextJsonFormatMarker.cs @@ -0,0 +1,14 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace DSharpPlus.Serialization; + +/// +/// Provides a marker type to force to use System.Text.Json. +/// +public abstract class SystemTextJsonFormatMarker +{ + // can't inherit it + internal SystemTextJsonFormatMarker() { } +} diff --git a/src/core/DSharpPlus.Shared/Serialization/SystemTextJsonSerializationBackend.cs b/src/core/DSharpPlus.Shared/Serialization/SystemTextJsonSerializationBackend.cs new file mode 100644 index 0000000000..73ae5a1c0c --- /dev/null +++ b/src/core/DSharpPlus.Shared/Serialization/SystemTextJsonSerializationBackend.cs @@ -0,0 +1,61 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Text.Json; + +using CommunityToolkit.HighPerformance.Buffers; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace DSharpPlus.Serialization; + +/// +/// Implements a System.Text.Json-based serialization backend; the default backend used across DSharpPlus. +/// +public sealed class SystemTextJsonSerializationBackend : ISerializationBackend +{ + private readonly JsonSerializerOptions? jsonOptions; + + public SystemTextJsonSerializationBackend + ( + IOptions formats, + IServiceProvider provider + ) + { + this.jsonOptions = provider.GetRequiredService>().Get("dsharpplus"); + + foreach (KeyValuePair map in formats.Value.InterfacesToConcrete) + { + this.jsonOptions.Converters.Add + ( + (JsonConverter)typeof(RedirectingConverter<,>) + .MakeGenericType(map.Key, map.Value) + .GetConstructor(Type.EmptyTypes)! + .Invoke(null)! + ); + } + + this.jsonOptions.MakeReadOnly(); + } + + // the default STJ backend gets to be just 'json' + public static string Id => "json"; + + /// + public TModel DeserializeModel(ReadOnlySpan data) + where TModel : notnull + => JsonSerializer.Deserialize(data, this.jsonOptions)!; + + /// + public void SerializeModel(TModel model, ArrayPoolBufferWriter target) + where TModel : notnull + { + using Utf8JsonWriter writer = new(target); + JsonSerializer.Serialize(writer, model, this.jsonOptions!); + } +} diff --git a/src/core/DSharpPlus.Shared/Snowflake.GenericMath.cs b/src/core/DSharpPlus.Shared/Snowflake.GenericMath.cs new file mode 100644 index 0000000000..71f0c7df9a --- /dev/null +++ b/src/core/DSharpPlus.Shared/Snowflake.GenericMath.cs @@ -0,0 +1,1028 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1031 + +using System; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DSharpPlus; + +public readonly partial record struct Snowflake : + IBinaryInteger, + IMinMaxValue, + IParsable, + ISpanFormattable, + ISpanParsable, + IIncrementOperators, + IDecrementOperators +{ + /// + public static Snowflake One => 1; + + /// + public static Snowflake Zero => 0; + + /// + public static Snowflake MaxValue => long.MaxValue; + + /// + public static Snowflake MinValue => new + ( + DiscordEpoch, + 0, + 0, + 0 + ); + + /// + static int INumberBase.Radix => 2; + + /// + static Snowflake IAdditiveIdentity.AdditiveIdentity { get; } = 0; + + /// + static Snowflake IMultiplicativeIdentity.MultiplicativeIdentity { get; } = 1; + + /// + public static Snowflake Parse + ( + ReadOnlySpan s, + NumberStyles style = NumberStyles.Integer | NumberStyles.AllowLeadingWhite, + IFormatProvider? provider = null + ) + { + return long.Parse + ( + s, + style, + provider + ); + } + + /// + public static Snowflake Parse + ( + string s, + NumberStyles style = NumberStyles.Integer | NumberStyles.AllowLeadingWhite, + IFormatProvider? provider = null + ) + { + return long.Parse + ( + s, + style, + provider + ); + } + + /// + public static Snowflake Parse + ( + ReadOnlySpan s, + IFormatProvider? provider = null + ) + { + return long.Parse + ( + s, + provider + ); + } + + /// + public static Snowflake Parse + ( + string s, + IFormatProvider? provider = null + ) + { + return long.Parse + ( + s, + provider + ); + } + + /// + public static bool TryParse + ( + ReadOnlySpan s, + NumberStyles style, + IFormatProvider? provider, + + [MaybeNullWhen(false)] + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + style, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + public static bool TryParse + ( + [NotNullWhen(true)] + string? s, + + NumberStyles style, + IFormatProvider? provider, + + [MaybeNullWhen(false)] + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + style, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + public static bool TryParse + ( + ReadOnlySpan s, + IFormatProvider? provider, + + [MaybeNullWhen(false)] + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + public static bool TryParse + ( + [NotNullWhen(true)] + string? s, + + IFormatProvider? provider, + + [MaybeNullWhen(false)] + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + static Snowflake INumberBase.Abs(Snowflake value) + => long.Abs(value); + + /// + static bool INumberBase.IsCanonical(Snowflake value) + => true; + + /// + static bool INumberBase.IsComplexNumber(Snowflake value) + => false; + + /// + static bool INumberBase.IsEvenInteger(Snowflake value) + => long.IsEvenInteger(value); + + /// + static bool INumberBase.IsFinite(Snowflake value) + => true; + + /// + static bool INumberBase.IsImaginaryNumber(Snowflake value) + => false; + + /// + static bool INumberBase.IsInfinity(Snowflake value) + => false; + + /// + static bool INumberBase.IsInteger(Snowflake value) + => true; + + /// + static bool INumberBase.IsNaN(Snowflake value) + => false; + + /// + static bool INumberBase.IsNegative(Snowflake value) + => long.IsNegative(value); + + /// + static bool INumberBase.IsNegativeInfinity(Snowflake value) + => false; + + /// + static bool INumberBase.IsNormal(Snowflake value) + => false; + + /// + static bool INumberBase.IsOddInteger(Snowflake value) + => long.IsOddInteger(value); + + /// + static bool INumberBase.IsPositive(Snowflake value) + => long.IsPositive(value); + + /// + static bool INumberBase.IsPositiveInfinity(Snowflake value) + => false; + + /// + static bool IBinaryNumber.IsPow2(Snowflake value) + => long.IsPow2(value); + + /// + static bool INumberBase.IsRealNumber(Snowflake value) + => true; + + /// + static bool INumberBase.IsSubnormal(Snowflake value) + => false; + + /// + static bool INumberBase.IsZero(Snowflake value) + => value == 0; + + /// + static Snowflake IBinaryNumber.Log2(Snowflake value) + => long.Log2(value); + + /// + static Snowflake INumberBase.MaxMagnitude(Snowflake x, Snowflake y) + => long.MaxMagnitude(x, y); + + /// + static Snowflake INumberBase.MaxMagnitudeNumber(Snowflake x, Snowflake y) + => long.MaxMagnitude(x, y); + + /// + static Snowflake INumberBase.MinMagnitude(Snowflake x, Snowflake y) + => long.MinMagnitude(x, y); + + /// + static Snowflake INumberBase.MinMagnitudeNumber(Snowflake x, Snowflake y) + => long.MinMagnitude(x, y); + + /// + static Snowflake INumberBase.Parse + ( + ReadOnlySpan s, + NumberStyles style, + IFormatProvider? provider + ) + { + return long.Parse + ( + s, + style, + provider + ); + } + + /// + static Snowflake INumberBase.Parse + ( + string s, + NumberStyles style, + IFormatProvider? provider + ) + { + return long.Parse + ( + s, + style, + provider + ); + } + + /// + static Snowflake ISpanParsable.Parse + ( + ReadOnlySpan s, + IFormatProvider? provider + ) + { + return long.Parse + ( + s, + provider + ); + } + + /// + static Snowflake IParsable.Parse + ( + string s, + IFormatProvider? provider + ) + { + return long.Parse + ( + s, + provider + ); + } + + /// + static Snowflake IBinaryInteger.PopCount(Snowflake value) + => long.PopCount(value); + + /// + static Snowflake IBinaryInteger.TrailingZeroCount(Snowflake value) + => long.TrailingZeroCount(value); + + /// + static bool INumberBase.TryConvertFromChecked + ( + TOther value, + out Snowflake result + ) + { + try + { + result = long.CreateChecked + ( + value + ); + + return true; + } + catch + { + result = default; + return false; + } + } + + /// + static bool INumberBase.TryConvertFromSaturating + ( + TOther value, + out Snowflake result + ) + { + try + { + result = long.CreateSaturating + ( + value + ); + + return true; + } + catch + { + result = default; + return false; + } + } + + /// + static bool INumberBase.TryConvertFromTruncating + ( + TOther value, + out Snowflake result + ) + { + try + { + result = long.CreateTruncating + ( + value + ); + + return true; + } + catch + { + result = default; + return false; + } + } + +#pragma warning disable CS8500 // we statically prove this is fine + /// + static unsafe bool INumberBase.TryConvertToChecked + ( + Snowflake value, + + [NotNullWhen(true)] + out TOther result + ) + { + if (typeof(TOther) == typeof(byte)) + { + byte actualResult = checked((byte)value); + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(char)) + { + char actualResult = checked((char)value); + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + decimal actualResult = value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(ushort)) + { + ulong actualResult = checked((ushort)value); + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(uint)) + { + ulong actualResult = checked((uint)value); + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(ulong)) + { + ulong actualResult = checked((ulong)value.Value); + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(UInt128)) + { + UInt128 actualResult = checked((UInt128)value.Value); + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(UIntPtr)) + { + UIntPtr actualResult = checked((UIntPtr)value.Value); + result = *(TOther*)&actualResult; + return true; + } + else + { + result = default!; + return false; + } + } + + /// + static unsafe bool INumberBase.TryConvertToSaturating + ( + Snowflake value, + + [MaybeNullWhen(false)] + out TOther result + ) + { + if (typeof(TOther) == typeof(byte)) + { + byte actualResult = value >= byte.MaxValue + ? byte.MaxValue + : value <= byte.MinValue + ? byte.MinValue + : (byte)value; + + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(char)) + { + char actualResult = value >= char.MaxValue + ? char.MaxValue + : value <= char.MinValue + ? char.MinValue + : (char)value; + + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + decimal actualResult = value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(ushort)) + { + ushort actualResult = value >= ushort.MaxValue + ? ushort.MaxValue + : value <= ushort.MinValue + ? ushort.MinValue + : (ushort)value; + + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(uint)) + { + uint actualResult = value >= uint.MaxValue + ? uint.MaxValue + : value <= uint.MinValue + ? uint.MinValue + : (uint)value; + + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(ulong)) + { + ulong actualResult = value <= 0 ? ulong.MinValue : (ulong)value.Value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(UInt128)) + { + UInt128 actualResult = (value <= 0) ? UInt128.MinValue : (UInt128)value.Value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(UIntPtr)) + { + UIntPtr actualResult = (value <= 0) ? 0 : (UIntPtr)value.Value; + result = *(TOther*)&actualResult; + return true; + } + else + { + result = default!; + return false; + } + } + + /// + static unsafe bool INumberBase.TryConvertToTruncating + ( + Snowflake value, + + [MaybeNullWhen(false)] + out TOther result + ) + { + if (typeof(TOther) == typeof(byte)) + { + byte actualResult = (byte)value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(char)) + { + char actualResult = (char)value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(decimal)) + { + decimal actualResult = value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(ushort)) + { + ushort actualResult = (ushort)value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(uint)) + { + uint actualResult = (uint)value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(ulong)) + { + ulong actualResult = (ulong)value.Value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(UInt128)) + { + UInt128 actualResult = (UInt128)value.Value; + result = *(TOther*)&actualResult; + return true; + } + else if (typeof(TOther) == typeof(UIntPtr)) + { + UIntPtr actualResult = (UIntPtr)value.Value; + result = *(TOther*)&actualResult; + return true; + } + else + { + result = default; + return false; + } + } +#pragma warning restore CS8500 + + /// + static bool INumberBase.TryParse + ( + ReadOnlySpan s, + NumberStyles style, + IFormatProvider? provider, + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + style, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + static bool INumberBase.TryParse + ( + [NotNullWhen(true)] + string? s, + + NumberStyles style, + IFormatProvider? provider, + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + style, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + static bool ISpanParsable.TryParse + ( + ReadOnlySpan s, + IFormatProvider? provider, + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + static bool IParsable.TryParse + ( + string? s, + IFormatProvider? provider, + out Snowflake result + ) + { + bool success = long.TryParse + ( + s, + provider, + out long value + ); + + result = success ? value : default; + + return success; + } + + /// + static bool IBinaryInteger.TryReadBigEndian + ( + ReadOnlySpan source, + bool isUnsigned, + out Snowflake value + ) + { + if (source.Length < 8) + { + value = default; + return false; + } + + long result = Unsafe.ReadUnaligned + ( + ref MemoryMarshal.GetReference(source) + ); + + value = BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(result) + : result; + + return true; + } + + /// + static bool IBinaryInteger.TryReadLittleEndian + ( + ReadOnlySpan source, + bool isUnsigned, + out Snowflake value + ) + { + if (source.Length < 8) + { + value = default; + return false; + } + + long result = Unsafe.ReadUnaligned + ( + ref MemoryMarshal.GetReference(source) + ); + + value = BitConverter.IsLittleEndian + ? result + : BinaryPrimitives.ReverseEndianness(result); + + return true; + } + + /// + public int CompareTo(object? obj) + => this.Value.CompareTo(obj); + + /// + public string ToString(string? format, IFormatProvider? formatProvider = null) + => this.Value.ToString(format, formatProvider); + + /// + public bool TryFormat + ( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider = null + ) + { + return this.Value.TryFormat + ( + destination, + out charsWritten, + format, + provider + ); + } + + /// + int IComparable.CompareTo(object? obj) + => this.Value.CompareTo(obj); + + /// + int IComparable.CompareTo(Snowflake other) + => this.Value.CompareTo(other); + + /// + bool IEquatable.Equals(Snowflake other) + => this.Value.Equals(other); + + /// + int IBinaryInteger.GetByteCount() + => 8; + + /// + int IBinaryInteger.GetShortestBitLength() + => 64 - BitOperations.LeadingZeroCount((ulong)this.Value); + + /// + string IFormattable.ToString + ( + string? format, + IFormatProvider? formatProvider + ) + { + return this.ToString + ( + format, + formatProvider + ); + } + + /// + bool ISpanFormattable.TryFormat + ( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider + ) + { + return this.TryFormat + ( + destination, + out charsWritten, + format, + provider + ); + } + + /// + bool IBinaryInteger.TryWriteBigEndian + ( + Span destination, + out int bytesWritten + ) + { + if (destination.Length < 8) + { + bytesWritten = 0; + return false; + } + + long value = BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(this.Value) + : this.Value; + + Unsafe.WriteUnaligned + ( + ref MemoryMarshal.GetReference(destination), + value + ); + + bytesWritten = 8; + return true; + } + + /// + bool IBinaryInteger.TryWriteLittleEndian + ( + Span destination, + out int bytesWritten + ) + { + if (destination.Length < 8) + { + bytesWritten = 0; + return false; + } + + long value = !BitConverter.IsLittleEndian + ? BinaryPrimitives.ReverseEndianness(this.Value) + : this.Value; + + Unsafe.WriteUnaligned + ( + ref MemoryMarshal.GetReference(destination), + value + ); + + bytesWritten = 8; + return true; + } + + /// + static Snowflake IUnaryPlusOperators.operator +(Snowflake value) + => +value.Value; + + /// + public static Snowflake operator +(Snowflake left, Snowflake right) + => left.Value + right.Value; + + /// + static Snowflake IAdditionOperators.operator +(Snowflake left, Snowflake right) + => left.Value + right.Value; + + static Snowflake IUnaryNegationOperators.operator -(Snowflake value) + => -value.Value; + + /// + public static Snowflake operator -(Snowflake left, Snowflake right) + => left.Value - right.Value; + + /// + static Snowflake ISubtractionOperators.operator -(Snowflake left, Snowflake right) + => left.Value - right.Value; + + /// + static Snowflake IBitwiseOperators.operator ~(Snowflake value) + => ~value.Value; + + /// + public static Snowflake operator ++(Snowflake value) + => value.Value + 1; + + /// + static Snowflake IIncrementOperators.operator ++(Snowflake value) + => value.Value + 1; + + /// + public static Snowflake operator --(Snowflake value) + => value.Value - 1; + + /// + static Snowflake IDecrementOperators.operator --(Snowflake value) + => value.Value - 1; + + /// + static Snowflake IMultiplyOperators.operator *(Snowflake left, Snowflake right) + => left.Value * right.Value; + + /// + static Snowflake IDivisionOperators.operator /(Snowflake left, Snowflake right) + => left.Value / right.Value; + + /// + static Snowflake IModulusOperators.operator %(Snowflake left, Snowflake right) + => left.Value % right.Value; + + /// + static Snowflake IBitwiseOperators.operator &(Snowflake left, Snowflake right) + => left.Value & right.Value; + + /// + static Snowflake IBitwiseOperators.operator |(Snowflake left, Snowflake right) + => left.Value | right.Value; + + /// + static Snowflake IBitwiseOperators.operator ^(Snowflake left, Snowflake right) + => left.Value ^ right.Value; + + /// + static Snowflake IShiftOperators.operator <<(Snowflake value, int shiftAmount) + => value.Value << shiftAmount; + + /// + static Snowflake IShiftOperators.operator >>(Snowflake value, int shiftAmount) + => value.Value >> shiftAmount; + + /// + static bool IEqualityOperators.operator ==(Snowflake left, Snowflake right) + => left.Value == right.Value; + + /// + static bool IEqualityOperators.operator !=(Snowflake left, Snowflake right) + => left.Value != right.Value; + + /// + static bool IComparisonOperators.operator <(Snowflake left, Snowflake right) + => left.Value < right.Value; + + /// + static bool IComparisonOperators.operator >(Snowflake left, Snowflake right) + => left.Value > right.Value; + + /// + static bool IComparisonOperators.operator <=(Snowflake left, Snowflake right) + => left.Value <= right.Value; + + /// + static bool IComparisonOperators.operator >=(Snowflake left, Snowflake right) + => left.Value >= right.Value; + + /// + static Snowflake IShiftOperators.operator >>>(Snowflake value, int shiftAmount) + => value.Value >>> shiftAmount; +} diff --git a/src/core/DSharpPlus.Shared/Snowflake.TimeOperations.cs b/src/core/DSharpPlus.Shared/Snowflake.TimeOperations.cs new file mode 100644 index 0000000000..b47ac49b3b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Snowflake.TimeOperations.cs @@ -0,0 +1,102 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +namespace DSharpPlus; + +public readonly partial record struct Snowflake +{ + public static Snowflake operator + + ( + Snowflake left, + TimeSpan right + ) + { + long time = (long)right.TotalMilliseconds << 22; + return left.Value + time; + } + + public static Snowflake operator - + ( + Snowflake left, + TimeSpan right + ) + { + long time = (long)right.TotalMilliseconds << 22; + return left.Value - time; + } + + public static bool operator ==(Snowflake left, DateTimeOffset right) + => left.Timestamp == right; + + public static bool operator !=(Snowflake left, DateTimeOffset right) + => left.Timestamp != right; + + public static bool operator <(Snowflake left, DateTimeOffset right) + => left.Timestamp < right; + + public static bool operator <=(Snowflake left, DateTimeOffset right) + => left.Timestamp <= right; + + public static bool operator >(Snowflake left, DateTimeOffset right) + => left.Timestamp > right; + + public static bool operator >=(Snowflake left, DateTimeOffset right) + => left.Timestamp >= right; + + /// + /// Returns the absolute difference in time between the two snowflakes. + /// + public static TimeSpan GetAbsoluteTimeDifference + ( + Snowflake first, + Snowflake second + ) + { + long absolute = long.Abs + ( + first - second + ); + + return new + ( + (absolute >> 22) * 10_000 + ); + } + + /// + /// Creates a new snowflake from an offset into the future. + /// + public static Snowflake FromFuture + ( + TimeSpan offset + ) + { + return new + ( + DateTimeOffset.UtcNow + offset, + 0, + 0, + 0 + ); + } + + /// + /// Creates a new snowflake from an offset into the past. + /// + public static Snowflake FromPast + ( + TimeSpan offset + ) + { + return new + ( + DateTimeOffset.UtcNow - offset, + 0, + 0, + 0 + ); + } +} diff --git a/src/core/DSharpPlus.Shared/Snowflake.cs b/src/core/DSharpPlus.Shared/Snowflake.cs new file mode 100644 index 0000000000..0cc139c62b --- /dev/null +++ b/src/core/DSharpPlus.Shared/Snowflake.cs @@ -0,0 +1,111 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA5394 + +using System; +using System.Globalization; + +namespace DSharpPlus; + +/// +/// Represents a discord snowflake; the type discord uses for IDs first and foremost. +/// +public readonly partial record struct Snowflake : + IComparable +{ + /// + /// The discord epoch; the start of 2015. All snowflakes are based upon this time. + /// + public static readonly DateTimeOffset DiscordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); + + /// + /// The snowflake's underlying value. + /// + public long Value { get; } + + /// + /// Returns the string representation of this snowflake. + /// + public override string ToString() => this.Value.ToString(CultureInfo.InvariantCulture); + + /// + /// The time when this snowflake was created. + /// + public DateTimeOffset Timestamp => DiscordEpoch.AddMilliseconds + ( + this.Value >> 22 + ); + + /// + /// The internal worker's ID that was used to generate the snowflake. + /// + public byte InternalWorkerId => (byte)((this.Value & 0x3E0000) >> 17); + + /// + /// The internal process' ID that was used to generate the snowflake. + /// + public byte InternalProcessId => (byte)((this.Value & 0x1F000) >> 12); + + /// + /// The internal worker-specific and process-specific increment. + /// + public ulong InternalIncrement => (ulong)(this.Value & 0xFFF); + + /// + /// Creates a new snowflake from a given integer. + /// + /// The numerical representation to translate from. + public Snowflake(long value) + => this.Value = value; + + /// + /// Creates a fake snowflake from scratch. If no parameters are provided, returns a newly generated snowflake. + /// + /// + /// If a value larger than allowed is supplied for the three numerical parameters, it will be cut off at + /// the maximum allowed value. + /// + /// + /// The date when the snowflake was created. If null, this defaults to the current time. + /// + /// + /// A 5 bit worker id that was used to create the snowflake. If null, generates a random number between 0 and 31. + /// + /// + /// A 5 bit process id that was used to create the snowflake. If null, generates a random number between 0 and 31. + /// + /// + /// A 12 bit integer which represents the number of previously generated snowflakes in the given context. + /// If null, generates a random number between 0 and 4,095. + /// + public Snowflake + ( + DateTimeOffset? timestamp = null, + byte? workerId = null, + byte? processId = null, + ushort? increment = null + ) + { + timestamp ??= DateTimeOffset.Now; + workerId ??= (byte)Random.Shared.Next(0, 32); + processId ??= (byte)Random.Shared.Next(0, 32); + increment ??= (ushort)Random.Shared.Next(0, 4095); + + this.Value = ((long)timestamp.Value.Subtract(DiscordEpoch).TotalMilliseconds << 22) + | ((long)workerId.Value << 17) + | ((long)processId.Value << 12) + | increment.Value; + } + + public int CompareTo(Snowflake other) + => this.Value.CompareTo(other.Value); + + public static bool operator <(Snowflake left, Snowflake right) => left.Value < right.Value; + public static bool operator <=(Snowflake left, Snowflake right) => left.Value <= right.Value; + public static bool operator >(Snowflake left, Snowflake right) => left.Value > right.Value; + public static bool operator >=(Snowflake left, Snowflake right) => left.Value >= right.Value; + public static implicit operator long(Snowflake snowflake) => snowflake.Value; + public static implicit operator Snowflake(long value) => value; +} diff --git a/src/core/DSharpPlus.Shared/ThrowHelper.cs b/src/core/DSharpPlus.Shared/ThrowHelper.cs new file mode 100644 index 0000000000..ddda4d03bc --- /dev/null +++ b/src/core/DSharpPlus.Shared/ThrowHelper.cs @@ -0,0 +1,30 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace DSharpPlus; + +/// +/// Contains methods relegating to throw statements. +/// +internal static class ThrowHelper +{ + [DoesNotReturn] + [DebuggerHidden] + [StackTraceHidden] + public static void ThrowOptionalNoValuePresent() + => throw new InvalidOperationException("This optional did not have a value specified."); + + [DoesNotReturn] + [DebuggerHidden] + [StackTraceHidden] + public static void ThrowFunc + ( + Func func + ) + => throw func(); +} diff --git a/src/core/readme.md b/src/core/readme.md new file mode 100644 index 0000000000..dcffa6eebd --- /dev/null +++ b/src/core/readme.md @@ -0,0 +1,13 @@ +# DSharpPlus Core Library + +This directory is home to everything needed to build the DSharpPlus core library, that is, the user-facing +convenience library without extension libraries. This includes the serialization models, the REST abstractions +and implementation, the Gateway abstractions and implementation, as well as the libraries and tools needed to +wrap these components into a well-curated, convenient library for end users. + +These components are generally included privately in the main library. If end users wish to consume these libraries +directly, they must reference them directly, not transiently through our main library. + +Because it is impossible to type-forward from a privately included library, some libraries must be included +publicly. These libraries are, therefore, versioned with the main library, and they contain a notice mentioning +this fact. diff --git a/src/extensions/DSharpPlus.Extensions.Internal.BadRequestHelper/DSharpPlus.Extensions.Internal.BadRequestHelper.csproj b/src/extensions/DSharpPlus.Extensions.Internal.BadRequestHelper/DSharpPlus.Extensions.Internal.BadRequestHelper.csproj new file mode 100644 index 0000000000..d3b5ef2909 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.BadRequestHelper/DSharpPlus.Extensions.Internal.BadRequestHelper.csproj @@ -0,0 +1,13 @@ + + + + $(_DSharpPlusExtensionsInternalBadRequestHelperVersion) + enable + + + + + + + + diff --git a/src/extensions/DSharpPlus.Extensions.Internal.BadRequestHelper/ResultExtensions.cs b/src/extensions/DSharpPlus.Extensions.Internal.BadRequestHelper/ResultExtensions.cs new file mode 100644 index 0000000000..657aeae6cb --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.BadRequestHelper/ResultExtensions.cs @@ -0,0 +1,46 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable CA1031 + +using System.Net; +using System.Text.Json; + +using DSharpPlus.Internal.Abstractions.Rest.Errors; +using DSharpPlus.Results; + +namespace DSharpPlus.Extensions.Internal.BadRequestHelper; + +public static class ResultExtensions +{ + public static Result ExpandBadRequestError(this Result result, object payload) + { + return result.MapError + ( + error => + { + if (error is not HttpError { StatusCode: HttpStatusCode.BadRequest }) + { + return error; + } + + try + { + using JsonDocument document = JsonDocument.Parse(error.Message); + + if (document.RootElement.TryGetProperty("errors", out JsonElement errors)) + { + return error; //placeholder + } + } + catch + { + return error; + } + + return error; //placeholder + } + ); + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Interactions/ModalBuilder.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Interactions/ModalBuilder.cs new file mode 100644 index 0000000000..f96af4308f --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Interactions/ModalBuilder.cs @@ -0,0 +1,149 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System; +using System.Collections.Generic; +using System.Linq; + +using DSharpPlus.Entities; +using DSharpPlus.Extensions.Internal.Toolbox.Errors; +using DSharpPlus.Extensions.Internal.Toolbox.Implementations; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Results; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Builders.Interactions; + +/// +/// Represents a modal under construction. +/// +public record struct ModalBuilder +{ + /// + public string? CustomId { get; set; } + + /// + public string? Title { get; set; } + + /// + public IReadOnlyList? Components { get; set; } +} + +public static class ModalBuilderExtensions +{ + /// + /// Sets the custom ID of the modal. + /// + /// The modal builder for chaining. + public static ref ModalBuilder WithCustomId(ref this ModalBuilder builder, string customId) + { + builder.CustomId = customId; + return ref builder; + } + + /// + /// Sets the title of the modal. + /// + /// The modal builder for chaining. + public static ref ModalBuilder WithTitle(ref this ModalBuilder builder, string title) + { + builder.Title = title; + return ref builder; + } + + /// + /// Adds a text input component to the modal. + /// + /// The modal builder for chaining. + public static ref ModalBuilder AddComponent(ref this ModalBuilder builder, ITextInputComponent component) + { + if (builder.Components is null) + { + builder.Components = [component]; + return ref builder; + } + + builder.Components = [.. builder.Components, component]; + return ref builder; + } + + /// + /// Verifies whether the modal builder can be transformed into a valid modal. + /// + public static Result Validate(ref this ModalBuilder builder) + { + List<(string, string)> errors = []; + + if (builder.CustomId is null) + { + errors.Add((nameof(builder.CustomId), "The custom ID of a modal must be provided.")); + } + else if (builder.CustomId.Length > 100) + { + errors.Add((nameof(builder.CustomId), "A custom ID cannot exceed 100 characters in length.")); + } + + if (builder.Title is null) + { + errors.Add((nameof(builder.Title), "The title of a modal must be provided.")); + } + else if (builder.Title.Length > 45) + { + errors.Add((nameof(builder.CustomId), "A modal title cannot exceed 45 characters in length.")); + } + + if (builder.Components is null || builder.Components.Count is not >= 1 and <= 5) + { + errors.Add((nameof(builder.Components), "There must be between one and five components provided.")); + } + + if (errors is not []) + { + return new BuilderValidationError + ( + "Some modal fields were invalid. See the attached dictionary for further information.", + [.. errors] + ); + } + else + { + return Result.Success; + } + } + + /// + /// Builds the modal. This does not enforce validity, past enforcing all fields are present. + /// + public static IInteractionResponse Build(ref this ModalBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder.Title); + ArgumentNullException.ThrowIfNull(builder.CustomId); + ArgumentNullException.ThrowIfNull(builder.Components); + + return new BuiltInteractionResponse + { + Type = DiscordInteractionCallbackType.Modal, + Data = new + ( + new BuiltModalCallbackData + { + Title = builder.Title, + CustomId = builder.CustomId, + Components = builder.Components.Select + ( + x => + { + return new BuiltActionRowComponent + { + Type = DiscordMessageComponentType.ActionRow, + Components = [x] + }; + } + ).ToArray() + } + ) + }; + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Interactions/TextInputComponentBuilder.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Interactions/TextInputComponentBuilder.cs new file mode 100644 index 0000000000..2d15030f41 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Interactions/TextInputComponentBuilder.cs @@ -0,0 +1,253 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using DSharpPlus.Entities; +using DSharpPlus.Extensions.Internal.Toolbox.Errors; +using DSharpPlus.Extensions.Internal.Toolbox.Implementations; +using DSharpPlus.Results; + +namespace DSharpPlus.Extensions.Internal.Builders.Interactions; + +/// +/// Represents a under construction. +/// +public record struct TextInputComponentBuilder +{ + /// + public string? CustomId { get; set; } + + /// + public DiscordTextInputStyle? Style { get; set; } + + /// + public string? Label { get; set; } + + /// + public Optional MinLength { get; set; } + + /// + public Optional MaxLength { get; set; } + + /// + public Optional Required { get; set; } + + /// + public Optional Placeholder { get; set; } + + /// + public Optional Value { get; set; } +} + +public static class TextInputComponentBuilderExtensions +{ + /// + /// Sets the custom ID of the component for later identification. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithCustomId + ( + ref this TextInputComponentBuilder builder, + string customId + ) + { + builder.CustomId = customId; + return ref builder; + } + + /// + /// Sets the style of the component. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithStyle + ( + ref this TextInputComponentBuilder builder, + DiscordTextInputStyle style + ) + { + builder.Style = style; + return ref builder; + } + + /// + /// Sets the label of the component. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithLabel + ( + ref this TextInputComponentBuilder builder, + string label + ) + { + builder.Label = label; + return ref builder; + } + + /// + /// Sets the minimum length of the text the user is required to put in. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithMinLength + ( + ref this TextInputComponentBuilder builder, + int minLength + ) + { + builder.MinLength = minLength; + return ref builder; + } + + /// + /// Sets the maximum length of the text the user is required to put in. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithMaxLength + ( + ref this TextInputComponentBuilder builder, + int maxLength + ) + { + builder.MaxLength = maxLength; + return ref builder; + } + + /// + /// Sets whether this input component is required to be filled. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder SetRequired + ( + ref this TextInputComponentBuilder builder, + bool required = true + ) + { + builder.Required = required; + return ref builder; + } + + /// + /// Sets the placeholder value of this component. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithPlaceholder + ( + ref this TextInputComponentBuilder builder, + string placeholder + ) + { + builder.Placeholder = placeholder; + return ref builder; + } + + /// + /// Sets the default value of this component. + /// + /// The component builder for chaining. + public static ref TextInputComponentBuilder WithValue + ( + ref this TextInputComponentBuilder builder, + string value + ) + { + builder.Value = value; + return ref builder; + } + + /// + /// Validates whether the builder can be converted into a legal component. + /// + public static Result Validate(ref this TextInputComponentBuilder builder) + { + List<(string, string)> errors = []; + + if (builder.CustomId is null) + { + errors.Add((nameof(builder.CustomId), "The custom ID of a text input component must be provided.")); + } + else if (builder.CustomId.Length > 100) + { + errors.Add((nameof(builder.CustomId), "A custom ID cannot exceed 100 characters in length.")); + } + + if (builder.Style is null) + { + errors.Add((nameof(builder.Style), "The style of a text input component must be provided.")); + } + + if (builder.Label is null) + { + errors.Add((nameof(builder.Label), "The label of a text input component must be provided")); + } + else if (builder.Label.Length > 45) + { + errors.Add((nameof(builder.Label), "A text input component's label cannot exceed 45 characters in length.")); + } + + if (builder.MinLength.HasValue && builder.MinLength.Value is not > 0 and < 4000) + { + errors.Add((nameof(builder.MinLength), "The minimum length must be between 0 and 4000.")); + } + + if (builder.MaxLength.HasValue && builder.MaxLength.Value is not > 1 and < 4000) + { + errors.Add((nameof(builder.MaxLength), "The maximum length must be between 1 and 4000.")); + } + + if (builder.Placeholder.HasValue && builder.Placeholder.Value!.Length > 100) + { + errors.Add((nameof(builder.Placeholder), "A placeholder cannot exceed 100 characters in length.")); + } + + if (builder.Value.HasValue && builder.Value.Value!.Length > 4000) + { + errors.Add((nameof(builder.Value), "The default value cannot exceed 4000 characters in length.")); + } + + if (errors is not []) + { + return new BuilderValidationError + ( + "Some component fields were invalid. See the attached dictionary for further information.", + [.. errors] + ); + } + else + { + return Result.Success; + } + } + + /// + /// Builds the text input component. This does not enforce validity, past enforcing all fields are present. + /// + [SuppressMessage("Usage", "CA2208", Justification = "We fully intend to pass builder.Style here.")] + public static ITextInputComponent Build(ref this TextInputComponentBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder.CustomId); + ArgumentNullException.ThrowIfNull(builder.Label); + + if (!builder.Style.HasValue) + { + throw new ArgumentNullException(nameof(builder.Style)); + } + + return new BuiltTextInputComponent + { + Type = DiscordMessageComponentType.TextInput, + CustomId = builder.CustomId, + Style = builder.Style.Value, + Label = builder.Label, + MinLength = builder.MinLength, + MaxLength = builder.MaxLength, + Required = builder.Required, + Value = builder.Value, + Placeholder = builder.Placeholder + }; + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Messages/EmbedBuilder.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Messages/EmbedBuilder.cs new file mode 100644 index 0000000000..3020bfca56 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Builders/Messages/EmbedBuilder.cs @@ -0,0 +1,267 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 + +using System; +using System.Collections.Generic; + +using DSharpPlus.Extensions.Internal.Toolbox.Errors; +using DSharpPlus.Extensions.Internal.Toolbox.Implementations; +using DSharpPlus.Internal.Abstractions.Models; + +using DSharpPlus.Results; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Builders.Messages; + +/// +/// Represents an currently under construction. +/// +public record struct EmbedBuilder +{ + /// + public Optional Title { get; set; } + + /// + public Optional Description { get; set; } + + /// + public Optional Color { get; set; } + + /// + public Optional Url { get; set; } + + /// + public Optional Timestamp { get; set; } + + /// + public Optional Footer { get; set; } + + /// + public Optional Author { get; set; } + + /// + public Optional> Fields { get; set; } +} + +public static class EmbedBuilderExtensions +{ + /// + /// Sets the title of the embed to the specified string. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithTitle(ref this EmbedBuilder builder, string title) + { + builder.Title = title; + return ref builder; + } + + /// + /// Sets the description of the embed to the specified string. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithDescription(ref this EmbedBuilder builder, string description) + { + builder.Description = description; + return ref builder; + } + + /// + /// Sets the sidebar color of the embed to the specified color code. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithColor(ref this EmbedBuilder builder, int color) + { + builder.Color = color; + return ref builder; + } + + /// + /// Sets the url of the embed to the specified link. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithUrl(ref this EmbedBuilder builder, string url) + { + builder.Url = url; + return ref builder; + } + + /// + /// Sets the url of the embed to the specified link. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithUrl(ref this EmbedBuilder builder, Uri url) + { + builder.Url = url.AbsoluteUri; + return ref builder; + } + + /// + /// Sets the timestamp of the embed to the specified value. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithTimestamp(ref this EmbedBuilder builder, DateTimeOffset timestamp) + { + builder.Timestamp = timestamp; + return ref builder; + } + + /// + /// Sets the footer of the embed to the specified value. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithFooter(ref this EmbedBuilder builder, IEmbedFooter footer) + { + builder.Footer = new(footer); + return ref builder; + } + + /// + /// Sets the author of the embed to the specified value. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder WithAuthor(ref this EmbedBuilder builder, IEmbedAuthor author) + { + builder.Author = new(author); + return ref builder; + } + + /// + /// Adds a field to the embed builder. + /// + /// The embed builder for chaining. + public static ref EmbedBuilder AddField(ref this EmbedBuilder builder, IEmbedField field) + { + builder.Fields = builder.Fields.MapOr>> + ( + transformation: fields => new([.. fields, field]), + value: new([field]) + ); + + return ref builder; + } + + /// + /// Verifies whether the embed builder can be transformed into a valid embed. + /// + public static Result Validate(ref this EmbedBuilder builder) + { + int totalCount = 0; + List<(string, string)> errors = []; + + if (builder.Title.TryGetNonNullValue(out string? title)) + { + if (title.Length > 256) + { + errors.Add((nameof(EmbedBuilder.Title), "The length of the title cannot exceed 256 characters.")); + } + + totalCount += title.Length; + } + + if (builder.Description.TryGetNonNullValue(out string? desc)) + { + if (desc.Length > 256) + { + errors.Add((nameof(EmbedBuilder.Description), "The length of the description cannot exceed 4096 characters.")); + } + + totalCount += desc.Length; + } + + if (builder.Color.TryGetNonNullValue(out int color)) + { + if (color is < 0x000000 or > 0xFFFFFF) + { + errors.Add((nameof(EmbedBuilder.Color), "The color code must be between 0x000000 and 0xFFFFFF.")); + } + } + + if (builder.Footer.TryGetNonNullValue(out IEmbedFooter? footer)) + { + if (footer.Text.Length > 2048) + { + errors.Add((nameof(EmbedBuilder.Footer), "The length of the footer text cannot exceed 2048 characters.")); + } + + totalCount += footer.Text.Length; + } + + if (builder.Author.TryGetNonNullValue(out IEmbedAuthor? author)) + { + if (author.Name.Length > 256) + { + errors.Add((nameof(EmbedBuilder.Author), "The length of the author name cannot exceed 256 characters.")); + } + + totalCount += author.Name.Length; + } + + if (builder.Fields.TryGetNonNullValue(out IReadOnlyList? fields)) + { + if (fields.Count > 25) + { + errors.Add((nameof(EmbedBuilder.Fields), "There cannot be more than 25 fields in an embed.")); + } + + for (int i = 0; i < 25; i++) + { + if (fields[i].Name.Length > 256) + { + errors.Add(($"Fields[{i}]", "The title of a field cannot exceed 256 characters.")); + } + + if (fields[i].Value.Length > 1024) + { + errors.Add(($"Fields[{i}]", "The length of a field value cannot exceed 1024 characters.")); + } + + totalCount += fields[i].Name.Length + fields[i].Value.Length; + } + } + + if (totalCount > 6000 && errors is not []) + { + return new BuilderValidationError + ( + "The total length of the embed exceeded 6000 characters, and some embed fields were invalid.", + [.. errors] + ); + } + else if (totalCount > 6000) + { + return new BuilderValidationError("The total length of the embed cannot exceed 6000 characters."); + } + else if (errors is not []) + { + return new BuilderValidationError + ( + "Some embed fields were invalid. See the attached dictionary for further information.", + [.. errors] + ); + } + else + { + return Result.Success; + } + } + + /// + /// Builds the embed. This does not enforce validity. + /// + public static IEmbed Build(ref this EmbedBuilder builder) + { + return new BuiltEmbed + { + Title = builder.Title, + Description = builder.Description, + Color = builder.Color, + Url = builder.Url, + Timestamp = builder.Timestamp, + Footer = builder.Footer, + Author = builder.Author, + Fields = builder.Fields + }; + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/DSharpPlus.Extensions.Internal.Toolbox.csproj b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/DSharpPlus.Extensions.Internal.Toolbox.csproj new file mode 100644 index 0000000000..6c6a23aa23 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/DSharpPlus.Extensions.Internal.Toolbox.csproj @@ -0,0 +1,13 @@ + + + + enable + $(_DSharpPlusExtensionsInternalToolboxVersion) + + + + + + + + diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Errors/BuilderValidationError.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Errors/BuilderValidationError.cs new file mode 100644 index 0000000000..51773eda31 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Errors/BuilderValidationError.cs @@ -0,0 +1,58 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +using DSharpPlus.Results.Errors; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Errors; + +/// +/// Represents an error encountered when attempting to validate a builder. +/// +public record BuilderValidationError : Error +{ + /// + /// If this error was caused by any specific parameters, contains the names of the invalid parameters. + /// + public IReadOnlyDictionary? ParameterNames { get; init; } + + /// + /// Initializes a new validation error. + /// + /// The human-readable error message. + /// If applicable, the names of parameters that failed validation. + public BuilderValidationError(string message, params (string Key, string Value)[] parameters) + : base(message) + { + if (parameters.Length == 0) + { + return; + } + + this.ParameterNames = parameters.ToDictionary(x => x.Key, x => x.Value); + } + + public override string ToString() + { + if (this.ParameterNames is null) + { + return base.ToString(); + } + + StringBuilder builder = new($"BuilderValidationError\n{{\n\t{this.Message},\n\tParameters:\n\t["); + + foreach (KeyValuePair kvp in this.ParameterNames) + { + _ = builder.Append(CultureInfo.InvariantCulture, $"\n\t\t{kvp.Key}: {kvp.Value}"); + } + + _ = builder.Append("\n\t]\n}"); + + return builder.ToString(); + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Extensions/InteractionRestAPIExtensions.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Extensions/InteractionRestAPIExtensions.cs new file mode 100644 index 0000000000..40dd9b9a85 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Extensions/InteractionRestAPIExtensions.cs @@ -0,0 +1,244 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 // we have a lot of early returns here that we don't want to become ternaries. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Extensions.Internal.Toolbox.Implementations; +using DSharpPlus.Extensions.Internal.Toolbox.Builders.Interactions; +using DSharpPlus.Extensions.Internal.Toolbox.Builders.Messages; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Results; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Extensions; + +/// +/// Contains extension methods on to enable using builders. +/// +public static class InteractionRestAPIExtensions +{ + /// + /// Responds to the specified interaction using a modal. + /// + /// The underlying interaction API. + /// The snowflake identifier of the interaction. + /// The interaction token received with the interaction. + /// The modal builder to respond with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + public static async ValueTask RespondWithModalAsync + ( + this IInteractionRestAPI underlying, + Snowflake interactionId, + string interactionToken, + ModalBuilder modal, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = modal.Validate(); + + if (!result.IsSuccess) + { + return result; + } + + return await underlying.CreateInteractionResponseAsync + ( + interactionId, + interactionToken, + modal.Build(), + info, + ct + ); + } + + /// + /// Responds to the specified interaction using an embed. + /// + /// The underlying interaction API. + /// The snowflake identifier of the interaction. + /// The interaction token received with the interaction. + /// The embed builder to respond with. + /// Specifies whether the response to this request should be ephemeral. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + public static async ValueTask RespondWithEmbedAsync + ( + this IInteractionRestAPI underlying, + Snowflake interactionId, + string interactionToken, + EmbedBuilder embed, + bool ephemeral = false, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = embed.Validate(); + + if (!result.IsSuccess) + { + return result; + } + + BuiltInteractionResponse response = new() + { + Type = DiscordInteractionCallbackType.ChannelMessageWithSource, + Data = new + ( + new BuiltMessageCallbackData() + { + Embeds = new Optional>([embed.Build()]), + Flags = ephemeral ? new(DiscordMessageFlags.Ephemeral) : Optional.None + } + ) + }; + + return await underlying.CreateInteractionResponseAsync + ( + interactionId, + interactionToken, + response, + info, + ct + ); + } + + /// + /// Creates a followup response containing an embed. If this is the first followup to a deferred interaction + /// response as created by , + /// ephemerality of this message will be dictated by the specified originally, + /// and will be ignored. + /// + /// The underlying interaction API. + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The embed builder to respond with. + /// Specifies whether this response should be ephemeral. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public static async ValueTask> FollowupWithEmbedAsync + ( + this IInteractionRestAPI underlying, + Snowflake applicationId, + string interactionToken, + EmbedBuilder embed, + bool ephemeral = false, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = embed.Validate(); + + if (!result.IsSuccess) + { + return Result.FromError(result.Error); + } + + return await underlying.CreateFollowupMessageAsync + ( + applicationId, + interactionToken, + new BuiltCreateFollowupMessagePayload + { + Embeds = new([embed.Build()]), + Flags = ephemeral ? new(DiscordMessageFlags.Ephemeral) : Optional.None + }, + info, + ct + ); + } + + /// + /// Edits the original response with the specified embed. This will leave all fields but embeds intact. + /// Ephemerality is dictated by the original response. + /// + /// The underlying interaction API. + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The embed builder to respond with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public static async ValueTask> ModifyResponseWithEmbedAsync + ( + this IInteractionRestAPI underlying, + Snowflake applicationId, + string interactionToken, + EmbedBuilder embed, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = embed.Validate(); + + if (!result.IsSuccess) + { + return Result.FromError(result.Error); + } + + return await underlying.EditInteractionResponseAsync + ( + applicationId, + interactionToken, + new BuiltEditInteractionResponsePayload + { + Embeds = new([embed.Build()]) + }, + info, + ct + ); + } + + /// + /// Edits the original response with the specified embed. This will leave all fields but embeds intact. + /// Ephemerality is dictated by the original response. + /// + /// The underlying interaction API. + /// The snowflake identifier of your application. + /// The interaction token received with the interaction. + /// The snowflake identifier of the followup message. + /// The embed builder to respond with. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + public static async ValueTask> ModifyFollowupWithEmbedAsync + ( + this IInteractionRestAPI underlying, + Snowflake applicationId, + string interactionToken, + Snowflake messageId, + EmbedBuilder embed, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = embed.Validate(); + + if (!result.IsSuccess) + { + return Result.FromError(result.Error); + } + + return await underlying.EditFollowupMessageAsync + ( + applicationId, + interactionToken, + messageId, + new BuiltEditFollowupMessagePayload + { + Embeds = new([embed.Build()]) + }, + info, + ct + ); + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Extensions/MessageRestAPIExtensions.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Extensions/MessageRestAPIExtensions.cs new file mode 100644 index 0000000000..1824866757 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Extensions/MessageRestAPIExtensions.cs @@ -0,0 +1,100 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0046 // we have a lot of early returns here that we don't want to become ternaries. + +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Extensions.Internal.Toolbox.Implementations; +using DSharpPlus.Extensions.Internal.Toolbox.Builders.Messages; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest; +using DSharpPlus.Internal.Abstractions.Rest.API; +using DSharpPlus.Results; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Extensions; + +/// +/// Contains extension methods on to enable using builders. +/// +public static class MessageRestAPIExtensions +{ + /// + /// Creates a new message comprising the specified embed in a channel. + /// + /// The underlying message API. + /// The snowflake identifier of the message's target channel. + /// The embed this message is to be comprised of. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly created message object. + public static async ValueTask> SendEmbedAsync + ( + this IMessageRestAPI underlying, + Snowflake channelId, + EmbedBuilder embed, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = embed.Validate(); + + if (!result.IsSuccess) + { + return Result.FromError(result.Error); + } + + return await underlying.CreateMessageAsync + ( + channelId, + new BuiltCreateMessagePayload + { + Embeds = new([embed.Build()]) + }, + info, + ct + ); + } + + /// + /// Modifies the specified message to comprise the specified embed. This will only update embeds for this message. + /// + /// The underlying message API. + /// The snowflake identifier of the channel this message was sent in. + /// The snowflake identifier of the message to modify with the embed. + /// The embed this message is to be comprised of. + /// Additional instructions regarding this request. + /// A cancellation token for this operation. + /// The newly modified message object. + public static async ValueTask> ModifyEmbedAsync + ( + this IMessageRestAPI underlying, + Snowflake channelId, + Snowflake messageId, + EmbedBuilder embed, + RequestInfo info = default, + CancellationToken ct = default + ) + { + Result result = embed.Validate(); + + if (!result.IsSuccess) + { + return Result.FromError(result.Error); + } + + return await underlying.EditMessageAsync + ( + channelId, + messageId, + new BuiltEditMessagePayload + { + Embeds = new([embed.Build()]) + }, + info, + ct + ); + } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltActionRowComponent.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltActionRowComponent.cs new file mode 100644 index 0000000000..864224a49c --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltActionRowComponent.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltActionRowComponent : IActionRowComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public required IReadOnlyList Components { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltCreateFollowupMessagePayload.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltCreateFollowupMessagePayload.cs new file mode 100644 index 0000000000..bdb029cacc --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltCreateFollowupMessagePayload.cs @@ -0,0 +1,42 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltCreateFollowupMessagePayload : ICreateFollowupMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional Tts { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional Poll { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltCreateMessagePayload.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltCreateMessagePayload.cs new file mode 100644 index 0000000000..186a374d1a --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltCreateMessagePayload.cs @@ -0,0 +1,54 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltCreateMessagePayload : ICreateMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional Nonce { get; init; } + + /// + public Optional Tts { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional MessageReference { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional> StickerIds { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional EnforceNonce { get; init; } + + /// + public Optional Poll { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditFollowupMessagePayload.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditFollowupMessagePayload.cs new file mode 100644 index 0000000000..1f1b7d0690 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditFollowupMessagePayload.cs @@ -0,0 +1,32 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltEditFollowupMessagePayload : IEditFollowupMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public Optional?> Attachments { get; init; } + + /// + public IReadOnlyList? Files { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditInteractionResponsePayload.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditInteractionResponsePayload.cs new file mode 100644 index 0000000000..cd2aba2c9e --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditInteractionResponsePayload.cs @@ -0,0 +1,35 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltEditInteractionResponsePayload : IEditInteractionResponsePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public Optional?> Attachments { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional Poll { get; init; } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditMessagePayload.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditMessagePayload.cs new file mode 100644 index 0000000000..8c7ff84bcf --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEditMessagePayload.cs @@ -0,0 +1,36 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Abstractions.Rest.Payloads; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltEditMessagePayload : IEditMessagePayload +{ + /// + public Optional Content { get; init; } + + /// + public Optional?> Embeds { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional?> Components { get; init; } + + /// + public IReadOnlyList? Files { get; init; } + + /// + public Optional?> Attachments { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEmbed.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEmbed.cs new file mode 100644 index 0000000000..208ede0923 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltEmbed.cs @@ -0,0 +1,53 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltEmbed : IEmbed +{ + /// + public Optional Title { get; init; } + + /// + public Optional Type { get; init; } + + /// + public Optional Description { get; init; } + + /// + public Optional Url { get; init; } + + /// + public Optional Timestamp { get; init; } + + /// + public Optional Color { get; init; } + + /// + public Optional Footer { get; init; } + + /// + public Optional Image { get; init; } + + /// + public Optional Thumbnail { get; init; } + + /// + public Optional Video { get; init; } + + /// + public Optional Provider { get; init; } + + /// + public Optional Author { get; init; } + + /// + public Optional> Fields { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltInteractionResponse.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltInteractionResponse.cs new file mode 100644 index 0000000000..935bc66e6e --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltInteractionResponse.cs @@ -0,0 +1,20 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +using OneOf; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltInteractionResponse : IInteractionResponse +{ + /// + public required DiscordInteractionCallbackType Type { get; init; } + + /// + public Optional> Data { get; init; } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltMessageCallbackData.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltMessageCallbackData.cs new file mode 100644 index 0000000000..8494473351 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltMessageCallbackData.cs @@ -0,0 +1,41 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltMessageCallbackData : IMessageCallbackData +{ + /// + public Optional Tts { get; init; } + + /// + public Optional Content { get; init; } + + /// + public Optional> Embeds { get; init; } + + /// + public Optional AllowedMentions { get; init; } + + /// + public Optional Flags { get; init; } + + /// + public Optional> Components { get; init; } + + /// + public Optional> Attachments { get; init; } + + /// + public Optional Poll { get; init; } + + /// + public IReadOnlyList? Files { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltModalCallbackData.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltModalCallbackData.cs new file mode 100644 index 0000000000..f126877def --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltModalCallbackData.cs @@ -0,0 +1,22 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Collections.Generic; + +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltModalCallbackData : IModalCallbackData +{ + /// + public required string CustomId { get; init; } + + /// + public required string Title { get; init; } + + /// + public required IReadOnlyList Components { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltScheduledEventRecurrenceDay.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltScheduledEventRecurrenceDay.cs new file mode 100644 index 0000000000..b2595c7cce --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltScheduledEventRecurrenceDay.cs @@ -0,0 +1,18 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltScheduledEventRecurrenceDay : IScheduledEventRecurrenceDay +{ + /// + public required int N { get; init; } + + /// + public required DiscordScheduledEventRecurrenceWeekday Day { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltScheduledEventRecurrenceRule.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltScheduledEventRecurrenceRule.cs new file mode 100644 index 0000000000..03bd96b511 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltScheduledEventRecurrenceRule.cs @@ -0,0 +1,45 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltScheduledEventRecurrenceRule : IScheduledEventRecurrenceRule +{ + /// + public required DateTimeOffset Start { get; init; } + + /// + public DateTimeOffset? End { get; init; } + + /// + public required DiscordScheduledEventRecurrenceFrequency Frequency { get; init; } + + /// + public required int Interval { get; init; } + + /// + public IReadOnlyList? ByWeekday { get; init; } + + /// + public IReadOnlyList? ByNWeekday { get; init; } + + /// + public IReadOnlyList? ByMonth { get; init; } + + /// + public IReadOnlyList? ByMonthDay { get; init; } + + /// + public IReadOnlyList? ByYearDay { get; init; } + + /// + public int? Count { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltTextInputComponent.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltTextInputComponent.cs new file mode 100644 index 0000000000..4aa1e3ee50 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/BuiltTextInputComponent.cs @@ -0,0 +1,38 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using DSharpPlus.Entities; + +namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations; + +/// +internal sealed record BuiltTextInputComponent : ITextInputComponent +{ + /// + public required DiscordMessageComponentType Type { get; init; } + + /// + public required string CustomId { get; init; } + + /// + public required DiscordTextInputStyle Style { get; init; } + + /// + public required string Label { get; init; } + + /// + public Optional MinLength { get; init; } + + /// + public Optional MaxLength { get; init; } + + /// + public Optional Required { get; init; } + + /// + public Optional Value { get; init; } + + /// + public Optional Placeholder { get; init; } +} \ No newline at end of file diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/PollResultMessage.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/PollResultMessage.cs new file mode 100644 index 0000000000..f8007c5e27 --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/PollResultMessage.cs @@ -0,0 +1,207 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Globalization; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox; + +/// +/// Provides a helper to deconstruct poll result messages into readable C# code. +/// +public readonly record struct PollResultMessage +{ + private PollResultMessage + ( + string questionText, + int answerVotes, + int totalVotes, + int? answerId, + string? answerText, + Snowflake? answerEmojiId, + string? answerEmojiName, + bool? isAnswerEmojiAnimated + ) + { + this.PollQuestionText = questionText; + this.WinningAnswerVoteCount = answerVotes; + this.TotalVotes = totalVotes; + this.WinningAnswerId = answerId; + this.WinningAnswerText = answerText; + this.WinningAnswerEmojiId = answerEmojiId; + this.WinningAnswerEmojiName = answerEmojiName; + this.IsWinningAnswerEmojiAnimated = isAnswerEmojiAnimated; + } + + /// + /// Attempts to deconstruct a message or partial message into a poll result message. + /// + /// The message object to attempt to deconstruct. + /// If possible, a C# representation of a poll result message. + /// A value indicating whether deconstruction was successful. + public static bool TryDeconstructPollResult(IPartialMessage message, out PollResultMessage result) + { + if (message.Type != DiscordMessageType.PollResult) + { + result = default; + return false; + } + + if (message.Embeds is not { HasValue: true, Value: [IEmbed embed] }) + { + result = default; + return false; + } + + if (embed.Type != "poll_result" || !embed.Fields.HasValue) + { + result = default; + return false; + } + + string questionText = "unknown"; + int answerVotes = 0; + int totalVotes = 0; + int? answerId = default; + string? answerText = default; + Snowflake? answerEmojiId = default; + string? answerEmojiName = default; + bool? isAnswerEmojiAnimated = default; + + // https://discord.com/developers/docs/resources/message#embed-fields-by-embed-type-poll-result-embed-fields + // i asked the devil whether they understood what the thought process here was, they said no. + foreach (IEmbedField field in embed.Fields.Value) + { + switch (field.Name) + { + case "poll_question_text": + questionText = field.Value; + break; + + case "victor_answer_votes": + + if (!int.TryParse(field.Value, CultureInfo.InvariantCulture, out answerVotes)) + { + result = default; + return false; + } + + break; + + case "total_votes": + + if (!int.TryParse(field.Value, CultureInfo.InvariantCulture, out totalVotes)) + { + result = default; + return false; + } + + break; + + case "victor_answer_id": + + if (!int.TryParse(field.Value, CultureInfo.InvariantCulture, out int id)) + { + result = default; + return false; + } + + answerId = id; + + break; + + case "victor_answer_text": + answerText = field.Value; + break; + + case "victor_answer_emoji_id": + + if (!Snowflake.TryParse(field.Value, CultureInfo.InvariantCulture, out Snowflake emojiId)) + { + result = default; + return false; + } + + answerEmojiId = emojiId; + + break; + + case "victor_answer_emoji_name": + answerEmojiName = field.Value; + break; + + case "victor_answer_emoji_animated": + + if (!bool.TryParse(field.Value, out bool isAnimated)) + { + result = default; + return false; + } + + isAnswerEmojiAnimated = isAnimated; + + break; + + default: + continue; + } + } + + result = new + ( + questionText, + answerVotes, + totalVotes, + answerId, + answerText, + answerEmojiId, + answerEmojiName, + isAnswerEmojiAnimated + ); + + return true; + } + + /// + /// The text of the original poll question. + /// + public string PollQuestionText { get; } + + /// + /// The amount of votes cast for the winning answer. + /// + public int WinningAnswerVoteCount { get; } + + /// + /// The amounts of votes cast in total, across all answers. + /// + public int TotalVotes { get; } + + /// + /// The of the winning answer. + /// + public int? WinningAnswerId { get; } + + /// + /// The text of the winning answer. + /// + public string? WinningAnswerText { get; } + + /// + /// The emoji ID associated with the winning answer. + /// + public Snowflake? WinningAnswerEmojiId { get; } + + /// + /// The emoji name associated with the winning answer. + /// + public string? WinningAnswerEmojiName { get; } + + /// + /// Indicates whether the winning answer emoji is animated, if applicable. + /// + public bool? IsWinningAnswerEmojiAnimated { get; } +} diff --git a/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/RecurrenceRule.cs b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/RecurrenceRule.cs new file mode 100644 index 0000000000..f5abe2b12c --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.Internal.Toolbox/RecurrenceRule.cs @@ -0,0 +1,189 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; + +using DSharpPlus.Entities; +using DSharpPlus.Extensions.Internal.Toolbox.Implementations; +using DSharpPlus.Internal.Abstractions.Models; + +namespace DSharpPlus.Extensions.Internal.Toolbox; + +/// +/// Provides utilities for writing recurrence rules. +/// +public static class RecurrenceRule +{ + /// + public static IScheduledEventRecurrenceRule Yearly(DateOnly date) + => Yearly(date, DateTimeOffset.UtcNow); + + /// + /// Creates a rule to recur every year on the specified date. + /// + /// The date to recur on. will be ignored. + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule Yearly(DateOnly date, DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Yearly, + ByMonth = [(DiscordScheduledEventRecurrenceMonth)date.Month], + ByMonthDay = [date.Day], + Interval = 1 + }; + } + + /// + public static IScheduledEventRecurrenceRule Monthly(int week, DayOfWeek day) + => Monthly(week, day, DateTimeOffset.UtcNow); + + /// + /// Creates a rule to recur every month on the specified day. + /// + /// Specifies the week within the month to recur in. + /// Specifies the day within the week to recur on. + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule Monthly(int week, DayOfWeek day, DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Monthly, + ByNWeekday = + [ + new BuiltScheduledEventRecurrenceDay + { + Day = ToMondayWeek(day), + N = week + } + ], + Interval = 1 + }; + } + + /// + public static IScheduledEventRecurrenceRule Weekly(DayOfWeek day) + => Weekly(day, DateTimeOffset.UtcNow); + + /// + /// Creates a new recurrence rule to recur every week on the specified weekday. + /// + /// The weekday to recur on. + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule Weekly(DayOfWeek day, DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Weekly, + ByWeekday = [ToMondayWeek(day)], + Interval = 1 + }; + } + + /// + public static IScheduledEventRecurrenceRule BiWeekly(DayOfWeek day) + => BiWeekly(day, DateTimeOffset.UtcNow); + + /// + /// Creates a new recurrence rule to recur every other week on the specified weekday. + /// + /// The weekday to recur on. + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule BiWeekly(DayOfWeek day, DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Weekly, + ByWeekday = [ToMondayWeek(day)], + Interval = 2 + }; + } + + /// + public static IScheduledEventRecurrenceRule Daily() + => Daily(DateTimeOffset.UtcNow); + + /// + /// Creates a new recurrence rule to recur every day. + /// + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule Daily(DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Daily, + Interval = 1 + }; + } + + /// + public static IScheduledEventRecurrenceRule OnWorkingDays() + => OnWorkingDays(DateTimeOffset.UtcNow); + + /// + /// Creates a new recurrence rule to recur every working day, monday to friday. + /// + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule OnWorkingDays(DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Daily, + ByWeekday = + [ + DiscordScheduledEventRecurrenceWeekday.Monday, + DiscordScheduledEventRecurrenceWeekday.Tuesday, + DiscordScheduledEventRecurrenceWeekday.Wednesday, + DiscordScheduledEventRecurrenceWeekday.Thursday, + DiscordScheduledEventRecurrenceWeekday.Friday + ], + Interval = 1 + }; + } + + /// + public static IScheduledEventRecurrenceRule OnWeekends() + => OnWeekends(DateTimeOffset.UtcNow); + + /// + /// Creates a new recurrence rule to recur every weekend, on saturday and sunday. + /// + /// The starting date of the recurring event. + public static IScheduledEventRecurrenceRule OnWeekends(DateTimeOffset startDate) + { + return new BuiltScheduledEventRecurrenceRule + { + Start = startDate, + Frequency = DiscordScheduledEventRecurrenceFrequency.Daily, + ByWeekday = + [ + DiscordScheduledEventRecurrenceWeekday.Saturday, + DiscordScheduledEventRecurrenceWeekday.Sunday + ], + Interval = 1 + }; + } + + // the enum layout of DayOfWeek and DiscordScheduledEventRecurrenceWeekday isn't the same. + private static DiscordScheduledEventRecurrenceWeekday ToMondayWeek(DayOfWeek day) + { + return day switch + { + DayOfWeek.Monday => DiscordScheduledEventRecurrenceWeekday.Monday, + DayOfWeek.Tuesday => DiscordScheduledEventRecurrenceWeekday.Tuesday, + DayOfWeek.Wednesday => DiscordScheduledEventRecurrenceWeekday.Wednesday, + DayOfWeek.Thursday => DiscordScheduledEventRecurrenceWeekday.Thursday, + DayOfWeek.Friday => DiscordScheduledEventRecurrenceWeekday.Friday, + DayOfWeek.Saturday => DiscordScheduledEventRecurrenceWeekday.Saturday, + DayOfWeek.Sunday => DiscordScheduledEventRecurrenceWeekday.Sunday, + _ => throw new NotImplementedException("There are seven weekdays.") + }; + } +} diff --git a/src/extensions/DSharpPlus.Extensions.sln b/src/extensions/DSharpPlus.Extensions.sln new file mode 100644 index 0000000000..cf259e04db --- /dev/null +++ b/src/extensions/DSharpPlus.Extensions.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSharpPlus.Extensions.Internal.Toolbox", "DSharpPlus.Extensions.Internal.Toolbox\DSharpPlus.Extensions.Internal.Toolbox.csproj", "{40CC040B-F757-42AA-8BC8-44F6EA6DCE7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSharpPlus.Extensions.Internal.BadRequestHelper", "DSharpPlus.Extensions.Internal.BadRequestHelper\DSharpPlus.Extensions.Internal.BadRequestHelper.csproj", "{F513621F-3E05-4028-96EF-D62F63736614}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {40CC040B-F757-42AA-8BC8-44F6EA6DCE7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40CC040B-F757-42AA-8BC8-44F6EA6DCE7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40CC040B-F757-42AA-8BC8-44F6EA6DCE7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40CC040B-F757-42AA-8BC8-44F6EA6DCE7E}.Release|Any CPU.Build.0 = Release|Any CPU + {F513621F-3E05-4028-96EF-D62F63736614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F513621F-3E05-4028-96EF-D62F63736614}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F513621F-3E05-4028-96EF-D62F63736614}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F513621F-3E05-4028-96EF-D62F63736614}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/Converters/ApplicationIntegrationTypeKeyTests.cs b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/ApplicationIntegrationTypeKeyTests.cs new file mode 100644 index 0000000000..9f9dc7ce3a --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/ApplicationIntegrationTypeKeyTests.cs @@ -0,0 +1,100 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Models.Serialization.Converters; + +namespace DSharpPlus.Internal.Models.Tests.Converters; + +public class ApplicationIntegrationTypeKeyTests +{ + private static ReadOnlySpan ValidPayload => + """ + { + "0": 2748, + "1": 28 + } + """u8; + + private static ReadOnlySpan InvalidFloatPayload => + """ + { + "0": 237, + "1.1": 47 + } + """u8; + + private static ReadOnlySpan InvalidStringPayload => + """ + { + "1": 83, + "fail": 23 + } + """u8; + + private readonly JsonSerializerOptions options; + + public ApplicationIntegrationTypeKeyTests() + { + this.options = new(); + this.options.Converters.Add(new ApplicationIntegrationTypeKeyConverter()); + } + + [Test] + public async Task TestSuccess() + { + Dictionary value = + JsonSerializer.Deserialize>(ValidPayload, this.options)!; + + using (Assert.Multiple()) + { + + await Assert.That(value.Count).IsEqualTo(2); + await Assert.That(value[DiscordApplicationIntegrationType.UserInstall]).IsEqualTo(28); + } + } + + [Test] + public async Task TestFloatFailure() + { + try + { + _ = JsonSerializer.Deserialize>(InvalidFloatPayload, this.options); + Assert.Fail("This should not have been reached."); + } + catch (JsonException exception) + { + await Assert.That(exception.Message).IsEqualTo("Expected an integer key."); + } + catch + { + Assert.Fail("Wrong exception type thrown."); + throw; + } + } + + [Test] + public async Task TestStringFailure() + { + try + { + _ = JsonSerializer.Deserialize>(InvalidStringPayload, this.options); + Assert.Fail("This should not have been reached."); + } + catch (JsonException exception) + { + await Assert.That(exception.Message).IsEqualTo("Expected an integer key."); + } + catch + { + Assert.Fail("Wrong exception type thrown."); + throw; + } + } +} diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/Converters/AuditLogChangeTests.cs b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/AuditLogChangeTests.cs new file mode 100644 index 0000000000..52b8db1474 --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/AuditLogChangeTests.cs @@ -0,0 +1,134 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using System; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Models.Extensions; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Internal.Models.Tests.Converters; + +public class AuditLogChangeTests +{ + private readonly SerializationService serializer; + + public AuditLogChangeTests() + { + IServiceCollection services = new ServiceCollection(); + services.RegisterDiscordModelSerialization(); + services.Configure + ( + options => options.SetFormat() + ); + + services.AddLogging + ( + builder => builder.ClearProviders().AddProvider(NullLoggerProvider.Instance) + ); + + services.AddSingleton(typeof(SerializationService<>)); + services.AddSingleton(); + + IServiceProvider provider = services.BuildServiceProvider(); + this.serializer = provider.GetRequiredService>(); + } + + private static readonly byte[] IntPayload = + """ + { + "key": "$test", + "new_value": 17, + "old_value": 83 + } + """u8.ToArray(); + + private static readonly byte[] StringPayload = + """ + { + "key": "$test", + "new_value": "this is the new value", + "old_value": "this was the old value" + } + """u8.ToArray(); + + private static readonly byte[] NewValueMissingPayload = + """ + { + "key": "$test", + "old_value": "this was the old value" + } + """u8.ToArray(); + + private static readonly byte[] OldValueMissingPayload = + """ + { + "key": "$test", + "new_value": "this is the new value" + } + """u8.ToArray(); + + [Test] + public async Task TestIntegerPayload() + { + IAuditLogChange change = this.serializer.DeserializeModel(IntPayload); + + using (Assert.Multiple()) + { + await Assert.That(change.NewValue.HasValue).IsTrue(); + await Assert.That(change.OldValue.HasValue).IsTrue(); + + await Assert.That(change.NewValue.Value).IsEqualTo("17"); + } + } + + [Test] + public async Task TestStringPayload() + { + IAuditLogChange change = this.serializer.DeserializeModel(StringPayload); + + using (Assert.Multiple()) + { + await Assert.That(change.NewValue.HasValue).IsTrue(); + await Assert.That(change.OldValue.HasValue).IsTrue(); + + await Assert.That(change.OldValue.Value).IsEqualTo("\"this was the old value\""); + } + } + + [Test] + public async Task TestNewValueMissing() + { + IAuditLogChange change = this.serializer.DeserializeModel(NewValueMissingPayload); + + using (Assert.Multiple()) + { + await Assert.That(change.NewValue.HasValue).IsFalse(); + await Assert.That(change.OldValue.HasValue).IsTrue(); + + await Assert.That(change.OldValue.Value).IsEqualTo("\"this was the old value\""); + } + } + + [Test] + public async Task TestOldValueMissing() + { + IAuditLogChange change = this.serializer.DeserializeModel(OldValueMissingPayload); + + using (Assert.Multiple()) + { + await Assert.That(change.NewValue.HasValue).IsTrue(); + await Assert.That(change.OldValue.HasValue).IsFalse(); + + await Assert.That(change.NewValue.Value).IsEqualTo("\"this is the new value\""); + } + } +} diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/Converters/ComponentTests.cs b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/ComponentTests.cs new file mode 100644 index 0000000000..e90165efab --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/ComponentTests.cs @@ -0,0 +1,116 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using System; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Models.Extensions; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Internal.Models.Tests.Converters; + +public class ComponentTests +{ + private readonly SerializationService serializer; + + public ComponentTests() + { + IServiceCollection services = new ServiceCollection(); + services.RegisterDiscordModelSerialization(); + services.Configure + ( + options => options.SetFormat() + ); + + services.AddLogging + ( + builder => builder.ClearProviders().AddProvider(NullLoggerProvider.Instance) + ); + + services.AddSingleton(typeof(SerializationService<>)); + services.AddSingleton(); + + IServiceProvider provider = services.BuildServiceProvider(); + this.serializer = provider.GetRequiredService>(); + } + + private static ReadOnlySpan ActionRowTestPayload => + """ + { + "id": 0, + "content": "This is a message with components", + "components": [ + { + "type": 1, + "components": [ + { + "type": 2, + "label": "Click me!", + "style": 1, + "custom_id": "click_one" + } + ] + } + ] + } + """u8; + + private static ReadOnlySpan OtherTopLevelComponentTestPayload => + """ + { + "id": 0, + "content": "This is a message with weird components!", + "components": [ + { + "type": 1, + "components": [ + { + "type": 2, + "label": "Don't click me!", + "style": 1, + "custom_id": "click_one" + } + ] + }, + { + "type": 2784, + "random_ignored_data": "ekh" + } + ] + } + """u8; + + [Test] + public async Task TestCorrectActionRowDeserialization() + { + IPartialMessage message = this.serializer.DeserializeModel(ActionRowTestPayload); + + using (Assert.Multiple()) + { + await Assert.That(message.Components.Value).HasSingleItem(); + await Assert.That(message.Components.Value[0]).IsAssignableTo(); + await Assert.That(((IActionRowComponent)message.Components.Value[0]).Components).HasSingleItem(); + } + } + + [Test] + public async Task TestCorrectUnknownComponentDeserialization() + { + IPartialMessage message = this.serializer.DeserializeModel(OtherTopLevelComponentTestPayload); + + using (Assert.Multiple()) + { + await Assert.That(message.Components.Value.Count).IsEqualTo(2); + await Assert.That(message.Components.Value[0]).IsAssignableTo(); + await Assert.That(message.Components.Value[1]).IsAssignableTo(); + } + } +} diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/Converters/DiscordPermissionTests.cs b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/DiscordPermissionTests.cs new file mode 100644 index 0000000000..f34f6ad5ff --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/DiscordPermissionTests.cs @@ -0,0 +1,33 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using System.Text.Json; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Internal.Models.Serialization.Converters; + +namespace DSharpPlus.Internal.Models.Tests.Converters; + +public class DiscordPermissionTests +{ + private readonly JsonSerializerOptions options; + + public DiscordPermissionTests() + { + this.options = new(); + this.options.Converters.Add(new DiscordPermissionConverter()); + } + + [Test] + public async Task DeserializePermissions() + { + DiscordPermissions expected = new(DiscordPermission.PrioritySpeaker); + DiscordPermissions actual = JsonSerializer.Deserialize("256", this.options); + + await Assert.That(actual).IsEqualTo(expected); + } +} diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/Converters/OneOfConverterTests.Models.cs b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/OneOfConverterTests.Models.cs new file mode 100644 index 0000000000..136fb95cb1 --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/OneOfConverterTests.Models.cs @@ -0,0 +1,91 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using System; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Abstractions.Models; +using DSharpPlus.Internal.Models.Extensions; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Internal.Models.Tests.Converters; + +// tests one-of deserialization on example payloads for actual models +partial class OneOfConverterTests +{ + private readonly SerializationService serializer; + + public OneOfConverterTests() + { + IServiceCollection services = new ServiceCollection(); + services.RegisterDiscordModelSerialization(); + services.Configure + ( + options => options.SetFormat() + ); + + services.AddLogging + ( + builder => builder.ClearProviders().AddProvider(NullLoggerProvider.Instance) + ); + + services.AddSingleton(typeof(SerializationService<>)); + services.AddSingleton(); + + IServiceProvider provider = services.BuildServiceProvider(); + this.serializer = provider.GetRequiredService>(); + } + + [Test] + public async Task TestApplicationCommandInteractionDataOptionUnion() + { + IApplicationCommandInteractionDataOption value = this.serializer.DeserializeModel + ( + """ + { + "name": "testificate", + "type": 1, + "options": [ + { + "name": "example", + "type": 10, + "value": 17 + } + ] + } + """u8 + ); + + await Assert.That(value.Options.Value![0].Value.Value).IsEqualTo(17); + } + + [Test] + public async Task TestInteractionResponseUnion() + { + IInteractionResponse response = this.serializer.DeserializeModel + ( + """ + { + "type": 8, + "data": { + "choices": [ + { + "name": "deimos", + "value": "thingie" + } + ] + } + } + """u8 + ); + + await Assert.That(response.Data.Value.AsT0.Choices[0].Value).IsEqualTo("thingie"); + } +} diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/Converters/OneOfConverterTests.Precedence.cs b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/OneOfConverterTests.Precedence.cs new file mode 100644 index 0000000000..d9d0c41740 --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/Converters/OneOfConverterTests.Precedence.cs @@ -0,0 +1,92 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Text.Json; +using System.Threading.Tasks; + +using DSharpPlus.Internal.Models.Serialization.Converters; + +using OneOf; + +namespace DSharpPlus.Internal.Models.Tests.Converters; + +// here we test whether we handle precedence correctly, with intentionally annoying unions +public partial class OneOfConverterTests +{ + private static ReadOnlySpan SnowflakeIntegerPayload => "983987834938"u8; + private static ReadOnlySpan SnowflakeStringPayload => "\"737837872\""u8; + private static ReadOnlySpan IntegerFloatPayload => "887673"u8; + + [Test] + public async Task TestSnowflakeLongPrecedence() + { + JsonSerializerOptions options = new(); + options.Converters.Add(new OneOfConverterFactory()); + options.Converters.Add(new SnowflakeConverter()); + + OneOf union = JsonSerializer.Deserialize> + ( + SnowflakeIntegerPayload, + options + ); + + await Assert.That(union.IsT0).IsTrue(); + + OneOf otherUnion = JsonSerializer.Deserialize> + ( + SnowflakeIntegerPayload, + options + ); + + await Assert.That(otherUnion.IsT1).IsTrue(); + } + + [Test] + public async Task TestSnowflakeStringPrecedence() + { + JsonSerializerOptions options = new(); + options.Converters.Add(new OneOfConverterFactory()); + options.Converters.Add(new SnowflakeConverter()); + + OneOf union = JsonSerializer.Deserialize> + ( + SnowflakeStringPayload, + options + ); + + await Assert.That(union.IsT0).IsTrue(); + + OneOf otherUnion = JsonSerializer.Deserialize> + ( + SnowflakeStringPayload, + options + ); + + await Assert.That(otherUnion.IsT1).IsTrue(); + } + + [Test] + public async Task TestIntegerFloatPrecedence() + { + JsonSerializerOptions options = new(); + options.Converters.Add(new OneOfConverterFactory()); + + OneOf union = JsonSerializer.Deserialize> + ( + IntegerFloatPayload, + options + ); + + await Assert.That(union.IsT0).IsTrue(); + + OneOf otherUnion = JsonSerializer.Deserialize> + ( + IntegerFloatPayload, + options + ); + + await Assert.That(otherUnion.IsT1).IsTrue(); + } +} diff --git a/tests/core/DSharpPlus.Internal.Models.Tests/DSharpPlus.Internal.Models.Tests.csproj b/tests/core/DSharpPlus.Internal.Models.Tests/DSharpPlus.Internal.Models.Tests.csproj new file mode 100644 index 0000000000..3e987fbde0 --- /dev/null +++ b/tests/core/DSharpPlus.Internal.Models.Tests/DSharpPlus.Internal.Models.Tests.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + + false + true + $(NoWarn);CA1515;CA1822;IDE0058;IDE1006 + + + + + + + + + + + + + diff --git a/tests/core/DSharpPlus.Shared.Tests/DSharpPlus.Shared.Tests.csproj b/tests/core/DSharpPlus.Shared.Tests/DSharpPlus.Shared.Tests.csproj new file mode 100644 index 0000000000..8fff45208a --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/DSharpPlus.Shared.Tests.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + true + + false + true + $(NoWarn);CA1515;CA1707;CA1822;IDE0058;IDE1006 + + + + + + + + + + diff --git a/tests/core/DSharpPlus.Shared.Tests/Permissions/BoundTests.cs b/tests/core/DSharpPlus.Shared.Tests/Permissions/BoundTests.cs new file mode 100644 index 0000000000..f5405fce66 --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/Permissions/BoundTests.cs @@ -0,0 +1,135 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// if you find youself here, my condolences. +// here we test whether we read or write out of bounds at any point. currently, these tests work only on windows +// (because i was too lazy to write allocation code for linux, this just uses terrafx on windows), and they're not very +// pleasant to run, but avoiding out of bounds memory accesses is probably worth it. +// also, yes, i am aware this leaks memory. for some reason beyond my comprehension, VirtualFree fails with the error message +// "success". and since this is a test type that will be torn down soon enough, and it's only 12kb, it's not the end of the +// world to leak this here. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; + +using DSharpPlus.Entities; + +using TerraFX.Interop.Windows; + +namespace DSharpPlus.Shared.Tests.Permissions; + +[SupportedOSPlatform("windows")] +public sealed unsafe class BoundTests +{ + private readonly byte* usableAllocation; + + public BoundTests() + { + nuint zero = nuint.Zero; + uint _; + + // allocate three pages, immediately commit them and mark them as NO_ACCESS + void* allocHandle = Windows.VirtualAlloc + ( + lpAddress: (void*)zero, + dwSize: (nuint)(3 * Environment.SystemPageSize), + flAllocationType: 0x3000, // RESERVE and COMMIT + flProtect: 0x1 // NO_ACCESS + ); + + if (allocHandle == null || allocHandle == (void*)nuint.Zero) + { + throw new InvalidOperationException($"Failed to allocate three sequential pages. Handle: {(nuint)allocHandle}"); + } + + // we allocated successfully, now mark the middle page as READWRITE + + BOOL success = Windows.VirtualProtect + ( + lpAddress: (byte*)allocHandle + Environment.SystemPageSize, + dwSize: (nuint)Environment.SystemPageSize, + flNewProtect: 0x4, // READWRITE + lpflOldProtect: &_ + ); + + if (success != BOOL.TRUE) + { + throw new InvalidOperationException("Failed to patch protection status of the middle page."); + } + + // everything is successful, store the pointers for use in tests + + this.usableAllocation = (byte*)allocHandle + Environment.SystemPageSize; + } + + private static int GetLength() => 16; + + private ref DiscordPermissions AllocateStart() + => ref Unsafe.As(ref Unsafe.AsRef(this.usableAllocation)); + + private ref DiscordPermissions AllocateEnd() + { + return ref Unsafe.As + ( + ref Unsafe.AsRef(this.usableAllocation + Environment.SystemPageSize - GetLength()) + ); + } + + [Test] + [Explicit] + public void VectorOpsInBounds_MemoryRegionStart() + { + try + { + // this includes the OR operator + scoped ref DiscordPermissions permissionsAdd = ref this.AllocateStart(); + permissionsAdd += (DiscordPermission)117; + + scoped ref DiscordPermissions permissionsRemove = ref this.AllocateStart(); + permissionsRemove -= (DiscordPermission)117; + + scoped ref DiscordPermissions permissionsNegate = ref this.AllocateStart(); + permissionsNegate = ~permissionsNegate; + + scoped ref DiscordPermissions permissionsAnd = ref this.AllocateStart(); + permissionsAnd &= DiscordPermissions.All; + + scoped ref DiscordPermissions permissionsXor = ref this.AllocateStart(); + permissionsXor ^= DiscordPermissions.All; + } + catch (AccessViolationException) + { + Assert.Fail("Access violation thrown, a starting boundary has been violated."); + } + } + + [Test] + [Explicit] + public void VectorOpsInBounds_MemoryRegionEnd() + { + try + { + // this includes the OR operator + scoped ref DiscordPermissions permissionsAdd = ref this.AllocateEnd(); + permissionsAdd += (DiscordPermission)117; + + scoped ref DiscordPermissions permissionsRemove = ref this.AllocateEnd(); + permissionsRemove -= (DiscordPermission)117; + + scoped ref DiscordPermissions permissionsNegate = ref this.AllocateEnd(); + permissionsNegate = ~permissionsNegate; + + scoped ref DiscordPermissions permissionsAnd = ref this.AllocateEnd(); + permissionsAnd &= DiscordPermissions.All; + + scoped ref DiscordPermissions permissionsXor = ref this.AllocateEnd(); + permissionsXor ^= DiscordPermissions.All; + } + catch (AccessViolationException) + { + Assert.Fail("Access violation thrown, an ending boundary has been violated."); + } + } +} diff --git a/tests/core/DSharpPlus.Shared.Tests/Permissions/ConstructionTests.cs b/tests/core/DSharpPlus.Shared.Tests/Permissions/ConstructionTests.cs new file mode 100644 index 0000000000..b85033d7bf --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/Permissions/ConstructionTests.cs @@ -0,0 +1,118 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Numerics; +using System.Threading.Tasks; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Shared.Tests.Permissions; + +public class ConstructionTests +{ + private static ReadOnlySpan FirstBit => + [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]; + + private static ReadOnlySpan FirstTwoBits => + [ + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]; + + private static ReadOnlySpan ThirtyThirdBit => + [ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]; + + [Test] + public async Task FirstBitSetCorrectly_Constructor() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + DiscordPermissions expected = new(FirstBit); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task FirstBitSetCorrectly_Add() + { + DiscordPermissions permissions = DiscordPermissions.None; + permissions.Add(DiscordPermission.CreateInvite); + DiscordPermissions expected = new(FirstBit); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task FirstTwoBitsSetCorrectly_Constructor() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite, DiscordPermission.KickMembers); + DiscordPermissions expected = new(FirstTwoBits); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task FirstTwoBitsSetCorrectly_Add() + { + DiscordPermissions permissions = DiscordPermissions.None; + permissions.Add(DiscordPermission.CreateInvite); + permissions.Add(DiscordPermission.KickMembers); + DiscordPermissions expected = new(FirstTwoBits); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task FirstTwoBitsSetCorrectly_AddMultiple() + { + DiscordPermissions permissions = DiscordPermissions.None; + permissions.Add(DiscordPermission.CreateInvite, DiscordPermission.KickMembers); + DiscordPermissions expected = new(FirstTwoBits); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task TestUnderlyingUInt32Rollover() + { + DiscordPermissions permissions = new(DiscordPermission.RequestToSpeak, DiscordPermission.CreateInvite); + DiscordPermissions expected = new(ThirtyThirdBit); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task FirstBitSetCorrectly_BigInteger() + { + BigInteger bigint = new(1); + DiscordPermissions permissions = new(bigint); + DiscordPermissions expected = new(FirstBit); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task ThirtyThirdBitSetCorrectly_BigInteger() + { + BigInteger bigint = new(4294967297); + DiscordPermissions permissions = new(bigint); + DiscordPermissions expected = new(ThirtyThirdBit); + + await Assert.That(expected).IsEqualTo(permissions); + } + + [Test] + public async Task OpImplicit() + { + DiscordPermissions expected = new(FirstBit); + + await Assert.That(expected).IsEqualTo(DiscordPermission.CreateInvite); + } +} diff --git a/tests/core/DSharpPlus.Shared.Tests/Permissions/EqualityTests.cs b/tests/core/DSharpPlus.Shared.Tests/Permissions/EqualityTests.cs new file mode 100644 index 0000000000..d23a786237 --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/Permissions/EqualityTests.cs @@ -0,0 +1,31 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading.Tasks; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Shared.Tests.Permissions; + +public class EqualityTests +{ + [Test] + public async Task EqualsCorrect_OneBit() + { + DiscordPermissions a = new(DiscordPermission.CreateInvite); + DiscordPermissions b = new(DiscordPermission.CreateInvite); + + // we explicitly don't want the default equals assertion here + await Assert.That(a.Equals(b)).IsTrue(); + } + + [Test] + public async Task EqualsCorrect_ThirtyThirdBit() + { + DiscordPermissions a = new(DiscordPermission.RequestToSpeak); + DiscordPermissions b = new(DiscordPermission.RequestToSpeak); + + await Assert.That(a.Equals(b)).IsTrue(); + } +} diff --git a/tests/core/DSharpPlus.Shared.Tests/Permissions/OperatorTests.cs b/tests/core/DSharpPlus.Shared.Tests/Permissions/OperatorTests.cs new file mode 100644 index 0000000000..284cf8e96b --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/Permissions/OperatorTests.cs @@ -0,0 +1,160 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Threading.Tasks; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Shared.Tests.Permissions; + +public class OperatorTests +{ + public static ReadOnlySpan AllButFirstBit => + [ + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + ]; + + [Test] + public async Task TestRemove_SingleBit() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + permissions.Remove(DiscordPermission.CreateInvite); + + await Assert.That(permissions).IsEqualTo(DiscordPermissions.None); + + permissions = new(DiscordPermission.CreateInvite); + await Assert.That(permissions - DiscordPermission.CreateInvite).IsEqualTo(DiscordPermissions.None); + } + + [Test] + public async Task TestRemove_Bulk() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite, DiscordPermission.BanMembers); + permissions.Remove([DiscordPermission.CreateInvite, DiscordPermission.BanMembers]); + + await Assert.That(permissions).IsEqualTo(DiscordPermissions.None); + } + + [Test] + public async Task TestRemove_Set() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite, DiscordPermission.BanMembers); + DiscordPermissions remove = new(DiscordPermission.CreateInvite); + + await Assert.That(permissions - remove).IsEqualTo(DiscordPermission.BanMembers); + } + + [Test] + public async Task TestOr_Flag() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + DiscordPermissions two = permissions | DiscordPermission.BanMembers; + + await Assert.That(two).IsEqualTo(new(DiscordPermission.CreateInvite, DiscordPermission.BanMembers)); + } + + [Test] + public async Task TestOr_Set() + { + DiscordPermissions one = new(DiscordPermission.AddReactions); + DiscordPermissions two = new(DiscordPermission.Administrator); + + DiscordPermissions actual = one | two; + + await Assert.That(actual).IsEqualTo(new(DiscordPermission.AddReactions, DiscordPermission.Administrator)); + } + + [Test] + public async Task TestAnd_Flag() + { + DiscordPermissions permissions = new(DiscordPermission.Administrator, DiscordPermission.Connect); + await Assert.That(permissions & DiscordPermission.Administrator).IsEqualTo(DiscordPermission.Administrator); + } + + [Test] + public async Task TestAnd_Set() + { + DiscordPermissions permissions = new + ( + DiscordPermission.Administrator, + DiscordPermission.ChangeNickname, + DiscordPermission.CreatePublicThreads, + DiscordPermission.Speak + ); + + DiscordPermissions mask = new + ( + DiscordPermission.Administrator, + DiscordPermission.CreateInvite + ); + + await Assert.That(permissions & mask).IsEqualTo(DiscordPermission.Administrator); + } + + [Test] + public async Task TestXor_Flag() + { + DiscordPermissions permissions = new(DiscordPermission.Administrator, DiscordPermission.Connect); + await Assert.That(permissions ^ DiscordPermission.Administrator).IsEqualTo(DiscordPermission.Connect); + } + + [Test] + public async Task TestXor_Set() + { + DiscordPermissions permissions = new + ( + DiscordPermission.Administrator, + DiscordPermission.ChangeNickname, + DiscordPermission.CreatePublicThreads, + DiscordPermission.Speak + ); + + DiscordPermissions mask = new + ( + DiscordPermission.Administrator, + DiscordPermission.CreateInvite + ); + + DiscordPermissions expected = new + ( + DiscordPermission.ChangeNickname, + DiscordPermission.CreatePublicThreads, + DiscordPermission.Speak, + DiscordPermission.CreateInvite + ); + + await Assert.That(permissions ^ mask).IsEqualTo(expected); + } + + [Test] + public async Task TestNot() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + DiscordPermissions expected = new(AllButFirstBit); + + await Assert.That(~permissions).IsEqualTo(expected); + } + + [Test] + public async Task TestCombine_NonOverlapping() + { + DiscordPermissions set1 = new(DiscordPermission.CreateInvite); + DiscordPermissions set2 = new(DiscordPermission.Connect); + DiscordPermissions expected = new(DiscordPermission.CreateInvite, DiscordPermission.Connect); + + await Assert.That(DiscordPermissions.Combine(set1, set2)).IsEqualTo(expected); + } + + [Test] + public async Task TestCombine_Overlapping() + { + DiscordPermissions set1 = new(DiscordPermission.CreateInvite, DiscordPermission.Connect); + DiscordPermissions set2 = new(DiscordPermission.Connect); + DiscordPermissions expected = new(DiscordPermission.CreateInvite, DiscordPermission.Connect); + + await Assert.That(DiscordPermissions.Combine(set1, set2)).IsEqualTo(expected); + } +} diff --git a/tests/core/DSharpPlus.Shared.Tests/Permissions/ToStringTests.cs b/tests/core/DSharpPlus.Shared.Tests/Permissions/ToStringTests.cs new file mode 100644 index 0000000000..ae72a1e1bb --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/Permissions/ToStringTests.cs @@ -0,0 +1,116 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Threading.Tasks; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Shared.Tests.Permissions; + +public class ToStringTests +{ + [Test] + public async Task TestFirstBit() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + await Assert.That(permissions.ToString()).IsEqualTo("1"); + } + + [Test] + public async Task TestByteOrder_SecondByte() + { + DiscordPermissions permissions = new(DiscordPermission.PrioritySpeaker); + await Assert.That(permissions.ToString()).IsEqualTo("256"); + } + + [Test] + public async Task TestInternalElementOrder_SecondElement() + { + DiscordPermissions permissions = new(DiscordPermission.RequestToSpeak); + await Assert.That(permissions.ToString()).IsEqualTo("4294967296"); + } + + [Test] + public async Task TestRawFirstBit() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + + await Assert.That(permissions.ToString("raw")).IsEqualTo + ( + "DiscordPermissions - raw value: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + ); + } + + [Test] + public async Task TestRawSecondElement() + { + DiscordPermissions permissions = new(DiscordPermission.RequestToSpeak); + + await Assert.That(permissions.ToString("raw")).IsEqualTo + ( + "DiscordPermissions - raw value: 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00" + ); + } + + [Test] + public async Task TestNameFirstElement() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite); + + await Assert.That(permissions.ToString("name")).IsEqualTo("Create Invites"); + } + + [Test] + public async Task TestNameOrder() + { + DiscordPermissions permissions = new(DiscordPermission.RequestToSpeak, DiscordPermission.CreateInvite); + + await Assert.That(permissions.ToString("name")).IsEqualTo("Create Invites, Request to Speak"); + } + + [Test] + public async Task TestUndefinedFlags() + { + DiscordPermissions permissions = new((DiscordPermission)48, (DiscordPermission)65, (DiscordPermission)97, (DiscordPermission)127); + + await Assert.That(permissions.ToString("name")).IsEqualTo("48, 65, 97, 127"); + } + + // when updating this test, try to find holes to use for this + [Test] + public async Task TestNameOrderUndefinedFlags() + { + DiscordPermissions permissions = new(DiscordPermission.ReadMessageHistory, (DiscordPermission)48, DiscordPermission.UseExternalApps); + + await Assert.That(permissions.ToString("name")).IsEqualTo("Read Message History, 48, Use External Apps"); + } + + [Test] + public async Task TestCustomFormatThrowsIfMalformed() + { + DiscordPermissions permissions = new(DiscordPermission.ReadMessageHistory, DiscordPermission.UseExternalApps); + + await Assert.ThrowsAsync(() => + { + _ = permissions.ToString("name:"); + return Task.CompletedTask; + }); + + await Assert.ThrowsAsync(() => + { + _ = permissions.ToString("name:with a format but without the permission marker"); + return Task.CompletedTask; + }); + } + + [Test] + public async Task TestCustomFormat() + { + DiscordPermissions permissions = new(DiscordPermission.ReadMessageHistory, DiscordPermission.UseExternalApps); + string expected = " - Read Message History\r\n - Use External Apps\r\n"; + + await Assert.That(permissions.ToString("name: - {permission}\r\n")).IsEqualTo(expected); + } +} diff --git a/tests/core/DSharpPlus.Shared.Tests/Permissions/UtilityTests.cs b/tests/core/DSharpPlus.Shared.Tests/Permissions/UtilityTests.cs new file mode 100644 index 0000000000..a65c518ef9 --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/Permissions/UtilityTests.cs @@ -0,0 +1,63 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading.Tasks; + +using DSharpPlus.Entities; + +namespace DSharpPlus.Shared.Tests.Permissions; + +public class UtilityTests +{ + [Test] + public async Task HasPermissionRespectsAdministrator() + { + DiscordPermissions permissions = new(DiscordPermission.Administrator, DiscordPermission.AddReactions); + await Assert.That(permissions.HasPermission(DiscordPermission.BanMembers)).IsTrue(); + + permissions = new(DiscordPermission.AddReactions); + await Assert.That(permissions.HasPermission(DiscordPermission.BanMembers)).IsFalse(); + } + + [Test] + public async Task HasAnyPermissionAlwaysSucceedsWithAdministrator() + { + DiscordPermissions permissions = new(DiscordPermission.Administrator, DiscordPermission.AddReactions); + await Assert.That(permissions.HasAnyPermission([DiscordPermission.BanMembers, DiscordPermission.CreateInvite])).IsTrue(); + } + + [Test] + public async Task HasAnyPermissionWithoutAdministrator() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite, DiscordPermission.AddReactions); + + using (Assert.Multiple()) + { + await Assert.That(permissions.HasAnyPermission([DiscordPermission.BanMembers, DiscordPermission.CreateInvite])).IsTrue(); + await Assert.That(permissions.HasAnyPermission([DiscordPermission.AttachFiles, DiscordPermission.Connect])).IsFalse(); + } + } + + [Test] + public async Task HasAllPermissionsAlwaysSucceedsWithAdministrator() + { + DiscordPermissions permissions = new(DiscordPermission.Administrator); + await Assert.That(permissions.HasAllPermissions([DiscordPermission.CreatePrivateThreads, DiscordPermission.KickMembers])).IsTrue(); + } + + [Test] + public async Task HasAllPermissionsWithoutAdministrator() + { + DiscordPermissions permissions = new(DiscordPermission.CreateInvite, DiscordPermission.AddReactions); + DiscordPermissions testificate = [DiscordPermission.CreateInvite, DiscordPermission.Connect]; + permissions += DiscordPermission.ManageGuild; + + using (Assert.Multiple()) + { + await Assert.That(permissions.HasAllPermissions([DiscordPermission.ManageGuild, DiscordPermission.CreateInvite])).IsTrue(); + await Assert.That(permissions.HasAllPermissions([DiscordPermission.CreateInvite, DiscordPermission.Connect])).IsFalse(); + await Assert.That(testificate.HasPermission(DiscordPermission.Connect)).IsTrue(); + } + } +} diff --git a/tests/core/DSharpPlus.Shared.Tests/TextWriters/IndentedArrayPoolUtf16.cs b/tests/core/DSharpPlus.Shared.Tests/TextWriters/IndentedArrayPoolUtf16.cs new file mode 100644 index 0000000000..edbfdcb941 --- /dev/null +++ b/tests/core/DSharpPlus.Shared.Tests/TextWriters/IndentedArrayPoolUtf16.cs @@ -0,0 +1,77 @@ +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System.Threading.Tasks; + +using DSharpPlus.RuntimeServices.TextWriters; + +namespace DSharpPlus.Shared.Tests.TextWriters; + +public class IndentedArrayPoolUtf16 +{ + [Test] + public async Task TestIndentationIncrementDecrement() + { + using IndentedArrayPoolUtf16TextWriter writer = new(); + + writer.WriteLine("{"); + writer.IncreaseIndentation(); + writer.WriteLine(); + writer.DecreaseIndentation(); + writer.WriteLine("}"); + + await Assert.That(writer.ToString().Trim(' ', '\r', '\n')).IsEqualTo + ( + """ + { + + } + """ + ); + } + + [Test] + public async Task TestIndentingWrite() + { + using IndentedArrayPoolUtf16TextWriter writer = new(); + + writer.WriteLine("{"); + writer.IncreaseIndentation(); + writer.WriteLine("\"stuff\":\r\n1", false); + writer.DecreaseIndentation(); + writer.WriteLine("}"); + + await Assert.That(writer.ToString().Trim(' ', '\r', '\n')).IsEqualTo + ( + """ + { + "stuff": + 1 + } + """ + ); + } + + [Test] + public async Task TestLiteralPreservingWrite() + { + using IndentedArrayPoolUtf16TextWriter writer = new(); + + writer.WriteLine("{"); + writer.IncreaseIndentation(); + writer.WriteLine("\"stuff\":\r\n1", true); + writer.DecreaseIndentation(); + writer.WriteLine("}"); + + await Assert.That(writer.ToString().Trim(' ', '\r', '\n')).IsEqualTo + ( + """ + { + "stuff": + 1 + } + """ + ); + } +} diff --git a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/DSharpPlus.Tools.CodeBlockLanguageListGen.csproj b/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/DSharpPlus.Tools.CodeBlockLanguageListGen.csproj deleted file mode 100644 index a914b65d95..0000000000 --- a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/DSharpPlus.Tools.CodeBlockLanguageListGen.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - Exe - - - - - - \ No newline at end of file diff --git a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/FromCodeAttribute.LanguageList.template b/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/FromCodeAttribute.LanguageList.template deleted file mode 100644 index 7885add0f0..0000000000 --- a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/FromCodeAttribute.LanguageList.template +++ /dev/null @@ -1,16 +0,0 @@ -// -// Last modified on {{Date}} - -using System; -using System.Collections.Frozen; -using System.Collections.Generic; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -partial class FromCodeAttribute : Attribute -{ - /// - /// A list of default languages from Highlight.Js that Discord uses for their codeblocks. - /// - public static readonly FrozenSet CodeBlockLanguages = {{CodeBlockLanguages}}; -} diff --git a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs b/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs deleted file mode 100644 index dd78f4be81..0000000000 --- a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs +++ /dev/null @@ -1,60 +0,0 @@ -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(); - } -} diff --git a/tools/generators/copy-concrete-implementations.csx b/tools/generators/copy-concrete-implementations.csx new file mode 100644 index 0000000000..52cff4f7d6 --- /dev/null +++ b/tools/generators/copy-concrete-implementations.csx @@ -0,0 +1,147 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:Spectre.Console, 0.47.1-preview.0.26" + +#load "../incremental-utility.csx" + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +using Spectre.Console; + +// Syntax: +// copy-concrete-implementations [path to meta file] +if (Args is ["-h" or "--help" or "-?"]) +{ + AnsiConsole.MarkupLine + ( + """ + [plum1]DSharpPlus Concrete Implementation Theft, v0.2.0[/] + + Usage: copy-concrete-implementations.csx [[path to meta file]] + Extracts the required concrete types for DSharpPlus.Extensions.Internal.Toolbox to remove dependency on concrete implementations. + + """ + ); + + return 0; +} + +AnsiConsole.MarkupLine +( + """ + [plum1]DSharpPlus Concrete Implementation Theft, v0.2.0[/] + """ +); + +string meta; +string basePath = "./src/extensions/DSharpPlus.Extensions.Internal.Toolbox/Implementations/"; + +if (Args.Count == 0) +{ + meta = "./meta/toolbox-concrete-types.json"; +} + +// there are args passed, which override the given instructions +// validate the passed arguments are correct +else if (Args.Any(path => !Directory.Exists(path))) +{ + AnsiConsole.MarkupLine + ( + """ + [red]The provided path could not be found on the file system.[/] + """ + ); + + return 1; +} + +// all args are fine +else +{ + meta = Args[0]; +} + +string[] files = JsonSerializer.Deserialize(File.ReadAllText(meta))!; + +Changes changes = GetFileChanges("copy-concrete-implementations", files); + +if (!changes.Added.Any() && !changes.Modified.Any() && !changes.Removed.Any()) +{ + AnsiConsole.MarkupLine + ( + """ + [darkseagreen1_1]There were no changes to the interface definitions, exiting copy-concrete-implementations.[/] + """ + ); + + return 0; +} + +if (changes.Removed.Any()) +{ + AnsiConsole.MarkupLine + ( + """ + [red]Some concrete types were deleted. Please update the meta file and the corresponding builder code, if applicable:[/] + """ + ); + + foreach (string removed in changes.Removed) + { + Console.WriteLine($" {removed}"); + } + + return 1; +} + +if (changes.Added.Any() || changes.Modified.Any()) +{ + AnsiConsole.MarkupLine("Extracting added and modified objects..."); +} + +IEnumerable toExtract = changes.Added.Concat(changes.Modified); + +bool success = toExtract.AsParallel() + .All + ( + path => + { + Console.WriteLine($" Extracting {path}"); + + try + { + string text = File.ReadAllText(path); + + int typeIndex = text.IndexOf("record"); + + text = text.Insert(typeIndex + 7, "Built"); + + int namespaceIndex = text.IndexOf("namespace"); + int endOfNamespaceLine = text.IndexOf('\n', namespaceIndex); + + text = text.Remove(namespaceIndex, endOfNamespaceLine - namespaceIndex); + text = text.Insert(namespaceIndex, "namespace DSharpPlus.Extensions.Internal.Toolbox.Implementations;"); + + text = text.Replace("public sealed record", "internal sealed record"); + + string filename = path.Split('/').Last(); + string outPath = basePath + "Built" + filename; + + File.WriteAllText(outPath, text); + } + catch (Exception e) + { + Console.WriteLine($"{e}: {e.Message}\n{e.StackTrace}"); + return false; + } + + return true; + } + ); + +return success ? 0 : 1; \ No newline at end of file diff --git a/tools/generators/generate-concrete-objects.csx b/tools/generators/generate-concrete-objects.csx new file mode 100644 index 0000000000..ac179d9bc3 --- /dev/null +++ b/tools/generators/generate-concrete-objects.csx @@ -0,0 +1,323 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:Microsoft.CodeAnalysis.CSharp, 4.8.0-2.final" +#r "nuget:Spectre.Console, 0.47.1-preview.0.26" + +#load "../incremental-utility.csx" +#load "./parse-interface.csx" + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +using Spectre.Console; + +// Syntax: +// generate-concrete-objects [abstraction path] [output path] +if (Args is ["-h" or "--help" or "-?"]) +{ + AnsiConsole.MarkupLine + ( + """ + [plum1]DSharpPlus Concrete API Object Generator, v0.2.0[/] + + Usage: generate-concrete-objects.csx [[abstraction root path]] [[output root path]] + """ + ); + + return 0; +} + +AnsiConsole.MarkupLine +( + """ + [plum1]DSharpPlus Concrete API Object Generator, v0.2.0[/] + """ +); + +string input, output; + +// there are no args passed, proceed with default args: +// args[0] = src/core/DSharpPlus.Internal.Abstractions.Models +// args[1] = src/core/DSharpPlus.Internal.Models +if (Args.Count == 0) +{ + input = "src/core/DSharpPlus.Internal.Abstractions.Models"; + output = "src/core/DSharpPlus.Internal.Models"; +} + +// there are args passed, which override the given instructions +// validate the passed arguments are correct +else if (Args.Any(path => !Directory.Exists(path))) +{ + AnsiConsole.MarkupLine + ( + """ + [red]The paths provided could not be found on the file system.[/] + """ + ); + + return 1; +} + +// all args are fine +else +{ + input = Args[0]; + output = Args[1]; +} + +string[] files = Directory.GetFiles(input, "I*.cs", SearchOption.AllDirectories); + +files = files + .Select + ( + path => + { + FileInfo file = new(path); + return file.FullName.Replace('\\', '/'); + } + ) + .ToArray(); + +Changes changes = GetFileChanges("generate-concrete-objects", files); + +if (!changes.Added.Any() && !changes.Modified.Any() && !changes.Removed.Any()) +{ + AnsiConsole.MarkupLine + ( + """ + [darkseagreen1_1]There were no changes to the specified records, exiting generate-concrete-objects.[/] + """ + ); + + return 0; +} + +AnsiConsole.MarkupLine +( + $""" + {changes.Added.Count} added, {changes.Modified.Count} modified, {changes.Removed.Count} removed. + """ +); + +if (changes.Removed.Any()) +{ + AnsiConsole.MarkupLine("Deleting counterparts to removed files..."); + + Parallel.ForEach + ( + changes.Removed, + path => + { + int index = path.LastIndexOf('/'); + string deletePath = path.Remove(index + 1, 1).Replace(input, output); + + AnsiConsole.MarkupLine($" Deleting '{deletePath}'..."); + + if (File.Exists(deletePath)) + { + File.Delete(deletePath); + } + } + ); +} + +if (changes.Added.Any() || changes.Modified.Any()) +{ + AnsiConsole.MarkupLine("Generating objects for modified/new definitions..."); +} + +IEnumerable added = changes.Added.Select +( + file => + { + FileInfo info = new(file); + return info.FullName.Replace('\\', '/'); + } +); + +IEnumerable modified = changes.Modified.Select +( + file => + { + FileInfo info = new(file); + return info.FullName.Replace('\\', '/'); + } +); + +IEnumerable editedFiles = added.Concat(modified); +List collectedMetadata = editedFiles + .AsParallel() + .Select(path => ParseInterface(path, input)) + .Where(meta => meta is not null) + .Cast() + .AsEnumerable() + .ToList(); + +for (int i = 0; i < collectedMetadata.Count; i++) +{ + InterfaceMetadata metadata = collectedMetadata[i]; + + // make sure we emit the children of partials if the partial was edited + if (metadata.IsPartial) + { + if (!files.Any(candidate => candidate.EndsWith(metadata.Name.Remove(1, 7)))) + { + IEnumerable candidateFiles = Directory.GetFiles(input, $"{metadata.Name.Remove(1, 7)}.cs", SearchOption.AllDirectories); + + if (candidateFiles.Any()) + { + collectedMetadata.Add(ParseInterface(candidateFiles.First().Replace('\\', '/'), input)!); + } + } + } + + int index = metadata.Path.LastIndexOf('/'); + string outputPath = metadata.Path.Remove(index + 1, 1).Replace(input, output); + + FileInfo info = new(outputPath); + + if (!Directory.Exists(info.DirectoryName!)) + { + Directory.CreateDirectory(info.DirectoryName!); + } + + StringBuilder writer = new(); + + AnsiConsole.MarkupLine($" Generating '{outputPath}'..."); + + // emit a marker type for marker interfaces + if (metadata.IsMarker) + { + writer.Append + ( + $""" + // This Source Code form is subject to the terms of the Mozilla Public + // License, v. 2.0. If a copy of the MPL was not distributed with this + // file, You can obtain one at https://mozilla.org/MPL/2.0/. + + using DSharpPlus.Internal.Abstractions.Models; + + namespace DSharpPlus.Internal.Models; + + /// + /// Placeholder implementation of a marker interface. Please report spotting this to library developers. + /// + internal sealed record {metadata.Name[1..]} : {metadata.Name}; + """ + ); + + File.WriteAllText(outputPath, writer.ToString()); + continue; + } + + writer.AppendLine + ( + """ + // This Source Code form is subject to the terms of the Mozilla Public + // License, v. 2.0. If a copy of the MPL was not distributed with this + // file, You can obtain one at https://mozilla.org/MPL/2.0/. + + """ + ); + + // we specifically want the grouping here, that's how we group usings together + // this doesn't currently handle using statics, but it doesn't need to just yet + // + // sorting system directives first is annoying, and not strictly necessary, but we want to do it anyway + // we're going to employ a hack and return "!" as the key if it's a system using, which is higher than any + // legal namespace name + IEnumerable> groupedUsings = metadata.UsingDirectives! + .Append("using DSharpPlus.Internal.Abstractions.Models;") + .Distinct() + .GroupBy + ( + directive => + { + int index = directive.IndexOf('.'); + int semicolonIndex = directive.IndexOf(';'); + return directive[..(index != -1 ? index : semicolonIndex)]; + } + ) + .OrderBy + ( + group => + { + if (group.First().StartsWith("using System")) + { + return "!"; + } + else + { + string directive = group.First(); + + int index = directive.IndexOf('.'); + int semicolonIndex = directive.IndexOf(';'); + return directive[..(index != -1 ? index : semicolonIndex)]; + } + }, + StringComparer.Ordinal + ); + + foreach(IGrouping group in groupedUsings) + { + writer.AppendLine + ( + $""" + {string.Join("\r\n", group.OrderBy(name => name))} + + """ + ); + } + + writer.AppendLine + ( + $$""" + namespace DSharpPlus.Internal.Models; + + /// + public sealed record {{metadata.Name[1..]}} : {{metadata.Name}} + { + """ + ); + + InterfaceMetadata principal = metadata.PartialParent ?? metadata; + InterfaceMetadata? overwrites = metadata.PartialParent is not null ? metadata : null; + + foreach (PropertyMetadata property in principal.Properties!) + { + bool required = !(property.IsOptional || property.IsNullable); + string type = property.Type; + + // strip optionality if we're being overwritten + if (overwrites is not null && overwrites.Properties!.Any(candidate => candidate.IsOverwrite && candidate.Name == property.Name)) + { + required = !property.IsNullable; + type = type.EndsWith('?') ? $"{type[9..^2]}?" : type[9..^1]; + } + + writer.AppendLine + ( + $$""" + /// + public {{(required ? "required " : "")}}{{type}} {{property.Name}} { get; init; } + + """ + ); + } + + writer.Append('}'); + + string code = writer.ToString(); + + code = code.Remove(code.Length - Environment.NewLine.Length - 1, Environment.NewLine.Length); + + File.WriteAllText(outputPath, code); +} + +return 0; \ No newline at end of file diff --git a/tools/generators/generate-rest-payloads.csx b/tools/generators/generate-rest-payloads.csx new file mode 100644 index 0000000000..e087a99a1e --- /dev/null +++ b/tools/generators/generate-rest-payloads.csx @@ -0,0 +1,276 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:Microsoft.CodeAnalysis.CSharp, 4.8.0-2.final" +#r "nuget:Spectre.Console, 0.47.1-preview.0.26" + +#load "../incremental-utility.csx" +#load "./parse-interface.csx" + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +using Spectre.Console; + +// Syntax: +// generate-rest-payloads [abstraction path] [output path] +if (Args is ["-h" or "--help" or "-?"]) +{ + AnsiConsole.MarkupLine + ( + """ + [plum1]DSharpPlus Rest Payload Generator, v0.1.0[/] + + Usage: generate-rest-payloads.csx [[abstraction root path]] [[output root path]] + """ + ); + + return 0; +} + +AnsiConsole.MarkupLine +( + """ + [plum1]DSharpPlus Rest Payload Generator, v0.1.0[/] + """ +); + +string input, output; + +// there are no args passed, proceed with default args: +// args[0] = src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads +// args[1] = src/core/DSharpPlus.Internal.Rest/Payloads +if (Args.Count == 0) +{ + input = "src/core/DSharpPlus.Internal.Abstractions.Rest/Payloads"; + output = "src/core/DSharpPlus.Internal.Rest/Payloads"; +} + +// there are args passed, which override the given instructions +// validate the passed arguments are correct +else if (Args.Any(path => !Directory.Exists(path))) +{ + AnsiConsole.MarkupLine + ( + """ + [red]The paths provided could not be found on the file system.[/] + """ + ); + + return 1; +} + +// all args are fine +else +{ + input = Args[0]; + output = Args[1]; +} + +string[] files = Directory.GetFiles(input, "I*.cs", SearchOption.AllDirectories); + +files = files + .Select + ( + path => + { + FileInfo file = new(path); + return file.FullName.Replace('\\', '/'); + } + ) + .ToArray(); + +Changes changes = GetFileChanges("generate-rest-payloads", files); + +if (!changes.Added.Any() && !changes.Modified.Any() && !changes.Removed.Any()) +{ + AnsiConsole.MarkupLine + ( + """ + [darkseagreen1_1]There were no changes to the interface definitions, exiting generate-rest-payloads.[/] + """ + ); + + return 0; +} + +AnsiConsole.MarkupLine +( + $""" + {changes.Added.Count} added, {changes.Modified.Count} modified, {changes.Removed.Count} removed. + """ +); + +if (changes.Removed.Any()) +{ + AnsiConsole.MarkupLine("Deleting counterparts to removed files..."); + + Parallel.ForEach + ( + changes.Removed, + path => + { + int index = path.LastIndexOf('/'); + string deletePath = path.Remove(index + 1, 1).Replace(input, output); + + AnsiConsole.MarkupLine($" Deleting '{deletePath}'..."); + + if (File.Exists(deletePath)) + { + File.Delete(deletePath); + } + } + ); +} + +if (changes.Added.Any() || changes.Modified.Any()) +{ + AnsiConsole.MarkupLine("Generating objects for modified/new definitions..."); +} + +IEnumerable added = changes.Added.Select +( + file => + { + FileInfo info = new(file); + return info.FullName.Replace('\\', '/'); + } +); + +IEnumerable modified = changes.Modified.Select +( + file => + { + FileInfo info = new(file); + return info.FullName.Replace('\\', '/'); + } +); + +IEnumerable editedFiles = added.Concat(modified); +List collectedMetadata = editedFiles + .AsParallel() + .Select(path => ParseInterface(path, input)) + .Where(meta => meta is not null) + .Cast() + .AsEnumerable() + .ToList(); + +for (int i = 0; i < collectedMetadata.Count; i++) +{ + InterfaceMetadata metadata = collectedMetadata[i]; + + int index = metadata.Path.LastIndexOf('/'); + string outputPath = metadata.Path.Remove(index + 1, 1).Replace(input, output); + + FileInfo info = new(outputPath); + + if (!Directory.Exists(info.DirectoryName!)) + { + Directory.CreateDirectory(info.DirectoryName!); + } + + StringBuilder writer = new(); + + AnsiConsole.MarkupLine($" Generating '{outputPath}'..."); + + writer.AppendLine + ( + """ + // This Source Code form is subject to the terms of the Mozilla Public + // License, v. 2.0. If a copy of the MPL was not distributed with this + // file, You can obtain one at https://mozilla.org/MPL/2.0/. + + """ + ); + + // we specifically want the grouping here, that's how we group usings together + // this doesn't currently handle using statics, but it doesn't need to just yet + // + // sorting system directives first is annoying, and not strictly necessary, but we want to do it anyway + // we're going to employ a hack and return "!" as the key if it's a system using, which is higher than any + // legal namespace name + IEnumerable> groupedUsings = metadata.UsingDirectives! + .Append("using DSharpPlus.Internal.Abstractions.Rest.Payloads;") + .Distinct() + .GroupBy + ( + directive => + { + int index = directive.IndexOf('.'); + int semicolonIndex = directive.IndexOf(';'); + return directive[..(index != -1 ? index : semicolonIndex)]; + } + ) + .OrderBy + ( + group => + { + if (group.First().StartsWith("using System")) + { + return "!"; + } + else + { + string directive = group.First(); + + int index = directive.IndexOf('.'); + int semicolonIndex = directive.IndexOf(';'); + return directive[..(index != -1 ? index : semicolonIndex)]; + } + }, + StringComparer.Ordinal + ); + + foreach(IGrouping group in groupedUsings) + { + writer.AppendLine + ( + $""" + {string.Join("\r\n", group.OrderBy(name => name))} + + """ + ); + } + + writer.AppendLine + ( + $$""" + namespace DSharpPlus.Internal.Rest.Payloads; + + /// + public sealed record {{metadata.Name[1..]}} : {{metadata.Name}} + { + """ + ); + + InterfaceMetadata principal = metadata.PartialParent ?? metadata; + + foreach (PropertyMetadata property in principal.Properties!) + { + bool required = !(property.IsOptional || property.IsNullable); + string type = property.Type; + + writer.AppendLine + ( + $$""" + /// + public {{(required ? "required " : "")}}{{type}} {{property.Name}} { get; init; } + + """ + ); + } + + writer.Append('}'); + + string code = writer.ToString(); + + code = code.Remove(code.Length - Environment.NewLine.Length - 1, Environment.NewLine.Length); + + File.WriteAllText(outputPath, code); +} + +return 0; \ No newline at end of file diff --git a/tools/generators/generate-serialization-registration.csx b/tools/generators/generate-serialization-registration.csx new file mode 100644 index 0000000000..223d9df8a0 --- /dev/null +++ b/tools/generators/generate-serialization-registration.csx @@ -0,0 +1,120 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:Spectre.Console, 0.47.1-preview.0.26" + +using System.IO; + +using Spectre.Console; + +// Syntax: +// generate-rest-payloads [abstraction path] [output path] +if (Args is ["-h" or "--help" or "-?"]) +{ + AnsiConsole.MarkupLine + ( + """ + [plum1]DSharpPlus Serialization Generator, v0.1.0[/] + + Usage: generate-serialization-registration.csx + """ + ); + + return 0; +} + +AnsiConsole.MarkupLine +( + """ + [plum1]DSharpPlus Serialization Generator, v0.1.0[/] + """ +); + +WriteSerializationRegistration +( + "src/core/DSharpPlus.Internal.Models", + "src/core/DSharpPlus.Internal.Models/Extensions/ServiceCollectionExtensions.Registration.cs", + "DSharpPlus.Internal.Abstractions.Models", + "DSharpPlus.Internal.Models", + "DSharpPlus.Internal.Models.Extensions" +); + +WriteSerializationRegistration +( + "src/core/DSharpPlus.Internal.Rest/Payloads", + "src/core/DSharpPlus.Internal.Rest/Extensions/ServiceCollectionExtensions.Registration.cs", + "DSharpPlus.Internal.Abstractions.Rest.Payloads", + "DSharpPlus.Internal.Rest.Payloads", + "DSharpPlus.Internal.Rest.Extensions" +); + +public void WriteSerializationRegistration +( + string input, + string output, + string abstractionNamespace, + string targetNamespace, + string outputNamespace +) +{ + string[] files = Directory.GetFiles(input, "*.cs", SearchOption.AllDirectories); + + files = files.Where + ( + x => + { + return !x.Contains("/Extensions/") && !x.Contains("\\Extensions\\") + && !x.Contains("/Serialization/") && !x.Contains("\\Serialization\\"); + } + ) + .Select + ( + path => + { + FileInfo file = new(path); + return file.Name.Replace(".cs", ""); + } + ) + .ToArray(); + + using StreamWriter writer = new(output); + + writer.Write($$""" +// This Source Code form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma warning disable IDE0058 + +using {{abstractionNamespace}}; +using {{targetNamespace}}; +using DSharpPlus.Serialization; + +using Microsoft.Extensions.DependencyInjection; + +namespace {{outputNamespace}}; + +partial class ServiceCollectionExtensions +{ + private static void RegisterSerialization(IServiceCollection services) + { + services.Configure + ( + options => + { + +"""); + + foreach (string s in files) + { + writer.Write($" options.AddModel();\r\n"); + } + + writer.Write(""" + } + ); + } +} +"""); +} \ No newline at end of file diff --git a/tools/generators/parse-interface.csx b/tools/generators/parse-interface.csx new file mode 100644 index 0000000000..c159246a57 --- /dev/null +++ b/tools/generators/parse-interface.csx @@ -0,0 +1,269 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:Microsoft.CodeAnalysis.CSharp, 4.8.0-2.final" +#r "nuget:Spectre.Console, 0.47.1-preview.0.26" + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +using Spectre.Console; + +// this script aims to provide a quick way for all object generators to obtain metadata about the interface they're working with, +// and thus covers all use-cases of these object generators. + +/// +/// Represents metadata about an interface definition. +/// +public sealed record InterfaceMetadata +{ + /// + /// The name of this interface. + /// + public required string Name { get; init; } + + /// + /// The file path of this interface. + /// + public required string Path { get; init; } + + /// + /// Indicates whether this interface is a marker interface. + /// + public required bool IsMarker { get; init; } + + /// + /// Indicates whether this interface is a partial interface, as in, represents a partially populated discord object. + /// + public required bool IsPartial { get; init; } + + /// + /// The properties of this interface. null if is true. + /// + public IReadOnlyList? Properties { get; init; } + + /// + /// Parsed metadata about all parent interfaces, except partial parents. + /// + public IReadOnlyList? ParentInterfaces { get; init; } + + /// + /// The partial parent interface, if present. + /// + public InterfaceMetadata? PartialParent { get; init; } + + /// + /// The using directives used by these files, necessary for referencing their property types. + /// + public IReadOnlyList? UsingDirectives { get; init; } +} + +/// +/// Represents metadata about one specific property. +/// +public readonly record struct PropertyMetadata +{ + /// + /// The declared type of this property. + /// + public required string Type { get; init; } + + /// + /// The declared name of this property. + /// + public required string Name { get; init; } + + /// + /// Indicates whether this property is optional. + /// + public required bool IsOptional { get; init; } + + /// + /// Indicates whether this property is nullable. + /// + public required bool IsNullable { get; init; } + + /// + /// Indicates whether this property overwrites a property in a partial parent, using the new keyword. + /// + /// + public required bool IsOverwrite { get; init; } +} + +/// +/// Parses an interface from a specified file, returning a metadata object if an interface was present. +/// +public InterfaceMetadata? ParseInterface(string filename, string baseDirectory) +{ + CompilationUnitSyntax root = SyntaxFactory.ParseCompilationUnit(File.ReadAllText(filename)); + + List usings = new(); + + foreach (UsingDirectiveSyntax @using in root.Usings) + { + usings.Add(@using.ToString()!); + } + + if + ( + root.Members.First() is not FileScopedNamespaceDeclarationSyntax fileScopedNamespace + || fileScopedNamespace.Members.First() is not InterfaceDeclarationSyntax interfaceSyntax + ) + { + AnsiConsole.MarkupLine($" No interface detected in '{filename}', abandoning..."); + + return null; + } + + IEnumerable tokens = interfaceSyntax.ChildTokens(); + SyntaxToken name = default; + + foreach (SyntaxToken token in tokens) + { + if (token is { RawKind: (int)SyntaxKind.IdentifierToken }) + { + name = token; + } + + if (token is { RawKind: (int)SyntaxKind.SemicolonToken }) + { + AnsiConsole.MarkupLine($" Marker interface detected in '{filename}'."); + + return new() + { + Name = name.Text, + Path = filename, + IsMarker = true, + IsPartial = false + }; + } + } + + // look at parent interfaces and potential partials + BaseListSyntax? baseInterfaceList = (BaseListSyntax?)interfaceSyntax.ChildNodes().FirstOrDefault(node => node is BaseListSyntax); + + string? partialParent = null; + List parents = new(); + + if (baseInterfaceList is not null) + { + foreach (SyntaxNode node in baseInterfaceList.ChildNodes()) + { + if (node is not SimpleBaseTypeSyntax candidate) + { + continue; + } + + if (node.ChildNodes().First() is not IdentifierNameSyntax identifier) + { + continue; + } + + foreach (SyntaxToken token in identifier.ChildTokens()) + { + if (token is { RawKind: (int)SyntaxKind.IdentifierToken }) + { + if (token.Text.StartsWith("IPartial")) + { + partialParent = token.Text; + break; + } + + parents.Add(token.Text); + break; + } + } + } + } + + // start extracting properties. Nullability/optionality checks are very primitive, but they're accurate enough for our use-case + // (which is basically just understanding when to put `required` on an emitted property) + + List properties = new(); + + foreach (MemberDeclarationSyntax member in interfaceSyntax.Members) + { + if (member is not PropertyDeclarationSyntax property) + { + continue; + } + + string type = property.Type.ToString(); + + properties.Add + ( + new() + { + Type = type, + Name = property.Identifier.ToString(), + IsOptional = type.StartsWith("Optional<"), + IsNullable = type.EndsWith('?') || (type.StartsWith("Optional<") && type.EndsWith("?>")), + IsOverwrite = property.ChildTokens().Any(token => token.RawKind == (int)SyntaxKind.NewKeyword) + } + ); + } + + List parentMetadata = new(); + + foreach (string interfaceName in parents) + { + IEnumerable path = Directory.GetFiles(baseDirectory, $"{interfaceName}.cs", SearchOption.AllDirectories); + + if (!path.Any()) + { + continue; + } + + InterfaceMetadata? potentialParent = ParseInterface(path.First(), baseDirectory); + + if (potentialParent is not null) + { + parentMetadata.Add(potentialParent); + } + } + + InterfaceMetadata? partialParentMetadata = null; + + if (partialParent is not null) + { + IEnumerable path = Directory.GetFiles(baseDirectory, $"{partialParent}.cs", SearchOption.AllDirectories); + + if (path.Any()) + { + partialParentMetadata = ParseInterface(path.First(), baseDirectory); + } + } + + // the most painful part of all: consolidate usings + if (partialParentMetadata?.UsingDirectives is not null) + { + usings.AddRange(partialParentMetadata.UsingDirectives); + } + + foreach (InterfaceMetadata i in parentMetadata) + { + if (i.UsingDirectives is not null) + { + usings.AddRange(i.UsingDirectives); + } + } + + return new() + { + Name = name.Text, + Path = filename, + IsMarker = false, + IsPartial = name.Text.StartsWith("IPartial"), + Properties = properties, + ParentInterfaces = parentMetadata, + PartialParent = partialParentMetadata, + UsingDirectives = usings.Distinct().ToArray() + }; +} diff --git a/tools/get-highlight-languages/.gitignore b/tools/get-highlight-languages/.gitignore deleted file mode 100644 index 6a7d6d8ef6..0000000000 --- a/tools/get-highlight-languages/.gitignore +++ /dev/null @@ -1,130 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* \ No newline at end of file diff --git a/tools/get-highlight-languages/index.js b/tools/get-highlight-languages/index.js deleted file mode 100644 index 56cd0ff970..0000000000 --- a/tools/get-highlight-languages/index.js +++ /dev/null @@ -1,6 +0,0 @@ -const hljs = require('highlight.js'); - -hljs.listLanguages().forEach((lang) => { - console.log(lang); - hljs.getLanguage(lang).aliases?.forEach((alias) => console.log(alias)); -}); \ No newline at end of file diff --git a/tools/get-highlight-languages/package-lock.json b/tools/get-highlight-languages/package-lock.json deleted file mode 100644 index a3bf656938..0000000000 --- a/tools/get-highlight-languages/package-lock.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "get-highlight-languages", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "get-highlight-languages", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "gitignore": "^0.7.0", - "highlight.js": "^11.9.0" - } - }, - "node_modules/gitignore": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/gitignore/-/gitignore-0.7.0.tgz", - "integrity": "sha512-6iE891OyeYQYVvdoWI/hcxDWJ0sOngSpIRadxLoYbsnZdqWIUsEfx+IrOpW3d/zWBA/eKYvs6ZZx6ogz2wEGoQ==", - "bin": { - "gitignore": "bin/gitignore.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", - "engines": { - "node": ">=12.0.0" - } - } - } -} diff --git a/tools/get-highlight-languages/package.json b/tools/get-highlight-languages/package.json deleted file mode 100644 index 947f5cf041..0000000000 --- a/tools/get-highlight-languages/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "get-highlight-languages", - "version": "1.0.0", - "description": "Get all the languages supported by highlight.js", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "OoLunar, Highlight.Js Contributors", - "license": "MIT", - "dependencies": { - "highlight.js": "^11.9.0" - } -} \ No newline at end of file diff --git a/tools/incremental-utility.csx b/tools/incremental-utility.csx new file mode 100644 index 0000000000..7e431ed70b --- /dev/null +++ b/tools/incremental-utility.csx @@ -0,0 +1,107 @@ +#!/usr/bin/env dotnet-script + +#nullable enable + +#r "nuget:System.IO.Hashing, 8.0.0-rc.2.23479.6" + +using System.Collections.Generic; +using System.IO; +using System.IO.Hashing; +using System.Linq; +using System.Text.Json; + +/// +/// Contains the changes made to files since the last run of the incremental utility. +/// +public sealed record Changes +{ + /// + /// The list of files that have been modified. + /// + public required IReadOnlyList Modified { get; init; } + + /// + /// The list of files that have been added. + /// + public required IReadOnlyList Added { get; init; } + + /// + /// The list of files that have been removed. + /// + public required IReadOnlyList Removed { get; init; } +} + +/// +/// Gets the changes made to the provided files since the last run. +/// +/// The name of the parent tool invoking this functionality. +/// The files covered by the parent tool. +/// A summary of modified, added and removed file paths. +public Changes GetFileChanges(string toolName, params string[] files) +{ + // load and deserialize the current hashes + if (!Directory.Exists("./artifacts/hashes")) + { + Directory.CreateDirectory("./artifacts/hashes"); + } + + if (!File.Exists($"./artifacts/hashes/{toolName}.json")) + { + File.Create($"./artifacts/hashes/{toolName}.json").Close(); + CalculateAndSaveHashes(files, toolName); + + return new Changes + { + Added = files, + Removed = Array.Empty(), + Modified = Array.Empty() + }; + } + + StreamReader reader = new($"./artifacts/hashes/{toolName}.json"); + + Dictionary oldHashes = JsonSerializer.Deserialize>(reader.ReadToEnd())!; + + reader.Close(); + + Dictionary newHashes = CalculateAndSaveHashes(files, toolName); + + string[] added = files.Where(name => !oldHashes.ContainsKey(name)).ToArray(); + string[] removed = oldHashes.Where(candidate => !newHashes.ContainsKey(candidate.Key)).Select(kvp => kvp.Key).ToArray(); + string[] modified = files.Where + ( + name => + { + return oldHashes.TryGetValue(name, out string? oldHash) + && newHashes.TryGetValue(name, out string? newHash) + && oldHash != newHash; + } + ).ToArray(); + + return new Changes + { + Added = added, + Removed = removed, + Modified = modified + }; +} + +private Dictionary CalculateAndSaveHashes(string[] files, string name) +{ + Dictionary dictionary = new(); + + foreach (string file in files) + { + XxHash3 xxh = new(); + xxh.Append(File.ReadAllBytes(file)); + + string hash = xxh.GetCurrentHashAsUInt64().ToString(); + dictionary.Add(file, hash); + } + + using StreamWriter writer = new($"./artifacts/hashes/{name}.json"); + + writer.Write(JsonSerializer.Serialize(dictionary)); + + return dictionary; +} \ No newline at end of file