From 113fcf0e459d973a665a79c0652daa1b23f637cb Mon Sep 17 00:00:00 2001 From: ImChill1n Date: Sun, 14 Sep 2025 12:00:45 +0200 Subject: [PATCH 01/20] Add changes --- disnake/role.py | 20 ++++++++++++++++++++ tests/test_role_gradient.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/test_role_gradient.py diff --git a/disnake/role.py b/disnake/role.py index 6673681adf..2834005115 100644 --- a/disnake/role.py +++ b/disnake/role.py @@ -444,6 +444,26 @@ def tertiary_color(self) -> Optional[Colour]: .. versionadded:: 2.11 """ return self.tertiary_colour + + @property + def is_gradient(self) -> bool: + """Whether the role is gradient. + + .. versionadded:: 2.11 + + :return type: :class:`bool` + """ + return self.secondary_color is not None and self.tertiary_color is None + + @property + def is_holographic(self) -> bool: + """Whether the role is holographic. + + .. versionadded:: 2.11 + + :return type: :class:`bool` + """ + return self.tertiary_color is not None @property def icon(self) -> Optional[Asset]: diff --git a/tests/test_role_gradient.py b/tests/test_role_gradient.py new file mode 100644 index 0000000000..1344fe5cad --- /dev/null +++ b/tests/test_role_gradient.py @@ -0,0 +1,31 @@ +def make_role(primary=None, secondary=None, tertiary=None): + class DummyRole: + def __init__(self, p, s, t): + self.primary_color = p + self.secondary_color = s + self.tertiary_color = t + + @property + def is_gradient(self) -> bool: + return self.secondary_color is not None and self.tertiary_color is None + + @property + def is_holographic(self) -> bool: + return self.tertiary_color is not None + + return DummyRole(primary, secondary, tertiary) + +def test_is_gradient_true(): + r = make_role(primary=0x111111, secondary=0x222222, tertiary=None) + assert r.is_gradient is True + assert r.is_holographic is False + +def test_is_holographic_true(): + r = make_role(primary=0x111111, secondary=0x222222, tertiary=0x333333) + assert r.is_holographic is True + assert r.is_gradient is False + +def test_basic_role(): + r = make_role(primary=0x111111, secondary=None, tertiary=None) + assert r.is_gradient is False + assert r.is_holographic is False \ No newline at end of file From 3abcb8aa1f1575905e886da2474876131ff87243 Mon Sep 17 00:00:00 2001 From: ImChill1n Date: Sun, 14 Sep 2025 13:09:48 +0200 Subject: [PATCH 02/20] Fix versionadded to 2.11.1 --- disnake/role.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/disnake/role.py b/disnake/role.py index 2834005115..d8f3c2b4f0 100644 --- a/disnake/role.py +++ b/disnake/role.py @@ -444,12 +444,12 @@ def tertiary_color(self) -> Optional[Colour]: .. versionadded:: 2.11 """ return self.tertiary_colour - + @property def is_gradient(self) -> bool: """Whether the role is gradient. - .. versionadded:: 2.11 + .. versionadded:: 2.11.1 :return type: :class:`bool` """ @@ -459,7 +459,7 @@ def is_gradient(self) -> bool: def is_holographic(self) -> bool: """Whether the role is holographic. - .. versionadded:: 2.11 + .. versionadded:: 2.11.1 :return type: :class:`bool` """ From 0b9761b3a4b563f646801036ab8e7fe9d7dc7a3a Mon Sep 17 00:00:00 2001 From: ImChill1n Date: Sun, 14 Sep 2025 14:41:41 +0200 Subject: [PATCH 03/20] Add changelog --- changelog/1379.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1379.feature.rst diff --git a/changelog/1379.feature.rst b/changelog/1379.feature.rst new file mode 100644 index 0000000000..4bd7508d71 --- /dev/null +++ b/changelog/1379.feature.rst @@ -0,0 +1 @@ +Adds Role.is_gradient and Role.is_holographic properties to check whether a role is gradient or holographic. \ No newline at end of file From 834ad1aae7388259c9dd5b59bd41fe4a705605cb Mon Sep 17 00:00:00 2001 From: ImChill1n Date: Sun, 14 Sep 2025 15:37:22 +0200 Subject: [PATCH 04/20] Add SPDX license identifier --- changelog/1379.feature.rst | 2 +- disnake/.gitattributes | 3 + disnake/.github/CODEOWNERS | 3 + disnake/.github/ISSUE_TEMPLATE/bug_report.yml | 81 + disnake/.github/ISSUE_TEMPLATE/config.yml | 7 + .../ISSUE_TEMPLATE/feature_request.yml | 49 + disnake/.github/PULL_REQUEST_TEMPLATE.md | 16 + disnake/.github/actions/cache-pdm/action.yml | 42 + disnake/.github/actions/setup-env/action.yml | 49 + disnake/.github/workflows/changelog.yaml | 47 + .../.github/workflows/check-pull-labels.yaml | 27 + .../.github/workflows/create-release-pr.yaml | 81 + disnake/.github/workflows/lint-test.yml | 297 + disnake/.github/workflows/release.yaml | 216 + .../.github/workflows/semantic-pr-title.yml | 23 + disnake/.gitignore | 36 + disnake/.libcst.codemod.yaml | 21 + disnake/.pre-commit-config.yaml | 59 + disnake/.readthedocs.yml | 19 + disnake/CONTRIBUTING.md | 128 + disnake/LICENSE | 22 + disnake/MANIFEST.in | 8 + disnake/README.md | 117 + disnake/RELEASE.md | 47 + disnake/assets/banner.png | Bin 0 -> 132183 bytes disnake/changelog/README.rst | 43 + disnake/changelog/_template.rst.jinja | 32 + disnake/disnake/__init__.py | 93 + disnake/disnake/__main__.py | 419 + disnake/disnake/abc.py | 2088 ++ disnake/disnake/activity.py | 974 + disnake/disnake/app_commands.py | 1325 ++ disnake/disnake/appinfo.py | 478 + .../disnake/application_role_connection.py | 112 + disnake/disnake/asset.py | 542 + disnake/disnake/audit_logs.py | 894 + disnake/disnake/automod.py | 803 + disnake/disnake/backoff.py | 80 + disnake/disnake/bans.py | 21 + disnake/disnake/bin/COPYING | 28 + disnake/disnake/bin/libopus-0.x64.dll | Bin 0 -> 441856 bytes disnake/disnake/bin/libopus-0.x86.dll | Bin 0 -> 366080 bytes disnake/disnake/channel.py | 5211 ++++ disnake/disnake/client.py | 3308 +++ disnake/disnake/colour.py | 336 + disnake/disnake/components.py | 1624 ++ disnake/disnake/context_managers.py | 68 + disnake/disnake/custom_warnings.py | 46 + disnake/disnake/embeds.py | 952 + disnake/disnake/emoji.py | 248 + disnake/disnake/entitlement.py | 193 + disnake/disnake/enums.py | 2485 ++ disnake/disnake/errors.py | 435 + disnake/disnake/ext/commands/__init__.py | 26 + disnake/disnake/ext/commands/_types.py | 37 + disnake/disnake/ext/commands/base_core.py | 914 + disnake/disnake/ext/commands/bot.py | 563 + disnake/disnake/ext/commands/bot_base.py | 609 + disnake/disnake/ext/commands/cog.py | 899 + .../disnake/ext/commands/common_bot_base.py | 545 + disnake/disnake/ext/commands/context.py | 387 + disnake/disnake/ext/commands/converter.py | 1390 ++ disnake/disnake/ext/commands/cooldowns.py | 391 + disnake/disnake/ext/commands/core.py | 2681 +++ .../disnake/ext/commands/ctx_menus_core.py | 466 + .../disnake/ext/commands/custom_warnings.py | 11 + disnake/disnake/ext/commands/errors.py | 1137 + .../disnake/ext/commands/flag_converter.py | 615 + disnake/disnake/ext/commands/flags.py | 181 + disnake/disnake/ext/commands/help.py | 1377 ++ .../ext/commands/interaction_bot_base.py | 1478 ++ disnake/disnake/ext/commands/params.py | 1404 ++ disnake/disnake/ext/commands/py.typed | 0 disnake/disnake/ext/commands/slash_core.py | 896 + disnake/disnake/ext/commands/view.py | 174 + disnake/disnake/ext/mypy_plugin/__init__.py | 14 + disnake/disnake/ext/tasks/__init__.py | 799 + disnake/disnake/ext/tasks/py.typed | 0 disnake/disnake/file.py | 133 + disnake/disnake/flags.py | 2929 +++ disnake/disnake/gateway.py | 1104 + disnake/disnake/guild.py | 5819 +++++ disnake/disnake/guild_preview.py | 115 + disnake/disnake/guild_scheduled_event.py | 703 + disnake/disnake/http.py | 3075 +++ disnake/disnake/i18n.py | 419 + disnake/disnake/integrations.py | 430 + disnake/disnake/interactions/__init__.py | 13 + .../interactions/application_command.py | 386 + disnake/disnake/interactions/base.py | 2160 ++ disnake/disnake/interactions/message.py | 235 + disnake/disnake/interactions/modal.py | 295 + disnake/disnake/invite.py | 601 + disnake/disnake/iterators.py | 1392 ++ disnake/disnake/member.py | 1322 ++ disnake/disnake/mentions.py | 160 + disnake/disnake/message.py | 3035 +++ disnake/disnake/mixins.py | 27 + disnake/disnake/object.py | 68 + disnake/disnake/oggparse.py | 101 + disnake/disnake/onboarding.py | 196 + disnake/disnake/opus.py | 490 + disnake/disnake/partial_emoji.py | 269 + disnake/disnake/permissions.py | 1451 ++ disnake/disnake/player.py | 816 + disnake/disnake/poll.py | 423 + disnake/disnake/py.typed | 0 disnake/disnake/raw_models.py | 575 + disnake/disnake/reaction.py | 204 + disnake/disnake/role.py | 739 + disnake/disnake/shard.py | 629 + disnake/disnake/sku.py | 154 + disnake/disnake/soundboard.py | 313 + disnake/disnake/stage_instance.py | 217 + disnake/disnake/state.py | 2596 ++ disnake/disnake/sticker.py | 519 + disnake/disnake/subscription.py | 123 + disnake/disnake/team.py | 144 + disnake/disnake/template.py | 304 + disnake/disnake/threads.py | 1257 + disnake/disnake/types/__init__.py | 11 + disnake/disnake/types/activity.py | 98 + disnake/disnake/types/appinfo.py | 75 + .../types/application_role_connection.py | 18 + disnake/disnake/types/audit_log.py | 342 + disnake/disnake/types/automod.py | 82 + disnake/disnake/types/channel.py | 204 + disnake/disnake/types/components.py | 262 + disnake/disnake/types/embed.py | 69 + disnake/disnake/types/emoji.py | 25 + disnake/disnake/types/entitlement.py | 22 + disnake/disnake/types/gateway.py | 700 + disnake/disnake/types/guild.py | 207 + .../disnake/types/guild_scheduled_event.py | 42 + disnake/disnake/types/i18n.py | 5 + disnake/disnake/types/integration.py | 63 + disnake/disnake/types/interactions.py | 457 + disnake/disnake/types/invite.py | 44 + disnake/disnake/types/member.py | 35 + disnake/disnake/types/message.py | 166 + disnake/disnake/types/onboarding.py | 34 + disnake/disnake/types/poll.py | 72 + disnake/disnake/types/role.py | 50 + disnake/disnake/types/sku.py | 17 + disnake/disnake/types/snowflake.py | 6 + disnake/disnake/types/soundboard.py | 31 + disnake/disnake/types/sticker.py | 68 + disnake/disnake/types/subscription.py | 23 + disnake/disnake/types/team.py | 26 + disnake/disnake/types/template.py | 28 + disnake/disnake/types/threads.py | 78 + disnake/disnake/types/user.py | 75 + disnake/disnake/types/voice.py | 70 + disnake/disnake/types/webhook.py | 43 + disnake/disnake/types/welcome_screen.py | 19 + disnake/disnake/types/widget.py | 45 + disnake/disnake/ui/__init__.py | 27 + disnake/disnake/ui/_types.py | 93 + disnake/disnake/ui/action_row.py | 1194 + disnake/disnake/ui/button.py | 364 + disnake/disnake/ui/container.py | 138 + disnake/disnake/ui/file.py | 121 + disnake/disnake/ui/item.py | 237 + disnake/disnake/ui/label.py | 106 + disnake/disnake/ui/media_gallery.py | 60 + disnake/disnake/ui/modal.py | 339 + disnake/disnake/ui/section.py | 101 + disnake/disnake/ui/select/__init__.py | 27 + disnake/disnake/ui/select/base.py | 278 + disnake/disnake/ui/select/channel.py | 288 + disnake/disnake/ui/select/mentionable.py | 261 + disnake/disnake/ui/select/role.py | 244 + disnake/disnake/ui/select/string.py | 358 + disnake/disnake/ui/select/user.py | 246 + disnake/disnake/ui/separator.py | 82 + disnake/disnake/ui/text_display.py | 58 + disnake/disnake/ui/text_input.py | 191 + disnake/disnake/ui/thumbnail.py | 100 + disnake/disnake/ui/view.py | 565 + disnake/disnake/user.py | 716 + disnake/disnake/utils.py | 1556 ++ disnake/disnake/voice_client.py | 661 + disnake/disnake/voice_region.py | 73 + disnake/disnake/webhook/__init__.py | 19 + disnake/disnake/webhook/async_.py | 2121 ++ disnake/disnake/webhook/sync.py | 1355 ++ disnake/disnake/welcome_screen.py | 196 + disnake/disnake/widget.py | 447 + disnake/docs/404.rst | 11 + disnake/docs/Makefile | 185 + disnake/docs/_static/codeblocks.css | 145 + disnake/docs/_static/copy.js | 36 + disnake/docs/_static/custom.js | 294 + disnake/docs/_static/disnake.svg | 38 + disnake/docs/_static/icons.css | 42 + disnake/docs/_static/icons.woff | Bin 0 -> 2608 bytes disnake/docs/_static/material-icons.woff | Bin 0 -> 128352 bytes disnake/docs/_static/scorer.js | 82 + disnake/docs/_static/settings.js | 108 + disnake/docs/_static/sidebar.js | 166 + disnake/docs/_static/style.css | 1651 ++ disnake/docs/_static/touch.js | 36 + disnake/docs/_templates/api_redirect.js_t | 41 + disnake/docs/_templates/layout.html | 166 + disnake/docs/_templates/localtoc.html | 9 + disnake/docs/_templates/relations.html | 2 + disnake/docs/api.rst | 11 + disnake/docs/api/abc.rst | 70 + disnake/docs/api/activities.rst | 96 + disnake/docs/api/app_commands.rst | 151 + disnake/docs/api/app_info.rst | 107 + disnake/docs/api/audit_logs.rst | 1776 ++ disnake/docs/api/automod.rst | 107 + disnake/docs/api/channels.rst | 254 + disnake/docs/api/clients.rst | 114 + disnake/docs/api/components.rst | 248 + disnake/docs/api/emoji.rst | 38 + disnake/docs/api/entitlements.rst | 36 + disnake/docs/api/events.rst | 1639 ++ disnake/docs/api/exceptions.rst | 183 + disnake/docs/api/guild_scheduled_events.rst | 70 + disnake/docs/api/guilds.rst | 191 + disnake/docs/api/index.rst | 127 + disnake/docs/api/integrations.rst | 80 + disnake/docs/api/interactions.rst | 210 + disnake/docs/api/invites.rst | 56 + disnake/docs/api/localization.rst | 47 + disnake/docs/api/members.rst | 57 + disnake/docs/api/messages.rst | 276 + disnake/docs/api/misc.rst | 181 + disnake/docs/api/permissions.rst | 27 + disnake/docs/api/roles.rst | 45 + disnake/docs/api/skus.rst | 43 + disnake/docs/api/soundboard.rst | 45 + disnake/docs/api/stage_instances.rst | 36 + disnake/docs/api/stickers.rst | 75 + disnake/docs/api/subscriptions.rst | 36 + disnake/docs/api/ui.rst | 232 + disnake/docs/api/users.rst | 96 + disnake/docs/api/utilities.rst | 43 + disnake/docs/api/voice.rst | 141 + disnake/docs/api/webhooks.rst | 76 + disnake/docs/api/widgets.rst | 56 + disnake/docs/conf.py | 506 + disnake/docs/discord.rst | 141 + disnake/docs/ext/commands/additional_info.rst | 68 + disnake/docs/ext/commands/api.rst | 10 + .../docs/ext/commands/api/app_commands.rst | 222 + disnake/docs/ext/commands/api/bots.rst | 155 + disnake/docs/ext/commands/api/checks.rst | 96 + disnake/docs/ext/commands/api/cogs.rst | 28 + disnake/docs/ext/commands/api/context.rst | 27 + disnake/docs/ext/commands/api/converters.rst | 109 + disnake/docs/ext/commands/api/events.rst | 149 + disnake/docs/ext/commands/api/exceptions.rst | 271 + .../docs/ext/commands/api/help_commands.rst | 39 + disnake/docs/ext/commands/api/index.rst | 25 + disnake/docs/ext/commands/api/misc.rst | 19 + .../docs/ext/commands/api/prefix_commands.rst | 92 + disnake/docs/ext/commands/cogs.rst | 184 + disnake/docs/ext/commands/commands.rst | 950 + disnake/docs/ext/commands/extensions.rst | 66 + disnake/docs/ext/commands/index.rst | 21 + disnake/docs/ext/commands/slash_commands.rst | 797 + disnake/docs/ext/tasks/index.rst | 157 + disnake/docs/extensions/_types.py | 10 + disnake/docs/extensions/attributetable.py | 310 + disnake/docs/extensions/builder.py | 99 + disnake/docs/extensions/collapse.py | 60 + disnake/docs/extensions/enumattrs.py | 49 + .../docs/extensions/exception_hierarchy.py | 47 + disnake/docs/extensions/fulltoc.py | 141 + .../docs/extensions/nitpick_file_ignorer.py | 34 + disnake/docs/extensions/redirects.py | 53 + disnake/docs/extensions/resourcelinks.py | 53 + disnake/docs/extensions/versionchange.py | 34 + disnake/docs/faq.rst | 415 + .../docs/images/app_commands/int_option.png | Bin 0 -> 14982 bytes disnake/docs/images/commands/flags1.png | Bin 0 -> 26037 bytes disnake/docs/images/commands/flags2.png | Bin 0 -> 28823 bytes disnake/docs/images/commands/flags3.png | Bin 0 -> 27449 bytes disnake/docs/images/commands/greedy1.png | Bin 0 -> 14289 bytes disnake/docs/images/commands/keyword1.png | Bin 0 -> 12758 bytes disnake/docs/images/commands/keyword2.png | Bin 0 -> 13041 bytes disnake/docs/images/commands/optional1.png | Bin 0 -> 10802 bytes disnake/docs/images/commands/positional1.png | Bin 0 -> 12046 bytes disnake/docs/images/commands/positional2.png | Bin 0 -> 12889 bytes disnake/docs/images/commands/positional3.png | Bin 0 -> 12379 bytes disnake/docs/images/commands/variable1.png | Bin 0 -> 14133 bytes disnake/docs/images/commands/variable2.png | Bin 0 -> 14523 bytes disnake/docs/images/commands/variable3.png | Bin 0 -> 12347 bytes disnake/docs/images/discord_add_to_server.png | Bin 0 -> 15996 bytes .../docs/images/discord_bot_acquire_token.png | Bin 0 -> 16426 bytes disnake/docs/images/discord_bot_tab.png | Bin 0 -> 8653 bytes .../docs/images/discord_bot_user_options.png | Bin 0 -> 141530 bytes .../docs/images/discord_create_app_button.png | Bin 0 -> 3792 bytes .../docs/images/discord_create_app_form.png | Bin 0 -> 79560 bytes .../discord_general_authorization_link.png | Bin 0 -> 16717 bytes disnake/docs/images/discord_general_scope.png | Bin 0 -> 23219 bytes disnake/docs/images/discord_oauth2_perms.png | Bin 0 -> 66272 bytes .../images/discord_privileged_intents.png | Bin 0 -> 51849 bytes disnake/docs/images/discord_url_generator.png | Bin 0 -> 12951 bytes .../images/discord_url_generator_scopes.png | Bin 0 -> 35984 bytes disnake/docs/images/disnake_logo.ico | Bin 0 -> 16980 bytes disnake/docs/images/snake.svg | 18 + disnake/docs/images/snake_dark.svg | 18 + disnake/docs/index.rst | 78 + disnake/docs/intents.rst | 226 + disnake/docs/intro.rst | 115 + disnake/docs/locale/ja/LC_MESSAGES/api.po | 19685 ++++++++++++++++ disnake/docs/locale/ja/LC_MESSAGES/discord.po | 178 + .../locale/ja/LC_MESSAGES/ext/commands/api.po | 6779 ++++++ .../ja/LC_MESSAGES/ext/commands/cogs.po | 178 + .../ja/LC_MESSAGES/ext/commands/commands.po | 900 + .../ja/LC_MESSAGES/ext/commands/extensions.po | 82 + .../ja/LC_MESSAGES/ext/commands/index.po | 26 + .../locale/ja/LC_MESSAGES/ext/tasks/index.po | 416 + disnake/docs/locale/ja/LC_MESSAGES/faq.po | 581 + disnake/docs/locale/ja/LC_MESSAGES/index.po | 82 + disnake/docs/locale/ja/LC_MESSAGES/intents.po | 429 + disnake/docs/locale/ja/LC_MESSAGES/intro.po | 122 + disnake/docs/locale/ja/LC_MESSAGES/logging.po | 76 + .../docs/locale/ja/LC_MESSAGES/migrating.po | 2547 ++ .../ja/LC_MESSAGES/migrating_to_async.po | 362 + .../docs/locale/ja/LC_MESSAGES/quickstart.po | 90 + disnake/docs/locale/ja/LC_MESSAGES/sphinx.po | 22 + .../ja/LC_MESSAGES/version_guarantees.po | 78 + .../docs/locale/ja/LC_MESSAGES/whats_new.po | 2936 +++ disnake/docs/logging.rst | 47 + disnake/docs/make.bat | 265 + disnake/docs/migrating.rst | 1173 + disnake/docs/migrating_to_async.rst | 321 + disnake/docs/quickstart.rst | 85 + disnake/docs/version_guarantees.rst | 41 + disnake/docs/whats_new.rst | 1814 ++ disnake/docs/whats_new_legacy.rst | 1192 + disnake/examples/.ruff.toml | 7 + disnake/examples/background_task.py | 45 + disnake/examples/basic_bot.py | 86 + disnake/examples/basic_voice.py | 143 + disnake/examples/components_v2.py | 38 + disnake/examples/converters.py | 82 + disnake/examples/custom_context.py | 55 + disnake/examples/edit_delete.py | 57 + disnake/examples/guessing_game.py | 44 + disnake/examples/interactions/autocomplete.py | 60 + .../interactions/basic_context_menus.py | 41 + disnake/examples/interactions/converters.py | 81 + disnake/examples/interactions/injections.py | 135 + .../examples/interactions/localizations.py | 89 + .../interactions/low_level_components.py | 123 + disnake/examples/interactions/modal.py | 154 + disnake/examples/interactions/param.py | 105 + disnake/examples/interactions/subcmd.py | 77 + disnake/examples/new_member.py | 26 + disnake/examples/reaction_roles.py | 99 + disnake/examples/reply.py | 28 + disnake/examples/secret.py | 99 + disnake/examples/user_echo_webhook.py | 57 + disnake/examples/views/button/confirm.py | 69 + .../views/button/ephemeral_counter.py | 60 + disnake/examples/views/button/link.py | 48 + disnake/examples/views/button/paginator.py | 96 + disnake/examples/views/button/row_buttons.py | 55 + disnake/examples/views/disable_view.py | 67 + disnake/examples/views/persistent.py | 78 + disnake/examples/views/select/dropdown.py | 73 + disnake/examples/views/tic_tac_toe.py | 150 + disnake/noxfile.py | 296 + disnake/pyproject.toml | 426 + disnake/scripts/__init__.py | 1 + disnake/scripts/ci/versiontool.py | 128 + disnake/scripts/codemods/__init__.py | 1 + disnake/scripts/codemods/base.py | 63 + disnake/scripts/codemods/combined.py | 40 + .../scripts/codemods/overloads_no_missing.py | 45 + disnake/scripts/codemods/typed_flags.py | 183 + disnake/scripts/codemods/typed_permissions.py | 190 + disnake/setup.py | 36 + disnake/tests/__init__.py | 1 + disnake/tests/ext/commands/test_base_core.py | 164 + disnake/tests/ext/commands/test_core.py | 68 + disnake/tests/ext/commands/test_params.py | 270 + disnake/tests/ext/commands/test_slash_core.py | 50 + disnake/tests/ext/commands/test_view.py | 62 + disnake/tests/ext/tasks/test_loops.py | 74 + disnake/tests/helpers.py | 79 + disnake/tests/interactions/test_base.py | 214 + disnake/tests/test_abc.py | 166 + disnake/tests/test_activity.py | 97 + disnake/tests/test_colour.py | 60 + disnake/tests/test_embeds.py | 482 + disnake/tests/test_events.py | 109 + disnake/tests/test_flags.py | 515 + disnake/tests/test_http.py | 47 + disnake/tests/test_mentions.py | 119 + disnake/tests/test_message.py | 57 + disnake/tests/test_object.py | 29 + disnake/tests/test_onboarding.py | 102 + disnake/tests/test_permissions.py | 339 + disnake/tests/test_role_gradient.py | 31 + disnake/tests/test_utils.py | 1040 + disnake/tests/ui/test_action_row.py | 318 + disnake/tests/ui/test_components.py | 50 + disnake/tests/ui/test_decorators.py | 70 + disnake/tests/ui/test_select.py | 47 + disnake/tests/utils_helper_module.py | 26 + tests/test_role_gradient.py | 26 +- 408 files changed, 158427 insertions(+), 15 deletions(-) create mode 100644 disnake/.gitattributes create mode 100644 disnake/.github/CODEOWNERS create mode 100644 disnake/.github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 disnake/.github/ISSUE_TEMPLATE/config.yml create mode 100644 disnake/.github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 disnake/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 disnake/.github/actions/cache-pdm/action.yml create mode 100644 disnake/.github/actions/setup-env/action.yml create mode 100644 disnake/.github/workflows/changelog.yaml create mode 100644 disnake/.github/workflows/check-pull-labels.yaml create mode 100644 disnake/.github/workflows/create-release-pr.yaml create mode 100644 disnake/.github/workflows/lint-test.yml create mode 100644 disnake/.github/workflows/release.yaml create mode 100644 disnake/.github/workflows/semantic-pr-title.yml create mode 100644 disnake/.gitignore create mode 100644 disnake/.libcst.codemod.yaml create mode 100644 disnake/.pre-commit-config.yaml create mode 100644 disnake/.readthedocs.yml create mode 100644 disnake/CONTRIBUTING.md create mode 100644 disnake/LICENSE create mode 100644 disnake/MANIFEST.in create mode 100644 disnake/README.md create mode 100644 disnake/RELEASE.md create mode 100644 disnake/assets/banner.png create mode 100644 disnake/changelog/README.rst create mode 100644 disnake/changelog/_template.rst.jinja create mode 100644 disnake/disnake/__init__.py create mode 100644 disnake/disnake/__main__.py create mode 100644 disnake/disnake/abc.py create mode 100644 disnake/disnake/activity.py create mode 100644 disnake/disnake/app_commands.py create mode 100644 disnake/disnake/appinfo.py create mode 100644 disnake/disnake/application_role_connection.py create mode 100644 disnake/disnake/asset.py create mode 100644 disnake/disnake/audit_logs.py create mode 100644 disnake/disnake/automod.py create mode 100644 disnake/disnake/backoff.py create mode 100644 disnake/disnake/bans.py create mode 100644 disnake/disnake/bin/COPYING create mode 100644 disnake/disnake/bin/libopus-0.x64.dll create mode 100644 disnake/disnake/bin/libopus-0.x86.dll create mode 100644 disnake/disnake/channel.py create mode 100644 disnake/disnake/client.py create mode 100644 disnake/disnake/colour.py create mode 100644 disnake/disnake/components.py create mode 100644 disnake/disnake/context_managers.py create mode 100644 disnake/disnake/custom_warnings.py create mode 100644 disnake/disnake/embeds.py create mode 100644 disnake/disnake/emoji.py create mode 100644 disnake/disnake/entitlement.py create mode 100644 disnake/disnake/enums.py create mode 100644 disnake/disnake/errors.py create mode 100644 disnake/disnake/ext/commands/__init__.py create mode 100644 disnake/disnake/ext/commands/_types.py create mode 100644 disnake/disnake/ext/commands/base_core.py create mode 100644 disnake/disnake/ext/commands/bot.py create mode 100644 disnake/disnake/ext/commands/bot_base.py create mode 100644 disnake/disnake/ext/commands/cog.py create mode 100644 disnake/disnake/ext/commands/common_bot_base.py create mode 100644 disnake/disnake/ext/commands/context.py create mode 100644 disnake/disnake/ext/commands/converter.py create mode 100644 disnake/disnake/ext/commands/cooldowns.py create mode 100644 disnake/disnake/ext/commands/core.py create mode 100644 disnake/disnake/ext/commands/ctx_menus_core.py create mode 100644 disnake/disnake/ext/commands/custom_warnings.py create mode 100644 disnake/disnake/ext/commands/errors.py create mode 100644 disnake/disnake/ext/commands/flag_converter.py create mode 100644 disnake/disnake/ext/commands/flags.py create mode 100644 disnake/disnake/ext/commands/help.py create mode 100644 disnake/disnake/ext/commands/interaction_bot_base.py create mode 100644 disnake/disnake/ext/commands/params.py create mode 100644 disnake/disnake/ext/commands/py.typed create mode 100644 disnake/disnake/ext/commands/slash_core.py create mode 100644 disnake/disnake/ext/commands/view.py create mode 100644 disnake/disnake/ext/mypy_plugin/__init__.py create mode 100644 disnake/disnake/ext/tasks/__init__.py create mode 100644 disnake/disnake/ext/tasks/py.typed create mode 100644 disnake/disnake/file.py create mode 100644 disnake/disnake/flags.py create mode 100644 disnake/disnake/gateway.py create mode 100644 disnake/disnake/guild.py create mode 100644 disnake/disnake/guild_preview.py create mode 100644 disnake/disnake/guild_scheduled_event.py create mode 100644 disnake/disnake/http.py create mode 100644 disnake/disnake/i18n.py create mode 100644 disnake/disnake/integrations.py create mode 100644 disnake/disnake/interactions/__init__.py create mode 100644 disnake/disnake/interactions/application_command.py create mode 100644 disnake/disnake/interactions/base.py create mode 100644 disnake/disnake/interactions/message.py create mode 100644 disnake/disnake/interactions/modal.py create mode 100644 disnake/disnake/invite.py create mode 100644 disnake/disnake/iterators.py create mode 100644 disnake/disnake/member.py create mode 100644 disnake/disnake/mentions.py create mode 100644 disnake/disnake/message.py create mode 100644 disnake/disnake/mixins.py create mode 100644 disnake/disnake/object.py create mode 100644 disnake/disnake/oggparse.py create mode 100644 disnake/disnake/onboarding.py create mode 100644 disnake/disnake/opus.py create mode 100644 disnake/disnake/partial_emoji.py create mode 100644 disnake/disnake/permissions.py create mode 100644 disnake/disnake/player.py create mode 100644 disnake/disnake/poll.py create mode 100644 disnake/disnake/py.typed create mode 100644 disnake/disnake/raw_models.py create mode 100644 disnake/disnake/reaction.py create mode 100644 disnake/disnake/role.py create mode 100644 disnake/disnake/shard.py create mode 100644 disnake/disnake/sku.py create mode 100644 disnake/disnake/soundboard.py create mode 100644 disnake/disnake/stage_instance.py create mode 100644 disnake/disnake/state.py create mode 100644 disnake/disnake/sticker.py create mode 100644 disnake/disnake/subscription.py create mode 100644 disnake/disnake/team.py create mode 100644 disnake/disnake/template.py create mode 100644 disnake/disnake/threads.py create mode 100644 disnake/disnake/types/__init__.py create mode 100644 disnake/disnake/types/activity.py create mode 100644 disnake/disnake/types/appinfo.py create mode 100644 disnake/disnake/types/application_role_connection.py create mode 100644 disnake/disnake/types/audit_log.py create mode 100644 disnake/disnake/types/automod.py create mode 100644 disnake/disnake/types/channel.py create mode 100644 disnake/disnake/types/components.py create mode 100644 disnake/disnake/types/embed.py create mode 100644 disnake/disnake/types/emoji.py create mode 100644 disnake/disnake/types/entitlement.py create mode 100644 disnake/disnake/types/gateway.py create mode 100644 disnake/disnake/types/guild.py create mode 100644 disnake/disnake/types/guild_scheduled_event.py create mode 100644 disnake/disnake/types/i18n.py create mode 100644 disnake/disnake/types/integration.py create mode 100644 disnake/disnake/types/interactions.py create mode 100644 disnake/disnake/types/invite.py create mode 100644 disnake/disnake/types/member.py create mode 100644 disnake/disnake/types/message.py create mode 100644 disnake/disnake/types/onboarding.py create mode 100644 disnake/disnake/types/poll.py create mode 100644 disnake/disnake/types/role.py create mode 100644 disnake/disnake/types/sku.py create mode 100644 disnake/disnake/types/snowflake.py create mode 100644 disnake/disnake/types/soundboard.py create mode 100644 disnake/disnake/types/sticker.py create mode 100644 disnake/disnake/types/subscription.py create mode 100644 disnake/disnake/types/team.py create mode 100644 disnake/disnake/types/template.py create mode 100644 disnake/disnake/types/threads.py create mode 100644 disnake/disnake/types/user.py create mode 100644 disnake/disnake/types/voice.py create mode 100644 disnake/disnake/types/webhook.py create mode 100644 disnake/disnake/types/welcome_screen.py create mode 100644 disnake/disnake/types/widget.py create mode 100644 disnake/disnake/ui/__init__.py create mode 100644 disnake/disnake/ui/_types.py create mode 100644 disnake/disnake/ui/action_row.py create mode 100644 disnake/disnake/ui/button.py create mode 100644 disnake/disnake/ui/container.py create mode 100644 disnake/disnake/ui/file.py create mode 100644 disnake/disnake/ui/item.py create mode 100644 disnake/disnake/ui/label.py create mode 100644 disnake/disnake/ui/media_gallery.py create mode 100644 disnake/disnake/ui/modal.py create mode 100644 disnake/disnake/ui/section.py create mode 100644 disnake/disnake/ui/select/__init__.py create mode 100644 disnake/disnake/ui/select/base.py create mode 100644 disnake/disnake/ui/select/channel.py create mode 100644 disnake/disnake/ui/select/mentionable.py create mode 100644 disnake/disnake/ui/select/role.py create mode 100644 disnake/disnake/ui/select/string.py create mode 100644 disnake/disnake/ui/select/user.py create mode 100644 disnake/disnake/ui/separator.py create mode 100644 disnake/disnake/ui/text_display.py create mode 100644 disnake/disnake/ui/text_input.py create mode 100644 disnake/disnake/ui/thumbnail.py create mode 100644 disnake/disnake/ui/view.py create mode 100644 disnake/disnake/user.py create mode 100644 disnake/disnake/utils.py create mode 100644 disnake/disnake/voice_client.py create mode 100644 disnake/disnake/voice_region.py create mode 100644 disnake/disnake/webhook/__init__.py create mode 100644 disnake/disnake/webhook/async_.py create mode 100644 disnake/disnake/webhook/sync.py create mode 100644 disnake/disnake/welcome_screen.py create mode 100644 disnake/disnake/widget.py create mode 100644 disnake/docs/404.rst create mode 100644 disnake/docs/Makefile create mode 100644 disnake/docs/_static/codeblocks.css create mode 100644 disnake/docs/_static/copy.js create mode 100644 disnake/docs/_static/custom.js create mode 100644 disnake/docs/_static/disnake.svg create mode 100644 disnake/docs/_static/icons.css create mode 100644 disnake/docs/_static/icons.woff create mode 100644 disnake/docs/_static/material-icons.woff create mode 100644 disnake/docs/_static/scorer.js create mode 100644 disnake/docs/_static/settings.js create mode 100644 disnake/docs/_static/sidebar.js create mode 100644 disnake/docs/_static/style.css create mode 100644 disnake/docs/_static/touch.js create mode 100644 disnake/docs/_templates/api_redirect.js_t create mode 100644 disnake/docs/_templates/layout.html create mode 100644 disnake/docs/_templates/localtoc.html create mode 100644 disnake/docs/_templates/relations.html create mode 100644 disnake/docs/api.rst create mode 100644 disnake/docs/api/abc.rst create mode 100644 disnake/docs/api/activities.rst create mode 100644 disnake/docs/api/app_commands.rst create mode 100644 disnake/docs/api/app_info.rst create mode 100644 disnake/docs/api/audit_logs.rst create mode 100644 disnake/docs/api/automod.rst create mode 100644 disnake/docs/api/channels.rst create mode 100644 disnake/docs/api/clients.rst create mode 100644 disnake/docs/api/components.rst create mode 100644 disnake/docs/api/emoji.rst create mode 100644 disnake/docs/api/entitlements.rst create mode 100644 disnake/docs/api/events.rst create mode 100644 disnake/docs/api/exceptions.rst create mode 100644 disnake/docs/api/guild_scheduled_events.rst create mode 100644 disnake/docs/api/guilds.rst create mode 100644 disnake/docs/api/index.rst create mode 100644 disnake/docs/api/integrations.rst create mode 100644 disnake/docs/api/interactions.rst create mode 100644 disnake/docs/api/invites.rst create mode 100644 disnake/docs/api/localization.rst create mode 100644 disnake/docs/api/members.rst create mode 100644 disnake/docs/api/messages.rst create mode 100644 disnake/docs/api/misc.rst create mode 100644 disnake/docs/api/permissions.rst create mode 100644 disnake/docs/api/roles.rst create mode 100644 disnake/docs/api/skus.rst create mode 100644 disnake/docs/api/soundboard.rst create mode 100644 disnake/docs/api/stage_instances.rst create mode 100644 disnake/docs/api/stickers.rst create mode 100644 disnake/docs/api/subscriptions.rst create mode 100644 disnake/docs/api/ui.rst create mode 100644 disnake/docs/api/users.rst create mode 100644 disnake/docs/api/utilities.rst create mode 100644 disnake/docs/api/voice.rst create mode 100644 disnake/docs/api/webhooks.rst create mode 100644 disnake/docs/api/widgets.rst create mode 100644 disnake/docs/conf.py create mode 100644 disnake/docs/discord.rst create mode 100644 disnake/docs/ext/commands/additional_info.rst create mode 100644 disnake/docs/ext/commands/api.rst create mode 100644 disnake/docs/ext/commands/api/app_commands.rst create mode 100644 disnake/docs/ext/commands/api/bots.rst create mode 100644 disnake/docs/ext/commands/api/checks.rst create mode 100644 disnake/docs/ext/commands/api/cogs.rst create mode 100644 disnake/docs/ext/commands/api/context.rst create mode 100644 disnake/docs/ext/commands/api/converters.rst create mode 100644 disnake/docs/ext/commands/api/events.rst create mode 100644 disnake/docs/ext/commands/api/exceptions.rst create mode 100644 disnake/docs/ext/commands/api/help_commands.rst create mode 100644 disnake/docs/ext/commands/api/index.rst create mode 100644 disnake/docs/ext/commands/api/misc.rst create mode 100644 disnake/docs/ext/commands/api/prefix_commands.rst create mode 100644 disnake/docs/ext/commands/cogs.rst create mode 100644 disnake/docs/ext/commands/commands.rst create mode 100644 disnake/docs/ext/commands/extensions.rst create mode 100644 disnake/docs/ext/commands/index.rst create mode 100644 disnake/docs/ext/commands/slash_commands.rst create mode 100644 disnake/docs/ext/tasks/index.rst create mode 100644 disnake/docs/extensions/_types.py create mode 100644 disnake/docs/extensions/attributetable.py create mode 100644 disnake/docs/extensions/builder.py create mode 100644 disnake/docs/extensions/collapse.py create mode 100644 disnake/docs/extensions/enumattrs.py create mode 100644 disnake/docs/extensions/exception_hierarchy.py create mode 100644 disnake/docs/extensions/fulltoc.py create mode 100644 disnake/docs/extensions/nitpick_file_ignorer.py create mode 100644 disnake/docs/extensions/redirects.py create mode 100644 disnake/docs/extensions/resourcelinks.py create mode 100644 disnake/docs/extensions/versionchange.py create mode 100644 disnake/docs/faq.rst create mode 100644 disnake/docs/images/app_commands/int_option.png create mode 100644 disnake/docs/images/commands/flags1.png create mode 100644 disnake/docs/images/commands/flags2.png create mode 100644 disnake/docs/images/commands/flags3.png create mode 100644 disnake/docs/images/commands/greedy1.png create mode 100644 disnake/docs/images/commands/keyword1.png create mode 100644 disnake/docs/images/commands/keyword2.png create mode 100644 disnake/docs/images/commands/optional1.png create mode 100644 disnake/docs/images/commands/positional1.png create mode 100644 disnake/docs/images/commands/positional2.png create mode 100644 disnake/docs/images/commands/positional3.png create mode 100644 disnake/docs/images/commands/variable1.png create mode 100644 disnake/docs/images/commands/variable2.png create mode 100644 disnake/docs/images/commands/variable3.png create mode 100644 disnake/docs/images/discord_add_to_server.png create mode 100644 disnake/docs/images/discord_bot_acquire_token.png create mode 100644 disnake/docs/images/discord_bot_tab.png create mode 100644 disnake/docs/images/discord_bot_user_options.png create mode 100644 disnake/docs/images/discord_create_app_button.png create mode 100644 disnake/docs/images/discord_create_app_form.png create mode 100644 disnake/docs/images/discord_general_authorization_link.png create mode 100644 disnake/docs/images/discord_general_scope.png create mode 100644 disnake/docs/images/discord_oauth2_perms.png create mode 100644 disnake/docs/images/discord_privileged_intents.png create mode 100644 disnake/docs/images/discord_url_generator.png create mode 100644 disnake/docs/images/discord_url_generator_scopes.png create mode 100644 disnake/docs/images/disnake_logo.ico create mode 100644 disnake/docs/images/snake.svg create mode 100644 disnake/docs/images/snake_dark.svg create mode 100644 disnake/docs/index.rst create mode 100644 disnake/docs/intents.rst create mode 100644 disnake/docs/intro.rst create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/api.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/discord.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/ext/commands/api.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/ext/commands/cogs.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/ext/commands/commands.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/ext/commands/extensions.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/ext/commands/index.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/ext/tasks/index.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/faq.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/index.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/intents.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/intro.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/logging.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/migrating.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/migrating_to_async.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/quickstart.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/sphinx.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/version_guarantees.po create mode 100644 disnake/docs/locale/ja/LC_MESSAGES/whats_new.po create mode 100644 disnake/docs/logging.rst create mode 100644 disnake/docs/make.bat create mode 100644 disnake/docs/migrating.rst create mode 100644 disnake/docs/migrating_to_async.rst create mode 100644 disnake/docs/quickstart.rst create mode 100644 disnake/docs/version_guarantees.rst create mode 100644 disnake/docs/whats_new.rst create mode 100644 disnake/docs/whats_new_legacy.rst create mode 100644 disnake/examples/.ruff.toml create mode 100644 disnake/examples/background_task.py create mode 100644 disnake/examples/basic_bot.py create mode 100644 disnake/examples/basic_voice.py create mode 100644 disnake/examples/components_v2.py create mode 100644 disnake/examples/converters.py create mode 100644 disnake/examples/custom_context.py create mode 100644 disnake/examples/edit_delete.py create mode 100644 disnake/examples/guessing_game.py create mode 100644 disnake/examples/interactions/autocomplete.py create mode 100644 disnake/examples/interactions/basic_context_menus.py create mode 100644 disnake/examples/interactions/converters.py create mode 100644 disnake/examples/interactions/injections.py create mode 100644 disnake/examples/interactions/localizations.py create mode 100644 disnake/examples/interactions/low_level_components.py create mode 100644 disnake/examples/interactions/modal.py create mode 100644 disnake/examples/interactions/param.py create mode 100644 disnake/examples/interactions/subcmd.py create mode 100644 disnake/examples/new_member.py create mode 100644 disnake/examples/reaction_roles.py create mode 100644 disnake/examples/reply.py create mode 100644 disnake/examples/secret.py create mode 100644 disnake/examples/user_echo_webhook.py create mode 100644 disnake/examples/views/button/confirm.py create mode 100644 disnake/examples/views/button/ephemeral_counter.py create mode 100644 disnake/examples/views/button/link.py create mode 100644 disnake/examples/views/button/paginator.py create mode 100644 disnake/examples/views/button/row_buttons.py create mode 100644 disnake/examples/views/disable_view.py create mode 100644 disnake/examples/views/persistent.py create mode 100644 disnake/examples/views/select/dropdown.py create mode 100644 disnake/examples/views/tic_tac_toe.py create mode 100644 disnake/noxfile.py create mode 100644 disnake/pyproject.toml create mode 100644 disnake/scripts/__init__.py create mode 100644 disnake/scripts/ci/versiontool.py create mode 100644 disnake/scripts/codemods/__init__.py create mode 100644 disnake/scripts/codemods/base.py create mode 100644 disnake/scripts/codemods/combined.py create mode 100644 disnake/scripts/codemods/overloads_no_missing.py create mode 100644 disnake/scripts/codemods/typed_flags.py create mode 100644 disnake/scripts/codemods/typed_permissions.py create mode 100644 disnake/setup.py create mode 100644 disnake/tests/__init__.py create mode 100644 disnake/tests/ext/commands/test_base_core.py create mode 100644 disnake/tests/ext/commands/test_core.py create mode 100644 disnake/tests/ext/commands/test_params.py create mode 100644 disnake/tests/ext/commands/test_slash_core.py create mode 100644 disnake/tests/ext/commands/test_view.py create mode 100644 disnake/tests/ext/tasks/test_loops.py create mode 100644 disnake/tests/helpers.py create mode 100644 disnake/tests/interactions/test_base.py create mode 100644 disnake/tests/test_abc.py create mode 100644 disnake/tests/test_activity.py create mode 100644 disnake/tests/test_colour.py create mode 100644 disnake/tests/test_embeds.py create mode 100644 disnake/tests/test_events.py create mode 100644 disnake/tests/test_flags.py create mode 100644 disnake/tests/test_http.py create mode 100644 disnake/tests/test_mentions.py create mode 100644 disnake/tests/test_message.py create mode 100644 disnake/tests/test_object.py create mode 100644 disnake/tests/test_onboarding.py create mode 100644 disnake/tests/test_permissions.py create mode 100644 disnake/tests/test_role_gradient.py create mode 100644 disnake/tests/test_utils.py create mode 100644 disnake/tests/ui/test_action_row.py create mode 100644 disnake/tests/ui/test_components.py create mode 100644 disnake/tests/ui/test_decorators.py create mode 100644 disnake/tests/ui/test_select.py create mode 100644 disnake/tests/utils_helper_module.py diff --git a/changelog/1379.feature.rst b/changelog/1379.feature.rst index 4bd7508d71..758796b38a 100644 --- a/changelog/1379.feature.rst +++ b/changelog/1379.feature.rst @@ -1 +1 @@ -Adds Role.is_gradient and Role.is_holographic properties to check whether a role is gradient or holographic. \ No newline at end of file +Adds Role.is_gradient and Role.is_holographic properties to check whether a role is gradient or holographic. diff --git a/disnake/.gitattributes b/disnake/.gitattributes new file mode 100644 index 0000000000..4630853702 --- /dev/null +++ b/disnake/.gitattributes @@ -0,0 +1,3 @@ +# force LF for pyproject to make hashFiles in CI consistent (windows <3) +# (see https://github.com/actions/runner/issues/498) +pyproject.toml text eol=lf diff --git a/disnake/.github/CODEOWNERS b/disnake/.github/CODEOWNERS new file mode 100644 index 0000000000..ccbf0ff248 --- /dev/null +++ b/disnake/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +/.github @DisnakeDev/maintainers +/scripts/ci @DisnakeDev/maintainers diff --git a/disnake/.github/ISSUE_TEMPLATE/bug_report.yml b/disnake/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..4151d7b6b9 --- /dev/null +++ b/disnake/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: MIT + +name: Bug Report +description: Report broken or incorrect behaviour +labels: ["unconfirmed bug"] +body: + - type: markdown + attributes: + value: > + Thanks for taking the time to fill out a bug. + If you want real-time support, consider joining our [Discord server](https://discord.gg/disnake) instead. + + Please note that this form is for bugs only! + - type: input + attributes: + label: Summary + description: A simple summary of your bug report + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: > + What you did to make it happen. + validations: + required: true + - type: textarea + attributes: + label: Minimal Reproducible Code + description: > + A short snippet of code that showcases the bug. + render: python + - type: textarea + attributes: + label: Expected Results + description: > + What did you expect to happen? + validations: + required: true + - type: textarea + attributes: + label: Actual Results + description: > + What actually happened? + validations: + required: true + - type: input + attributes: + label: Intents + description: > + What intents are you using for your bot? + This is the `disnake.Intents` class you pass to the client. + validations: + required: true + - type: textarea + attributes: + render: markdown + label: System Information + description: > + Run `python -m disnake -v` and paste the output below. + + If you are unable to run this command, show some basic information involving + your system such as operating system and Python version. + validations: + required: true + - type: checkboxes + attributes: + label: Checklist + description: > + Let's make sure you've properly done due diligence when reporting this issue! + options: + - label: I have searched the open issues for duplicates. + required: true + - label: I have shown the entire traceback, if possible. + required: true + - label: I have removed my token from display, if visible. + required: true + - type: textarea + attributes: + label: Additional Context + description: If there is anything else to say, please do so here. diff --git a/disnake/.github/ISSUE_TEMPLATE/config.yml b/disnake/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..56276a7391 --- /dev/null +++ b/disnake/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: MIT + +blank_issues_enabled: false +contact_links: + - name: Discord Server + about: Use our official Discord server to ask for help and questions as well. + url: https://discord.gg/disnake diff --git a/disnake/.github/ISSUE_TEMPLATE/feature_request.yml b/disnake/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..615285d010 --- /dev/null +++ b/disnake/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MIT + +name: Feature Request +description: Suggest a feature for this library +labels: ["feature request"] +body: + - type: input + attributes: + label: Summary + description: > + A short summary of what your feature request is. + validations: + required: true + - type: dropdown + attributes: + multiple: false + label: What is the feature request for? + options: + - The core library + - disnake.ext.commands + - disnake.ext.tasks + - The documentation + validations: + required: true + - type: textarea + attributes: + label: The Problem + description: > + What problem is your feature trying to solve? + What becomes easier or possible when this feature is implemented? + validations: + required: true + - type: textarea + attributes: + label: The Ideal Solution + description: > + What is your ideal solution to the problem? + What would you like this feature to do? + validations: + required: true + - type: textarea + attributes: + label: The Current Solution + description: > + What is the current solution to the problem, if any? + - type: textarea + attributes: + label: Additional Context + description: If there is anything else to say, please do so here. diff --git a/disnake/.github/PULL_REQUEST_TEMPLATE.md b/disnake/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..14622d2731 --- /dev/null +++ b/disnake/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## Summary + + + +## Checklist + + + +- [ ] If code changes were made, then they have been tested + - [ ] I have updated the documentation to reflect the changes + - [ ] I have formatted the code properly by running `pdm lint` + - [ ] I have type-checked the code by running `pdm pyright` +- [ ] This PR fixes an issue +- [ ] This PR adds something new (e.g. new method or parameters) +- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed) +- [ ] This PR is **not** a code change (e.g. documentation, README, ...) diff --git a/disnake/.github/actions/cache-pdm/action.yml b/disnake/.github/actions/cache-pdm/action.yml new file mode 100644 index 0000000000..321cc54228 --- /dev/null +++ b/disnake/.github/actions/cache-pdm/action.yml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: MIT + +name: Configure PDM cache +description: . +inputs: + env-already-initialized: + description: Whether Python/PDM is already configured + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Get metadata for cache + id: get-cache-meta + shell: bash + run: | + echo "date=$(date -u "+%Y%m%d")" >> $GITHUB_OUTPUT + + - name: Setup/Restore cache + id: cache + uses: actions/cache@v4 + with: + path: | + pdm.lock + # cache lockfile for the current day, roughly + key: pdm-${{ steps.get-cache-meta.outputs.date }}-${{ hashFiles('pyproject.toml') }} + # pdm lockfiles should be platform-agnostic + enableCrossOsArchive: true + + - if: ${{ steps.cache.outputs.cache-hit != 'true' && inputs.env-already-initialized != 'true' }} + name: Set up PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: 3.8 + version: "2.20.1" + + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + name: Lock all dependencies + shell: bash + run: | + pdm lock -G:all # create pdm.lock diff --git a/disnake/.github/actions/setup-env/action.yml b/disnake/.github/actions/setup-env/action.yml new file mode 100644 index 0000000000..d61b6c6f06 --- /dev/null +++ b/disnake/.github/actions/setup-env/action.yml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MIT + +name: Set up environment +description: . +inputs: + python-version: + description: The python version to install + required: true +outputs: + python-version: + description: The python version that was installed. + value: ${{ steps.python-version.outputs.python-version }} + +runs: + using: composite + steps: + - name: Set up pdm with python ${{ inputs.python-version }} + id: setup-python + uses: pdm-project/setup-pdm@v4 + with: + python-version: ${{ inputs.python-version }} + version: "2.20.1" # last version to support python 3.8 + cache: false + + - name: Disable PDM version check + shell: bash + run: | + pdm config check_update false + + - name: Ignore saved pythons + shell: bash + run: | + echo "PDM_IGNORE_SAVED_PYTHON=1" >> $GITHUB_ENV + + - name: Update pip, wheel, setuptools + shell: bash + run: python -m pip install -U pip wheel setuptools + + - name: Install nox + shell: bash + run: pip install nox + + - name: Set python version + id: python-version + shell: bash + run: echo "python-version=$(python -c 'import sys; print(".".join(map(str,sys.version_info[:2])))')" >> $GITHUB_OUTPUT + + - name: Configure cache + uses: ./.github/actions/cache-pdm diff --git a/disnake/.github/workflows/changelog.yaml b/disnake/.github/workflows/changelog.yaml new file mode 100644 index 0000000000..f3bc2e659b --- /dev/null +++ b/disnake/.github/workflows/changelog.yaml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: MIT + +name: changelog entry + +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled, reopened] + branches: + - master + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-changelog-entry: + name: Check for changelog entry + runs-on: ubuntu-latest + if: contains(github.event.pull_request.labels.*.name, 'skip news') != true + + steps: + - uses: actions/checkout@v4 + with: + # towncrier needs a non-shallow clone + fetch-depth: '0' + + - name: Set up environment + uses: ./.github/actions/setup-env + with: + python-version: '3.9' + + - name: Install dependencies + run: pdm install -dG changelog + + - name: Check for presence of a Change Log fragment (only pull requests) + # NOTE: The pull request' base branch needs to be fetched so towncrier + # is able to compare the current branch with the base branch. + run: | + if ! pdm run towncrier check --compare-with origin/${BASE_BRANCH}; then + echo "::error::Please see https://github.com/DisnakeDev/disnake/blob/master/changelog/README.rst for details on how to create a changelog entry." >&2 + exit 1 + fi + env: + BASE_BRANCH: ${{ github.base_ref }} diff --git a/disnake/.github/workflows/check-pull-labels.yaml b/disnake/.github/workflows/check-pull-labels.yaml new file mode 100644 index 0000000000..d72aadf536 --- /dev/null +++ b/disnake/.github/workflows/check-pull-labels.yaml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MIT + +name: check-pull-labels + +on: + pull_request_target: + types: [opened, synchronize, labeled, unlabeled, reopened] + +permissions: + pull-requests: read + +jobs: + check-pull-labels: + runs-on: ubuntu-latest + + steps: + - name: Check for the do not merge label + if: "${{ contains(github.event.pull_request.labels.*.name, 'do not merge') == true }}" + run: exit 1 + + - name: Check for the blocked label + if: "${{ contains(github.event.pull_request.labels.*.name, 's: blocked') == true }}" + run: exit 1 + + - name: Check for the waiting for api/docs label + if: "${{ contains(github.event.pull_request.labels.*.name, 's: waiting for api/docs') == true }}" + run: exit 1 diff --git a/disnake/.github/workflows/create-release-pr.yaml b/disnake/.github/workflows/create-release-pr.yaml new file mode 100644 index 0000000000..7320774787 --- /dev/null +++ b/disnake/.github/workflows/create-release-pr.yaml @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: MIT + +name: Create Release PR + +on: + workflow_dispatch: + inputs: + version: + description: "The new version number, e.g. `1.2.3`." + type: string + required: true + +permissions: {} + +jobs: + create-release-pr: + name: Create Release PR + runs-on: ubuntu-latest + + env: + VERSION_INPUT: ${{ inputs.version }} + + steps: + # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow + - name: Generate app token + id: generate_token + uses: actions/create-github-app-token@f2acddfb5195534d487896a656232b016a682f3c # v1.9.0 + with: + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + token: ${{ steps.generate_token.outputs.token }} + persist-credentials: false + + - name: Set git name/email + env: + GIT_USER: ${{ vars.GIT_APP_USER_NAME }} + GIT_EMAIL: ${{ vars.GIT_APP_USER_EMAIL }} + run: | + git config user.name "$GIT_USER" + git config user.email "$GIT_EMAIL" + + - name: Set up environment + uses: ./.github/actions/setup-env + with: + python-version: 3.8 + + - name: Install dependencies + run: pdm install -dG changelog + + - name: Update version + run: | + python scripts/ci/versiontool.py --set "$VERSION_INPUT" + git commit -a -m "chore: update version to $VERSION_INPUT" + + - name: Build changelog + run: | + pdm run towncrier build --yes --version "$VERSION_INPUT" + git commit -a -m "docs: build changelog" + + - name: Create pull request + uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e # v6.0.2 + with: + token: ${{ steps.generate_token.outputs.token }} + branch: auto/release-v${{ inputs.version }} + delete-branch: true + title: "chore(release): v${{ inputs.version }}" + body: | + Automated release PR, triggered by @${{ github.actor }} for ${{ github.sha }}. + + ### Tasks + - [ ] Add changelogs from backports, if applicable. + - [ ] Once merged, create + push a tag. + + https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + labels: | + t: release + assignees: | + ${{ github.actor }} diff --git a/disnake/.github/workflows/lint-test.yml b/disnake/.github/workflows/lint-test.yml new file mode 100644 index 0000000000..ea3913ea8c --- /dev/null +++ b/disnake/.github/workflows/lint-test.yml @@ -0,0 +1,297 @@ +# SPDX-License-Identifier: MIT + +name: Lint & Test + +on: + push: + branches: + - "master" + - "v[0-9]+.[0-9]+.x" # matches to backport branches, e.g. v3.6.x + - "run-ci/**" + tags: + - "*" + pull_request: + merge_group: + types: [checks_requested] + +permissions: read-all + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +env: + GITHUB_STEP_SUMMARY_HEADER: "
#name#\n
"
+  GITHUB_STEP_SUMMARY_FOOTER: "
\n" + +jobs: + lock-dependencies: + # The only purpose of this is to create a lockfile, which will be cached + # to be used with subsequent jobs. + # This provides somewhat of a middle ground and avoids having each job lock dependencies on its own, + # while still not needing to commit a lockfile to the repo, which is discouraged for libraries as per + # https://pdm-project.org/en/latest/usage/lockfile/ + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Instead of setup-env, we call the cache-pdm action here directly. + # This avoids having to install PDM, only to find out the cache is already up to date sometimes. + - name: Configure cache + uses: ./.github/actions/cache-pdm + with: + env-already-initialized: false + + # Used to determine which python versions to test against. + # noxfile.py is the source of truth, which in turn draws from + # pyproject.toml's `project.requires-python` and `project.classifiers`. + python-versions: + runs-on: ubuntu-latest + outputs: + min-python: ${{ steps.set-matrix.outputs.min-python }} + docs: ${{ steps.set-matrix.outputs.docs }} + libcst: ${{ steps.set-matrix.outputs.libcst }} + pyright: ${{ steps.set-matrix.outputs.tests }} + tests: ${{ steps.set-matrix.outputs.tests }} + steps: + - uses: actions/checkout@v4 + - name: Set up environment + uses: ./.github/actions/setup-env + with: + python-version: 3.8 + + - name: Determine Python versions to test and lint against + id: set-matrix + run: | + echo min-python=$(nox -s test --list --json | jq -rc '[.[].python][0]') | tee --append $GITHUB_OUTPUT + echo docs=$(nox -s docs --list --json | jq -rc '.[].python') | tee --append $GITHUB_OUTPUT + echo libcst=$(nox -s codemod --list --json | jq -rc '[.[].python][0]') | tee --append $GITHUB_OUTPUT + echo tests=$(nox -s test --list --json | jq -c '[.[].python]') | tee --append $GITHUB_OUTPUT + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run pre-commit + id: pre-commit + uses: pre-commit/action@v3.0.1 + env: + RUFF_OUTPUT_FORMAT: github + + docs: + # unlike the other workflows, we explicitly use the same version as + # readthedocs (see .readthedocs.yml) here for consistency + runs-on: ubuntu-24.04 + needs: + - python-versions + - lock-dependencies + - lint + steps: + - uses: actions/checkout@v4 + + - name: Set up environment + id: setup-env + uses: ./.github/actions/setup-env + with: + python-version: ${{ needs.python-versions.outputs.docs }} + + - name: Run sphinx-build + run: nox --force-python ${{ steps.setup-env.outputs.python-version }} -s docs -- --keep-going -W -w $GITHUB_STEP_SUMMARY + + pyright: + runs-on: ubuntu-latest + needs: + - python-versions + - lock-dependencies + - lint + strategy: + matrix: + # TODO: add 3.14 once we switch to uv + python-version: ${{ fromJson(needs.python-versions.outputs.pyright) }} + experimental: [false] + fail-fast: false + continue-on-error: ${{ matrix.experimental }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up environment + id: setup-env + uses: ./.github/actions/setup-env + env: + PDM_USE_VENV: true + PDM_VENV_IN_PROJECT: true + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + # `--no-venv` to install in the main pdm venv instead of nox's pyright-specific one + # because nox uses pdm internally to install, this means that the venv that is used is + # the one that pdm would create in project: `.venv` + run: | + nox -s pyright --no-venv --install-only --force-python ${{ steps.setup-env.outputs.python-version }} + + - name: Add .venv/bin to PATH + run: dirname "$(pdm info --python)" >> $GITHUB_PATH + + - name: Set pyright version + id: pyright-version + run: | + echo "pyright-version=$(pdm run python -c 'import pyright; print(pyright.__pyright_version__)')" >> $GITHUB_OUTPUT + + - name: Run pyright (Linux) + uses: jakebailey/pyright-action@v2.2.1 + id: pyright-linux + with: + version: ${{ steps.pyright-version.outputs.pyright-version }} + python-version: ${{ steps.setup-env.outputs.python-version }} + python-platform: "Linux" + annotate: ${{ matrix.python-version == needs.python-versions.outputs.min-python }} # only add comments for one version + warnings: true + + - name: Run pyright (Windows) + uses: jakebailey/pyright-action@v2.2.1 + if: always() && (steps.pyright-linux.outcome == 'success' || steps.pyright-linux.outcome == 'failure') + with: + version: ${{ steps.pyright-version.outputs.pyright-version }} + python-version: ${{ steps.setup-env.outputs.python-version }} + python-platform: "Windows" + annotate: false # only add comments for one platform (see above) + warnings: true + + misc: + runs-on: ubuntu-latest + needs: + - python-versions + - lock-dependencies + - lint + steps: + - uses: actions/checkout@v4 + + - name: Set up environment + id: setup + uses: ./.github/actions/setup-env + with: + python-version: ${{ needs.python-versions.outputs.min-python }} + + - name: Run slotscheck + if: (success() || failure()) && steps.setup.outcome == 'success' + run: nox -s slotscheck + + - name: Run check-manifest + if: (success() || failure()) && steps.setup.outcome == 'success' + run: nox -s check-manifest + + # This only runs if the previous steps were successful, no point in running it otherwise + - name: Try building package + run: | + pdm install -dG build + + pdm run python -m build + ls -la dist/ + + - name: Check README.md renders properly on PyPI + run: | + pdm run twine check --strict dist/* + + codemod: + runs-on: ubuntu-latest + needs: + - python-versions + - lock-dependencies + - lint + steps: + - uses: actions/checkout@v4 + + - name: Set up environment + id: setup + uses: ./.github/actions/setup-env + with: + python-version: ${{ needs.python-versions.outputs.libcst }} + + # run the libcst parsers and check for changes + - name: libcst codemod + run: | + nox -s codemod -- run-all + + - name: Check for changes made by libcst codemod + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "::error::Please run 'nox -s codemod -- run-all' locally and commit the changes." >&2; + echo "$GITHUB_STEP_SUMMARY_HEADER" | sed "s/#name#/LibCST Codemod/" >> $GITHUB_STEP_SUMMARY + echo "The libcst codemod made changes to the codebase. Please run 'nox -s codemod -- run-all' locally and commit the changes." >> $GITHUB_STEP_SUMMARY + echo "::group::git diff" + git diff |& tee -a $GITHUB_STEP_SUMMARY + echo "::endgroup::" + echo "$GITHUB_STEP_SUMMARY_FOOTER" >> $GITHUB_STEP_SUMMARY + exit 1; + else + exit 0; + fi + + pytest: + runs-on: ${{ matrix.os }} + needs: + - python-versions + - lock-dependencies + - lint + strategy: + matrix: + os: ["windows-latest", "ubuntu-latest", "macos-latest"] + # TODO: add 3.14 once we switch to uv + python-version: ${{ fromJson(needs.python-versions.outputs.tests) }} + experimental: [false] + fail-fast: true + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@v4 + + - name: Set up environment + id: setup-env + uses: ./.github/actions/setup-env + with: + python-version: ${{ matrix.python-version }} + + - name: Install nox environment + run: | + nox -s test --no-venv --install-only --force-python ${{ steps.setup-env.outputs.python-version }} + + - name: Run pytest + id: run_tests + # use non-utc timezone, to test time/date-dependent features properly + env: + TZ: "America/New_York" + run: | + echo "$GITHUB_STEP_SUMMARY_HEADER" | sed "s/#name#/Test Summary/" >> $GITHUB_STEP_SUMMARY + pdm run nox --no-venv -s test -- --color=no --cov-report= | tee -a $GITHUB_STEP_SUMMARY + echo "$GITHUB_STEP_SUMMARY_FOOTER" >> $GITHUB_STEP_SUMMARY + + - name: Print Coverage Output + if: always() && (steps.run_tests.outcome == 'success' || steps.run_tests.outcome == 'failure') + run: | + echo "$GITHUB_STEP_SUMMARY_HEADER" | sed "s/#name#/Coverage Summary/" >> $GITHUB_STEP_SUMMARY + pdm run nox --no-venv -s coverage -- report | tee -a $GITHUB_STEP_SUMMARY + echo "$GITHUB_STEP_SUMMARY_FOOTER" >> $GITHUB_STEP_SUMMARY + + # thanks to aiohttp for this part of the workflow + check: # This job does nothing and is only used for the branch protection + if: always() + needs: + - lint + - docs + - pyright + - misc + - codemod + - pytest + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/disnake/.github/workflows/release.yaml b/disnake/.github/workflows/release.yaml new file mode 100644 index 0000000000..480b6cc996 --- /dev/null +++ b/disnake/.github/workflows/release.yaml @@ -0,0 +1,216 @@ +# SPDX-License-Identifier: MIT + +name: Release + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: read + +jobs: + # Builds sdist and wheel, runs `twine check`, and uploads artifacts. + build: + name: Build package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up environment + id: setup + uses: ./.github/actions/setup-env + with: + python-version: 3.8 + + - name: Install dependencies + run: pdm install -dG build + + - name: Build package + run: | + pdm run python -m build + ls -la dist/ + + - name: Twine check + run: pdm run twine check --strict dist/* + + - name: Show metadata + run: | + mkdir out/ + tar -xf dist/*.tar.gz -C out/ + + echo -e "
Metadata\n" >> $GITHUB_STEP_SUMMARY + cat out/*/PKG-INFO | sed 's/^/ /' | tee -a $GITHUB_STEP_SUMMARY + echo -e "\n
\n" >> $GITHUB_STEP_SUMMARY + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + if-no-files-found: error + + + # Ensures that git tag and built version match. + validate-tag: + name: Validate tag + runs-on: ubuntu-latest + needs: + - build + env: + GIT_TAG: ${{ github.ref_name }} + outputs: + bump_dev: ${{ steps.check-dev.outputs.bump_dev }} + + steps: + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Compare sdist version to git tag + run: | + mkdir out/ + tar -xf dist/*.tar.gz -C out/ + + SDIST_VERSION="$(grep "^Version:" out/*/PKG-INFO | cut -d' ' -f2-)" + echo "git tag: $GIT_TAG" + echo "sdist version: $SDIST_VERSION" + + if [ "$GIT_TAG" != "v$SDIST_VERSION" ]; then + echo "error: git tag does not match sdist version" >&2 + exit 1 + fi + + - name: Determine if dev version PR is needed + id: check-dev + run: | + BUMP_DEV= + # if this is a new major/minor version, create a PR later + if [[ "$GIT_TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then + BUMP_DEV=1 + fi + echo "bump_dev=$BUMP_DEV" | tee -a $GITHUB_OUTPUT + + + # Creates a draft release on GitHub, and uploads the artifacts there. + release-github: + name: Create GitHub draft release + runs-on: ubuntu-latest + needs: + - build + - validate-tag + permissions: + contents: write # required for creating releases + + steps: + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Calculate versions + id: versions + env: + GIT_TAG: ${{ github.ref_name }} + run: | + # v1.2.3 -> v1-2-3 (for changelog) + echo "docs_version=${GIT_TAG//./-}" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2.0.4 + with: + files: dist/* + draft: true + body: | + TBD. + + **Changelog**: https://docs.disnake.dev/en/stable/whats_new.html#${{ steps.versions.outputs.docs_version }} + **Git history**: https://github.com/${{ github.repository }}/compare/vTODO...${{ github.ref_name }} + + + # Creates a PyPI release (using an environment which requires separate confirmation). + release-pypi: + name: Publish package to pypi.org + environment: + name: release-pypi + url: https://pypi.org/project/disnake/ + runs-on: ubuntu-latest + needs: + - build + - validate-tag + permissions: + id-token: write # this permission is mandatory for trusted publishing + + steps: + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Upload to pypi + uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 + with: + print-hash: true + + + # Creates a PR to bump to an alpha version for development, if applicable. + create-dev-version-pr: + name: Create dev version bump PR + runs-on: ubuntu-latest + if: needs.validate-tag.outputs.bump_dev + needs: + - validate-tag + - release-github + - release-pypi + + steps: + # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow + - name: Generate app token + id: generate_token + uses: actions/create-github-app-token@f2acddfb5195534d487896a656232b016a682f3c # v1.9.0 + with: + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + token: ${{ steps.generate_token.outputs.token }} + persist-credentials: false + ref: master # the PR action wants a proper base branch + + - name: Set git name/email + env: + GIT_USER: ${{ vars.GIT_APP_USER_NAME }} + GIT_EMAIL: ${{ vars.GIT_APP_USER_EMAIL }} + run: | + git config user.name "$GIT_USER" + git config user.email "$GIT_EMAIL" + + - name: Update version to dev + id: update-version + run: | + NEW_VERSION="$(python scripts/ci/versiontool.py --set dev)" + git commit -a -m "chore: update version to v$NEW_VERSION" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Create pull request + uses: peter-evans/create-pull-request@70a41aba780001da0a30141984ae2a0c95d8704e # v6.0.2 + with: + token: ${{ steps.generate_token.outputs.token }} + branch: auto/dev-v${{ steps.update-version.outputs.new_version }} + delete-branch: true + base: master + title: "chore: update version to v${{ steps.update-version.outputs.new_version }}" + body: | + Automated dev version PR. + + https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + labels: | + skip news + t: meta diff --git a/disnake/.github/workflows/semantic-pr-title.yml b/disnake/.github/workflows/semantic-pr-title.yml new file mode 100644 index 0000000000..72db88061e --- /dev/null +++ b/disnake/.github/workflows/semantic-pr-title.yml @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: MIT + +name: Validate PR Title + +on: + pull_request_target: + types: + - opened + - reopened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + validate-pr-title: + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/disnake/.gitignore b/disnake/.gitignore new file mode 100644 index 0000000000..d5b3a7975b --- /dev/null +++ b/disnake/.gitignore @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: MIT + +*.json +*.py[cod] +*.log +*.log.[0-9]* +*.egg-info +venv +.venv +docs/_build +docs/crowdin.py +*.buildinfo +*.mp3 +*.m4a +*.wav +*.jpg +*.flac +*.mo +test.py +TODO.md +build +dist +ADVANCED.rst +secrets/ +*_test.py +*.env +!example.env +venvs/ +.nox/ +.idea +.coverage* +coverage.xml +__pypackages__/ +.pdm.toml +.pdm-python +pdm.lock diff --git a/disnake/.libcst.codemod.yaml b/disnake/.libcst.codemod.yaml new file mode 100644 index 0000000000..9ff84d61b2 --- /dev/null +++ b/disnake/.libcst.codemod.yaml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: MIT + +# String that LibCST should look for in code which indicates that the +# module is generated code. +generated_code_marker: '@generated_module' +# Command line and arguments for invoking a code formatter. Anything +# specified here must be capable of taking code via stdin and returning +# formatted code via stdout. +formatter: ['ruff', 'format', '-'] +# List of regex patterns which LibCST will evaluate against filenames to +# determine if the module should be touched. +blacklist_patterns: [] +# List of modules that contain codemods inside of them. +modules: + - 'scripts.codemods' + - 'autotyping' +# - 'libcst.codemod.commands' +# Absolute or relative path of the repository root, used for providing +# full-repo metadata. Relative paths should be specified with this file +# location as the base. +repo_root: '.' diff --git a/disnake/.pre-commit-config.yaml b/disnake/.pre-commit-config.yaml new file mode 100644 index 0000000000..2dedadf9a5 --- /dev/null +++ b/disnake/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: MIT + +## Pre-commit setup + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.15.0 + hooks: + - id: pretty-format-yaml + args: [--autofix, --indent, '2', --offset, '2', '--preserve-quotes'] + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + args: ['--write'] + additional_dependencies: + - tomli + + - repo: local + hooks: + - id: no-symlinks + name: no symlinks + description: "Check for symlinks" + entry: "symlinks may not be committed due to platform support" + language: fail + types: [symlink] + - id: changelogs-name + name: check changelog fragment name + entry: changelog filenames should be formatted like `..rst` + language: fail + files: 'changelog/' + exclude: '/(\d+\.[a-z]+(\.\d+)?\.rst|_template.rst.jinja|README.rst)$' + - id: spdx-licensing + name: SPDX License Identifiers + description: Check for the SPDX-License-Identifier in each file. + language: pygrep + entry: 'SPDX-License-Identifier: ' + args: [--negate] + types: [text] + exclude_types: [json, pofile] + exclude: 'changelog/|py.typed|disnake/bin/COPYING|.github/PULL_REQUEST_TEMPLATE.md|.github/CODEOWNERS|LICENSE|MANIFEST.in|.gitattributes' + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.12.12 + hooks: + - id: ruff-format + - id: ruff-check + args: [--fix, --fixable=I] diff --git a/disnake/.readthedocs.yml b/disnake/.readthedocs.yml new file mode 100644 index 0000000000..6cc4e82301 --- /dev/null +++ b/disnake/.readthedocs.yml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MIT + +version: 2 +formats: + - htmlzip +build: + os: ubuntu-24.04 + tools: + python: "3.8" +sphinx: + configuration: docs/conf.py + fail_on_warning: false + builder: html +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/disnake/CONTRIBUTING.md b/disnake/CONTRIBUTING.md new file mode 100644 index 0000000000..49a4a3efc3 --- /dev/null +++ b/disnake/CONTRIBUTING.md @@ -0,0 +1,128 @@ + + +# Contributing to disnake + +First off, thanks for taking the time to contribute! It makes the library substantially better. :tada: + +The following is a set of guidelines for contributing to the repository. These are not necessarily hard rules, but they streamline the process for everyone involved. + +### Table of Contents + +- [Bug Reports](#good-bug-reports) +- [Creating Pull Requests](#creating-a-pull-request) + - [Overview](#overview) + - [Initial setup](#initial-setup) + - [Commit/PR Naming Guidelines](#commitpr-naming-guidelines) + + +## This is too much to read! I want to ask a question! + +> [!IMPORTANT] +> Please try your best not to create new issues in the issue tracker just to ask questions, unless they provide value to a larger audience. + +Generally speaking, questions are better suited in our resources below. + +- The official Discord server: https://discord.gg/disnake +- The [FAQ in the documentation](https://docs.disnake.dev/en/latest/faq.html) +- The project's [discussions section](https://github.com/DisnakeDev/disnake/discussions) + +--- + +## Good Bug Reports + +To report bugs (or to suggest new features), visit our [issue tracker](https://github.com/DisnakeDev/disnake/issues). +The issue templates will generally walk you through the steps, but please be aware of the following things: + +1. **Don't open duplicate issues**. Before you submit an issue, search the issue tracker to see if an issue for your problem already exists. If you find a similar issue, you can add a comment with additional information or context to help us understand the problem better. +2. **Include the *complete* traceback** when filing a bug report about exceptions or tracebacks. Without the complete traceback, it will be much more difficult for others to understand (and perhaps fix) your issue. +3. **Add a minimal reproducible code snippet** that results in the behavior you're seeing. This helps us quickly confirm a bug or point out a solution to your problem. We cannot reliably investigate bugs without a way to reproduce them. + +If the bug report is missing this information, it'll take us longer to fix the issue. We may ask for clarification, and if no response was given, the issue will be closed. + +--- + +## Creating a Pull Request + +Creating a pull request is fairly straightforward. Make sure it focuses on a single aspect and avoids scope creep, then it's probably good to go. + +If you're unsure about some aspect of development, feel free to use existing files as a guide or reach out via the Discord server. + +### Overview + +The general workflow can be summarized as follows: + +1. Fork + clone the repository. +2. Initialize the development environment: `pdm run setup_env`. +3. Create a new branch. +4. Commit your changes, update documentation if required. +5. Add a changelog entry (e.g. `changelog/1234.feature.rst`). +6. Push the branch to your fork, and [submit a pull request!](https://github.com/DisnakeDev/disnake/compare) + +Specific development aspects are further explained below. + + +### Initial setup + +We use [`PDM`](https://pdm-project.org/) as our dependency manager. If it isn't already installed on your system, you can follow the installation steps [here](https://pdm-project.org/latest/#installation) to get started. + +Once PDM is installed, use the following command to initialize a virtual environment, install the necessary development dependencies, and install the [`pre-commit`](#pre-commit) hooks. +``` +$ pdm run setup_env +``` + +Other tools used in this project include [ruff](https://docs.astral.sh/ruff) (formatter and linter), and [pyright](https://microsoft.github.io/pyright/#/) (type-checker). For the most part, these automatically run on every commit with no additional action required - see below for details. + +All of the following checks also automatically run for every PR on GitHub, so don't worry if you're not sure whether you missed anything. A PR cannot be merged as long as there are any failing checks. + + +### Commit/PR Naming Guidelines + +This project uses the commonly known [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/). +While not necessarily required (but appreciated) for individual commit messages, please make sure to title your PR according to this schema: + +``` +(): + │ │ │ │ + │ │ │ └─⫸ Summary in present tense, not capitalized, no period at the end + │ │ │ + │ │ └─⫸ [optional] `!` indicates a breaking change + │ │ + │ └─⫸ [optional] Commit Scope: The affected area, e.g. gateway, user, ... + │ + └─⫸ Commit Type: feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert +``` + +Examples: `feat: support new avatar format` or `fix(gateway): use correct url for resuming connection`. +Details about the specific commit types can be found [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json). + + +### Formatting + +This project follows PEP-8 guidelines (mostly) with a column limit of 100 characters, and uses the tools mentioned above to enforce a consistent coding style. + +The installed [`pre-commit`](https://pre-commit.com/) hooks will automatically run before every commit, which will format/lint the code +to match the project's style. Note that you will have to stage and commit again if anything was updated! +Most of the time, running pre-commit will automatically fix any issues that arise. + + +### Pyright + +For type-checking, run `pdm run pyright` (append `-w` to have it automatically re-check on every file change). +> [!NOTE] +> If you're using VSCode and pylance, it will use the same type-checking settings, which generally means that you don't necessarily have to run `pyright` separately. +> However, since we use a specific version of `pyright` (which may not match pylance's version), there can be version differences which may lead to different results. + + +### Changelogs + +We use [towncrier](https://github.com/twisted/towncrier) for managing our changelogs. Each change is required to have at least one file in the [`changelog/`](changelog/README.rst) directory, unless it's a trivial change. There is more documentation in that directory on how to create a changelog entry. + + +### Documentation +We use Sphinx to build the project's documentation, which includes [automatically generating](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html) the API Reference from docstrings using the [NumPy style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html). +To build the documentation locally, use `pdm run docs` and visit http://127.0.0.1:8009/ once built. + +Changes should be marked with a [version directive](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#describing-changes-between-versions) as documented on the Sphinx documentation. + +For the `version` argument, provide ``|vnext|`` as the argument. +We have a custom role which replaces ``|vnext|`` with the next version of the library upon building the documentation. diff --git a/disnake/LICENSE b/disnake/LICENSE new file mode 100644 index 0000000000..4c0a44eb30 --- /dev/null +++ b/disnake/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Disnake Development + +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/disnake/MANIFEST.in b/disnake/MANIFEST.in new file mode 100644 index 0000000000..462660ad53 --- /dev/null +++ b/disnake/MANIFEST.in @@ -0,0 +1,8 @@ +include README.md +include LICENSE +include disnake/bin/* +include disnake/py.typed +include disnake/ext/commands/py.typed +include disnake/ext/tasks/py.typed +global-exclude *.py[cod] +exclude pdm.lock diff --git a/disnake/README.md b/disnake/README.md new file mode 100644 index 0000000000..49d7a80dfc --- /dev/null +++ b/disnake/README.md @@ -0,0 +1,117 @@ + + +[![Disnake Banner](https://raw.githubusercontent.com/DisnakeDev/disnake/master/assets/banner.png)](https://disnake.dev/) + +disnake +======= +

+ + Discord server invite + PyPI version info + PyPI supported Python versions + Commit activity +

+ +A modern, easy to use, feature-rich, and async-ready API wrapper for Discord written in Python. + +Key Features +------------ + +- Proper rate limit handling. +- Type-safety measures. +- [FastAPI](https://fastapi.tiangolo.com/)-like slash command syntax. + +The syntax and structure of `discord.py 2.0` is preserved. + +Installing +---------- + +**Python 3.8 or higher is required.** + +To install the library without full voice support, you can just run the +following command: + +``` sh +# Linux/macOS +python3 -m pip install -U disnake + +# Windows +py -3 -m pip install -U disnake +``` + +Installing `disnake` with full voice support requires you to replace `disnake` here, with `disnake[voice]`. To learn more about voice support (or installing the development version), please visit [this section of our guide](https://guide.disnake.dev/prerequisites/installing-disnake/). + +(You can optionally install [PyNaCl](https://pypi.org/project/PyNaCl/) for voice support.) + +Note that voice support on Linux requires installation of `libffi-dev` and `python-dev` packages, via your preferred package manager (e.g. `apt`, `dnf`, etc.) before running the following commands. + +Versioning +---------- + +This project does **not** quite follow semantic versioning; for more details, see [version guarantees](https://docs.disnake.dev/en/latest/version_guarantees.html). + +To be on the safe side and avoid unexpected breaking changes, pin the dependency to a minor version (e.g. `disnake==a.b.*` or `disnake~=a.b.c`) or an exact version (e.g. `disnake==a.b.c`). + +Quick Example +------------- + +### Slash Commands Example + +``` py +import disnake +from disnake.ext import commands + +bot = commands.InteractionBot(test_guilds=[12345]) + +@bot.slash_command() +async def ping(inter): + await inter.response.send_message("Pong!") + +bot.run("BOT_TOKEN") +``` + +### Context Menus Example + +``` py +import disnake +from disnake.ext import commands + +bot = commands.InteractionBot(test_guilds=[12345]) + +@bot.user_command() +async def avatar(inter, user): + embed = disnake.Embed(title=str(user)) + embed.set_image(url=user.display_avatar.url) + await inter.response.send_message(embed=embed) + +bot.run("BOT_TOKEN") +``` + +### Prefix Commands Example + +``` py +import disnake +from disnake.ext import commands + +bot = commands.Bot(command_prefix=commands.when_mentioned) + +@bot.command() +async def ping(ctx): + await ctx.send("Pong!") + +bot.run("BOT_TOKEN") +``` + +You can find more examples in the [examples directory](https://github.com/DisnakeDev/disnake/tree/master/examples). + +
+

+ Documentation + ⁕ + Guide + ⁕ + Discord Server + ⁕ + Discord Developers +

+
diff --git a/disnake/RELEASE.md b/disnake/RELEASE.md new file mode 100644 index 0000000000..3919b696e5 --- /dev/null +++ b/disnake/RELEASE.md @@ -0,0 +1,47 @@ + + +# Release Procedure + +This document provides general information and steps about the project's release procedure. +If you're reading this, this will likely not be useful to you, unless you have administrator permissions in the repository or want to replicate this setup in your own project :p + +The process is largely automated, with manual action only being needed where higher permissions are required. +Note that pre-releases (alpha/beta/rc) don't quite work with the current setup; we don't currently anticipate making pre-releases, but this may still be improved in the future. + + +## Steps + +These steps are mostly equivalent for major/minor (feature) and micro (bugfix) releases. +The branch should be `master` for major/minor releases and e.g. `1.2.x` for micro releases. + +1. Run the `Create Release PR` workflow from the GitHub UI (or CLI), specifying the correct branch and new version. + 1. Wait until a PR containing the changelog and version bump is created. Update the changelog description and merge the PR. + 2. In the CLI, fetch changes and create + push a tag for the newly created commit, which will trigger another workflow. + - [if latest] Also force-push a `stable` tag for the same ref. + 3. Update the visibility of old/new versions on https://readthedocs.org. +2. Approve the environment deployment when prompted, which will push the package to PyPI. + 1. Update and publish the created GitHub draft release, as well as a Discord announcement. 🎉 +3. [if major/minor] Create a `v1.2.x` branch for future backports, and merge the newly created dev version PR. + + +### Manual Steps + +If the automated process above does not work for some reason, here's the abridged version of the manual release process: + +1. Update version in `__init__.py`, run `towncrier build`. Commit, push, create + merge PR. +2. Follow steps 1.ii. + 1.iii. like above. +3. Run `python -m build`, attach artifacts to GitHub release. +4. Run `twine check dist/*` + `twine upload dist/*`. +5. Follow steps 2.i. + 3. like above. + + +## Repository Setup + +This automated process requires some initial one-time setup in the repository to work properly: + +1. Create a GitHub App ([docs](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow)), enable write permissions for `content` and `pull_requests`. +2. Install the app in the repository. +3. Set repository variables `GIT_APP_USER_NAME` and `GIT_APP_USER_EMAIL` accordingly. +4. Set repository secrets `BOT_APP_ID` and `BOT_PRIVATE_KEY`. +5. Create a `release-pypi` environment, add protection rules. +6. Set up trusted publishing on PyPI ([docs](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)). diff --git a/disnake/assets/banner.png b/disnake/assets/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..03dd68bb055a1e7598a3719173f0a04c912d6970 GIT binary patch literal 132183 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¼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%0ANTAwN;fh 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..rst``, where +```` is an issue number, and ```` is one of: + +* ``breaking``: a change which may break existing code, such as feature removal or behavior change. +* ``deprecate``: feature deprecation. +* ``feature``: new user facing features, support of new API features, and/or otherwise new behavior. +* ``bugfix``: fixes a bug. +* ``doc``: documentation improvement. +* ``misc``: fixing a small typo or internal change that might be noteworthy. + +So for example: ``123.feature.rst``, ``456.bugfix.rst``. + +If your PR fixes an issue, use that number here. If there is no issue, +then after you submit the PR and get the PR number you can add a +changelog using that instead. + +If there are multiple changes of the same type for the same issue, use the following naming for the conflicting changes: +``...rst`` + +If you are not sure what issue type to use, don't hesitate to ask in your PR. + +``towncrier`` preserves multiple paragraphs and formatting (code blocks, lists, and so on), but for entries +other than ``features`` it is usually better to stick to a single paragraph to keep it concise. + +You can also run ``pdm run docs`` to build the documentation +with the draft changelog (http://127.0.0.1:8009/whats_new.html) if you want to get a preview of how your change will look in the final release notes. + + +~~~~~ + +This file is adapted from `pytest's changelog documentation `_ diff --git a/disnake/changelog/_template.rst.jinja b/disnake/changelog/_template.rst.jinja new file mode 100644 index 0000000000..307341a2c8 --- /dev/null +++ b/disnake/changelog/_template.rst.jinja @@ -0,0 +1,32 @@ +.. _vp{{ versiondata.version.replace(".", "p") }}: + +{{ "v" + versiondata.version }} +{{ top_underline * ((versiondata.version)|length + 1) }} +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %} + +{% if sections[section] %} +{% for category, category_data in definitions.items() if category in sections[section] %} +{{ category_data['name'] }} +{{ underline * category_data['name']|length }} +{% if category_data['showcontent'] %} +{% for text, issues in sections[section][category].items() %} +{% set lines = text.strip().splitlines() %} +- {{ lines[0] }} {% if issues %}({{ issues|join(', ') }}){% endif %} + +{% if lines|length > 1 %} + {{ lines[1:]|join('\n ') }} +{% endif %} +{% endfor %} + +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} diff --git a/disnake/disnake/__init__.py b/disnake/disnake/__init__.py new file mode 100644 index 0000000000..92eae6bec7 --- /dev/null +++ b/disnake/disnake/__init__.py @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: MIT + +"""Discord API Wrapper +~~~~~~~~~~~~~~~~~~~ + +A basic wrapper for the Discord API. + +:copyright: (c) 2015-2021 Rapptz, 2021-present Disnake Development +:license: MIT, see LICENSE for more details. + +""" + +__title__ = "disnake" +__author__ = "Rapptz, EQUENOS" +__license__ = "MIT" +__copyright__ = "Copyright 2015-present Rapptz, 2021-present EQUENOS" +__version__ = "2.12.0a" + +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + +import logging +from typing import Literal, NamedTuple + +from . import abc as abc, opus as opus, ui as ui, utils as utils # explicitly re-export modules +from .activity import * +from .app_commands import * +from .appinfo import * +from .application_role_connection import * +from .asset import * +from .audit_logs import * +from .automod import * +from .bans import * +from .channel import * +from .client import * +from .colour import * +from .components import * +from .custom_warnings import * +from .embeds import * +from .emoji import * +from .entitlement import * +from .enums import * +from .errors import * +from .file import * +from .flags import * +from .guild import * +from .guild_preview import * +from .guild_scheduled_event import * +from .i18n import * +from .integrations import * +from .interactions import * +from .invite import * +from .member import * +from .mentions import * +from .message import * +from .object import * +from .onboarding import * +from .partial_emoji import * +from .permissions import * +from .player import * +from .poll import * +from .raw_models import * +from .reaction import * +from .role import * +from .shard import * +from .sku import * +from .soundboard import * +from .stage_instance import * +from .sticker import * +from .subscription import * +from .team import * +from .template import * +from .threads import * +from .user import * +from .voice_client import * +from .voice_region import * +from .webhook import * +from .welcome_screen import * +from .widget import * + + +class VersionInfo(NamedTuple): + major: int + minor: int + micro: int + releaselevel: Literal["alpha", "beta", "candidate", "final"] + serial: int + + +# fmt: off +version_info: VersionInfo = VersionInfo(major=2, minor=12, micro=0, releaselevel="alpha", serial=0) +# fmt: on + +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/disnake/disnake/__main__.py b/disnake/disnake/__main__.py new file mode 100644 index 0000000000..e1a5d2cdfd --- /dev/null +++ b/disnake/disnake/__main__.py @@ -0,0 +1,419 @@ +# SPDX-License-Identifier: MIT + +import argparse +import importlib.metadata +import platform +import sys +from pathlib import Path +from typing import List, Tuple, Union + +import aiohttp + +import disnake + + +def show_version() -> None: + entries: List[str] = [] + + sys_ver = sys.version_info + entries.append( + f"- Python v{sys_ver.major}.{sys_ver.minor}.{sys_ver.micro}-{sys_ver.releaselevel}" + ) + disnake_ver = disnake.version_info + entries.append( + f"- disnake v{disnake_ver.major}.{disnake_ver.minor}.{disnake_ver.micro}-{disnake_ver.releaselevel}" + ) + try: + version = importlib.metadata.version("disnake") + except importlib.metadata.PackageNotFoundError: + pass + else: + entries.append(f" - disnake importlib.metadata: v{version}") + + entries.append(f"- aiohttp v{aiohttp.__version__}") + uname = platform.uname() + entries.append(f"- system info: {uname.system} {uname.release} {uname.version} {uname.machine}") + print("\n".join(entries)) + + +def core(parser: argparse.ArgumentParser, args) -> None: + # this method runs when no subcommands are provided + # as such, we can assume that we want to print help + if args.version: + show_version() + else: + parser.print_help() + + +_interaction_bot_init = """super().__init__(**kwargs)""" +_commands_bot_init = ( + 'super().__init__(command_prefix=commands.when_mentioned_or("{prefix}"), **kwargs)' +) +_bot_template = """#!/usr/bin/env python3 + +from disnake.ext import commands +import disnake +import config + + +class Bot(commands.{base}): + def __init__(self, **kwargs): + {init} + for cog in config.cogs: + try: + self.load_extension(cog) + except Exception as exc: + print(f"Could not load extension {{cog}} due to {{exc.__class__.__name__}}: {{exc}}") + + async def on_ready(self): + print(f"Logged on as {{self.user}} (ID: {{self.user.id}})") + + +bot = Bot() + +# write general commands here + +if __name__ == "__main__": + bot.run(config.token) +""" + +_gitignore_template = """# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Our configuration files +config.py +""" + +_cog_template = '''from disnake.ext import commands +import disnake + +class {name}(commands.Cog{attrs}): + """The description for {name} goes here.""" + + def __init__(self, bot): + self.bot = bot +{extra} +def setup(bot): + bot.add_cog({name}(bot)) +''' + +# everything that is a _cog_special_method goes here. +_cog_extras = """ + async def cog_load(self): + # (async) loading logic goes here + pass + + def cog_unload(self): + # clean up logic goes here + pass + + ### Prefix Commands ### + + async def cog_check(self, ctx): + # checks that apply to every prefix command in here + return True + + async def bot_check(self, ctx): + # checks that apply to every prefix command to the bot + return True + + async def bot_check_once(self, ctx): + # check that apply to every prefix command but is guaranteed to be called only once + return True + + async def cog_command_error(self, ctx, error): + # error handling to every prefix command in here + pass + + async def cog_before_invoke(self, ctx): + # called before a prefix command is called here + pass + + async def cog_after_invoke(self, ctx): + # called after a prefix command is called here + pass + + ### Slash Commands ### + + # These are similar to the ones in the previous section, but for slash commands + + async def cog_slash_command_check(self, inter): + return True + + async def bot_slash_command_check(self, inter): + return True + + async def bot_slash_command_check_once(self, inter): + return True + + async def cog_slash_command_error(self, inter, error): + ... + + async def cog_before_slash_command_invoke(self, inter): + ... + + async def cog_after_slash_command_invoke(self, inter): + ... + + ### Message (Context Menu) Commands ### + + async def cog_message_command_check(self, inter): + return True + + async def bot_message_command_check(self, inter): + return True + + async def bot_message_command_check_once(self, inter): + return True + + async def cog_message_command_error(self, inter, error): + ... + + async def cog_before_message_command_invoke(self, inter): + ... + + async def cog_after_message_command_invoke(self, inter): + ... + + ### User (Context Menu) Commands ### + + async def cog_user_command_check(self, inter): + return True + + async def bot_user_command_check(self, inter): + return True + + async def bot_user_command_check_once(self, inter): + return True + + async def cog_user_command_error(self, inter, error): + ... + + async def cog_before_user_command_invoke(self, inter): + ... + + async def cog_after_user_command_invoke(self, inter): + ... +""" + + +# certain file names and directory names are forbidden +# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx +# although some of this doesn't apply to Linux, we might as well be consistent +_ascii_table = dict.fromkeys('<>:"|?*', "-") + +# NUL (0) and 1-31 are disallowed +_byte_table = dict.fromkeys(map(chr, range(32))) +_base_table = {**_ascii_table, **_byte_table} + +_translation_table = str.maketrans(_base_table) + + +def to_path(parser, name: Union[str, Path], *, replace_spaces: bool = False) -> Path: + if isinstance(name, Path): + return name + + if sys.platform == "win32": + forbidden = ( + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + ) + if len(name) <= 4 and name.upper() in forbidden: + parser.error("invalid directory name given, use a different one") + + name = name.translate(_translation_table) + if replace_spaces: + name = name.replace(" ", "-") + return Path(name) + + +def newbot(parser, args) -> None: + new_directory = to_path(parser, args.directory) / to_path(parser, args.name) + + # as a note exist_ok for Path is a 3.5+ only feature + # since we already checked above that we're >3.5 + try: + new_directory.mkdir(exist_ok=True, parents=True) + except OSError as exc: + parser.error(f"could not create our bot directory ({exc})") + + cogs = new_directory / "cogs" + + try: + cogs.mkdir(exist_ok=True) + init = cogs / "__init__.py" + init.touch() + except OSError as exc: + print(f"warning: could not create cogs directory ({exc})") + + try: + with open(str(new_directory / "config.py"), "w", encoding="utf-8") as fp: + fp.write('token = "place your token here"\ncogs = []\n') + except OSError as exc: + parser.error(f"could not create config file ({exc})") + + try: + with open(str(new_directory / "bot.py"), "w", encoding="utf-8") as fp: + if args.interaction_client: + init = _interaction_bot_init + base = "AutoShardedInteractionBot" if args.sharded else "InteractionBot" + else: + init = _commands_bot_init.format(prefix=args.prefix) + base = "AutoShardedBot" if args.sharded else "Bot" + fp.write(_bot_template.format(base=base, init=init)) + except OSError as exc: + parser.error(f"could not create bot file ({exc})") + + if not args.no_git: + try: + with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp: + fp.write(_gitignore_template) + except OSError as exc: + print(f"warning: could not create .gitignore file ({exc})") + + print("successfully made bot at", new_directory) + + +def newcog(parser, args) -> None: + cog_dir = to_path(parser, args.directory) + try: + cog_dir.mkdir(exist_ok=True) + except OSError as exc: + print(f"warning: could not create cogs directory ({exc})") + + directory = cog_dir / to_path(parser, args.name) + directory = directory.with_suffix(".py") + try: + with open(str(directory), "w", encoding="utf-8") as fp: + attrs = "" + extra = _cog_extras if args.full else "" + if args.class_name: + name = args.class_name + else: + name = str(directory.stem) + if "-" in name or "_" in name: + translation = str.maketrans("-_", " ") + name = name.translate(translation).title().replace(" ", "") + else: + name = name.title() + + if args.display_name: + attrs += f', name="{args.display_name}"' + if args.hide_commands: + attrs += ", command_attrs=dict(hidden=True)" + fp.write(_cog_template.format(name=name, extra=extra, attrs=attrs)) + except OSError as exc: + parser.error(f"could not create cog file ({exc})") + else: + print("successfully made cog at", directory) + + +def add_newbot_args(subparser) -> None: + parser = subparser.add_parser("newbot", help="creates a command bot project quickly") + parser.set_defaults(func=newbot) + + parser.add_argument("name", help="the bot project name") + parser.add_argument( + "directory", help="the directory to place it in (default: .)", nargs="?", default=Path.cwd() + ) + + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--prefix", help="the bot prefix (default: $)", default="$", metavar="" + ) + group.add_argument( + "--app-commands-only", + help="whether to only process application commands", + action="store_true", + dest="interaction_client", + ) + parser.add_argument( + "--sharded", help="whether to use an automatically sharded bot", action="store_true" + ) + parser.add_argument( + "--no-git", help="do not create a .gitignore file", action="store_true", dest="no_git" + ) + + +def add_newcog_args(subparser) -> None: + parser = subparser.add_parser("newcog", help="creates a new cog template quickly") + parser.set_defaults(func=newcog) + + parser.add_argument("name", help="the cog name") + parser.add_argument( + "directory", + help="the directory to place it in (default: cogs)", + nargs="?", + default=Path("cogs"), + ) + parser.add_argument( + "--class-name", help="the class name of the cog (default: )", dest="class_name" + ) + parser.add_argument("--display-name", help="the cog name (default: )") + parser.add_argument( + "--hide-commands", help="whether to hide all commands in the cog", action="store_true" + ) + parser.add_argument("--full", help="add all special methods as well", action="store_true") + + +def parse_args() -> Tuple[argparse.ArgumentParser, argparse.Namespace]: + parser = argparse.ArgumentParser(prog="disnake", description="Tools for helping with disnake") + parser.add_argument("-v", "--version", action="store_true", help="shows the library version") + parser.set_defaults(func=core) + + subparser = parser.add_subparsers(dest="subcommand", title="subcommands") + add_newbot_args(subparser) + add_newcog_args(subparser) + return parser, parser.parse_args() + + +def main() -> None: + parser, args = parse_args() + args.func(parser, args) + + +if __name__ == "__main__": + main() diff --git a/disnake/disnake/abc.py b/disnake/disnake/abc.py new file mode 100644 index 0000000000..1cc6b95d6d --- /dev/null +++ b/disnake/disnake/abc.py @@ -0,0 +1,2088 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +import copy +from abc import ABC +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Mapping, + Optional, + Protocol, + Sequence, + Tuple, + TypeVar, + Union, + cast, + overload, + runtime_checkable, +) + +from . import utils +from .context_managers import Typing +from .enums import ( + ChannelType, + PartyType, + ThreadLayout, + ThreadSortOrder, + VideoQualityMode, + try_enum_to_int, +) +from .errors import ClientException +from .file import File +from .flags import ChannelFlags, MessageFlags +from .invite import Invite +from .mentions import AllowedMentions +from .object import Object +from .partial_emoji import PartialEmoji +from .permissions import PermissionOverwrite, Permissions +from .role import Role +from .sticker import GuildSticker, StandardSticker, StickerItem +from .utils import _overload_with_permissions +from .voice_client import VoiceClient, VoiceProtocol + +__all__ = ( + "Snowflake", + "User", + "PrivateChannel", + "GuildChannel", + "Messageable", + "Connectable", +) + +VoiceProtocolT = TypeVar("VoiceProtocolT", bound=VoiceProtocol) + +if TYPE_CHECKING: + from datetime import datetime + + from typing_extensions import Self + + from .asset import Asset + from .channel import CategoryChannel, DMChannel, GroupChannel, PartialMessageable + from .client import Client + from .embeds import Embed + from .emoji import Emoji + from .enums import InviteTarget + from .guild import Guild, GuildChannel as AnyGuildChannel, GuildMessageable + from .guild_scheduled_event import GuildScheduledEvent + from .iterators import ChannelPinsIterator, HistoryIterator + from .member import Member + from .message import Message, MessageReference, PartialMessage + from .poll import Poll + from .state import ConnectionState + from .threads import AnyThreadArchiveDuration, ForumTag + from .types.channel import ( + Channel as ChannelPayload, + DefaultReaction as DefaultReactionPayload, + GuildChannel as GuildChannelPayload, + OverwriteType, + PermissionOverwrite as PermissionOverwritePayload, + ) + from .types.guild import ChannelPositionUpdate as ChannelPositionUpdatePayload + from .types.threads import PartialForumTag as PartialForumTagPayload + from .ui._types import MessageComponents + from .ui.view import View + from .user import ClientUser + from .voice_region import VoiceRegion + + MessageableChannel = Union[GuildMessageable, DMChannel, GroupChannel, PartialMessageable] + # include non-messageable channels, e.g. category/forum + AnyChannel = Union[MessageableChannel, AnyGuildChannel] + + SnowflakeTime = Union["Snowflake", datetime] + +MISSING = utils.MISSING + + +@runtime_checkable +class Snowflake(Protocol): + """An ABC that details the common operations on a Discord model. + + Almost all :ref:`Discord models ` meet this + abstract base class. + + If you want to create a snowflake on your own, consider using + :class:`.Object`. + + Attributes + ---------- + id: :class:`int` + The model's unique ID. + """ + + __slots__ = () + id: int + + +@runtime_checkable +class User(Snowflake, Protocol): + """An ABC that details the common operations on a Discord user. + + The following classes implement this ABC: + + - :class:`~disnake.User` + - :class:`~disnake.ClientUser` + - :class:`~disnake.Member` + + This ABC must also implement :class:`~disnake.abc.Snowflake`. + + Attributes + ---------- + name: :class:`str` + The user's username. + discriminator: :class:`str` + The user's discriminator. + + .. note:: + This is being phased out by Discord; the username system is moving away from ``username#discriminator`` + to users having a globally unique username. + The value of a single zero (``"0"``) indicates that the user has been migrated to the new system. + See the `help article `__ for details. + + global_name: Optional[:class:`str`] + The user's global display name, if set. + This takes precedence over :attr:`.name` when shown. + + .. versionadded:: 2.9 + + bot: :class:`bool` + Whether the user is a bot account. + """ + + __slots__ = () + + name: str + discriminator: str + global_name: Optional[str] + bot: bool + + @property + def display_name(self) -> str: + """:class:`str`: Returns the user's display name.""" + raise NotImplementedError + + @property + def mention(self) -> str: + """:class:`str`: Returns a string that allows you to mention the given user.""" + raise NotImplementedError + + @property + def avatar(self) -> Optional[Asset]: + """Optional[:class:`~disnake.Asset`]: Returns an :class:`~disnake.Asset` for + the avatar the user has. + """ + raise NotImplementedError + + +# FIXME: this shouldn't be a protocol. isinstance(thread, PrivateChannel) returns true, and issubclass doesn't work. +@runtime_checkable +class PrivateChannel(Snowflake, Protocol): + """An ABC that details the common operations on a private Discord channel. + + The following classes implement this ABC: + + - :class:`~disnake.DMChannel` + - :class:`~disnake.GroupChannel` + + This ABC must also implement :class:`~disnake.abc.Snowflake`. + + Attributes + ---------- + me: :class:`~disnake.ClientUser` + The user representing yourself. + """ + + __slots__ = () + + me: ClientUser + + +class _Overwrites: + __slots__ = ("id", "allow", "deny", "type") + + ROLE = 0 + MEMBER = 1 + + def __init__(self, data: PermissionOverwritePayload) -> None: + self.id: int = int(data["id"]) + self.allow: int = int(data.get("allow", 0)) + self.deny: int = int(data.get("deny", 0)) + self.type: OverwriteType = data["type"] + + def _asdict(self) -> PermissionOverwritePayload: + return { + "id": self.id, + "allow": str(self.allow), + "deny": str(self.deny), + "type": self.type, + } + + def is_role(self) -> bool: + return self.type == 0 + + def is_member(self) -> bool: + return self.type == 1 + + +class GuildChannel(ABC): + """An ABC that details the common operations on a Discord guild channel. + + The following classes implement this ABC: + + - :class:`.TextChannel` + - :class:`.VoiceChannel` + - :class:`.CategoryChannel` + - :class:`.StageChannel` + - :class:`.ForumChannel` + - :class:`.MediaChannel` + + This ABC must also implement :class:`.abc.Snowflake`. + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`.Guild` + The guild the channel belongs to. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. + e.g. the top channel is position 0. + """ + + __slots__ = () + + id: int + name: str + guild: Guild + type: ChannelType + position: int + category_id: Optional[int] + _flags: int + _state: ConnectionState + _overwrites: List[_Overwrites] + + if TYPE_CHECKING: + + def __init__( + self, *, state: ConnectionState, guild: Guild, data: Mapping[str, Any] + ) -> None: ... + + def __str__(self) -> str: + return self.name + + @property + def _sorting_bucket(self) -> int: + raise NotImplementedError + + def _update(self, guild: Guild, data: Dict[str, Any]) -> None: + raise NotImplementedError + + async def _move( + self, + position: int, + parent_id: Optional[int] = None, + lock_permissions: bool = False, + *, + reason: Optional[str], + ) -> None: + if position < 0: + raise ValueError("Channel position cannot be less than 0.") + + http = self._state.http + bucket = self._sorting_bucket + channels = [c for c in self.guild.channels if c._sorting_bucket == bucket] + channels = cast("List[GuildChannel]", channels) + + channels.sort(key=lambda c: c.position) + + try: + # remove ourselves from the channel list + channels.remove(self) + except ValueError: + # not there somehow lol + return + else: + index = next( + (i for i, c in enumerate(channels) if c.position >= position), len(channels) + ) + # add ourselves at our designated position + channels.insert(index, self) + + payload: List[ChannelPositionUpdatePayload] = [] + for index, c in enumerate(channels): + d: ChannelPositionUpdatePayload = {"id": c.id, "position": index} + if parent_id is not MISSING and c.id == self.id: + d.update(parent_id=parent_id, lock_permissions=lock_permissions) + payload.append(d) + + await http.bulk_channel_update(self.guild.id, payload, reason=reason) + + async def _edit( + self, + *, + name: str = MISSING, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + type: ChannelType = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + bitrate: int = MISSING, + user_limit: int = MISSING, + rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + video_quality_mode: VideoQualityMode = MISSING, + flags: ChannelFlags = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + default_layout: ThreadLayout = MISSING, + reason: Optional[str] = None, + ) -> Optional[ChannelPayload]: + parent_id: Optional[int] + if category is not MISSING: + # if category is given, it's either `None` (no parent) or a category channel + parent_id = category.id if category else None + else: + # if it's not given, don't change the category + parent_id = MISSING + + rtc_region_payload: Optional[str] + if rtc_region is not MISSING: + rtc_region_payload = str(rtc_region) if rtc_region is not None else None + else: + rtc_region_payload = MISSING + + video_quality_mode_payload: Optional[int] + if video_quality_mode is not MISSING: + video_quality_mode_payload = int(video_quality_mode) + else: + video_quality_mode_payload = MISSING + + default_auto_archive_duration_payload: Optional[int] + if default_auto_archive_duration is not MISSING: + default_auto_archive_duration_payload = ( + int(default_auto_archive_duration) + if default_auto_archive_duration is not None + else default_auto_archive_duration + ) + else: + default_auto_archive_duration_payload = MISSING + + lock_permissions: bool = bool(sync_permissions) + + overwrites_payload: List[PermissionOverwritePayload] = MISSING + + if position is not MISSING: + await self._move( + position, parent_id=parent_id, lock_permissions=lock_permissions, reason=reason + ) + parent_id = MISSING # no need to change it again in the edit request below + elif lock_permissions: + if parent_id is not MISSING: + p_id = parent_id + else: + p_id = self.category_id + + if p_id is not None and (parent := self.guild.get_channel(p_id)): + overwrites_payload = [c._asdict() for c in parent._overwrites] + + if overwrites not in (MISSING, None): + overwrites_payload = [] + for target, perm in overwrites.items(): + if not isinstance(perm, PermissionOverwrite): + raise TypeError( + f"Expected PermissionOverwrite, received {perm.__class__.__name__}" + ) + + allow, deny = perm.pair() + payload: PermissionOverwritePayload = { + "allow": str(allow.value), + "deny": str(deny.value), + "id": target.id, + "type": _Overwrites.ROLE if isinstance(target, Role) else _Overwrites.MEMBER, + } + overwrites_payload.append(payload) + + type_payload: int + if type is not MISSING: + if not isinstance(type, ChannelType): + raise TypeError("type field must be of type ChannelType") + type_payload = type.value + else: + type_payload = MISSING + + flags_payload: int + if flags is not MISSING: + if not isinstance(flags, ChannelFlags): + raise TypeError("flags field must be of type ChannelFlags") + flags_payload = flags.value + else: + flags_payload = MISSING + + available_tags_payload: List[PartialForumTagPayload] = MISSING + if available_tags is not MISSING: + available_tags_payload = [tag.to_dict() for tag in available_tags] + + default_reaction_emoji_payload: Optional[DefaultReactionPayload] = MISSING + if default_reaction is not MISSING: + if default_reaction is not None: + emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) + default_reaction_emoji_payload = { + "emoji_name": emoji_name, + "emoji_id": emoji_id, + } + else: + default_reaction_emoji_payload = None + + default_sort_order_payload: Optional[int] = MISSING + if default_sort_order is not MISSING: + default_sort_order_payload = ( + try_enum_to_int(default_sort_order) if default_sort_order is not None else None + ) + + default_layout_payload: int = MISSING + if default_layout is not MISSING: + default_layout_payload = try_enum_to_int(default_layout) + + options: Dict[str, Any] = { + "name": name, + "parent_id": parent_id, + "topic": topic, + "bitrate": bitrate, + "nsfw": nsfw, + "user_limit": user_limit, + # note: not passing `position` as it already got updated before, if passed + "permission_overwrites": overwrites_payload, + "rate_limit_per_user": slowmode_delay, + "default_thread_rate_limit_per_user": default_thread_slowmode_delay, + "type": type_payload, + "rtc_region": rtc_region_payload, + "video_quality_mode": video_quality_mode_payload, + "default_auto_archive_duration": default_auto_archive_duration_payload, + "flags": flags_payload, + "available_tags": available_tags_payload, + "default_reaction_emoji": default_reaction_emoji_payload, + "default_sort_order": default_sort_order_payload, + "default_forum_layout": default_layout_payload, + } + options = {k: v for k, v in options.items() if v is not MISSING} + + if options: + return await self._state.http.edit_channel(self.id, reason=reason, **options) + + def _fill_overwrites(self, data: GuildChannelPayload) -> None: + self._overwrites = [] + everyone_index = 0 + everyone_id = self.guild.id + + for index, overridden in enumerate(data.get("permission_overwrites", [])): + overwrite = _Overwrites(overridden) + self._overwrites.append(overwrite) + + if overwrite.type == _Overwrites.MEMBER: + continue + + if overwrite.id == everyone_id: + # the @everyone role is not guaranteed to be the first one + # in the list of permission overwrites, however the permission + # resolution code kind of requires that it is the first one in + # the list since it is special. So we need the index so we can + # swap it to be the first one. + everyone_index = index + + # do the swap + tmp = self._overwrites + if tmp: + tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] + + @property + def changed_roles(self) -> List[Role]: + """List[:class:`.Role`]: Returns a list of roles that have been overridden from + their default values in the :attr:`.Guild.roles` attribute. + """ + ret: List[Role] = [] + g = self.guild + for overwrite in filter(lambda o: o.is_role(), self._overwrites): + role = g.get_role(overwrite.id) + if role is None: + continue + + role = copy.copy(role) + role.permissions.handle_overwrite(overwrite.allow, overwrite.deny) + ret.append(role) + return ret + + @property + def mention(self) -> str: + """:class:`str`: The string that allows you to mention the channel.""" + return f"<#{self.id}>" + + @property + def created_at(self) -> datetime: + """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + def overwrites_for(self, obj: Union[Role, User]) -> PermissionOverwrite: + """Returns the channel-specific overwrites for a member or a role. + + Parameters + ---------- + obj: Union[:class:`.Role`, :class:`.abc.User`] + The role or user denoting + whose overwrite to get. + + Returns + ------- + :class:`~disnake.PermissionOverwrite` + The permission overwrites for this object. + """ + predicate: Callable[[_Overwrites], bool] + if isinstance(obj, User): + predicate = lambda p: p.is_member() + elif isinstance(obj, Role): + predicate = lambda p: p.is_role() + else: + predicate = lambda p: True + + for overwrite in filter(predicate, self._overwrites): + if overwrite.id == obj.id: + allow = Permissions(overwrite.allow) + deny = Permissions(overwrite.deny) + return PermissionOverwrite.from_pair(allow, deny) + + return PermissionOverwrite() + + @property + def overwrites(self) -> Dict[Union[Role, Member], PermissionOverwrite]: + """Returns all of the channel's overwrites. + + This is returned as a dictionary where the key contains the target which + can be either a :class:`~disnake.Role` or a :class:`~disnake.Member` and the value is the + overwrite as a :class:`~disnake.PermissionOverwrite`. + + Returns + ------- + Dict[Union[:class:`~disnake.Role`, :class:`~disnake.Member`], :class:`~disnake.PermissionOverwrite`] + The channel's permission overwrites. + """ + ret = {} + for ow in self._overwrites: + allow = Permissions(ow.allow) + deny = Permissions(ow.deny) + overwrite = PermissionOverwrite.from_pair(allow, deny) + target = None + + if ow.is_role(): + target = self.guild.get_role(ow.id) + elif ow.is_member(): + target = self.guild.get_member(ow.id) + + # TODO: There is potential data loss here in the non-chunked + # case, i.e. target is None because get_member returned nothing. + # This can be fixed with a slight breaking change to the return type, + # i.e. adding disnake.Object to the list of it + # However, for now this is an acceptable compromise. + if target is not None: + ret[target] = overwrite + return ret + + @property + def category(self) -> Optional[CategoryChannel]: + """Optional[:class:`~disnake.CategoryChannel`]: The category this channel belongs to. + + If there is no category then this is ``None``. + """ + if isinstance(self.guild, Object): + return None + return self.guild.get_channel(self.category_id) # type: ignore + + @property + def permissions_synced(self) -> bool: + """:class:`bool`: Whether or not the permissions for this channel are synced with the + category it belongs to. + + If there is no category then this is ``False``. + + .. versionadded:: 1.3 + """ + if self.category_id is None or isinstance(self.guild, Object): + return False + + category = self.guild.get_channel(self.category_id) + return bool(category and category.overwrites == self.overwrites) + + @property + def flags(self) -> ChannelFlags: + """:class:`.ChannelFlags`: The channel flags for this channel. + + .. versionadded:: 2.6 + """ + return ChannelFlags._from_value(self._flags) + + @property + def jump_url(self) -> str: + """A URL that can be used to jump to this channel. + + .. versionadded:: 2.4 + + .. note:: + + This exists for all guild channels but may not be usable by the client for all guild channel types. + """ + return f"https://discord.com/channels/{self.guild.id}/{self.id}" + + def _apply_implict_permissions(self, base: Permissions) -> None: + # if you can't send a message in a channel then you can't have certain + # permissions as well + if not base.send_messages: + base.send_tts_messages = False + base.send_voice_messages = False + base.send_polls = False + base.mention_everyone = False + base.embed_links = False + base.attach_files = False + + # if you can't view a channel then you have no permissions there + if not base.view_channel: + denied = Permissions.all_channel() + base.value &= ~denied.value + + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + """Handles permission resolution for the :class:`~disnake.Member` + or :class:`~disnake.Role`. + + This function takes into consideration the following cases: + + - Guild owner + - Guild roles + - Channel overrides + - Member overrides + - Timeouts + + If a :class:`~disnake.Role` is passed, then it checks the permissions + someone with that role would have, which is essentially: + + - The default role permissions + - The permissions of the role used as a parameter + - The default role permission overwrites + - The permission overwrites of the role used as a parameter + + .. note:: + If the channel originated from an :class:`.Interaction` and + the :attr:`.guild` attribute is unavailable, such as with + user-installed applications in guilds, this method will not work + due to an API limitation. + Consider using :attr:`.Interaction.permissions` or :attr:`~.Interaction.app_permissions` instead. + + .. versionchanged:: 2.0 + The object passed in can now be a role object. + + Parameters + ---------- + obj: Union[:class:`~disnake.Member`, :class:`~disnake.Role`] + The object to resolve permissions for. This could be either + a member or a role. If it's a role then member overwrites + are not computed. + ignore_timeout: :class:`bool` + Whether or not to ignore the user's timeout. + Defaults to ``False``. + + .. versionadded:: 2.4 + + .. note:: + + This only applies to :class:`~disnake.Member` objects. + + .. versionchanged:: 2.6 + + The default was changed to ``False``. + + Raises + ------ + TypeError + ``ignore_timeout`` is only supported for :class:`~disnake.Member` objects. + + Returns + ------- + :class:`~disnake.Permissions` + The resolved permissions for the member or role. + """ + # The current cases can be explained as: + # Guild owner get all permissions -- no questions asked. Otherwise... + # The @everyone role gets the first application. + # After that, the applied roles that the user has in the channel + # (or otherwise) are then OR'd together. + # After the role permissions are resolved, the member permissions + # have to take into effect. + # After all that is done.. you have to do the following: + + # The operation first takes into consideration the denied + # and then the allowed. + + # Timeouted users have only view_channel and read_message_history + # if they already have them. + if ignore_timeout is not MISSING and isinstance(obj, Role): + raise TypeError("ignore_timeout is only supported for disnake.Member objects") + + if ignore_timeout is MISSING: + ignore_timeout = False + + if self.guild.owner_id == obj.id: + return Permissions.all() + + default = self.guild.default_role + base = Permissions(default.permissions.value) + + # Handle the role case first + if isinstance(obj, Role): + base.value |= obj._permissions + + if base.administrator: + return Permissions.all() + + # Apply @everyone allow/deny first since it's special + try: + maybe_everyone = self._overwrites[0] + if maybe_everyone.id == self.guild.id: + base.handle_overwrite(allow=maybe_everyone.allow, deny=maybe_everyone.deny) + except IndexError: + pass + + if obj.is_default(): + return base + + overwrite = utils.get(self._overwrites, type=_Overwrites.ROLE, id=obj.id) + if overwrite is not None: + base.handle_overwrite(overwrite.allow, overwrite.deny) + + return base + + roles = obj._roles + get_role = self.guild.get_role + + # Apply guild roles that the member has. + for role_id in roles: + role = get_role(role_id) + if role is not None: + base.value |= role._permissions + + # Guild-wide Administrator -> True for everything + # Bypass all channel-specific overrides + if base.administrator: + return Permissions.all() + + # Apply @everyone allow/deny first since it's special + try: + maybe_everyone = self._overwrites[0] + if maybe_everyone.id == self.guild.id: + base.handle_overwrite(allow=maybe_everyone.allow, deny=maybe_everyone.deny) + remaining_overwrites = self._overwrites[1:] + else: + remaining_overwrites = self._overwrites + except IndexError: + remaining_overwrites = self._overwrites + + denies = 0 + allows = 0 + + # Apply channel specific role permission overwrites + for overwrite in remaining_overwrites: + if overwrite.is_role() and roles.has(overwrite.id): + denies |= overwrite.deny + allows |= overwrite.allow + + base.handle_overwrite(allow=allows, deny=denies) + + # Apply member specific permission overwrites + for overwrite in remaining_overwrites: + if overwrite.is_member() and overwrite.id == obj.id: + base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) + break + + # if you have a timeout then you can't have any permissions + # except read messages and read message history + if not ignore_timeout and obj.current_timeout: + allowed = Permissions(view_channel=True, read_message_history=True) + base.value &= allowed.value + + return base + + async def delete(self, *, reason: Optional[str] = None) -> None: + """|coro| + + Deletes the channel. + + You must have :attr:`.Permissions.manage_channels` permission to do this. + + Parameters + ---------- + reason: Optional[:class:`str`] + The reason for deleting this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have proper permissions to delete the channel. + NotFound + The channel was not found or was already deleted. + HTTPException + Deleting the channel failed. + """ + await self._state.http.delete_channel(self.id, reason=reason) + + @overload + async def set_permissions( + self, + target: Union[Member, Role], + *, + overwrite: Optional[PermissionOverwrite] = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + @_overload_with_permissions + async def set_permissions( + self, + target: Union[Member, Role], + *, + reason: Optional[str] = ..., + add_reactions: Optional[bool] = ..., + administrator: Optional[bool] = ..., + attach_files: Optional[bool] = ..., + ban_members: Optional[bool] = ..., + change_nickname: Optional[bool] = ..., + connect: Optional[bool] = ..., + create_events: Optional[bool] = ..., + create_forum_threads: Optional[bool] = ..., + create_guild_expressions: Optional[bool] = ..., + create_instant_invite: Optional[bool] = ..., + create_private_threads: Optional[bool] = ..., + create_public_threads: Optional[bool] = ..., + deafen_members: Optional[bool] = ..., + embed_links: Optional[bool] = ..., + external_emojis: Optional[bool] = ..., + external_stickers: Optional[bool] = ..., + kick_members: Optional[bool] = ..., + manage_channels: Optional[bool] = ..., + manage_emojis: Optional[bool] = ..., + manage_emojis_and_stickers: Optional[bool] = ..., + manage_events: Optional[bool] = ..., + manage_guild: Optional[bool] = ..., + manage_guild_expressions: Optional[bool] = ..., + manage_messages: Optional[bool] = ..., + manage_nicknames: Optional[bool] = ..., + manage_permissions: Optional[bool] = ..., + manage_roles: Optional[bool] = ..., + manage_threads: Optional[bool] = ..., + manage_webhooks: Optional[bool] = ..., + mention_everyone: Optional[bool] = ..., + moderate_members: Optional[bool] = ..., + move_members: Optional[bool] = ..., + mute_members: Optional[bool] = ..., + pin_messages: Optional[bool] = ..., + priority_speaker: Optional[bool] = ..., + read_message_history: Optional[bool] = ..., + read_messages: Optional[bool] = ..., + request_to_speak: Optional[bool] = ..., + send_messages: Optional[bool] = ..., + send_messages_in_threads: Optional[bool] = ..., + send_polls: Optional[bool] = ..., + send_tts_messages: Optional[bool] = ..., + send_voice_messages: Optional[bool] = ..., + speak: Optional[bool] = ..., + start_embedded_activities: Optional[bool] = ..., + stream: Optional[bool] = ..., + use_application_commands: Optional[bool] = ..., + use_embedded_activities: Optional[bool] = ..., + use_external_apps: Optional[bool] = ..., + use_external_emojis: Optional[bool] = ..., + use_external_sounds: Optional[bool] = ..., + use_external_stickers: Optional[bool] = ..., + use_slash_commands: Optional[bool] = ..., + use_soundboard: Optional[bool] = ..., + use_voice_activation: Optional[bool] = ..., + view_audit_log: Optional[bool] = ..., + view_channel: Optional[bool] = ..., + view_creator_monetization_analytics: Optional[bool] = ..., + view_guild_insights: Optional[bool] = ..., + ) -> None: ... + + async def set_permissions( + self, + target, + *, + overwrite: Optional[PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + **permissions, + ) -> None: + """|coro| + + Sets the channel specific permission overwrites for a target in the + channel. + + The ``target`` parameter should either be a :class:`.Member` or a + :class:`.Role` that belongs to guild. + + The ``overwrite`` parameter, if given, must either be ``None`` or + :class:`.PermissionOverwrite`. For convenience, you can pass in + keyword arguments denoting :class:`.Permissions` attributes. If this is + done, then you cannot mix the keyword arguments with the ``overwrite`` + parameter. + + If the ``overwrite`` parameter is ``None``, then the permission + overwrites are deleted. + + You must have :attr:`.Permissions.manage_roles` permission to do this. + + .. note:: + + This method *replaces* the old overwrites with the ones given. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` instead of ``InvalidArgument``. + + Examples + -------- + Setting allow and deny: :: + + await message.channel.set_permissions(message.author, view_channel=True, + send_messages=False) + + Deleting overwrites :: + + await channel.set_permissions(member, overwrite=None) + + Using :class:`.PermissionOverwrite` :: + + overwrite = disnake.PermissionOverwrite() + overwrite.send_messages = False + overwrite.view_channel = True + await channel.set_permissions(member, overwrite=overwrite) + + Parameters + ---------- + target: Union[:class:`.Member`, :class:`.Role`] + The member or role to overwrite permissions for. + overwrite: Optional[:class:`.PermissionOverwrite`] + The permissions to allow and deny to the target, or ``None`` to + delete the overwrite. + **permissions + A keyword argument list of permissions to set for ease of use. + Cannot be mixed with ``overwrite``. + reason: Optional[:class:`str`] + The reason for doing this action. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to edit channel specific permissions. + HTTPException + Editing channel specific permissions failed. + NotFound + The role or member being edited is not part of the guild. + TypeError + ``overwrite`` is invalid, + the target type was not :class:`.Role` or :class:`.Member`, + both keyword arguments and ``overwrite`` were provided, + or invalid permissions were provided as keyword arguments. + """ + http = self._state.http + + if isinstance(target, User): + perm_type = _Overwrites.MEMBER + elif isinstance(target, Role): + perm_type = _Overwrites.ROLE + else: + raise TypeError("target parameter must be either Member or Role") + + if overwrite is MISSING: + if len(permissions) == 0: + raise TypeError("No overwrite provided.") + try: + overwrite = PermissionOverwrite(**permissions) + except (ValueError, TypeError) as e: + raise TypeError("Invalid permissions given to keyword arguments.") from e + else: + if len(permissions) > 0: + raise TypeError("Cannot mix overwrite and keyword arguments.") + + # TODO: wait for event + + if overwrite is None: + await http.delete_channel_permissions(self.id, target.id, reason=reason) + elif isinstance(overwrite, PermissionOverwrite): + (allow, deny) = overwrite.pair() + await http.edit_channel_permissions( + self.id, target.id, allow.value, deny.value, perm_type, reason=reason + ) + else: + raise TypeError("Invalid overwrite type provided.") + + async def _clone_impl( + self, + base_attrs: Dict[str, Any], + *, + name: Optional[str] = None, + category: Optional[Snowflake] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> Self: + # if the overwrites are MISSING, defaults to the + # original permissions of the channel + overwrites_payload: List[PermissionOverwritePayload] + if overwrites is not MISSING: + if not isinstance(overwrites, dict): + raise TypeError("overwrites parameter expects a dict.") + + overwrites_payload = [] + for target, perm in overwrites.items(): + if not isinstance(perm, PermissionOverwrite): + raise TypeError( + f"Expected PermissionOverwrite, received {perm.__class__.__name__}" + ) + + allow, deny = perm.pair() + payload: PermissionOverwritePayload = { + "allow": str(allow.value), + "deny": str(deny.value), + "id": target.id, + "type": (_Overwrites.ROLE if isinstance(target, Role) else _Overwrites.MEMBER), + } + overwrites_payload.append(payload) + else: + overwrites_payload = [x._asdict() for x in self._overwrites] + base_attrs["permission_overwrites"] = overwrites_payload + if category is not MISSING: + base_attrs["parent_id"] = category.id if category else None + else: + # if no category was given don't change the category + base_attrs["parent_id"] = self.category_id + base_attrs["name"] = name or self.name + channel_type = base_attrs.get("type") or self.type.value + guild_id = self.guild.id + cls = self.__class__ + data = await self._state.http.create_channel( + guild_id, channel_type, reason=reason, **base_attrs + ) + obj = cls(state=self._state, guild=self.guild, data=data) + + # temporarily add it to the cache + self.guild._channels[obj.id] = obj # type: ignore + return obj + + async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> Self: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionadded:: 1.1 + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel name. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`.abc.GuildChannel` + The channel that was created. + """ + raise NotImplementedError + + @overload + async def move( + self, + *, + beginning: bool, + offset: int = ..., + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def move( + self, + *, + end: bool, + offset: int = ..., + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def move( + self, + *, + before: Snowflake, + offset: int = ..., + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def move( + self, + *, + after: Snowflake, + offset: int = ..., + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + async def move(self, **kwargs: Any) -> None: + """|coro| + + A rich interface to help move a channel relative to other channels. + + If exact position movement is required, ``edit`` should be used instead. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. note:: + + Voice channels will always be sorted below text channels. + This is a Discord limitation. + + .. versionadded:: 1.7 + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + beginning: :class:`bool` + Whether to move the channel to the beginning of the + channel list (or category if given). + This is mutually exclusive with ``end``, ``before``, and ``after``. + end: :class:`bool` + Whether to move the channel to the end of the + channel list (or category if given). + This is mutually exclusive with ``beginning``, ``before``, and ``after``. + before: :class:`.abc.Snowflake` + The channel that should be before our current channel. + This is mutually exclusive with ``beginning``, ``end``, and ``after``. + after: :class:`.abc.Snowflake` + The channel that should be after our current channel. + This is mutually exclusive with ``beginning``, ``end``, and ``before``. + offset: :class:`int` + The number of channels to offset the move by. For example, + an offset of ``2`` with ``beginning=True`` would move + it 2 after the beginning. A positive number moves it below + while a negative number moves it above. Note that this + number is relative and computed after the ``beginning``, + ``end``, ``before``, and ``after`` parameters. + category: Optional[:class:`.abc.Snowflake`] + The category to move this channel under. + If ``None`` is given then it moves it out of the category. + This parameter is ignored if moving a category channel. + sync_permissions: :class:`bool` + Whether to sync the permissions with the category (if given). + reason: Optional[:class:`str`] + The reason for moving this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to move the channel. + HTTPException + Moving the channel failed. + TypeError + A bad mix of arguments were passed. + ValueError + An invalid position was given. + """ + if not kwargs: + return + + beginning, end = kwargs.get("beginning"), kwargs.get("end") + before, after = kwargs.get("before"), kwargs.get("after") + offset = kwargs.get("offset", 0) + if sum(bool(a) for a in (beginning, end, before, after)) > 1: + raise TypeError("Only one of [before, after, end, beginning] can be used.") + + bucket = self._sorting_bucket + parent_id = kwargs.get("category", MISSING) + if parent_id not in (MISSING, None): + parent_id = parent_id.id + channels = [ + ch + for ch in self.guild.channels + if ch._sorting_bucket == bucket and ch.category_id == parent_id + ] + else: + channels = [ + ch + for ch in self.guild.channels + if ch._sorting_bucket == bucket and ch.category_id == self.category_id + ] + + channels.sort(key=lambda c: (c.position, c.id)) + channels = cast("List[GuildChannel]", channels) + + try: + # Try to remove ourselves from the channel list + channels.remove(self) + except ValueError: + # If we're not there then it's probably due to not being in the category + pass + + index = None + if beginning: + index = 0 + elif end: + index = len(channels) + elif before: + index = next((i for i, c in enumerate(channels) if c.id == before.id), None) + elif after: + index = next((i + 1 for i, c in enumerate(channels) if c.id == after.id), None) + + if index is None: + raise ValueError("Could not resolve appropriate move position") + + channels.insert(max((index + offset), 0), self) + payload: List[ChannelPositionUpdatePayload] = [] + lock_permissions = kwargs.get("sync_permissions", False) + reason = kwargs.get("reason") + for index, channel in enumerate(channels): + d: ChannelPositionUpdatePayload = {"id": channel.id, "position": index} + if parent_id is not MISSING and channel.id == self.id: + d.update(parent_id=parent_id, lock_permissions=lock_permissions) + payload.append(d) + + await self._state.http.bulk_channel_update(self.guild.id, payload, reason=reason) + + async def create_invite( + self, + *, + reason: Optional[str] = None, + max_age: int = 0, + max_uses: int = 0, + temporary: bool = False, + unique: bool = True, + target_type: Optional[InviteTarget] = None, + target_user: Optional[User] = None, + target_application: Optional[Union[Snowflake, PartyType]] = None, + guild_scheduled_event: Optional[GuildScheduledEvent] = None, + ) -> Invite: + """|coro| + + Creates an instant invite from a text or voice channel. + + You must have :attr:`.Permissions.create_instant_invite` permission to + do this. + + Parameters + ---------- + max_age: :class:`int` + How long the invite should last in seconds. If set to ``0``, then the invite + doesn't expire. Defaults to ``0``. + max_uses: :class:`int` + How many uses the invite could be used for. If it's 0 then there + are unlimited uses. Defaults to ``0``. + temporary: :class:`bool` + Whether the invite grants temporary membership + (i.e. they get kicked after they disconnect). Defaults to ``False``. + unique: :class:`bool` + Whether a unique invite URL should be created. Defaults to ``True``. + If this is set to ``False`` then it will return a previously created + invite. + target_type: Optional[:class:`.InviteTarget`] + The type of target for the voice channel invite, if any. + + .. versionadded:: 2.0 + + target_user: Optional[:class:`User`] + The user whose stream to display for this invite, required if ``target_type`` is :attr:`.InviteTarget.stream`. + The user must be streaming in the channel. + + .. versionadded:: 2.0 + + target_application: Optional[:class:`.Snowflake`] + The ID of the embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.9 + ``PartyType`` is deprecated, and :class:`.Snowflake` should be used instead. + + guild_scheduled_event: Optional[:class:`.GuildScheduledEvent`] + The guild scheduled event to include with the invite. + + .. versionadded:: 2.3 + + reason: Optional[:class:`str`] + The reason for creating this invite. Shows up on the audit log. + + Raises + ------ + HTTPException + Invite creation failed. + NotFound + The channel that was passed is a category or an invalid channel. + + Returns + ------- + :class:`.Invite` + The newly created invite. + """ + if isinstance(target_application, PartyType): + utils.warn_deprecated( + "PartyType is deprecated and will be removed in future version", + stacklevel=2, + ) + target_application = Object(target_application.value) + data = await self._state.http.create_invite( + self.id, + reason=reason, + max_age=max_age, + max_uses=max_uses, + temporary=temporary, + unique=unique, + target_type=try_enum_to_int(target_type), + target_user_id=target_user.id if target_user else None, + target_application_id=target_application.id if target_application else None, + ) + invite = Invite.from_incomplete(data=data, state=self._state) + invite.guild_scheduled_event = guild_scheduled_event + return invite + + async def invites(self) -> List[Invite]: + """|coro| + + Returns a list of all active instant invites from this channel. + + You must have :attr:`.Permissions.manage_channels` permission to use this. + + Raises + ------ + Forbidden + You do not have proper permissions to get the information. + HTTPException + An error occurred while fetching the information. + + Returns + ------- + List[:class:`.Invite`] + The list of invites that are currently active. + """ + state = self._state + data = await state.http.invites_from_channel(self.id) + guild = self.guild + return [Invite(state=state, data=invite, channel=self, guild=guild) for invite in data] + + +class Messageable: + """An ABC that details the common operations on a model that can send messages. + + The following classes implement this ABC: + + - :class:`~disnake.TextChannel` + - :class:`~disnake.DMChannel` + - :class:`~disnake.GroupChannel` + - :class:`~disnake.User` + - :class:`~disnake.Member` + - :class:`~disnake.Thread` + - :class:`~disnake.VoiceChannel` + - :class:`~disnake.StageChannel` + - :class:`~disnake.ext.commands.Context` + - :class:`~disnake.PartialMessageable` + """ + + __slots__ = () + _state: ConnectionState + + async def _get_channel(self) -> MessageableChannel: + raise NotImplementedError + + @overload + async def send( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embed: Embed = ..., + file: File = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + components: MessageComponents = ..., + poll: Poll = ..., + ) -> Message: ... + + @overload + async def send( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embed: Embed = ..., + files: List[File] = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + components: MessageComponents = ..., + poll: Poll = ..., + ) -> Message: ... + + @overload + async def send( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embeds: List[Embed] = ..., + file: File = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + components: MessageComponents = ..., + poll: Poll = ..., + ) -> Message: ... + + @overload + async def send( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embeds: List[Embed] = ..., + files: List[File] = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + components: MessageComponents = ..., + poll: Poll = ..., + ) -> Message: ... + + async def send( + self, + content: Optional[str] = None, + *, + tts: bool = False, + embed: Optional[Embed] = None, + embeds: Optional[List[Embed]] = None, + file: Optional[File] = None, + files: Optional[List[File]] = None, + stickers: Optional[Sequence[Union[GuildSticker, StandardSticker, StickerItem]]] = None, + delete_after: Optional[float] = None, + nonce: Optional[Union[str, int]] = None, + suppress_embeds: Optional[bool] = None, + flags: Optional[MessageFlags] = None, + allowed_mentions: Optional[AllowedMentions] = None, + reference: Optional[Union[Message, MessageReference, PartialMessage]] = None, + mention_author: Optional[bool] = None, + view: Optional[View] = None, + components: Optional[MessageComponents] = None, + poll: Optional[Poll] = None, + ): + """|coro| + + Sends a message to the destination with the content given. + + The content must be a type that can convert to a string through ``str(content)``. + + At least one of ``content``, ``embed``/``embeds``, ``file``/``files``, + ``stickers``, ``components``, ``poll`` or ``view`` must be provided. + + To upload a single file, the ``file`` parameter should be used with a + single :class:`~disnake.File` object. To upload multiple files, the ``files`` + parameter should be used with a :class:`list` of :class:`~disnake.File` objects. + **Specifying both parameters will lead to an exception**. + + To upload a single embed, the ``embed`` parameter should be used with a + single :class:`.Embed` object. To upload multiple embeds, the ``embeds`` + parameter should be used with a :class:`list` of :class:`.Embed` objects. + **Specifying both parameters will lead to an exception**. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + content: Optional[:class:`str`] + The content of the message to send. + tts: :class:`bool` + Whether the message should be sent using text-to-speech. + embed: :class:`.Embed` + The rich embed for the content to send. This cannot be mixed with the + ``embeds`` parameter. + embeds: List[:class:`.Embed`] + A list of embeds to send with the content. Must be a maximum of 10. + This cannot be mixed with the ``embed`` parameter. + + .. versionadded:: 2.0 + + file: :class:`~disnake.File` + The file to upload. This cannot be mixed with the ``files`` parameter. + files: List[:class:`~disnake.File`] + A list of files to upload. Must be a maximum of 10. + This cannot be mixed with the ``file`` parameter. + stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StandardSticker`, :class:`.StickerItem`]] + A list of stickers to upload. Must be a maximum of 3. + + .. versionadded:: 2.0 + + nonce: Union[:class:`str`, :class:`int`] + The nonce to use for sending this message. If the message was successfully sent, + then the message will have a nonce with this value. + delete_after: :class:`float` + If provided, the number of seconds to wait in the background + before deleting the message we just sent. If the deletion fails, + then it is silently ignored. + allowed_mentions: :class:`.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`.Client.allowed_mentions` + are used instead. + + .. versionadded:: 1.4 + + reference: Union[:class:`.Message`, :class:`.MessageReference`, :class:`.PartialMessage`] + A reference to the :class:`.Message` to which you are replying, this can be created using + :meth:`.Message.to_reference` or passed directly as a :class:`.Message`. You can control + whether this mentions the author of the referenced message using the :attr:`.AllowedMentions.replied_user` + attribute of ``allowed_mentions`` or by setting ``mention_author``. + + .. versionadded:: 1.6 + + .. note:: + + Passing a :class:`.Message` or :class:`.PartialMessage` will only allow replies. To forward a message + you must explicitly transform the message to a :class:`.MessageReference` using :meth:`.Message.to_reference` and specify the :class:`.MessageReferenceType`, + or use :meth:`.Message.forward`. + + mention_author: Optional[:class:`bool`] + If set, overrides the :attr:`.AllowedMentions.replied_user` attribute of ``allowed_mentions``. + + .. versionadded:: 1.6 + + view: :class:`.ui.View` + A Discord UI View to add to the message. This cannot be mixed with ``components``. + + .. versionadded:: 2.0 + + components: |components_type| + A list of components to include in the message. This cannot be mixed with ``view``. + + .. versionadded:: 2.4 + + .. note:: + Passing v2 components here automatically sets the :attr:`~.MessageFlags.is_components_v2` flag. + Setting this flag cannot be reverted. Note that this also disables the + ``content``, ``embeds``, ``stickers``, and ``poll`` fields. + + suppress_embeds: :class:`bool` + Whether to suppress embeds for the message. This hides + all the embeds from the UI if set to ``True``. + + .. versionadded:: 2.5 + + flags: :class:`.MessageFlags` + The flags to set for this message. + Only :attr:`~.MessageFlags.suppress_embeds`, :attr:`~.MessageFlags.suppress_notifications`, + and :attr:`~.MessageFlags.is_components_v2` are supported. + + If parameter ``suppress_embeds`` is provided, + that will override the setting of :attr:`.MessageFlags.suppress_embeds`. + + .. versionadded:: 2.9 + + poll: :class:`.Poll` + The poll to send with the message. + + .. versionadded:: 2.10 + + Raises + ------ + HTTPException + Sending the message failed. + Forbidden + You do not have the proper permissions to send the message. + TypeError + Specified both ``file`` and ``files``, + or you specified both ``embed`` and ``embeds``, + or you specified both ``view`` and ``components``, + or the ``reference`` object is not a :class:`.Message`, + :class:`.MessageReference` or :class:`.PartialMessage`. + ValueError + The ``files`` or ``embeds`` list is too large, or + you tried to send v2 components together with ``content``, ``embeds``, ``stickers``, or ``poll``. + + Returns + ------- + :class:`.Message` + The message that was sent. + """ + channel = await self._get_channel() + state = self._state + content = str(content) if content is not None else None + + if file is not None and files is not None: + raise TypeError("cannot pass both file and files parameter to send()") + + if file is not None: + if not isinstance(file, File): + raise TypeError("file parameter must be File") + files = [file] + + if embed is not None and embeds is not None: + raise TypeError("cannot pass both embed and embeds parameter to send()") + + if embed is not None: + embeds = [embed] + + embeds_payload = None + if embeds is not None: + if len(embeds) > 10: + raise ValueError("embeds parameter must be a list of up to 10 elements") + for embed in embeds: + if embed._files: + files = files or [] + files.extend(embed._files.values()) + embeds_payload = [embed.to_dict() for embed in embeds] + + stickers_payload = None + if stickers is not None: + stickers_payload = [sticker.id for sticker in stickers] + + poll_payload = None + if poll: + poll_payload = poll._to_dict() + + allowed_mentions_payload = None + if allowed_mentions is None: + allowed_mentions_payload = state.allowed_mentions and state.allowed_mentions.to_dict() + elif state.allowed_mentions is not None: + allowed_mentions_payload = state.allowed_mentions.merge(allowed_mentions).to_dict() + else: + allowed_mentions_payload = allowed_mentions.to_dict() + + if mention_author is not None: + allowed_mentions_payload = allowed_mentions_payload or AllowedMentions().to_dict() + allowed_mentions_payload["replied_user"] = bool(mention_author) + + reference_payload = None + if reference is not None: + try: + reference_payload = reference.to_message_reference_dict() + except AttributeError: + raise TypeError( + "reference parameter must be Message, MessageReference, or PartialMessage" + ) from None + + is_v2 = False + if view is not None and components is not None: + raise TypeError("cannot pass both view and components parameter to send()") + elif view: + if not hasattr(view, "__discord_ui_view__"): + raise TypeError(f"view parameter must be View not {view.__class__!r}") + components_payload = view.to_components() + elif components: + from .ui.action_row import normalize_components_to_dict + + components_payload, is_v2 = normalize_components_to_dict(components) + else: + components_payload = None + + # set cv2 flag automatically + if is_v2: + flags = MessageFlags._from_value(0 if flags is None else flags.value) + flags.is_components_v2 = True + # components v2 cannot be used with other content fields + if flags and flags.is_components_v2 and (content or embeds or stickers or poll): + raise ValueError("Cannot use v2 components with content, embeds, stickers, or polls") + + flags_payload = None + if suppress_embeds is not None: + flags = MessageFlags._from_value(0 if flags is None else flags.value) + flags.suppress_embeds = suppress_embeds + if flags is not None: + flags_payload = flags.value + + if files is not None: + if len(files) > 10: + raise ValueError("files parameter must be a list of up to 10 elements") + elif not all(isinstance(file, File) for file in files): + raise TypeError("files parameter must be a list of File") + + try: + data = await state.http.send_files( + channel.id, + files=files, + content=content, + tts=tts, + embeds=embeds_payload, + nonce=nonce, + allowed_mentions=allowed_mentions_payload, + message_reference=reference_payload, + stickers=stickers_payload, + components=components_payload, + poll=poll_payload, + flags=flags_payload, + ) + finally: + for f in files: + f.close() + else: + data = await state.http.send_message( + channel.id, + content, + tts=tts, + embeds=embeds_payload, + nonce=nonce, + allowed_mentions=allowed_mentions_payload, + message_reference=reference_payload, + stickers=stickers_payload, + components=components_payload, + poll=poll_payload, + flags=flags_payload, + ) + + ret = state.create_message(channel=channel, data=data) + if view: + state.store_view(view, ret.id) + + if delete_after is not None: + await ret.delete(delay=delete_after) + return ret + + async def trigger_typing(self) -> None: + """|coro| + + Triggers a *typing* indicator to the destination. + + *Typing* indicator will go away after 10 seconds, or after a message is sent. + """ + channel = await self._get_channel() + await self._state.http.send_typing(channel.id) + + def typing(self) -> Typing: + """Returns a context manager that allows you to type for an indefinite period of time. + + This is useful for denoting long computations in your bot. + + .. note:: + + This is both a regular context manager and an async context manager. + This means that both ``with`` and ``async with`` work with this. + + Example Usage: :: + + async with channel.typing(): + # simulate something heavy + await asyncio.sleep(10) + + await channel.send('done!') + + """ + return Typing(self) + + async def fetch_message(self, id: int, /) -> Message: + """|coro| + + Retrieves a single :class:`.Message` from the destination. + + Parameters + ---------- + id: :class:`int` + The message ID to look for. + + Raises + ------ + NotFound + The specified message was not found. + Forbidden + You do not have the permissions required to get a message. + HTTPException + Retrieving the message failed. + + Returns + ------- + :class:`.Message` + The message asked for. + """ + channel = await self._get_channel() + data = await self._state.http.get_message(channel.id, id) + return self._state.create_message(channel=channel, data=data) + + def pins( + self, *, limit: Optional[int] = 50, before: Optional[SnowflakeTime] = None + ) -> ChannelPinsIterator: + """Returns an :class:`.AsyncIterator` that enables receiving the destination's pinned messages. + + You must have the :attr:`.Permissions.read_message_history` and :attr:`.Permissions.view_channel` permissions to use this. + + .. note:: + + Due to a limitation with the Discord API, the :class:`.Message` + objects returned by this method do not contain complete + :attr:`.Message.reactions` data. + + .. versionchanged:: 2.11 + Now returns an :class:`.AsyncIterator` to support changes in Discord's API. + ``await``\\ing the result of this method remains supported, but only returns the + last 50 pins and is deprecated in favor of ``async for msg in channel.pins()``. + + Examples + -------- + Usage :: + + counter = 0 + async for message in channel.pins(limit=100): + if message.author == client.user: + counter += 1 + + Flattening to a list :: + + pinned_messages = await channel.pins(limit=100).flatten() + # pinned_messages is now a list of Message... + + All parameters are optional. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of pinned messages to retrieve. + If ``None``, retrieves every pinned message in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages pinned before this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + + Raises + ------ + HTTPException + Retrieving the pinned messages failed. + + Yields + ------ + :class:`.Message` + The pinned message from the parsed message data. + """ + from .iterators import ChannelPinsIterator # due to cyclic imports + + return ChannelPinsIterator(self, limit=limit, before=before) + + def history( + self, + *, + limit: Optional[int] = 100, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + around: Optional[SnowflakeTime] = None, + oldest_first: Optional[bool] = None, + ) -> HistoryIterator: + """Returns an :class:`.AsyncIterator` that enables receiving the destination's message history. + + You must have :attr:`.Permissions.read_message_history` permission to use this. + + Examples + -------- + Usage :: + + counter = 0 + async for message in channel.history(limit=200): + if message.author == client.user: + counter += 1 + + Flattening into a list: :: + + messages = await channel.history(limit=123).flatten() + # messages is now a list of Message... + + All parameters are optional. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to retrieve. + If ``None``, retrieves every message in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages before this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages after this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + around: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages around this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + When using this argument, the maximum limit is 101. Note that if the limit is an + even number then this will return at most limit + 1 messages. + oldest_first: Optional[:class:`bool`] + If set to ``True``, return messages in oldest->newest order. Defaults to ``True`` if + ``after`` is specified, otherwise ``False``. + + Raises + ------ + Forbidden + You do not have permissions to get channel message history. + HTTPException + The request to get message history failed. + + Yields + ------ + :class:`.Message` + The message with the message data parsed. + """ + from .iterators import HistoryIterator # cyclic import + + return HistoryIterator( + self, limit=limit, before=before, after=after, around=around, oldest_first=oldest_first + ) + + +class Connectable(Protocol): + """An ABC that details the common operations on a channel that can + connect to a voice server. + + The following classes implement this ABC: + + - :class:`~disnake.VoiceChannel` + - :class:`~disnake.StageChannel` + + Note + ---- + This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass` + checks. + """ + + __slots__ = () + _state: ConnectionState + guild: Guild + id: int + + def _get_voice_client_key(self) -> Tuple[int, str]: + raise NotImplementedError + + def _get_voice_state_pair(self) -> Tuple[int, int]: + raise NotImplementedError + + async def connect( + self, + *, + timeout: float = 60.0, + reconnect: bool = True, + cls: Callable[[Client, Connectable], VoiceProtocolT] = VoiceClient, + ) -> VoiceProtocolT: + """|coro| + + Connects to voice and creates a :class:`VoiceClient` to establish + your connection to the voice server. + + This requires :attr:`Intents.voice_states`. + + Parameters + ---------- + timeout: :class:`float` + The timeout in seconds to wait for the voice endpoint. + reconnect: :class:`bool` + Whether the bot should automatically attempt + a reconnect if a part of the handshake fails + or the gateway goes down. + cls: Type[:class:`VoiceProtocol`] + A type that subclasses :class:`VoiceProtocol` to connect with. + Defaults to :class:`VoiceClient`. + + Raises + ------ + asyncio.TimeoutError + Could not connect to the voice channel in time. + ClientException + You are already connected to a voice channel. + opus.OpusNotLoaded + The opus library has not been loaded. + + Returns + ------- + :class:`VoiceProtocol` + A voice client that is fully connected to the voice server. + """ + key_id, _ = self._get_voice_client_key() + state = self._state + + if state._get_voice_client(key_id): + raise ClientException("Already connected to a voice channel.") + + client = state._get_client() + voice = cls(client, self) + + if not isinstance(voice, VoiceProtocol): + raise TypeError("Type must meet VoiceProtocol abstract base class.") + + state._add_voice_client(key_id, voice) + + try: + await voice.connect(timeout=timeout, reconnect=reconnect) + except asyncio.TimeoutError: + try: + await voice.disconnect(force=True) + except Exception: + # we don't care if disconnect failed because connection failed + pass + raise # re-raise + + return voice diff --git a/disnake/disnake/activity.py b/disnake/disnake/activity.py new file mode 100644 index 0000000000..b9b89bd693 --- /dev/null +++ b/disnake/disnake/activity.py @@ -0,0 +1,974 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload + +from .asset import Asset +from .colour import Colour +from .enums import ActivityType, StatusDisplayType, try_enum +from .partial_emoji import PartialEmoji + +__all__ = ( + "BaseActivity", + "Activity", + "Streaming", + "Game", + "Spotify", + "CustomActivity", +) + +"""If curious, this is the current schema for an activity. + +It's fairly long so I will document it here: + +All keys are optional. + +state: str (max: 128), +details: str (max: 128) +timestamps: dict + start: int (min: 1) + end: int (min: 1) +assets: dict + large_image: str (max: 32) + large_text: str (max: 128) + small_image: str (max: 32) + small_text: str (max: 128) +party: dict + id: str (max: 128), + size: List[int] (max-length: 2) + elem: int (min: 1) +secrets: dict + match: str (max: 128) + join: str (max: 128) + spectate: str (max: 128) +instance: bool +application_id: str +name: str (max: 128) +url: str +type: int +sync_id: str +session_id: str +flags: int +buttons: list[str (max: 32)] + +There are also activity flags which are mostly uninteresting for the library atm. + +t.ActivityFlags = { + INSTANCE: 1, + JOIN: 2, + SPECTATE: 4, + JOIN_REQUEST: 8, + SYNC: 16, + PLAY: 32 +} +""" + +if TYPE_CHECKING: + from .state import ConnectionState + from .types.activity import ( + Activity as ActivityPayload, + ActivityAssets, + ActivityEmoji as ActivityEmojiPayload, + ActivityParty, + ActivityTimestamps, + ) + from .types.emoji import PartialEmoji as PartialEmojiPayload + from .types.widget import WidgetActivity as WidgetActivityPayload + + +class _BaseActivity: + __slots__ = ("_created_at", "_timestamps", "assets") + + def __init__( + self, + *, + created_at: Optional[float] = None, + timestamps: Optional[ActivityTimestamps] = None, + assets: Optional[ActivityAssets] = None, + **kwargs: Any, # discarded + ) -> None: + self._created_at: Optional[float] = created_at + self._timestamps: ActivityTimestamps = timestamps or {} + self.assets: ActivityAssets = assets or {} + + @property + def created_at(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC. + + .. versionadded:: 1.3 + """ + if self._created_at is not None: + return datetime.datetime.fromtimestamp( + self._created_at / 1000, tz=datetime.timezone.utc + ) + + @property + def start(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable. + + .. versionchanged:: 2.6 + This attribute can now be ``None``. + """ + try: + timestamp = self._timestamps["start"] / 1000 + except KeyError: + return None + else: + return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) + + @property + def end(self) -> Optional[datetime.datetime]: + """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable. + + .. versionchanged:: 2.6 + This attribute can now be ``None``. + """ + try: + timestamp = self._timestamps["end"] / 1000 + except KeyError: + return None + else: + return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) + + def to_dict(self) -> ActivityPayload: + raise NotImplementedError + + def _create_image_url(self, asset: str) -> Optional[str]: + # `asset` can be a simple ID (see `Activity._create_image_url`), + # or a string of the format `:` + prefix, _, asset_id = asset.partition(":") + + if asset_id and (url_fmt := _ACTIVITY_URLS.get(prefix)): + return url_fmt.format(asset_id) + return None + + @property + def large_image_url(self) -> Optional[str]: + """Optional[:class:`str`]: Returns a URL pointing to the large image asset of this activity, if applicable. + + .. versionchanged:: 2.10 + Moved from :class:`Activity` to base type, making this available to all activity types. + Additionally, supports dynamic asset urls using the ``mp:`` prefix now. + """ + try: + large_image = self.assets["large_image"] + except KeyError: + return None + else: + return self._create_image_url(large_image) + + @property + def small_image_url(self) -> Optional[str]: + """Optional[:class:`str`]: Returns a URL pointing to the small image asset of this activity, if applicable. + + .. versionchanged:: 2.10 + Moved from :class:`Activity` to base type, making this available to all activity types. + Additionally, supports dynamic asset urls using the ``mp:`` prefix now. + """ + try: + small_image = self.assets["small_image"] + except KeyError: + return None + else: + return self._create_image_url(small_image) + + @property + def large_image_text(self) -> Optional[str]: + """Optional[:class:`str`]: Returns the large image asset hover text of this activity, if applicable. + + .. versionchanged:: 2.10 + Moved from :class:`Activity` to base type, making this available to all activity types. + """ + return self.assets.get("large_text") + + @property + def small_image_text(self) -> Optional[str]: + """Optional[:class:`str`]: Returns the small image asset hover text of this activity, if applicable. + + .. versionchanged:: 2.10 + Moved from :class:`Activity` to base type, making this available to all activity types. + """ + return self.assets.get("small_text") + + @property + def large_image_link(self) -> Optional[str]: + """Optional[:class:`str`]: Returns the large image asset URL of this activity, if applicable. + + .. versionadded:: 2.11 + """ + return self.assets.get("large_url") + + @property + def small_image_link(self) -> Optional[str]: + """Optional[:class:`str`]: Returns the small image asset URL of this activity, if applicable. + + .. versionadded:: 2.11 + """ + return self.assets.get("small_url") + + +# tag type for user-settable activities +class BaseActivity(_BaseActivity): + """The base activity that all user-settable activities inherit from. + + A user-settable activity is one that can be used in :meth:`Client.change_presence`. + + The following types currently count as user-settable: + + - :class:`Activity` + - :class:`Game` + - :class:`Streaming` + - :class:`CustomActivity` + + Note that although these types are considered user-settable by the library, + Discord typically ignores certain combinations of activity depending on + what is currently set. This behaviour may change in the future so there are + no guarantees on whether Discord will actually let you set these types. + + .. versionadded:: 1.3 + """ + + __slots__ = () + + +# There are additional urls for twitch/youtube/spotify, however +# it appears that Discord does not want to document those: +# https://github.com/discord/discord-api-docs/pull/4617 +# They are partially supported by different properties, e.g. `Spotify.album_cover_url`. +_ACTIVITY_URLS = { + "mp": "https://media.discordapp.net/{}", +} + + +class Activity(BaseActivity): + """Represents an activity in Discord. + + This could be an activity such as streaming, playing, listening + or watching. + + For memory optimisation purposes, some activities are offered in slimmed + down versions: + + - :class:`Game` + - :class:`Streaming` + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the activity. + url: Optional[:class:`str`] + A stream URL that the activity could be doing. + type: :class:`ActivityType` + The type of activity currently being done. + + Attributes + ---------- + application_id: Optional[:class:`int`] + The application ID of the game. + name: Optional[:class:`str`] + The name of the activity. + url: Optional[:class:`str`] + A stream URL that the activity could be doing. + type: :class:`ActivityType` + The type of activity currently being done. + state: Optional[:class:`str`] + The user's current state. For example, "In Game". + details: Optional[:class:`str`] + The detail of the user's current activity. + assets: :class:`dict` + A dictionary representing the images and their hover text of an activity. + It contains the following optional keys: + + - ``large_image``: A string representing the ID for the large image asset. + - ``large_text``: A string representing the text when hovering over the large image asset. + - ``large_url``: A string representing an URL that is opened when clicking on the large image. + - ``small_image``: A string representing the ID for the small image asset. + - ``small_text``: A string representing the text when hovering over the small image asset. + - ``small_url``: A string representing a URL that is opened when clicking on the small image. + party: :class:`dict` + A dictionary representing the activity party. It contains the following optional keys: + + - ``id``: A string representing the party ID. + - ``size``: A list of two integers denoting (current_size, maximum_size). + buttons: List[str] + A list of strings representing the labels of custom buttons shown in a rich presence. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.6 + Changed type to ``List[str]`` to match API types. + + emoji: Optional[:class:`PartialEmoji`] + The emoji that belongs to this activity. + details_url: Optional[:class:`str`] + An URL that is linked when clicking on the details text of an activity. + + .. versionadded:: 2.11 + state_url: Optional[:class:`str`] + An URL that is linked when clicking on the state text of an activity. + + .. versionadded:: 2.11 + status_display_type: Optional[:class:`StatusDisplayType`] + Controls which field is displayed in the user's status activity text in the member list. + + .. versionadded:: 2.11 + """ + + __slots__ = ( + "state", + "details", + "party", + "flags", + "type", + "name", + "url", + "application_id", + "emoji", + "buttons", + "id", + "platform", + "sync_id", + "session_id", + "details_url", + "state_url", + "status_display_type", + ) + + def __init__( + self, + *, + name: Optional[str] = None, + url: Optional[str] = None, + type: Optional[Union[ActivityType, int]] = None, + state: Optional[str] = None, + state_url: Optional[str] = None, + details: Optional[str] = None, + details_url: Optional[str] = None, + party: Optional[ActivityParty] = None, + application_id: Optional[Union[str, int]] = None, + flags: Optional[int] = None, + buttons: Optional[List[str]] = None, + emoji: Optional[Union[PartialEmojiPayload, ActivityEmojiPayload]] = None, + id: Optional[str] = None, + platform: Optional[str] = None, + sync_id: Optional[str] = None, + session_id: Optional[str] = None, + status_display_type: Optional[Union[StatusDisplayType, int]] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.state: Optional[str] = state + self.state_url: Optional[str] = state_url + self.details: Optional[str] = details + self.details_url: Optional[str] = details_url + self.party: ActivityParty = party or {} + self.application_id: Optional[int] = ( + int(application_id) if application_id is not None else None + ) + self.name: Optional[str] = name + self.url: Optional[str] = url + self.flags: int = flags or 0 + self.buttons: List[str] = buttons or [] + + # undocumented fields: + self.id: Optional[str] = id + self.platform: Optional[str] = platform + self.sync_id: Optional[str] = sync_id + self.session_id: Optional[str] = session_id + + activity_type = type if type is not None else 0 + self.type: ActivityType = ( + activity_type + if isinstance(activity_type, ActivityType) + else try_enum(ActivityType, activity_type) + ) + + self.status_display_type: Optional[StatusDisplayType] = ( + try_enum(StatusDisplayType, status_display_type) + if isinstance(status_display_type, int) + else status_display_type + ) + + self.emoji: Optional[PartialEmoji] = ( + PartialEmoji.from_dict(emoji) if emoji is not None else None + ) + + def __repr__(self) -> str: + attrs = ( + ("type", self.type), + ("name", self.name), + ("url", self.url), + ("details", self.details), + ("application_id", self.application_id), + ("session_id", self.session_id), + ("emoji", self.emoji), + ) + inner = " ".join(f"{k!s}={v!r}" for k, v in attrs) + return f"" + + def to_dict(self) -> Dict[str, Any]: + ret: Dict[str, Any] = {} + for attr in self.__slots__: + value = getattr(self, attr, None) + if value is None: + continue + + if isinstance(value, dict) and len(value) == 0: + continue + + ret[attr] = value + + # fix type field + ret["type"] = int(self.type) + + if self.status_display_type: + ret["status_display_type"] = int(self.status_display_type) + + if self.emoji: + ret["emoji"] = self.emoji.to_dict() + # defined in base class slots + if self._timestamps: + ret["timestamps"] = self._timestamps + return ret + + def _create_image_url(self, asset: str) -> Optional[str]: + # if parent method already returns valid url, use that + if url := super()._create_image_url(asset): + return url + + # if it's not a `:` asset and we have an application ID, create url + if ":" not in asset and self.application_id: + return f"{Asset.BASE}/app-assets/{self.application_id}/{asset}.png" + + # else, it's an unknown asset url + return None + + +class Game(BaseActivity): + """A slimmed down version of :class:`Activity` that represents a Discord game. + + This is typically displayed via **Playing** on the official Discord client. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two games are equal. + + .. describe:: x != y + + Checks if two games are not equal. + + .. describe:: hash(x) + + Returns the game's hash. + + .. describe:: str(x) + + Returns the game's name. + + Parameters + ---------- + name: :class:`str` + The game's name. + + Attributes + ---------- + name: :class:`str` + The game's name. + assets: :class:`dict` + A dictionary with the same structure as :attr:`Activity.assets`. + """ + + __slots__ = ("name", "platform") + + def __init__( + self, + name: str, + *, + platform: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.name: str = name + + # undocumented + self.platform: Optional[str] = platform + + @property + def type(self) -> Literal[ActivityType.playing]: + """:class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.playing`. + """ + return ActivityType.playing + + def __str__(self) -> str: + return str(self.name) + + def __repr__(self) -> str: + return f"" + + def to_dict(self) -> ActivityPayload: + return { + "type": ActivityType.playing.value, + "name": str(self.name), + "timestamps": self._timestamps, + "assets": self.assets, + } + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Game) and other.name == self.name + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + return hash(self.name) + + +class Streaming(BaseActivity): + """A slimmed down version of :class:`Activity` that represents a Discord streaming status. + + This is typically displayed via **Streaming** on the official Discord client. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two streams are equal. + + .. describe:: x != y + + Checks if two streams are not equal. + + .. describe:: hash(x) + + Returns the stream's hash. + + .. describe:: str(x) + + Returns the stream's name. + + Attributes + ---------- + platform: Optional[:class:`str`] + Where the user is streaming from (ie. YouTube, Twitch). + + .. versionadded:: 1.3 + + name: Optional[:class:`str`] + The stream's name. + details: Optional[:class:`str`] + An alias for :attr:`name` + game: Optional[:class:`str`] + The game being streamed. + + .. versionadded:: 1.3 + + url: :class:`str` + The stream's URL. + assets: :class:`dict` + A dictionary with the same structure as :attr:`Activity.assets`. + """ + + __slots__ = ("platform", "name", "game", "url", "details") + + def __init__( + self, + *, + name: Optional[str], + url: str, + details: Optional[str] = None, + state: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.platform: Optional[str] = name + self.name: Optional[str] = details or name + self.details: Optional[str] = self.name # compatibility + self.url: str = url + self.game: Optional[str] = state + + @property + def type(self) -> Literal[ActivityType.streaming]: + """:class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.streaming`. + """ + return ActivityType.streaming + + def __str__(self) -> str: + return str(self.name) + + def __repr__(self) -> str: + return f"" + + @property + def twitch_name(self) -> Optional[str]: + """Optional[:class:`str`]: If provided, the twitch name of the user streaming. + + This corresponds to the ``large_image`` key of the :attr:`Streaming.assets` + dictionary if it starts with ``twitch:``. Typically set by the Discord client. + """ + try: + name = self.assets["large_image"] + except KeyError: + return None + else: + return name[7:] if name[:7] == "twitch:" else None + + def to_dict(self) -> Dict[str, Any]: + ret: Dict[str, Any] = { + "type": ActivityType.streaming.value, + "name": str(self.name), + "url": str(self.url), + "assets": self.assets, + } + if self.details: + ret["details"] = self.details + return ret + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Streaming) and other.name == self.name and other.url == self.url + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + return hash(self.name) + + +class Spotify(_BaseActivity): + """Represents a Spotify listening activity from Discord. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two activities are equal. + + .. describe:: x != y + + Checks if two activities are not equal. + + .. describe:: hash(x) + + Returns the activity's hash. + + .. describe:: str(x) + + Returns the string 'Spotify'. + """ + + __slots__ = ( + "_state", + "_details", + "_party", + "_sync_id", + "_session_id", + ) + + def __init__( + self, + *, + state: Optional[str] = None, + details: Optional[str] = None, + party: Optional[ActivityParty] = None, + sync_id: Optional[str] = None, + session_id: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self._state: str = state or "" + self._details: str = details or "" + self._party: ActivityParty = party or {} + self._sync_id: str = sync_id or "" + self._session_id: Optional[str] = session_id + + @property + def type(self) -> Literal[ActivityType.listening]: + """:class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.listening`. + """ + return ActivityType.listening + + @property + def colour(self) -> Colour: + """:class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`. + + There is an alias for this named :attr:`color` + """ + return Colour(0x1DB954) + + @property + def color(self) -> Colour: + """:class:`Colour`: Returns the Spotify integration colour, as a :class:`Colour`. + + There is an alias for this named :attr:`colour` + """ + return self.colour + + def to_dict(self) -> Dict[str, Any]: + return { + "flags": 48, # SYNC | PLAY + "name": "Spotify", + "assets": self.assets, + "party": self._party, + "sync_id": self._sync_id, + "session_id": self._session_id, + "timestamps": self._timestamps, + "details": self._details, + "state": self._state, + } + + @property + def name(self) -> str: + """:class:`str`: The activity's name. This will always return "Spotify".""" + return "Spotify" + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Spotify) + and other._session_id == self._session_id + and other._sync_id == self._sync_id + and other.start == self.start + ) + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + return hash(self._session_id) + + def __str__(self) -> str: + return "Spotify" + + def __repr__(self) -> str: + return f"" + + @property + def title(self) -> str: + """:class:`str`: The title of the song being played.""" + return self._details + + @property + def artists(self) -> List[str]: + """List[:class:`str`]: The artists of the song being played.""" + return self._state.split("; ") + + @property + def artist(self) -> str: + """:class:`str`: The artist of the song being played. + + This does not attempt to split the artist information into + multiple artists. Useful if there's only a single artist. + """ + return self._state + + @property + def album(self) -> str: + """:class:`str`: The album that the song being played belongs to.""" + return self.assets.get("large_text", "") + + @property + def album_cover_url(self) -> str: + """:class:`str`: The album cover image URL from Spotify's CDN.""" + large_image = self.assets.get("large_image", "") + if large_image[:8] != "spotify:": + return "" + album_image_id = large_image[8:] + return f"https://i.scdn.co/image/{album_image_id}" + + @property + def track_id(self) -> str: + """:class:`str`: The track ID used by Spotify to identify this song.""" + return self._sync_id + + @property + def track_url(self) -> str: + """:class:`str`: The track URL to listen on Spotify. + + .. versionadded:: 2.0 + """ + return f"https://open.spotify.com/track/{self.track_id}" + + @property + def duration(self) -> Optional[datetime.timedelta]: + """Optional[:class:`datetime.timedelta`]: The duration of the song being played, if applicable. + + .. versionchanged:: 2.6 + This attribute can now be ``None``. + """ + start, end = self.start, self.end + if start and end: + return end - start + return None + + @property + def party_id(self) -> str: + """:class:`str`: The party ID of the listening party.""" + return self._party.get("id", "") + + +class CustomActivity(BaseActivity): + """Represents a Custom activity from Discord. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two activities are equal. + + .. describe:: x != y + + Checks if two activities are not equal. + + .. describe:: hash(x) + + Returns the activity's hash. + + .. describe:: str(x) + + Returns the custom status text. + + .. versionadded:: 1.3 + + Attributes + ---------- + name: Optional[:class:`str`] + The custom activity's name. + emoji: Optional[:class:`PartialEmoji`] + The emoji to pass to the activity, if any. + + This currently cannot be set by bots. + """ + + __slots__ = ("name", "emoji", "state") + + def __init__( + self, + name: Optional[str], + *, + emoji: Optional[Union[ActivityEmojiPayload, str, PartialEmoji]] = None, + state: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.name: Optional[str] = name + # Fall back to `name`, since `state` is the relevant field for custom status (`name` is not shown) + self.state: Optional[str] = state or name + + # The official client uses "Custom Status" as the name, the actual name is in `state` + if self.name == "Custom Status": + self.name = self.state + + self.emoji: Optional[PartialEmoji] + if emoji is None: + self.emoji = emoji + elif isinstance(emoji, dict): + self.emoji = PartialEmoji.from_dict(emoji) + elif isinstance(emoji, str): + self.emoji = PartialEmoji(name=emoji) + elif isinstance(emoji, PartialEmoji): + self.emoji = emoji + else: + raise TypeError( + f"Expected str, PartialEmoji, or None, received {type(emoji)!r} instead." + ) + + @property + def type(self) -> Literal[ActivityType.custom]: + """:class:`ActivityType`: Returns the activity's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.custom`. + """ + return ActivityType.custom + + def to_dict(self) -> ActivityPayload: + o: ActivityPayload + if self.name == self.state: + o = { + "type": ActivityType.custom.value, + "state": self.name, + "name": "Custom Status", + } + else: + o = { + "type": ActivityType.custom.value, + "name": self.name or "", + } + + if self.emoji: + o["emoji"] = self.emoji.to_dict() # type: ignore + return o + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, CustomActivity) + and other.name == self.name + and other.emoji == self.emoji + ) + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + return hash((self.name, str(self.emoji))) + + def __str__(self) -> str: + if self.emoji: + if self.name: + return f"{self.emoji} {self.name}" + return str(self.emoji) + else: + return str(self.name) + + def __repr__(self) -> str: + return f"" + + +ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify] + + +@overload +def create_activity( + data: Union[ActivityPayload, WidgetActivityPayload], *, state: Optional[ConnectionState] = None +) -> ActivityTypes: ... + + +@overload +def create_activity(data: None, *, state: Optional[ConnectionState] = None) -> None: ... + + +def create_activity( + data: Optional[Union[ActivityPayload, WidgetActivityPayload]], + *, + state: Optional[ConnectionState] = None, +) -> Optional[ActivityTypes]: + if not data: + return None + + activity: ActivityTypes + game_type = try_enum(ActivityType, data.get("type", -1)) + if game_type is ActivityType.playing and not ( + "application_id" in data or "session_id" in data or "state" in data + ): + activity = Game(**data) # type: ignore # pyright bug(?) + elif game_type is ActivityType.custom and "name" in data: + activity = CustomActivity(**data) # type: ignore + elif game_type is ActivityType.streaming and "url" in data: + # url won't be None here + activity = Streaming(**data) # type: ignore + elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data: + activity = Spotify(**data) + else: + activity = Activity(**data) # type: ignore + + if isinstance(activity, (Activity, CustomActivity)) and activity.emoji and state: + activity.emoji._state = state + + return activity diff --git a/disnake/disnake/app_commands.py b/disnake/disnake/app_commands.py new file mode 100644 index 0000000000..d9a5638750 --- /dev/null +++ b/disnake/disnake/app_commands.py @@ -0,0 +1,1325 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import math +import re +from abc import ABC +from typing import TYPE_CHECKING, ClassVar, List, Mapping, Optional, Sequence, Tuple, Union + +from .enums import ( + ApplicationCommandPermissionType, + ApplicationCommandType, + ChannelType, + Locale, + OptionType, + enum_if_int, + try_enum, + try_enum_to_int, +) +from .flags import ApplicationInstallTypes, InteractionContextTypes +from .i18n import Localized +from .permissions import Permissions +from .utils import MISSING, _get_as_snowflake, _maybe_cast, deprecated, warn_deprecated + +if TYPE_CHECKING: + from typing_extensions import Self + + from .i18n import LocalizationProtocol, LocalizationValue, LocalizedOptional, LocalizedRequired + from .state import ConnectionState + from .types.interactions import ( + ApplicationCommand as ApplicationCommandPayload, + ApplicationCommandOption as ApplicationCommandOptionPayload, + ApplicationCommandOptionChoice as ApplicationCommandOptionChoicePayload, + ApplicationCommandOptionChoiceValue, + ApplicationCommandPermissions as ApplicationCommandPermissionsPayload, + EditApplicationCommand as EditApplicationCommandPayload, + GuildApplicationCommandPermissions as GuildApplicationCommandPermissionsPayload, + ) + + Choices = Union[ + Sequence["OptionChoice"], + Sequence[ApplicationCommandOptionChoiceValue], + Mapping[str, ApplicationCommandOptionChoiceValue], + Sequence[Localized[str]], + ] + + APIApplicationCommand = Union["APIUserCommand", "APIMessageCommand", "APISlashCommand"] + + +__all__ = ( + "application_command_factory", + "ApplicationCommand", + "SlashCommand", + "APISlashCommand", + "UserCommand", + "APIUserCommand", + "MessageCommand", + "APIMessageCommand", + "OptionChoice", + "Option", + "ApplicationCommandPermissions", + "GuildApplicationCommandPermissions", +) + + +def application_command_factory(data: ApplicationCommandPayload) -> APIApplicationCommand: + cmd_type = try_enum(ApplicationCommandType, data.get("type", 1)) + if cmd_type is ApplicationCommandType.chat_input: + return APISlashCommand.from_dict(data) + if cmd_type is ApplicationCommandType.user: + return APIUserCommand.from_dict(data) + if cmd_type is ApplicationCommandType.message: + return APIMessageCommand.from_dict(data) + + raise TypeError(f"Application command of type {cmd_type} is not valid") + + +def _validate_name(name: str) -> None: + # used for slash command names and option names + # see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming + + if not isinstance(name, str): + raise TypeError( + f"Slash command name and option names must be an instance of class 'str', received '{name.__class__}'" + ) + + if name != name.lower() or not re.fullmatch(r"[\w-]{1,32}", name): + raise ValueError( + f"Slash command or option name '{name}' should be lowercase, " + "between 1 and 32 characters long, and only consist of " + "these symbols: a-z, 0-9, -, _, and other languages'/scripts' symbols" + ) + + +class OptionChoice: + """Represents an option choice. + + Parameters + ---------- + name: Union[:class:`str`, :class:`.Localized`] + The name of the option choice (visible to users). + + .. versionchanged:: 2.5 + Added support for localizations. + + value: Union[:class:`str`, :class:`int`] + The value of the option choice. + """ + + def __init__( + self, + name: LocalizedRequired, + value: ApplicationCommandOptionChoiceValue, + ) -> None: + name_loc = Localized._cast(name, True) + self.name: str = name_loc.string + self.name_localizations: LocalizationValue = name_loc.localizations + self.value: ApplicationCommandOptionChoiceValue = value + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other) -> bool: + return ( + self.name == other.name + and self.value == other.value + and self.name_localizations == other.name_localizations + ) + + def to_dict(self, *, locale: Optional[Locale] = None) -> ApplicationCommandOptionChoicePayload: + localizations = self.name_localizations.data + + name: Optional[str] = None + # if `locale` provided, get localized name from dict + if locale is not None and localizations: + name = localizations.get(str(locale)) + + # fall back to default name if no locale or no localized name + if name is None: + name = self.name + + payload: ApplicationCommandOptionChoicePayload = { + "name": name, + "value": self.value, + } + # if no `locale` provided, include all localizations in payload + if locale is None and localizations: + payload["name_localizations"] = localizations + return payload + + @classmethod + def from_dict(cls, data: ApplicationCommandOptionChoicePayload) -> Self: + return cls( + name=Localized(data["name"], data=data.get("name_localizations")), + value=data["value"], + ) + + def localize(self, store: LocalizationProtocol) -> None: + self.name_localizations._link(store) + + +class Option: + """Represents a slash command option. + + Parameters + ---------- + name: Union[:class:`str`, :class:`.Localized`] + The option's name. + + .. versionchanged:: 2.5 + Added support for localizations. + + description: Optional[Union[:class:`str`, :class:`.Localized`]] + The option's description. + + .. versionchanged:: 2.5 + Added support for localizations. + + type: :class:`OptionType` + The option type, e.g. :class:`OptionType.user`. + required: :class:`bool` + Whether this option is required. + choices: Union[Sequence[:class:`OptionChoice`], Sequence[Union[:class:`str`, :class:`int`, :class:`float`]], Mapping[:class:`str`, Union[:class:`str`, :class:`int`, :class:`float`]]] + The pre-defined choices for this option. + options: List[:class:`Option`] + The list of sub options. Normally you don't have to specify it directly, + instead consider using ``@main_cmd.sub_command`` or ``@main_cmd.sub_command_group`` decorators. + channel_types: List[:class:`ChannelType`] + The list of channel types that your option supports, if the type is :class:`OptionType.channel`. + By default, it supports all channel types. + autocomplete: :class:`bool` + Whether this option can be autocompleted. + min_value: Union[:class:`int`, :class:`float`] + The minimum value permitted. + max_value: Union[:class:`int`, :class:`float`] + The maximum value permitted. + min_length: :class:`int` + The minimum length for this option if this is a string option. + + .. versionadded:: 2.6 + + max_length: :class:`int` + The maximum length for this option if this is a string option. + + .. versionadded:: 2.6 + + Attributes + ---------- + name: :class:`str` + The option's name. + description: :class:`str` + The option's description. + type: :class:`OptionType` + The option type, e.g. :class:`OptionType.user`. + required: :class:`bool` + Whether this option is required. + choices: List[:class:`OptionChoice`] + The list of pre-defined choices. + options: List[:class:`Option`] + The list of sub options. Normally you don't have to specify it directly, + instead consider using ``@main_cmd.sub_command`` or ``@main_cmd.sub_command_group`` decorators. + channel_types: List[:class:`ChannelType`] + The list of channel types that your option supports, if the type is :class:`OptionType.channel`. + By default, it supports all channel types. + autocomplete: :class:`bool` + Whether this option can be autocompleted. + min_value: Union[:class:`int`, :class:`float`] + The minimum value permitted. + max_value: Union[:class:`int`, :class:`float`] + The maximum value permitted. + min_length: :class:`int` + The minimum length for this option if this is a string option. + + .. versionadded:: 2.6 + + max_length: :class:`int` + The maximum length for this option if this is a string option. + + .. versionadded:: 2.6 + """ + + __slots__ = ( + "name", + "description", + "type", + "required", + "choices", + "options", + "channel_types", + "autocomplete", + "min_value", + "max_value", + "name_localizations", + "description_localizations", + "min_length", + "max_length", + ) + + def __init__( + self, + name: LocalizedRequired, + description: LocalizedOptional = None, + type: Optional[Union[OptionType, int]] = None, + required: bool = False, + choices: Optional[Choices] = None, + options: Optional[list] = None, + channel_types: Optional[List[ChannelType]] = None, + autocomplete: bool = False, + min_value: Optional[float] = None, + max_value: Optional[float] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + ) -> None: + name_loc = Localized._cast(name, True) + _validate_name(name_loc.string) + self.name: str = name_loc.string + self.name_localizations: LocalizationValue = name_loc.localizations + + desc_loc = Localized._cast(description, False) + self.description: str = desc_loc.string or "-" + self.description_localizations: LocalizationValue = desc_loc.localizations + + self.type: OptionType = enum_if_int(OptionType, type) or OptionType.string + self.required: bool = required + self.options: List[Option] = options or [] + + if min_value and self.type is OptionType.integer: + min_value = math.ceil(min_value) + if max_value and self.type is OptionType.integer: + max_value = math.floor(max_value) + + self.min_value: Optional[float] = min_value + self.max_value: Optional[float] = max_value + + self.min_length: Optional[int] = min_length + self.max_length: Optional[int] = max_length + + if channel_types is not None and not all(isinstance(t, ChannelType) for t in channel_types): + raise TypeError("channel_types must be a list of `ChannelType`s") + + self.channel_types: List[ChannelType] = channel_types or [] + + self.choices: List[OptionChoice] = [] + if choices is not None: + if autocomplete: + raise TypeError("can not specify both choices and autocomplete args") + + if isinstance(choices, str): # str matches `Sequence[str]`, but isn't meant to be used + raise TypeError("choices argument should be a list/sequence or dict, not str") + + if isinstance(choices, Mapping): + self.choices = [OptionChoice(name, value) for name, value in choices.items()] + else: + for c in choices: + if isinstance(c, Localized): + c = OptionChoice(c, c.string) + elif not isinstance(c, OptionChoice): + c = OptionChoice(str(c), c) + self.choices.append(c) + + self.autocomplete: bool = autocomplete + + def __repr__(self) -> str: + return ( + f"%Q0WxVV9B=(GDFxAo4^IJ+WYC>P zs(xX)|G_&pIcA+5gR+XRYo|tHu73eYnF)SA?%NRyklqLhZ08%Cg^-e;&hZ!CKwF}| z?Gjta>nDhuLjF#9U8jAg@xm#c#67&2A8DTNG>zofkb;Z_WQU?%tnVp3> zg1dyO!n_c$fwJtu90kZ5C=rO!A7zt< z@}DprhbzRn05HX$yqA7@g}G199Ubm4n`aN46J9Ak#ZX_h}2ha1<_ zUn_JWR+Lg=1w1BW<11__R$ew!-AP;>8`>-{H5OyNP}R$|&*moB8%S>DMTwYkdw?ae*x)RnFe%8*Ru0Jb}3Jp0|v&c8)v!lbBwv?KErlqLJg^hLz_OUUh)u z>7|(Sx)AWD}j=^H=b${b+|w}mJ<>dq_@QX0la z!|)F6QXldoI!^x=@R51vkKv;jVK;v8pCBTuo};}hDfH2|h-d!0i6~m;Q#GZ0^2H{W~tLl2z-e9pzkhsimjZh2RUTWZfppxrvoS zQm%SjH#(D}iSNHgEflRLa-BsZ(ZnkC79m9`S$|hGP&IGKxVXFOSt|B^93}6nNt5ZI zS75KE)hAcI#(3-kf4TK!d((})@QHRShCZH8JLUtN<}z_we1LT#w@JyxJJpv4NtvyD_HS>u!{1ob*XeG#z!f z+{JC&*^YTv1Ce-^GRDPG_h+(~v7JVFp+()h=Tf?e*}Qd40)HqBi88&W(V4$PJeY{WRO=z*vef<3= z2O!Q@X|YYeHCt(7@;Li4moKG^?rU*G%L}NCO__e-A5Y)OFZPMtvd`ckzc_4doYPNx z`G}VPj(D3fw{X|LU}1d(Cp^2iy~DFK$~UC@*HRaz%B~>?&oW zcFfpAEs&bHEg9v7SXz*hyA#`fe*5`|7@4`wh|%sqeJ~@3!?mKtHTqZ}swjry%AWizJpG*A%+n)+B_SRTK<-4mN4zceJ!rP!DP~$or(Od zrZr-AP~({*P3rG>!{_xN@ojYr`91_c0tGCSBUS0Iots+m!M~yp_~ZjkXb|`dr{ZqN zyP8f0;6yyk23@MQ8J>_yW-&waZgW+6TMs}l!nP;MG*JiGK&HfNO%E2)Q)wfIOFlkB zCHse(fnhz6A&w)dd_53eCp?FO=vkr0)B=BMK}F`9CEyXOWDGMdMA;TDgAgV6gYaWp zxLh|mHBy9Jq`XIfI_l3tchtCv7j!ML(R3Jn`?^abGy`umnr##%(si21;sso2QLysbv<`+hfz(vTF_DFjZ*Vv z@XI<)0O)=Uk2iUj470=xjpA8lbi#~`ab7s9GIOaJU$|(s8KU)6nDG8)j81rJ$_#M` zKmVBKhS2l(xp5ko0u@9Je}dVOA33?Jv^uQ(;@nbzdVy}%kg`P)x3N|)aJ`Ugft0S% zz&6HIi|e^u-=4dUaD7+qI>_~!T>WWW&&^$9>DXA3y9PM}V3{wI7P03a{y*EjJ7PNX z&W@?M7PNScpjPXigU0Zp94LmD4#8nK=!7d}k>Z%+nK)_Qbe3fvcpDEPv$e^^8M3yn z`Zp#YlUQr(w}nKR&a%v`lUNB@TV|En&KD*H4mphi#OxmRz-oL)D_ZBzvl>rug_?S8 z55Fi?=UY#1XkqGe3rc2b15xVUg#xGW(m_}X2R%Sq{cX_(*RRtit-k~(`Sm;VaEA!y zXVX=tu=Y3P6snZ>*Akz-8sGD9DUN;Tz}ofegevQaeY%tP0WI%+73%%L36QBy8MDBw zWExNW=aXRn{#_>_xn(Jnkym4}pK8Wy_&a=1I@cijM9x~~ZCW!^D`dFg&7G<#n}-wj zW71i{-@U z?mF$s2;%!dCyB@n9lurL%6y|lLHZ;=`kG$ah2Fb*R@I72y7^4qgc-?aFVTUNx8#Bu zZ7a;B`rBJPE9F%ojqwLypJADI7KD*%tXTqfC2vuQqmYg9NzS}tq-T>GYbw0?vl`#x z{i>JA*~4#BUJ&%DyAV<-PH;PZd zTt~LKa3CGnjo`4Ag=w*|+ao##j;v`E#NDtxMNC5dnAvW5elH&O+i>HjpbnA9Q?FKs z1KlC-yi@+XdolhI3`{vF^%{l|4Oy+{&_WTG=Bc7yKY#ZkL%Za0s-g^(T8*fn{UtY` zlgg_fi~em{UN6D;cVIQQ1CM_MMFp&>!NRPkfrlSyA+0AHb9-M~nmq=opRB9I?$A&H zz?P7_F_1G<{FL!fCL>ShO#V30#q+SA=yw)i2(zQ70$Vd-nkB6LsU`lZm+Kb@&0z9^ z-G;CV=aoS!HI;P6L~*JIxQJ*xB0aP@_K{3tDV`lY8v0t zf6f|n>H#_i$F39cyn59Z{^ro^&65J{$@G}?DI_+<<=B-wu4qTO4*#DosJ}lb0_%7J z4MqP}t`q$?iXO$7Fv7NwfA(C+&`nzh$EBWsnQ z?`>zf!6?oN7zPYEyD8n(A@d1y8J z1K|=}T~qVGHKt8I21j`yGw{Q+b4yb2JgSi% z?*fTXnbP3uX9O4oFU?Jrm3kdvOM@x(*N_K$u;U9g<<1Y7a>wmvxB~9IpVx~1pr70G zfgAXaIX{mR&*jteFvDFsDC|cr0zl#%VXeIykcg)4n-oobJrhqQz<~c;-a=#X)K_sO zcq^W|uOsIEO;J4c_4Zi8lfy>QXR&>m*+|%(T{wC@XFC4>Bmb8mcn&{OF+I*%_#U(# z5v2h1d=WB*|H_=ZxBz;oj%cd_IbQ@m$D^(bH6EPVw0z=4e)$@_2(7XJyc9sabWbOe zr_Z3i^cibev{9|7Abk|fEXm!>Ea8q=HmEasbW@q0RSrJm;;K+yUf1+w1hsUs|2?(8 z>w4CFy?5gM6SSh-K1?@E52Nni_OqocY!ih@sy1yMquD!<$&m6+Q>(5=?ri@zv}uUe z^se;FkYerDEooF;i5Bd$5`KrsFBe80iT3rUxR!6hsrS^r9!U^qYTK9f4A~ojVSIO$ zN2nta%%e1w8WT;-Iy3aDotkwf$}59S+2%P$HMypF?&pP~p#m)xg&aUTO9$Z~R+X{V z%rTNZ#Pc$ZG#b){o&Xpl$;A>>Y|=oI+n3@Bdp^Eg1U?eJTnN5gOc-2`Z9mQ{A!oBN zKOtm7zRfxb5XD6j-`sAlPK2D4c;qmqoT&vxm}QqBm%C^%4*0vovbb+P9||0eCfXt# z?$7dB>@yHNVWtJHn~8cgf>+fI9ocL&u~tpXI2ADU^;3>opi+@DoVEHq3^fOlvxh?{ zv!+^rW{ShWV}95%FGDJf0|V&1D#drHcxL9+1CS-o+6uNR*iU3BUBF4#dzVC`f?-1; zyYaG2A$d3=2@)B)Ch~FES!rJUYU>PRX;FeW02G=Emu0?QrKuuban*L)Gd!|x>4r4% z%x)%NxD+PV!#OrL+hucuxs5T+DN?MjbF7s{nA*HX-M#0V+HA*B=ufh?X`T3Au(r8w z*xKfOqpWQ%Hr6&mhsN4QDT21!sYZD%f!sUHYI<{|+-Xma(dwGXenynwRz)h2sCf#N zA2*eSn-`NGB|qI?j46rV+16WFwbP32tR^WzGOfr}2($5_`Gwu|*Jt$=6W$#Y&rIe= zFec8*+`*ydlmHX5KH|(nPbjXrg6CQ>;h#)X(Lgpl?T4Z~tQAM~_3MAAv)O8VjvLG> zS3V(lyR&2e-ZNRQe=vZJh9FJANV@&Y~Sk#>Zqtsq`M0Poq>+! zty4JEtGbqrt$M*ZV#Qw0^)T)})c!ux5nM6X{+JhdAYvfyuKZW-=;7QbqFPGJ^;y&X z2zL@%@i55cb*hKJl3RpeD@8nUSs|$={OpyIK3fmA47Av*S89;G??qq+K^U$j;N%bW zk?`st>dV}zFC0JG;5vTd^Z13fVj17{_&JZhYS)W7`YQOd9KHxIayFYGIY&u)PzG5A zLMXxw4#6LqI2Zyng7Hbx0x2pQ`2?I7bNX3_Q!`4!yF<{|MC&?)`YXTYjYmj?R}yj% zr}c%V9K&L!A{00l3miu$>>XD_^heCSyCUxU%}i(NpJO4i#Dbh;AIju0x23M)5*Guf zoo)2el-6MrH)9u>+nA~ZT*nf7x_%{-yQ?07bHT1F&^lxyxI2>AdH#OGi(RP?Mj5za zN;bRiFcKzs4r9M3H7q1Ui|hm2Wgpm1AbJehTCly8eH&S1@gvjCs zCD(iDt8MQQ5ixhLT8e#{6PymU|0S;~-X(&4vzbi_9q4Ve3KSVe-o&D+0NTqkCoqM` z)nq4C(kZsRw$bBUQX>*|A9|2W>IfL4%iAO*;DNS~Cl$Cw+Jb2jW<-<;))%*H7ZLj? z^JUcVA*ZpM+2y>EhxC`AVK|C3Z!vfFT#g1_)RBbPC>H3JHYWEWFGWf2BeYu~fHkB~ zCJxygar$~Tn08xb(|{1c`8^E~QsR@g?;)>a3o3H^A>{awJV5)(-sr09O&7c>pU1e` ztRnvKwzE}3RBVh^85d(ukDY0@mQ%4t%LA!0g+%Tx#CV#Uxj&&1Y}I7gZO}IQS*htW zm&vV$O<6LwY5a@-n{c1e#8^<@JmCd)0KwzMt&j0R?pe)iSE!99#Ma<|(kMABN`x1e zw+bFH(~I2MsrY@6jg)3O*J(=IIVdA>XNRWrmbYb%vsU~Bw=g~kD?d|`S-J?}o(BRE z_n}R>MT@v=*Xwi6N^c2nF45Z2Qf}I{fTQOeg^u9l@SlU)OTq|}<46cET&d6TyY`>? zMMAYmcyHcZ4dITZ=OI19Nas657O6*|d)t|a^LAYMvct}J3~2FV$jW@Kh%};KG9(SdxG6*UQu~M*A1GSjUtuIis9>WB0tTNJiXF6fi6nf8&|)~4cD@s5 zaN)Rn3xn&A`Suw4JDwo=SoF+IiPDFntaZ*f-nV;Zs`w%0jiJC8r{dO#SS=OBZnUd* zH-ePxPCPVH{;MFGIqPu)-5{?XM#OH^*ytl_DLLMb9M0Y#Ztqf`1;x#**CIHFHG~r5 z0DnUE@oT@y;>0i$Tdl`RRlv{}M^VeLg45W4&W{aoZ~KUO^5ybMc_KHcYT$^pnjnPR z%Sqk?e~`MaTp%?`YDA3Y#Bbb`@Lz1QLAAsJEwYMul{X;;Y+_&bA%y!&9Gzd__s)V? z;3Nudb&<#UG=spV**@b%Z3{WSkeJ7F)E+tCEV!Tiz%o0DoZ(Lp>w(91SdZ;v_OMy9 z9-|#|8KlZ%+r3MqSdX=tOaG&);5^4OG$^d8*Rv6>)`IR6u3cqWn#hZNakJZ?1P9Ek> z_Ac;)%&hk53<+-N>g-|`~R5~4m#nZ9B0l}rO4j(xRd zihN`_0t*-r9B*FlMC7=Q5IIpSwm;gkdihUz5l_%dk{}{$EARX5)QqJg^5IT093K=@ z$*A0}@x+NKZ^ek|>0}52CDm)+5!;;w@!x?R_H|IAXAo0D?H7O3tbZh-ctz1O*h9BO zaC@a&`IKOD{>w<>GP`M#0Lf}T%Jb*&Cg5kUHoC#3pvk;^Fe=D0^TY!P`x1=dWc}4s zc10Z$#5C|2EQeH(axg`)n5_S>b5&fH zk)p_e?`Yyveh?M{FWH)J*Z6$fi0Yb}aRx&#>}l;m-xANGz@xN`M?sE=GV~MF3-ney zCrBSi1-#HKS1FDwb2RiFNq3i|kNIMWR^-3w%}}tY*ID4eZe;9@yO!P9ci50m=eP#6 z$TfhN(L|2TxdyZjy9N+18H!4YuzD{L-Gq-kHl~VIH)WTmc`{3C`1g06U#q#zU6B9P z_Ufi7%nbYKa5%89+SiU+0DtjLy4fKI5$1S=KgDKm^zHal7@uJT($20&?5<Hh<_oR#*x{a_#QcR68JDh-OpqtMDIi7bg&-kX14YO44i*G@(}PV4hjHEyV(yo zOi`l!z{%JeOI)U@i)$8f8)62{I*Ve+%eR-eXfLo%Bj)t{31h+cFsa>UQsvQPs^AAy zXgq?D10-;+Ip)nMR6J7}3cNHlrMRWEo=>=k0%>Az7e*5;!&BP+U!&aNm4_+!t434q zC4#^rk8=fD$xHYXdA1SKEHOb-%7*Bc(Wm3y zy)YKe!AIEc!yx!B?oi$y6;IT46p+nrQ%H(!E94bawMpnHi6yj)a&16 zr(Vd!EV~j(f`b+(p6==;YKl|eXrS55S8zHOJGMD52qNh052sd%>`iVB(Ntc|&6@$)yuezC z3}6RP87U65%t4)XdS4h5$CHQq4yC(&#t*JpAZCAapJ{NcTo8X+PdX!Gi7k;`eMyM(`ArEgAoDdcRGyg(la`?5L8 z9TS*X;si@~oc9e&M7w0)LzLEF0*j`kjjg{O9i+r^;%tsfisH_LcW*;JgTC)UOG^5>7SOggO9_(G8feV3RuBk-Z3kN#wt1aV{Lcs_QsGK3dIC0KMz-|TmKmB+qJina<;pD`oB3Lj zUOg0KMpEGlP|{`5#5%1}?9QV%VDlt>V#s$)`|h*03yXVZ=bZi5O6eVa`uaZlh4TTg`W! zc~GYDN$%ue;GIt3>Okgm$U`V9!R0WP*k+itQd@PPG3?B(66%|QOFv&K=j!sQf zIuOT{;3CecE2tA@|kfU`~*?$%fh0m;r<^I;luR!*i48=|ECa_CPGx4VgVjOHp# zEQI49gpityFo>zkwm{NS`V(l5v_Xp#@(@ z%2dwjdNO~1wFIMH7u*f10Yn86T4WaeR{G+32tC4ikXzvuFQ7Zb6|qz57tC~8mKB0A zdq0aThi#}+)A;J4HX!-Ik_*7nW09}mtLL-3+QTdMH_2J=>AVKmzEFoe@2n?xb)AO% zNFH6LFdH0Au{+bx(~b@wTsrK|(>dq$-9|dKjL4CO-pF|&Vnx)l?7c&zM(@R3BB5;U z9nXUyGGk`S|KZ#+SZn{0)+33vnsCJ0_@Ozsy3<{}h!ccFU3w*FVM~sFJUEB}e7Cy6 zXkh>SZ_-EN&>W&p`_mO_Cr6fxRLslIT#FsoO^MBpcJAM#$91bZmb{#-#$ACM_x(&UC16(fvl@` z)7Q3UeVJ!}2dPQg(d*4Z*58AOwanCL*1quMx8vo;buPQn2D{NFMn>A1RwL34RhdR= z_XsKUUX8<^-!>~tn4(#dWdhB?uxtMwzp>ewFZ~WI6E2P>cleWG|0FdNE~`XeS7mUS zwdR`~jUMvw_GltpxmnT*1i2@fMj_e2?#xY?`)saIapF+sf64Y|tx%!HKY2t#l8SWmSqKgvGN209IO6`asvr{)sx z=OGQZ_9-$}nZ61V?Pu~@*xYxv{KF%kH3ybCnB3wZGQA4~J)ywip5^M{luvs-)CZC7 z{KF&cp~Nn)bDCg=Ye;yfU0vy8URrQ_wU>9RC(sE7go6{{{MTtzq3fSiZ|dJ4b=S@1 zo_xuUH{9&bR8A6XYSg~&^>Ofc`~s(|Fg&}|pO-%B4+z#7iaGuU_e~&eKq8r7Ee0Ifo5>AOP25vjKOf)~Xs5!(H?hfj&2#c;t2VllWN%%{1?Q=UqRN zYu@|TpYV~z=YM43|M*9gy5yXZd|5?ixvV3ma>#YmC_;fFObia-nkA7gX38{Q zBPL5R)z0~ZQA;|mMksOJ52y3%rJo$4d_5D8;5GH#a3-)sP6Jz_Dn(Ojo~wpv9g z6FUJ4a_x5qH5QSOV@@mFUC`spz5@l9zvlW#qVZ8)A&t?Oz9_fciQk!U2A?CHt8sr@ zIPd*Dw6;E2^z5e#sWf#*b_ihcc3^Fk-53 z5PR7&*sun)!~{nPPa|KBwe{wF92)q2FOkmzdk`SFeq>x>H4+JQIE|0I^ zzy8BmBvie;>;pzFPWC|QTm6s6@J0i_7m2$=J|v_lb=;kSt2H~B5*_c*^L{Ql1P-fgAH zNipBumGWeX`R zYy9T8 za7*(W4=x?w*e+f$wxhRbg1L%spONw>jmIYP>|{>?ZPyak?a2#JTOo108yPUbZf%v7 zOMQ$;4p;mddTB$;8xe09K9jbN(+ z*b2)Ivk2HKBW^N+OkgW0*a}ShlVM8&&2#zJc407}EEsVLOCBD{zdBn=3e@yrr?H<( zA?F8W+^51M`Di?@w^FE88z)g_yRmXVi%amw6d}4&CIlH0-w@f82+$KtnJpbjndn5Y z0V$G0d$1#A7xYmQr<-l@Z>Z+JJTl3n^F@*Pr!~!~0=?fY8<9+RR5*!c~Q1 zShh;Yk_&>@TeumB$aCgRYl88iQ;nPW8CE1Jw>6qkKoFLCki|mBw+n~9<4~!YuYa7k z0!I~8nw@Km=;7G@!kv@fc`6m3hG6bvNI#u{C^_kXAH?4Tq#%z;SHE&J<7k$@q|k{^ z>;08eH@SDbn4Hu`A$2?n;Yv-|PXcs+JKs%*Iq)%kyj!|Up^f>3WvKZxj0?St(y|mw zHMVFx&hwcWpB=~!kE{2FWVX2GBPh~yl|?V&n}^}hV4`!qn0a&bTfR6hYkWw7alMw! z+`QYwe983fsKK6C{~41wsXl>}uFTKoW3qr48fK_IjF5`hC6L=PlwpEqA|bzJ09g%* z?r`#0CNclhgJS-JY_o&aDLjC2^u|lidMYiBYOj6g_7!wEH&Lnh!a;@Xxt=V$rs>P^ZryT6eqRabgn~gLZW;Qcnwm#ApbD!D7YtEJm z02s%d62LiVEY`LIM<{)dBZxprT0_c(U-w!=`h4#N)PfDtOWB(%Xavj0GMisc8F`dB zCo?N6WC8Nsa*1MKmF*@~gNm1d-R>(?=IXm%xfi^_>CZgQ6Oi6t7vy1{|7BRTV&KP2adc{gG-XCN0*NP@>9sUw11A_|==$d$Q3bhNp^|GXTRIGt_DkJP>xiNCdwYA!~F&Ovs7eNh5tY7=X&Q+R$x%|1{|3`!O zp{~QldK)C1BEj|Hf)5XlWU2--t8FglZ3RanzjrK-F5mGDK!sB=h=Nc1wx$w{R$(>% zCHyuuF)=h4L1{CaBUH7MXZ$rYu)}E34N6M>tydcmf&;z|>#4TtzC+mq)#+}0nBiNE zKt?P;wQo1D8Yuvk*R}N$*?O&77%d;CEfb%ER>IXryWgy#Hu>91w)Ot4r{6YnCb{n|Gay0W&P=K-}$R{!2!kLH*0<=R26C;aIfgU&v zoe!fhdk*6tx`ah(ktmqtBw|iO&Z~p=3$Xhik2;7O&4C~{F#PV#aS;ie=D2|D%{bd2~CbNH1qW%2c6b;_-VTAnVBhK4YB5|s?*CRSM{UM zjdgS#>kU-(gCTYGzN*t!?6mVDmzPTUGKS03y%VOiI0Oe&Zc#-J`E!xRA`+a&23oF8 z-5Pn#LmK}MQS2;$>#12Rb4%e3d-)DSQ^@umRADuKfjS`vzyUWn@#zTJpxW$axP+{y zY-wl<2i0NtCqHgT6$rhii_j|b?P~hWym|BbdeGnXT!nBTK0QPR8f#i&85k@NqO}sZ}(ETIFfQV z@x1p#o1sKhZK&i5Qxb7688bJZ^*)N42z?37q~eQFAzN#|GDdI^&ugmYg4ASA=3x-j zE`)_};|q~9JW7iXr^54x;FZ9QKk3$I+>HWp@($}bRupolPoN!61;V5x#Te{{FhW;S zCpbT7pnk-82jAc7_10FRh@AGdE^pOL`*ue3!C2@uMLm-*+AHAf%5VwF5L zgFjMTh3));rZ(}CY4q-eh)VMskZ-o)i=(bo9l!|i=55x^2QX<)9D^>RrOeFz6ZRaC(T+bI>7Kq*R(ha!G}D=P8Hznp-@O` zgFHl|p6oBKnaK2saSGa_Q7BJ)38Ul?*u_g;==9YX(X zFM7?ain6_9c}Njz#n}UG#&fZusx{;sb6z1hJ*#9xdZ!uNhb&^ZpQ%l7%Yh?ihG z>Q0{!chZ1J2<*QVWR1CnyKI7u)H^i?GYh0FnTUJCQ6#1&Z{K^dMCVYOxR(O<*`i2W zxa9q;Cj9wz<9qQPc`mgc?f9Z z?1Qu$7tA1^AxK^NYjAR2g(s*$d~i|K)H9a*FmzrRS^O$Yfb|Wl)hUUUScWnfAnk_C z=iW5lfl`Z*b{!c5)8JJk>S+o&)X*E5Fa8=7Q#aY{E>>PEnRSbs;r{6}y9%VFHzNt$ z@QV>}G!u26NIWf>7cB1tC<|4+YOVVf(9Jnh>2yV%cI0I+J45@?dBymO{+KEe_ha=p zGZ%E1M?d0 z{8O?{o7EhjZ#9#3v2(K+(dqz}f6d0+M^-;53QV;ZtL9bImm-3)ecP+TwadmKyb3#K z*zHcZEKB_Hu%v#ak?c-eKC+8FG0Z||UQiwZc6Pg#GE{ZUTKh2_MtHi0S%C0dnB>pt zOmaG6jw>n#d7b1M_<==y@kQ3weB0L+W<(d;SYa2^^WMvC#)}zI?}c2bs8u#{Eff*F zDL>*nfUO&LiBLFc983FqKc^iW=OY*rc7jWCgq1H|5_3#Mft>x8_bo$8VXCIS4B6h< zDb&!k+v2tRJpCte`){Dajr;}|VJzhun|LL-nq0KKf1y(pN=c1i&70V+2qc*w{RQLl z-VhxXUzEDAI;A%B;_4E2Mn10$%FXY=XY$?U=7UC+%Ev@X92v(g!h}+0kJF}{Y}lN$ z`TnV#S!LqKQF=J+^w)gKd0-7ty}T!n6VTXFbe_um#e#t>G5npK*Z+r6?|eu~7Dy-d zM^2vxen10&o=P!mOE4ptgJvpeG*iE5+6poYBxT@W^eSJ!l?!TR*1v$Z&QYaD`ks*y zbjW#GSq{**^|U*P!*FJetZRP%OLNO<8omNqsA3OCs z+xNph>On*9fhqhe_A&34>MH__&abwAR0_;}oY<*v*Q#oAdxS(-6WXmcKh$Y2fL->Pc-G|Gkb2*m#g-&e0y~}UAKRVae{p^J+ce$W|=V51)yhi7bvjSw!x}1)XlP;I=qL=jAR&1vn#owi7F<`~W&NDbyyimB?*2NUUA+^I7V z6C?J+PCr~W=>KD+q=@K52@s+{83f8O^3V9X(AA;`h&m_9pIvS@LwSGzexUA;IIp9~ zSKWEYb?}tssyLbX%Doa}`=h?!V_*)4D<;&KE7uUitOl-}Jc{rb=4Wn3wo*d%@pJ?r=%NQL%J<1^f{-C}|4LD!wB*sFAE*g^>$H zQW_UQx0*UK(;j7udoJB1pD&q>dbYUIXq<00y4%oHoLf0_Wz0rFLPL}ly6qO(2Y9P6 z${6=1G6;+?#+BvhA27!Klf#*N_KC*l*S=? zmU#brr_OLn55T*#%i6JQ;FOgKP#I}iQpE=-8!TXl&q&4gP9;Ep9HG^AvoLHCJ9{7k zf0b-2idMa4tx191;lQt>iT&}!jwuIqj$z=F${fW{M{(c;J5Bt{t2%DQ2>vghWkHyN zs^#gIplUyW3?ZnR0M{Pn5u*IJ@(5Y}rm5-_{-|~csJm$sCkOcSM1LezTpo8`M7ed_ zuyTvAI(63&XKLTDeiZlxi-q=<@~KPuw0DN~+M;lky%A?Rt`)KJQh0PWkw)ct;IK3LX+=Bg zIKPBNx7PNl!1;x}g(DFrs?R1WJ%{F1E4bwe`k2zV@0Vbg^4vbrHo@J)a(}>cS)A;L zGbdH0bqlgr81ueE=h|u+vbP$I>=$wKk0wsaTs|rqu6q3pr*?bkOs8}^(QSvo%Om3C zoFhf~?l zs0BUUyesJWtIG}a2twq7VjSUf1h9OAka6FIzmYtC?9Z^>Vh&kidcv55K=TSI{YVp; z?Dt#W_(!giH+$cItb;TK{&%j1W9cW zKce9<$@fHvY}(2*lkI8RH7(WFbK8lu-ECeY1nY3s5j9Lm5xO%%&iYAi7n0tuFuaSKw>moo*raea=%_ezWS?W z9uw%qeh;b?W0j~9=(LEQs!rKxe&FMNX-v$Cr}62}2wiLf7&rrKmkyze#VSdBw>-ly zdS6A;fKE~%jkwdT@Q_G3PE4Wvz7D&oomo{-4KZdtQP;_UG;{7=)`gh%=uc38=IYNv z{aI{XxT3s)_cE_v3ryp`*3FmXUFZDP_^rLjGaZf{6q9hwoTtpAzm}w)>F$Yfc4a;4 zgtubu?WB?M{DnOCpCW}sK&~v4wtYIX!oHh+XQyWV&UR*!77$_#>pW2sFj&yzk^BdL zzDXa09n#^(G}*qHSi0Xt{s}6Znz?$0Tug2&h+^3N_xKt=!MWb%)j~?FCaE|l|H0p> z!Dv<1T0<6StS6+sbzatCpy8MRnN-lB(O~Y=+T5$T)aG7qe*d}rVLeLsm*EsPSxG*J zlkp|fWxW?06zkiiBz=SS3CZ*?Lhj7`;|%CYt) zg;5WrQ)Bf&Y@2vUd|J%*-pn=Io5;kEaV2MNw{#`3Otx>*ZPoFwU8uk&-VJ#h9-!2A zI+h;sXDRcQm6P{ zq9SOeAa~pTys2J{$Pm{LA7jK${*io)Ud9Y|>xg^@k@HXIfn2VG-O9(v5dPBu<6b_-E|T0@F7yurZyh|Q zKTFy~F9j|k$Ianv@-apTc%{>Mi2l#zV?-!3A|K;HyzILgYnNmnp&&b~fy+&vx3N2h1Rp~Tp9gXYf3&!|*3e=tAe z@WXe>&p7<>UGg&yKOD}_sML&CBOvM{DN2sM&gEwum7K9R>cdu5{?AN+)%YH8p20sk zOkt*4h+rB~2s+UADIp_~caiHVo9TLOdFQNkEhnKiGd{3)9`f?QdV=K9IU7NvE6PnW zB1F5SC@!-}+u;S>rFk%kF&tAKM{2GyNsYL8uzW6UQ%}Md>U8rHOD(|OPxN%EaUpw2 zfw73+MND}8nI5{{!)$Bg<4NOGLNzbpEt==RiDveG7L2yg4xD0(FAWnRueN*%8UDg} zvQt7@k_KL3C!QkTV?E6q{5H9e?>aTBq0cV@N`y$+zE=p)d4EwffW~s?fW$(+6J83) zhauydB#qOB_RjG<@E{)nEXNR>b5TMAJ>aK{8R3|Is5Cm2NQVY=^Khezv)6gBUWJ z3Wl@B8_gsgl)y3s6R zCy=rnoU+TD*TeZ9G0r$Y-y@&Rb;Jmk4M)1i3<8e9{Jw`Zao_X zVLDyD#|0SUqOkKi)40iYzjzk2pVhe)NG)hE^Z%wVY~6M+hEWUeXLF~WCO^dCL*mEY zk`eMY1Kcb^@%7-&>sf|Ub~XH#m-ewbOqQT?b3dwZHzGZD{YmAZ8Gz0?na7_j3@#QQ zJ6UWn@^rAr{6M=q*O+_^?mw{ey_Jv@U`Eh_5vQ4t6KcMrFLr<0o_ZGRan| zbeSeoCMhx-JHV`D$RsGKRH)JTfbmURDEsWfmo1<#|S1YBik{WOre#97dggHPvq zx^~Y|{dt3VdQEHdmRXzCX{`NsXxVC23py5F=ux?uqT?`74wlA zAWrTMGIrcDj33~)Fh`0O;M4HOvVl4Wy1IuYYswL7J}q*)tNt0L7VGj&W}lG{XAzk~ z!VfFQSj1|41~rJ%V1`000?G4!rWbAJY{>UBp=NSK&2VntsDz&z*XtgerWImP2m!j0 zSF?7r2-u?C`n?@^YVR8R6BOni?(&kiz0ZtZRfx_`94j$&xy*}DV~`G zpeEhtMA=fQ3`>(-x=&v}**-U@oNS*Pen-QK?lLmlCw|TFCDxh*eKkXzk@~L5<|hHt zP`1zSK*W4!RJKoBlHE6GFlT?>ESWiq*TUv9F0uz85Xjq!I_tlX>Jv;v6pH0B_o8+`3q^r);(mP$4i*%Wj}q_B%fZuL1%yf zXs`c~9G|fS^X#QbwY&aM;KB1n;&_jYG&E@cpH1<3ogo{~P4PLIEXK^D=cO$QWO2{m zGjIRzCHbU3BRV7VC6ni~n{Zu*(!yII1kFQ^PvwD29ETabDVjJ%n$L~b4db74@_n-8 zZQ79|@_nkvgj7=L4E$b*6!k(aA;EIFJ~vj%RITwZwRaO)&Rui{OGzk7womMh{@eLJ zV_3kp>fBJUDseZ46%^egBE%rK9Q_-r+G1GbHOGH1MZ$s>6l>?F`?T?IUoZLctv{(G zweU}bOWrNvC-JdzmIYGwq?T_;qhcab#@e+d? zhCEy~P-k4?U1IeI$PbDy1Q8WHjXjw``;ee82{^m%gDrv?dX2%78T4m^nL+=3bY{>U zJcbgva@ns$zm3Way8l1P49Y=zPCn2boRW60LCz-aP485;5oiSOjrh09;e=_Ge2)Hp_Y{q>)V z!zK}x5>W-tJn4*&*l!_svY-QH>k9oyzL*cGb#F1k#V9NNd#K52*I-=uoDoJqN-N#I7T4D&dfx*X`}|2a#Tos82ohDnS=?f zoqV+)jkA3+55LaNB?R;j7R3{D{e6c}C`g!nHA;+abDUDln`V|c5q9*!P_J(-c{051vkvgIi1sz@=3D2Dp7DC>z_i3=t3NqJ@)eB_bj>N zE_?Zj*>~JiLo^@fILQQ%afh*i`6qk%+jrh`M>ORx4^P<5nEcKugoH&%8;Wb@Mv@c1 z^~Hj`>@~-4%vv|JV8*|s;)aD|3i6h`?}o+txxDc4Q)jZt{<5!?M{u3f|JXIw|4!dW z?D@jrodxjk?fIcp;m5e_Wm^?q$3>GU&X5~--PF_5Heem1X)31)ZZ+v(H z$*c=cEh@-sE6jeOz_cDpO+3S;_4MndR`Q!_C^hkQ3ZB7!EH8CsxA*Ld`|hX4h^rOZnj15v<7i1Cd8g&Ux;X8bD|p05iS4uV zX}utgCtk|V#P7Y#v6$`57UusIldcYWHVi~?)U<*LkQ_1>WoY}1Xgm<#`NaEFYM1F!;cM%=@@HdFYCllCS1x?xr@8rvJ(7#7uQ!{oV-jp49xZSSmOv zHJ9~?6k$@J1qxy$g-xV$qVZsjb^Rsf^kLap>|n&Gxii>D`Ckj8-B^qbAg+xy_k$-hx@){KD^|!gLSf(l=9i$Xk&IsVKVEh2@ZYpx)I-8axL#Zm49{A;GGc9E|KyxtlhhPuo?!`y zAf=>{0LTy!u>N1{At)?nY@7k!1~`m$%O$rFQh)j@Bdr_Vw`yKnQ?Aa|D3|hym78hXl(cJ*Bjd+uDr4R zP3~&2yBgb9D1)-8jHF6&MVt7Yh4lDl!JFxu*1b$i9>kz9dr93Pb>f+R^@WplF6RBX z>-=Bd-UU3W>U#LjZ9*U!&VURCAsQvA(HKC38XBWBkjNQHG+xjIqhhJ3wJL=fz$%(J zNhKVoQhT$1FSWI;m)dGCBH%3{1Of;I@B(5bK(%KaR4xh$5$F4@y=RgM((iqr_j{i& z&oepuy7t<)wbx#I?X{!sJ0+Z*{CQ^Z`*|hAAwyEThDy$PsNV!-V5}0`|9A?DdF>Lg zQTN3PoO)n%wMV}-4=Jr!tP)9)GZ|x1_c6}SrMyY~_GoDPCsA4Q-0pGLYygp}owM%P2+p4>0({*>GPd?0$tC z{f~VG_8qTd$~l;kXMaL@IJf{PV@}2OLSbmc({C}YwEPKbe3=;$2UgSkBZ^!0o5tUq zuLCIX`FS!g3))^zjvR@NVf!tecaVMR5mb6c*&_d8$+)djflNEEB{Im)tWHXLIay_u z@>uxF7>6u;ztr&JuwKoQ(12tbZhvr<@(i+HAvCftA-}ziDEldbV}X*Nfy7Mzp{%np z1`FcEOS1cgK85XUg(f$`dXodg6xlT8jor0T_Xo6_44E3_os45ewUqH%u5wCS-xThe zN(yPiZLl7+lZ!@wU8Wdq5w@tC{o{OzfC5g(kyy_kfvHNC3$~i}8Ot0C$lSC}y;N}!D$_UbE@XE5#6PI%82HV$3j+?_`LdwPE8~>UyEw&dlKIMyP7Pcez|!v$vC6yixbK zZMdu_ZADIhxt&OsMMY4@NDP=?6en<@E)}a1^%FT%S3U@;>eXXcJEdJ*3==9 z5W;G%XGt-Fhzj&Be@seM>xa5!WqSp!SPuA@nQ%AN2r@i;uaV^=>i!QfVS;4k0iGb?Ufqc4k=&j^{R_FIaDc_Ob8-udDb~XcNnPnlzSMQm-F^G>HsjSHvzs$u*M^vU@L^PEatcA^ zi#;iA#ko@WBE~Ho*Y!+ZO)T~%}%Pz|xY_XZqUw*9UHo=E{I*XJa zhx01)U%{bV6F_nKM?yR}Ts1TyQTgKPp>2jMZCGV0`><9QyF|l(l2OnhrfDQx1scOL zD+^nz=+4&gAfs`ybj(@a{gtIoUv)~-{FB3sj0Alt;<}8pd@4WvG|Xi-w5rZL%Ue(Q zQo~g~j83c^_A+NoBz~H}mnE=80y_z5A*546x+G+O;4cEw7wme#C}@pj#qmPr!=;f* z^N&e5e>H}ARoQ|&B?P`8r{Q;U7@G_@7sJNn=iSdJrTIKbMj`kXf^T-?uq1|?z?X9S z@Gb=J!sGES1nK8h`Cpk3K4Sn=iFP>bBnuhPfhcyAHP+{*xKB zy3L8B_+glrZLKP7i#59cHsh@1h*Qh!XbhKe)?fug@a!@h!m;+9L&MFKh-!)O==QB9 z&?A8pBruok69~zb5SQV~8W#AgK#VnSyg^UtpHECPK~?aQ=G?EEliR?qAv_t8Nvt$v z0wvJS{`}X8h)$@%p%+LQCkFlT&;w)HR^N)`*dGd->xDWPKRGDl%4gh#tZrsYj3g{@ zh3`~B? z*EO4e`vdPttHhh+q!|5D?V*PjIxY;Ybf{2ya>95@W(@SfR|7@ES0e-XYQO>hUB-o! z5P5>(+gBe1g3zx^Ul;n-*ia4qLW0Mjq4B(sv8877l?R%y?3vws<*~(AM%%>yW%HF^ zL)))BA_064HDB2)PJN!yeC5IBE02=?8{%B@o84TUvAB9jJYM-F+$5auK>o1rHkI0A zgsW6vvYSQq%r1wCs@iY*1S8rFpv~1n{+jR{injUO~7T}(Wdx`p(|wxHm!|O_b7s`%3haO?FD?PZ;Lgn zYnZG)YbpW&&PjS*J`@1C+3ZVwN7iO8T1{o!ST0+xQuq`5T2e>XgwGewq$S)AWS{jw zE_a!HgphMKxf*7bZ_`y2a*0Q(IPU?TK}$oXo9iz}sTlV)kL4k2NOopG|XY#VR61U`#1+KTTV%Yf+Hm%Kom z;TdFm2_3jE*-YO6Futvc?RB`I-@j3ju*P+PA@-jEV}HZHgguPxF@D}3WTFav>tbH6 zAd}fJNsWxFgZE^o1(34m7UepAK8lwy7I;j{J3;Dkp@|S3?s%A)dL^PaUx9r#-4w}< zoO(FPu568gs`>@b@Z-&tz9=BAsaX2$$ME7-Ck&(V8Q z!IHxUM_MhcAp1~<{R2@)R(n?6e_}z;*=`~j?uq(~rPGGmtLPHT`7(jXFS);V6hAS3 zwiu3;vYRj6?&maNgPo&+>AjTd{8quN3u+0i3vXAd8k>4r?Ggx*Nz@q_?1}-H> z)ScQPMV1kYrLoCUUT}SsOK;TnRt?YEAGyJDjwXfu1L7&;@|E@E4PDk?ze_QeGdfiv zEXk3#cG^>PGFacXwNn`o zb8r)?L2Hc0Pr-raHk$V0sN&L8VjFhwOc*34hC=SouZME?Oi`^nlBQ9)qwewj z`A+p&YrwV$Y<&+k>Y{_~C^GE}-=_M#jaVZwmW~=s&qv)mC{GL%iu+yMGJKfW9lnPi zR+Wtmse0(2%7w&?ae;8-ha!!Wmp9or^%KOt`NRr+4H$D1D&&}ggBp}e4McV;&<)Cx ztRk3wM(PlAkEe79fri6IDCEp1o3haeq+uG;aDgPMhQI(;Ry^~J1VG=c7C;GbKI6i< zlJf#*hg?1nxj*1z9}}_mKsdt_;QUF!N!U|jcPHdrPEL!f?CzHYDibIPsE-S{zXA@G ze)z28eZwZoI)e}%RjD^T|EOzQTnX|(#w?rkEn;6FcX6G zUnBHgUG8bU}M_-lA~nJ7eVmaC6SYYXmRT$x(3pr7I-ax`-){V*8PF8Co;T%dv)HW^pu$-oQi!GV1QS5#Z5beyFB5dQ|?6q(?M|8oir zQ>ZHq&1iS=y{eEAt4Q@Osq%;GRG;8q&5zRVrj;4wHHb_4_zzOLpSw=7);V5Q-}qNt%x-M`1J& zn|^$xwF@*p?p*bCp>w;_{m*M@a>hX?BjVkOvyG!gtB9q~zY6C=EBopbI2FvC7m%96 z`SBoPulGQsP`_`aKc%Bw&JrE#me?01_U!)h%~Ho;qS=#|I`&V;vB}JIoU9AKi}$S) z@YegQ;+FQ^^ExN4-raLQuHKx{q-{U?$)Wb1okec}puJ~&(W`uoMkH-X?OVS1j1Wf6 zGmIM2xOYmL-n#r^$=5Uf{GEJ_M%mL4>-s$0Lhbsj^MfUmf`)_$nAwY}GsQ!8nE zT}7w!UA^bP^EuK!;wXwL++1BMbht`Zi)YJ%Dp)zOYb% z%UacbsAl8pJ?mCD7OR{!t?fPiBQV|{f=dT?ycG0-h1N)!t--gK39?8)81RV z1Ayy5dduoP;pg)Ykyso>LnJm*yQO_+?H;~c#Vt7QY2R75x4pM!bwaVyH0u%0wnysR ztK_?7-qwlj;hpa3;^`?^)xPCu(d~l9Q3lq%46ML7FmZlDe=weQ5o7=OV(FUxn#k_G zfBZ=5?K<@ZJ^;=ZW^8iDRiaWPH7fgHFAxgEE4WYb3cC(&Yn6QY%v;y|k`i zL>WzCMF0y# zM&tGv30;Rzlh7-OhlJMfIgW(hA~_yJg+h%;nbiMKup-VsOP-MP7hHj%h;I|k$A2Ob zt{mxKnXIp=@3)C&n+mUSS~|W9A|r%V5zSxgxB)~nRi|^lMo*J&Ao0B5r>Za^p050L zIz^mtLQIb*o(cU;2KRE6q0Z5wzBQ1jfOH^bTLb`{*a`Z_&?sjdsyv8r2if zPnUa)E+a-qUFUzz&;b^mzcWYYuWJM0F2WE37raeXg+5cU+D-gEB2HEV?pUx4B&-PZ zP_nLuy9ba|OOf3h1pEN9doQUJ**yTiHhZb2x9=gGXY3jvZhyD*czU~C$9_dldyK>m zq_@u#t?2Diaj5x#LssMe9eVpXY2)M0$Iu*7-H}P$lz3l?S@p#{kT=Vq>^HQuD5&yGaT{ z&8h@ywUoT>3>9AIaLG4P(+Ry@Pk6p7hFF36)*cu@Z}+MKp|?j>u7UKneOv9P?K^8e zhU9)iGv*xDCA1ON1zmPtD_HNW&A(B;dutuazqdB$VpSVRbT6wzZ%tbQrb4RkmS{-z zt@4eN>h;xlVx;G4!4h`en5rYCQLg26l-4E8u+0p zwHF($Nozt+Hn>;1e5)L>(TVe=837M)gJ8Ps!djl z&r0iLbJc+qxfL~!o|FaYYttD>QLO!N?XpIlgR1Wr$$_V|aA1N62iQ1@(nEsMUTiOJ zk7CKTU-wYWooF`Q4FaZ>m1z$l7=&)p{#44F5OoiGg+MuK4fbXScv{!#A=-ZSyAqmT zj9beML^jomQHlI5D9Od$8An0C@{8`xt5)2)7=+RQm582WcZndZ!hLXmN;rvchLpfl7K* zlCDkM>B~N!5rNP%5s_1EvmcQhjfmtz&rJ62VAN`7fzBZNIUq)M+oNdprGiWW4y#*S z!^O2W((x@>!Wu8CMIt=-5i;7Xeg{Xw5m&J6=1N*D;AD?lf1%RKNRM1@53Zt)m^c&V z^u;2GG@FOwTC7}GxI~)$(<`JIFzb4q%U-74R)bt~crFD+*p8HD=eOY-ql`@)sOTy(c>mPEAB?PIol|g?`WK8WmPehZ!-Ith_+mN`@P6YME zt4yB$6G57&8Jxywfp`>DAUn(wyN*tL=i#%zI!B?BiVAChcSYQ{=#&@pvF{u!x!m8= zL03!A3<*NXT#^r6%TNe>Cl&#RJF&Tkk-p#$pOzns%!{qfV!;A{9ke%Gn`L5uKZX8Me zK@67P;Xt_}dkw~hQ8eqS3{|~@KNq)M8LH^Qjaa5)Ket7;>)X?zDy-PP(_FFLp3amH zF3L^vXG$N;q$1Tfv;kAvyHCbuzr@f$m&0G6MDsI=vzETj|JF}44zEI2D(YVGvNTSI zwJqvih7$-VBk_s47f?UiB-XD=<4@juDPh)z+y}9-D~&LNM6|(aBJL@Pg-6|av8Kro zIM@AMN|0!#CStd!l&IRBvF02$8J!#H@N#a#haaL=l*t(IhN@G-A|1CJpTHCKiC&0h zj0X4Jh7L6y$uN6>Q7_ycvH~tVqo!f3Ja|cEsr$4_;djm@jU32g0mbc>kl<;O-Y7s| zQ0v}CHE?%TajBs-JQ4oV-9ZqmNKT20`b3y(=S8?-A0-Rydfi^WT)U8~z6J25b?~L^ z2PHjdh^MF!kGW;H*&05-vT&U**kjyZKw@ERh2KZ0AVBKy*C8CK3kx8A8*#Vu>*9y@ zhuR%Up`FIcE-?QkJtIXu`IMk2kX%JKEBCPP=jD=K_7415z$Zv4gaD}~=LVf#Vx@Fg z>zblbIt)x|a*DLuz40=NqoSE~GVKU5`BT8%X8>R?N9uq(yy0w4A}*R>F~O3O1> z-UTbALeL?(Y!;dfV9#6g?&GNZrT$-r@Hy@kNiS@_OFbLV^tZD zxv&_LLXqFM(T3bkGDP6qvDA*&^Pa{LEPJs`5#`Zn#3qhUND^~jn0(nqjFDtCjsPp>9(RGc=H_@4P|S$8_%MgdeoayYPE}~SHaF(g z(^^Ql%4D3AAiSX(cU4V*E)>>EOj2cJ`AA&W9xyAMtg zAwI6~!O2E#9^}IHwI&xL?{Z_JhnWeo_bOS9_2!L`pS+I|!+x2|@D!?nA_tU8%(P3|9}SVd=RIDs6rW!Nx;! z#xA}w;(PEDD8@nKlmAqP+|!s}*xM?HoWmzTrd{rA1*n!|mHT&_Yg^ddC5@GmXo^~A zriyfR7Iw7uq*FA#RP!FWsGrMmv5w3cslWN3BbFo+aY`aa`>Dkztix&?7wb04IjqwW zUX#CGyQMG|^4gCvfJv~_x~xyy&3T3i;@?hjFa5YJ3t=Y!4%ul9_ym#VK8ROaCp(gHIQJ0BWOeU+dRrGWnR7Q(f|8z)>Z z;W8fa(QyQSs)MWK9)vSzG8AUprYvI7SRukgGy$q z9O0_58;quBNlj2vUyz7&99K$aA-%NhD^hZ!@XvUjR-Q9-rcykb+McU||4bSh75(z? zD;zb;$f52sx`kG`#%+E1W`*WM^-QUmHLG`Vg!iKIa42NjG_NLShA zDx0Q-;V)>er6vYRvCw8Y_g*5SD(4)Jbbbem*;WF4MtKWyMlSu-j@he(A ze2-Y<=dCSyjL7+n&c9D3mBsK;T&7XJinv!3@^8RxG`@n47U(%X16uf8;KccXK6qD4 zp3IylR91mE8CMM6ttzm?XgU=%6o(l8j6tmUYs-DEB+Qh{WU;!r2TA(OoN7LLMLv9S zyl(E%3Rb7lbVeVn-ziv)%yBhL*O6F{xVLZ_jgy%DjLP9;G#c+C4HZ#52Qo;O2$y?1 z$;vI~bUyYIQ)#Gb1+YE_@18z*cPn^dUCa_4>@NcLUsc2exFYNg(dc(cT&eRq34WM! z>!h%se^Os2=2?VFRTZx>29JLRp{Y0sGU2|maRx?8JGE%S7k-Pb*Zfu@?veufaIG@V z;rvM%=>H2zP55V|j`xoYmQu%?#L*Y(>wlf5{fYnz-Nc7Njiz@&PZb!xMvCgEUvh=s za?jT3Dvk1-dj*swqvU7e%5=QIN1EHH*lsl8R&|5RvD_Ys$#gdJ(Ul#KOAk@^H4-9*^Iei*yl2Mb1u~k+b^tBO3od z#pBC!Uc;GCKQ-nA*`E^*vLD7$mbxFr6+`w}iHjrq0^N^rCs3i}^NgF*CAKN&$9&3- z3g;<&2lNsmhHZ9v1s&7f{Vn`O0MQoSB@yxScaTX3hHkR2OZ%`;<&5nN)O`y&NIXnB zdsjJy{9F|h@Bb5}{l29Ahorr=521K^;ZI9*-XukpQNF`}CZqUo+OwewRqnCw>y@tp z-!^*1y#DIkvh^AFTNmIQf{U^@xgdu2nw|Km3Vr$|w< zA-IQClrb%EjmFo-gFYx8J?CG;#p3jD`N-mAH2Tui{9muqf2}=|-e`P@-2s^&!=p3* zR0ZFrHW}^)Jh~IEli)|rRjJ)S=nFO__+c?Ht5qi0>b8i=#6mEQ9pSm_#Lcl>IX!AL z-Ve>vJAl{83!!*}kLLVkyTzRpt?uwE$;I*8Px->3PbD*}Ml~D&r*L~#H(U=-&rUGB z`ghmE)8qP@-SXRfx9}65{$q}I*y?@2X4<(zfc4h4po*|dGz^@N5uh9o#2vHa4(U%7 zHBmWwo)^W1qW8g)@}Fj&auA1ZM1$x?tp45AEYuXZp?z=N2SluJJVinRy7k#tf!*G7 zjpI}3Z=7fU2sbBm+uGaej>&mqds|H}XlkCFlHl11>fv_`A3Qq;aOgeM?&8$WRrZX?*@Q=@^~>htF&^rI%!qHH_#XVb z_<9wO{w&9(@aRu31-ql4yp3?-4aYnZwf7`2=!Q?`*unRx8i#cw+o@zaK_hc^;M)>aAUc`bq6MZSp^B6`b`~B|vJu z8m5~XZ$#|{rd#UmzXV2GF|dK@WP{so?-3HpvEA!=PeZA2oPgWzr=PTkBbzcczfESx z5RleDA}iwjwxl`;3Ne2BeI!5ucVc;wpZ4?HtcQn55M#Ul3%{M1IeZ~AE0$jx@;P#A z48mgL;&^`h9-=kBy_v~;+Ul6AYm*6rU8KoFq_m6e3=C}WXNAuf%mNbk1$Y6si zU5h8qZ^LZ=R#I8(6~Fzs%Axt~1*C>9E$s_Z{Pyni$sFUiD~K{0zYwUU?g`2x_lwB1 zfmE89wc;`w9|t=cRru{|VQb<%mh%}&mzlGVkLDjA#TDZpUzNBR|0wW`Mh7h!epa+) zxh1;az5x(`ew;Q;<|Eg`^ikxdiuWdy|5@|_YVEYEp;ur?>~ zv$SBdN+BpbfXisC)vdf$d4zvd&6&%8mMH$aA0vi!%+1HHE2E+AR{-ltDm^lZaNQ&0 z6^OyQcQ4byihsOIMfCHJ<0P)sd9nmQd^_pGjr@!8kKtnK8|NQC#H0AfHXKw|_{Z%y z10$uKT4K{kxNXf}B;qb9I@`lB{_#?B*-u_XS#Krq)0%(0UrL$CqPs{OrL^roaWEG` zd}L8>`ZxHg62lErR^NP~Jcmi5@DCJF=G%2G;Tx4(wU6e{(lr?2FGu6l{N=gg%5+@D zN19LZmqiq#hOn?~UFQ*#={zfzxzxRxfLJ$wDRFU@4Z)O5?z_;(Q2gabia}Q8Lrxtp zlQz6wg(2D#es?}0GIHQ2Eu7!tC$Ejim*?Dp^F;jQ6v9F7;aJL2_j$Nt$XzFKapb<^esUQw^h|gWR}2eP zj_{M;g3e&Rh;;TNQpn#F(;bI@qO>Cr`QrTK zu?do}Liow6h&WMcFObCkisSh3$&?2DM&C#HN&Yh*LH(DK;2lYD81`0+rThMKKDaPg zcKVr6MWg8oNC3N@zVpG@jk?$=;csBKj0dMF>i&VRHF8P(uu#jVwh|FL0CHbSQrwA$ zg{1_+=;k~wf*eC17XB9Z|LU;NlJxPz!k^;#zkXQw;A7AwL2b^1f}#jO(7#gdVK89S zL*$A4VxrEz!@>zVy~M^OjWtF48WfCA*`MVk>3FvXtQC*X?phxfF4L*t{7-OL=n20B zLVf!^eO~zWWCg?dLzUCzm>4TZAAJd)8b^IzIA5g}tZJNRtCTg4gY^QTrf5cA+`#j~ z|54#J&QEnZ$KTag4#NhX7oMWhab74TBCFBogdPX@G4Wo>jAsnivp9S0uQ0koA?>`b8@5dYH!4Vl;T$FmaXPR2v(q?sp>EfjLU9SOnW4h1kvD44uS~MDe>h7P>crnT4X06!hXlNm; z4@k~QC34BF#l_ii;1EaO2#buylSo=A_AbSfjt8Rvj)1cgOBvpf=j-rSk{f-TKn6N) z;s4+>tvgUpoR<+?n3iEQt&|Yan+yF+j#VS*VfoOsYsJdQ`oN)vl2B4oi*>Kd|Ko;| zpW^i2(NIFwTl{qmC8TfhS2dLUOuiR4lyDur#Xr8GfyY}b_ z0iyX{R|H$0X;M=a_&oy6<~uUNCqaAS-Emy6GIma&pLvb4(GKnc9;ssAR|pU(fmfj^bl7 zl0jnBQ4X~(%JrvRG#Hyh(X~qkMGi=CE5SVJ>ypVWrzuAg*9z(F-5gRyYfc$kW;*at zO70UB{Fjm_kO2;57Y+7LqCtvOeN}Cj3^LyAu>WHASC!|z5?VtlA6*;%9-R||j*&5e z9zK;IsQ{0weg`w~X;qCbj`Y3zn+UHra%pMLIX$i}(SJ+BNKL=1A@ z5sa>M%p1=cV=50VcQA{%XopGK`C~9~qgsYz7NuZoZMSE-WJa_pGm{jdY+)&VM0pNU zhJ~`;HlY|}y$7%!tS%gdykfF({z%&bOz5VO&Dr+NL(yRC4YM)S_j55iv=S>(-sNe8 za1#`hTfY+JTjU3~J#Y(6X0@(l}Gzroi){NF2I- zh`pq|trHk&Re0#}=A72N6cmvE!T;iQk=xM##PZ=HRCqVoZ^86h7#f#5|5YXSIlv}Z zV(nbq);AHfDoM_rRCKi`eHS{#1BJXXVooXveA(>0Nx@Xmn%y(-*kO>3gZP(nqfv;) zY;g4ixkt>~is*f)eozy$IBHHR`w&Oe$eFw|{nrHp%~pvp3;r06PS zC@R!flZ($pK6Jv5bh-BJdvnJsV&|mE-Zd+r|wEq+g{88mqyb6!%C~$yCj`?bdwp}D9Y1h z92Yo(1)2k7hIcR|B-Z+sb58S zBx-}r5-Rok8DUTj>k#fH1xMY}28i6!W=hFK(oNhZ3l6sJWT0UH;4PFeS+_)Evb4Y9 zm0gD&LL#`U-C>_6AVUwZGC}E}3R3c^v0`Gzvjm3R)wKpAUVIN37N!=Sl}WE}4fn_W zUB_9+#f6;BMA*wEIOJ>+cZax(=fgbUTc`-d=>~UaK z0Y&1^2^rA+-&&jFOM+?uo)pT4Xz4X{Ge9aWhn3Fs7UNyrW^t)fE51I%+_u?QyxrV} zxq+!vxciv#kZ2^6tjgL?no5g$L@*PsOpc-8n~L!O{}(A5y@_0o=6G z$*3T^Dy<wvMz_rJJGMrS1Ngm8}^VADeL&`Mnv8M&)AtX1j?V1<^eY z1q*(nvKb}hbmj2Ug)tX)qmL)JFMCgJWH>g zE8Sf0=WWLn`!{Jk6^x`N$;RUJ@$ww1&0qh13F~`uJO2kqT0uL>pJOab{%+ckzzy<{ zRd3%e-)r@;Ki|seH~No9YxB9clvLKuzXdHqS4si!D!evtdfFNDjVd0A z6i#JqtFNieznPg~j&5OzQS%{u3FU&3v}86>#;eJ*!RMser73}-z~ElaDxUAcCjZAo zVXm*Rm1(zHtRq(=5@^h;2W^YZI(mPLajtBM(hFvXX+5m;DUr%}y7*_p8x5=3jt%Qh zyURxtV^k{FK9xyB!E%G(UILX6#={V)?&p_eDbY4<*eqEGgL_Z2dJeZ{_(#Aq-ear~ zhSbiy41LYLMFyS*QxQ=GY`CC-D|tuV$!5l zHwm}qk+nw}8d9iE2bGk|w0A=d2@Ncu^*q5v52OT!Sru;uyHIN%7I-Pxk=k7GR=HJi zlbF7Q{o^*8exWo}aW_vCN#oB{%%J@?}OGGrBv7hRVL(~?+tfb({GjJ zNR*JV74V)dd4U&#B`5B-({6dp0@q7l%dleQ3E z?0T3nb2%RVGLp@~7s!OIV3@nC>4rJSC|D9c&(xQC9w!C%KZcCDYgr z+9{2_epYEm;G7d1>F2WGd&5$a^3pF#uYk`2+*-VRR`X%83a)lt58c?uuR3x!_EtwIP5 z4c|A>cW;ea6%ar-qwd>qGM`*c z(Lh$PH>xf+Er&5MEp5z}k^IIJ?3Y4MX5tRB^4 zX&M<5GUmAzl`FC$#kIrbUntB`jNf8ijxBw++#6f^s6RH)VoTr2n51XvOSYe4;9>uk zL~!*hB^VF|+?~uPh&yR$dR%`k2Rhc4ayD|ML;Qu&7GG*-u)ApP!I1Z}VE3>E>C55! zliC_u%K^1q*5F{zP_J=mOPkj!Z%tD?oV2gnzaiopOA^{IFqm2UbDATPhAb*rMu8P-u z39sRbJP+Pvs2DAt++`j)gh*}f6ke#?zFg)rtUepO?Gd;A(IoX~< zE|~YIJDm`#!xn=Y)V(kNapN?>)?{wE7$-ab;@cC9lTEW8{wDVi7{s*PpY#YPw>j#y zR{5;0X7Lto^e*Wz8Fr=b8;#e?3}^7M7g!ap#v`v#HtwJWwBfSD_0ObBV<0Rl-DNb& z)vNID`B!EA^KjQbZM$q!+;-WY?*W+Jjb9OUm*ErJWs^fSFGD|g_7SFag24e=DY{s6 z^I1CRh8Z%-Hre?)^dvQ^35dF{*YFU`@Yc@xH{6EJZ+2kVBU_7IGlf~?R0KD1j|?4c z#%#D`B-D{~ew7(CaF(-dDj6oY+cQ-#tfV z;Os-R0alCw5ErrE#!Yjr<712UJgM7Ak>RXu#tkkZCoudm7!a#Ivu*pN1!D*; zTmWmW#<{A}3IQWaA65FJ26Ll;DFO@tKqf1m^G^v-E(1y&9|h+gfg^MbIJamx-w`+; zBf&Zj&W#Ch4$c=iipDD%#6wXu&O`txgKE85KuyE9CwmpA{pdtWClv(70DsT8(*66C!IB%*SImt@o7WG+DJ+XAszF zoOK|rYhfD-zB-HW+Jx;NlI-a%X z0g*mfnX)Fhux}8RJHs3Nlo1@u56c4~$JdgC*Pph>H~29gO}{y$7G=Fzxr7d1`Wnna z5>z{{G{-{T;lItLC zo?GtF1hQpNr7bo!I!$}+k0CKq8K6$d;6P&#-|S!pC>wSyB_{M@Y|Ja%V(%uw5FOB_ z0K#nZmYP;hg-CT~IWFK+8Jeg#xDv6d*(d{1%48%`*R&%N5`S@^3H_}1 zaZ_eRH~e|F+H!DVzq#Vy_B)hPzsQxu8!$!%_w|FTJt8hYo-Q$`EFT@H`fVEh=D*Y` zExb1)ILifY^Wc1oo1*uhC%nxWkXM<~!pz!R%9D~xyZuA<2d9m2A^M68v!-B&B86^s z><`WwaZE34>_Gd?JM_uRlzojMpp<)kIE%%~zLkG4Ij1WanZUqi#s>RutP3zYa}`e& z5TSmm$kHWS+uMIj4sW4~F_;V~YNAKsnQF?U10@H0#2NPQu>-unVb%zF1Olv@r%+zggR{Zsk>jo*7XJ=uTJ;q(mQ zSu&&UZ4||zJlnL|p>A);IJP$O3~4%?gU`z9Fhe6+^Z?#cuq1tP`IZvS1x${pmE{u* zt2`t789|->40gEh$YxKI>$C1Rm94z-8_#n zhC}Cz==SnmjH4E21P5C~>MM7Z)Q2CTtTpAkN*b(998`@AhIzLRqZli`q(>uJD|lWe z8gZpR0iMXY!JgE> zIpWTU&cej=o&fU41SYpG!>~-vO8dPVI@1x(2S<< z`gyh&;m3=2vR8Uli?OooJ+L75Mj-wpStt1yN%~&H>E{_>7ei&kQl+o5N@aN%VviO1 zaWJoxe=H0Qy-AGZxk$XgL@y1o_3d~X3yJL_D?}{SoCU4HkWH&}0fXV?6n2AqFtsxR zW58)Ghs!%llKq42jkG@!{h@Z=A=y+6#v9xag>be2M?a+2w>5g3Wm^g9%~@N5N58?s z&E|tFBe_ZTfB5Rm5r5OVg*ByaFNX{Bs^vk@kmGzQT5O`M(!(KZoyMR842ncT;1RtH zelpCMrP`0%PI@9T+P*PX;QYPPayYjeLZe0-P?a6*9yhCB7*ykfGBPAzX`aI zxMkSQxl`?{UckP{52Xo-d!<6+-lxV>C_@?>K99+#+dQjd4f73y(EcoFQ-#L5|K&l5 ziL#?B{Ix9IZu?P^#LNfX$|IjbBvUPKHe^S5>7XxLQ6qK8!5IhV9Ao$uJmekGrpKkQ zJHei=1s4HsxvGoaZ=QO-A`M^qI-D)(JP@>Wo8IQwLa&}-CXc&zJ_+~es70%_$&R;rrg8kg9 zG8vI=H7v{uAEP(UfMXjZ%P+zn>wQ7=EG1U#pb>y#c0@**lb(|z0%yx!E8SQ&Xm)d| z>M>6VXZmpcz?%?!T5Ja$vs%M{rvk<@3=3gt*j`E}QoXB``Id5MyccqE|9Bn`xbJMs zmu0mB6NYo8%m(c9>J^zc-|V{!q=#W@S!@h*7N<+4`{!QlDY-k@Ujmp&&}#SUVWR-V z@?!Ukxk&Y--Eu86%RUo0Q2(ZBjB5LZFX-HLboJR37cLW$(_AzddZ3rUna6XsVn=43 zvLU1TA&~}%SP2@Co1tg`d=TX}EC%fUXc8xqb%ZA}L;o#*7smk55JOn;!S=) zWy9f`FZc`w9NndKyZdw4OEC2xSAr*Q=E!WW+783B9fk)>L{+)!I2@Bi_HxVT+DnueD59{RF|!HnYQB<$&g)0Xu0r5t;QPxy?Q34DALf>DW!)-t z*<_U}G`(PU^WxW~9%RC~iV-Y~mx>KO{j_lO_IFq^K&}(KKw>(jR%P2iBaoSoF(l!^ zXoXYaZ&KQ5831pCee^Sxgc%yIFN|Y_O`Z7(DEEum$w>oiDlzdI3frenj8@6)>9AiA zcovy0-yHkNeou|PwBIw+4)%L)v*-7FO#AkJPqkgw?^$SH(dTKdmnvVLNGU5!|?C`e>(9`T2RDPwSfe?cw zY+f;sG(_fD&NfoXlMVKRB!QCsXJrS?rYMhnZv-)b^LLU088d{+1u8R02XFE|agmjf zRNJRA28LVC1*9ZJi7IS!gu|=N_69@=B!7F)3&*Zu1FKr2pEtKR z?&;^Hoq2$iDvY-OhZz}b7dQv@Xp16uTF$gM`fWsm{%r~9zy7J9zvn-K{`NTfkCP63 zvIYH8#DF= z+h2h=g5>82^1AJB06X2_@eWAX6)Aopncf7W{Edhq#Q)v>d+{**SA+Qi^}1w#nDzh(hrK8Ma^M zM}jOl&VM-mAqPmu<=LR~JeD&sPmHJFe7A2w<1nR%)$A&Wn6ofcpH)vI zt~o00%H}Yd-+Qw<1B%hNoDbz_L*6%eh9rsao9rcB)cFR{RCQOMw5lKG+e!>2!?EZ>n2rdd3* z*Sb3k^LAOB?}sP)f~ztPO)>5!_{i=`s|723a=WDkpD(y~5Y|n$XZTWAnZd2}7MHsE zVk=WSk)vNAU^x^i+#m{;+w6I#`HpP$9ogdJNrsig$zOe<{<#_Kqy}9Gn9}mV6^)A_ zzHNKBipi^Q9v)}&8E>o)oWcbKE++Zbwn%;n@&zLvbziD76^=hV5w1V!zOjC96=iSx z)VD1x+07$6eA~LJGCLCPgGrdxs*2bhO9PsfnVr6EHYu0-8rr0x#PZ-o@Ttt)DmHa0 zGqp*uqKe}A*eB7P$iT>t)&Xk35b)RztmXZNTKQsou6B`O06gaW+;G$>$6xe=oJ=?EG`eEu&dhNN599=<03DteCFG^=@Eq38l}@2t(aT)unj{9ifW}Xb@E3bvXLpl1ej2zApfEBOISjsvWO$?!HrH;fZDJ!F>~ocX~x58KKhw z;la)*nNc8n*UMb?XCFnQi*G70n{Prgc2hRL-0;xkq@?~%dh3y`!97{VFItgl{i0>@R3KiFEw^;655m%MrZtr}JF_ECc@g0t z-YF1~l@jx8MC>!fE@Sjo&f;1_U}V(&a1A_7hwb|py!t=+XJtngaVW&vb720IlA9Fn zSGeqRH9pfqSr^#no(LVEQNC#)K1S0Ig*^osg@+s*S!*n<*`x4yMKo4sb;yx}Us6U< z%fBL5L`$+cigU-+@oO*>l(7H5wY#TyXGKidcYM1CsMhiAeg^G6d(x=DHODu+sT1pc z>r~ln{_i!s%mS(FV%l9AyF@B24sKGckxnSy>a{khZm_de#ceuZRMee$o$7#&opeC) zhtdJzCI#z?Y_l{xe{z2x{DoMl2S?F=m7Un}h?z$D#^bv{T|0_(fkG=b7A6#Lg}4+b zlpr8>6G!N=zPNp0U)1Qnpc~Xpiu*VuLbjp%f_i0$I+#l?4PE1%-hXjuCO21<)HN{D zzJaRg+d{=mOsg&YG>s2gGn>DQY=?KWMX0rCzicPaMx9Ja*_S{pJ&PAzWsmks z!*WKA)IdZnN>j!T0V0xx*riL}c^m{sDED+wvXa9ID>d@>O6wMzi$h)EZW>b@mDXkn z^I7lv2wXbk3o0&>$zpwS1R38UUA#)IHc4}4W*QPObv&j>OBGk&S6j9+Cb=`$? zV6MBgqH>&3$&9HvFT(e%ry+{iD^DhgH*k5HGF`K7z+KiUl z^cvt(PuTv#&B(zztE}`&>vLbUeM-qi1zd_L?8I%RPOq+Ft}rtnzgWvr0h)2_ zaa=otMBGlR3e87s+#A>@m#Z$7PC~~0MBdX~{lbfcO2sk{nTYR}IN zP390y?5rnhPpGRq3Wv6;(8A=()MJ9`*d3~&0W8H*=4%Xwo`+u!f!4M zWfc=sX>F{sR@=T<?7NirfQ%+y^Id?FmN6}K&IKlq>{@H>7S=Tx!)MTD z{|2V)u0Q1araH!0XLsVZ)`U}-d|4Fj$*k>K!q zaXC_M;&cqXnAdrX@*4I{V3r^>S-(~GJfcCH3(HbvF@p}6uO$l)kxPQoE&el>q?|MU zqM`*uOykWK(|D$BE=$iQ`$uXf(n)IqAC)gj@z!^LH^8&Xm!i<$>33IIVoP4c^PB81 zC@?HJU7*|WUX2F9<%UiGs>h`^xR?O(u-bJTW%c7t44pu zhilZILIXMwM~!+)nhg1e_boY%DW9}3E0FCex$pR6l?q;j_^utHt7z-dCrF?tYGmXI zqC^cKH2rC*z^UC*T-D1En9ima)NBew*Cy9ADmK&XA^^#|N*Z&!l7XI_Sa-Q!Xmy9D zLZGT3nHg1u6Z2$D&@zCSBUP*x*Fq{-&u=lmrPT5<`}`+$9c%h)cbh&ms*W8R>DoCH zYIj_M#M&JfrD}%-FhtZ}+smjQM6tf9g=r+Us-HA2Fl>o887=$Vi^c`gJtg-bQ=?wY zDbbC{QMIB?uP=^zN)|ol&ye{ynjuG%L63NR;FK%rZ`C_;jO1h>bEJyQK%Sc{?BsP7 zro}=3S-!&eWPr<_4at;O>37)a$R#2}>lZziqyevbSDFH#uT&$CSk#du0@JFO?H33a zWsQ&wUD4d%c(){cB5$#b?S|Dd#1p9%Z*5%HqX^XT^a(y4mSF&^HcHs9)WtLC>ltup zGvLr>4u~T+ z`y?kA&th~57nXj7VglN@lVy(sFkF}^To|)){t)2+mQqH zCS+pHH?$>MBGR;$pw@AF^ zMBQ627aAKF5p|omAi55)=Q-!6X%o@bo=HMvdp63Bp_7xBpWu-Ez2Icq#h2_vyu zC%DY94?~y$I+GD3wUQBJ-6=;nXqZrKG)&MgxtD$UUOChfGzYxTB06J8f3JZpTq^nu zeP@}dkZ@q;oGOP1tPgj~(tm?~gOy7ZJg{-U!Nc{#k`0&XGKTqFhRc&{4D;sVpMZY? zeh+>R{%ZWy_-EpuX{N3xXbwU9&D2#sR8a!UmLx4sj+_ZQxm+30*m*x==IEBbbNS7k zGOMTzZ#z}rKM+d?XM^;u^I16!0M8o*&&vNhsBr^pStLi$N}>?ET+o`;k5&n2i7uRK zvtjzUG$qK5lhN_ekRz!Rv?e9X0oP#rE%GYtS$ksH9#qGz+@VZtdO1cmY_~d^=synA zGHH*sgJCD5(BA%QBU7Zsk@x zE8FNrGHcAx{aI%7rD;kt*KGcY&^P1Nmf45?j3SOLQ?c-yC26NAivwoxv*f@*GxSsG z&6QGzC25luC(E^SxjJ}IpUc2Km1ymtpr~=kSZ1!vy&9S8^-x9Tdc8W8cD%Zt0u)Oy zCtXlOe=e<|N9Lo^i0Kk0GRiPfcg-}#RW+VNkf6+rdQ*yDoE&YT^wKqRzgkkB2M>Qu z-nGo`Ik}ND%8g}{lTm?+I7>Ip8)~(#DbJdd8*TNLcFad*vx~T>ghFaJW2&I!=(} z1vql;ITsz5h4D%9<8n34p&A1-UPwlDAY+DpDL5QXN%m*fS7v9smQ*_Kj<^q}r1*!5 zKX=K)j=RYoNjY!b;)k<2&xZW1N@2rb20u>@tc^@5-m01@bXhVl@B9|?B)P%vQ47#E z8*2DRzJNG zRnBa{h!efUuuVtHeYIsqHZ#Bmkor(EnApuFPCCYM|X0Y z&D{}hvCA=HzCCzjoZxZG19a@A1PEuKA87K(Rz-|XazDIvsu3hMvlYvGS^cA&4(S2La>qp^^7`3Tm?K$mFTXP z;O>TV+#j0afIG59hk=bN`W^NQOgqrRhOTU~Et0Z?G&_$l|H2m#q}LohPh*F9Oy?fU zaTX;tsfWhUBMq()RRF~k{YE+=@_ltpzxL9Inyy3sMQu{Z2_;-go@xl+-5s&oq{KA+N(qI~R;7 zTow9pa%eKlf4Z`lWD%dMrRSb54;;QO%sev1;aY zPtw)Q`45iD!jED-Ylg1PQvEqCbWx%4YWd0Cvzt@Q&{ZkYoL_`T$w;J|Q`lRvI*Ll3 z@RxJ7+d$1*(qIvNc<=QCm#Y8E4q8r*};oc!UGE4l=shHRuD<<=I6 zmDl)HTk+0Vd87Nw8?r)*GoERkJINb7dUm9caiPjO8;33ohbk=_hbqp%AuJ7=nJW(t zV%Vb9P?={|G@_qV&IZA#XyeEARsy-ODrCcRJ$OAhS)J>M(69z#ecbz|o~ZkF5{RC; zlK2^oe_%YD)_!#BC-}!;#sC3Z3!aIW1`eB{iIiPehNHwRJS1OaWNT8) zE#IK+3jb412m)h?$VIYa%gCB#!>dmd;qTwrAZk4-|2PGM90~@R5x|i#&M&=0MsmSV zAMs(VDAi|yK8K}}{$Tja>Qgjd! ziINUCumT^V=9I#G(fIb=7E%0JqK*zlpTh1&sdOnozlsAY-4(OXrxjU*e|LI6Krbk+ z$F##fL&g~xk5Gb=I&$I}q)32L;fC%1f7pBPu%?!-4|FyOCA5Hoiim)sfLKs0D1rn* zf`DSLD2QMe1VWLLgn$Au_TIa4>>WG7Ua^ZE+aZ{vqUTt!<^E>x;KB2q_r2|(d!OeE zE@$uAvuDq&Su?X{tu;#{!Eu zE*QrJ;}Kvv&eES#?KF=@$MP=7>vf>>WS2D2K_)OC8||>mBH39{!%q%V)EFf4h;a0&yW$Y;=*~ z^CSok?aVk=E+vrK&!zVuhfbBp(8gEUHtr(6c~uKZMHexAo^e$~KNRLt_EvPv3g+aB zuV$&f7`Y!NbqP@hVT8+NGpFO9XNV%!1PM$e12j*l=*^${I!XgLY07Ya&fjVlia`@} z09UZ}HLZIEKZF-VQL&D_Wl_Pd$AAQEUs6OtD-D!rwRpo11%;?vYs3|Wmfw`CPax_; zi_9)bW?>woAFd*t<;S{36{ottA@)-{pf4p}Rq=be^pew!CxI2`LCN zp;_$FG@x08p%!8*?FRZq6ICCc=GHxk^C+QeUKIk9(C^x=I|?jb6noiL-+?tEIt;dt zL%+6&s((R?7WWX1R<}(@I&5%flb)%YR6?C#hJas%;z!rw2o{v=SVe_no9Z8 z3G~WIgVi@BDlx_?cD}(%fICZDTp+I9Lgz#Uaq!M|<0K@AB?bNXSVveDL|~Aofw|~o ziB&STUIkg5oP*~IF`q${fj)}*s6>wHWy`rC=-1YRvh$2413}1 z(5KcIqoqGx^AedTDy-U5zgo2q1nLT&{(;Fa4@}h+2Kop5Umlq8O7!sth^NvWEam^a!6CaQ^d`tkAvWiKEVcT1WAL%m>E{ijy+D zr2gGd?&OG#>oiKhW+YZhycJ?6Mv>)DlD%mBk*65%oFmYPL7r56h&B@gp|sdhm@tB@ zoFk%WptLLyM^GVfIDD5p$2&@qlc;$KJ#SjXzRU{Jf|*fbmwA(erGW8DDEc?|xwa49B(aenzv8KEgTll!-xkm4=DX+x_HWe##YV{q@0LBbDeT zQ?C)et}VanPCf@RRD4HK@gQ6*gI?OB^!jJ8kx)pA;U<`eJ$DQ5UUN1gn$uP+?@&M8 zP?ZJ7P!-AeJjp(dc}{C4=6w_?b3c_MSzbw61K$_OD@iC`Du%~LW$w8IVBW*R* za58L}QTYg37uKC+U71uZFTu&sFI!RzYIOA&bb>m20#`y(@IUqhx@d~fzp(Px+1hjg z9Y4T4+EnxM2;~alJ>+~REW3G{54f+pU2q;g!HFxtT~iq-+Ry}SgSURKpoQkt&o{J_ zxjljo5Vh9s5wxa7`G2`b@H=KMZGFw~pC8h+I}F1A$KJrYvJ2H@mwS-RQn@N7NQVQM zAgK%^$Wa_~pY_lKRxn%#$_P?cL8Q{%<*P2NAgD5&rx6!KehO1Q4=b2*2z$g!Le$@k zdMk6EdTufzy_JF0Zph@M&h|NwMcH(yAq#@GXi7lgbR9G_$V(SQmmFEU086Nue?Rkr zLEA!nz6%wdFfl?Zpp0C>?v{jOutH1GNtBRaYqx(OEI zIqMw#eu(l*sEYqC(0ib_U$PmMYQp8I&M@;0TvQ8ZgHIr#%G^qtGE5nCD9qz*fbd{g zwO=pciDO-@<{M#-KNqBY8|d*WJmyijPq}c;c+3XWvkNwIyFlb-9OiMpugc~fhlc$~!64|eg32(8o`e4r$o9%a3LT%~G7y$nx{^R8{(aVjQDTLhQkZ)S z#T2Q6z&lPkB#!wk=KZL;fyxRG8c|Oy(1fCb-n4>nN=z-)eM*IMsF)KRY*@jdyYwRk zk|)R;pS%l!vTD#1e1rWUHi5nXfpg!PjCYWEpz^C$0&$xwD$-lIkgZ5SVc2`$k7|Ch zGf?v0Pxka{evpt;5EU8)7B`{_!1D`!3DD5VUD z3dj!xrNorNqf%xCI*N!_j_d-x2hI6ag^!G>W4s{ru<%{5K!h(s41nQ|R8&Oz(Z0Bn zb*=_wHXQ=$Dzk&YVdmY)4gfKIf*pOh^0r9vvPX#ei%}m{6WWJJIPa0DknbtdsX@kf z*^bAWDWP!p{6bjz%6trz^6poS21`*$H(`Hu{F~@?r%-0LCg_eF^~l1iefg+GRVTcl zvyJq`P#d&FmNkb|HD_Z&xft)rIaT!#!LA*t9Z)0c%c3{}c4eC4zKHjzz;M=Qw|+Rw zb@9iOu$UW=Zeu@!2xA$$E#G1#2ttGjAcO}~t!lB7uMWlb@GxcaH8-rE3XBVaO~QQ6 zB*lwWbjCqbxfB|~TR?YHi9i=@a+YTS=h~c>OqJ`>1K+@2oW#4F_b_%}w?{dp51eZP z&rBc+*5}eW)B%g8bxKI1nmw(Bwby&|Y}Rh?OlVKATFc6u~A2 zhp|4u#%3%)PZ?k*G7ZE)ik}`S7waHJtjCJ@`d1b!ya&JwP^7cnj_UslYVs*+BHI27 zYJ5=9|BRYnal<;GFHS&#Gz-IP@zZMOPy9sT2}>hTUX=!c!{KNo4SQ56jfC@HTKAcq z6l=+3EJr3VbUO>D7(&HW7DX`a4e|Mr)JAwnh(i(#;uJNe67wS*daSS*1$w|3zzc+G zmdj_X!LSfWyh_pI4g6Aj9d^Y=hqef@hnfy%tgmXMCRBk6whF9;Q=szvVJcvRQnxcK z1e6-!EyU|1{`aay^+NUq>W-Ywn=q~6!C>%mpvPIAZBHN*NgMH!UHn0NSCr>O9_I>r z@=@@rj%-hR+M2RCtOjP53q=j3#LHnEx+xMtiJ-zspy}uAxB-K`I(lEq{nS;FIKf^v z40g&f_oza=k!JB8jOR{7vlAi5MzOLF@I^^J4u;UBBu!$!{ij&{0|Q5!_h5U$hJ>kH zNS-J}dsPT59-t|OiQ_Po|eV`?3WQ3AMR%nVa1JV*u(F-Q&y$=Pv30ZaB%sNTC3Ufm@8S{MYvKHrjtC?ju)Fm4DBosWb5QZuJ1RdR(_ z$6+Dd0mDT>zKp<#-~YU+5gz8&ag~<319iD#ht-}W{nsou zd;vFo^w+p;UlF(E*SJ(@`m_sJoPcq<P^gs~~LDvZiLm0JX*mDWa9(6>Ew+hQ<|_=x$Xiq*n&L@ZgSO!0b=;?;z}7tZwVf zDOmBth=W~B9T6j`s`qD&J@TqmKqQY4tF{tqZ3-Xh(8Q*B;>3$sufQVgH&*N~T_nDeer%dHn| zLm-dAiWA7Q3oN>>2Bv!JC%{_A$YRwVwm?+K4te|49scDV zKePT-X3om&Swl7Uyb@34J9!Opg8vzJ4Z%A{sZ$Ji*1ok3sDwn0U5h5uwP+^07KQQf zDKDL>ud!x16cTflPn3q8(5+ck)2BMKGuA6DAczd<+Wkqgfwq}uE*5Pe1$>xEmq-j% zY35oRr6He=110_ihF{U^z+-6DhcWcyd~4*0@v0DON-$H8Aqs@Fk1k-@smh#nY@^@F z+TN9}?b&n(|64k1HXZn2$YOy)uHLLaBaGD;juU+xQc#Gbi5QOIB61?~qJjb(rGbH2 zj1dZUmmewFG1Mhf=4Uruat}>)V+_d<&bUJ5)4-!V*$h72?El535w&eSj{?v<#ELMy z3?EeuG|PZmZc@-p#U%)r6@#<-7}{3Av_Crm z*H3CCj=2D=Z9PJ<_P1{)8@)3g>JA8;SKu`_Z4=F>-?-6Q_%kcbiy}(P>{^Ct+q2s0x9N&B;Jv@CDqddOJ~9&A}Hv)DknC{F-bO z0x_24G~^cgI#_5{^rP9 zq-{%f3=`^XPrG2=Jg<@C6Iv)XX&NlflFviA1`PK?DJfCn19hDL$i`mxhCK6UBi8m| zc5#5d%*v_!L3ES}a?&hHz&?(ij4#2olnP4_ut5+)LIXXl1(87%X5dxwZ@Om%YZ1rh z1(~QmuL+H4-~Y@{Z4CQhCm4MYBFEs4=_mL)H8kPH6~NBrwC7J-I&=f9DSqK!#L8|^ z|FFjvVXMdcFm}_sF!v3*CBDNt`dNqTK<{=E6Hze=jg8}h@QXw5FuxP6urS!*V|loJ zNyw-25bUdkxiiP^!5PK*hBTLJED|Lc2$IJ^ND?8(!~zX=SFz$dlM*afL)idhOHvNp ztHEDIs5Pcy-IX|zSqT{KV!BZ|t~n%orwoe8LtL9-nuSR13(Jbbicgp~8k`;WXM_~^ zQPvxV7xp2_?{R?z9@;9zX_~>iGv|bdrE`w)nfy=2Cq((^|6V1PccJq89oxXz>Mf(y zQ_$AHfjl~hM?TZ>ZO7%y)jIfaQpziUE1HDI+62j~f4$ZGdP|ApXJVDeTT~1l2<(on z{bqEOt`XHxh$*e8vKZOH?OK|K&;@b?bfC3SB~EN7L|-UyLT***_^-CV{FHm5e>$OT zpK0cFqftq6I=D54MTNd#(F(X}94S^Rxq{;y2t`*SvDS>j=`4t)IcOTQ z0xl(2m!B!R*Ga(#Uy9T;Q;kcV_(trtqPE(>^keX>qcov-kXQYn>Hpfye#r_^CuN8b zdz5J^xVT0%8E&t0w=^5g8Sa|aU9*WMSM77d^>Gf784#=(UfoEM8C*@($Lx-jdbCa! znnLzfOHB?96;j|9{j@J|3lDBIg`s9V`{XQ^JLjBOL()BTP~gUZUo1we{E(8H&tc4D zIM#q-_I$Dx+mYbwNInu}(v_Sl1_etop5YUo1K3tk8FrzG(JDyaP{}65m~5fRWwCro z^I{V%9jl(^{01R^zCM3TjB`J8KVO?W%7O71-w@_521e-Ky6QkCq%L*x5n%#~s3p(Z z`&#;}y{~O4?!GRCrf;A~1%-khSqh~|;L?$jC=_ZOiR$C@G>XBVrBMx)j<5qeJFc_b znPvx2qs*tqf?`$iTogOcV$4>!_(3ThF~&(+vj#Y<9%C-lh!+o0q?Ktfq$4cn9{L_f zz8DYTNQ7l<^pb4NyA@QMPk99PTXerE`cE&~3L>%6PV94^9&mO}+gV)Dm1f%h8<&rk z@>#p7IyD2Xg&qO%_UmZFmM237e~I-4tUZT0iY18J`GCbc#T(bYl#L6))&z^uW^6lfgbj>3?m!RV9CB3YJ!+KT+FxiY^9<7x5(pSUU4x)5p$G_Dd%FM z5VMDiIZI>Mul<8ZfWeH4k7fe;-`DOO*Ex!tzeDq3$KoeyUjyyGnI@O!013MaCo_S! z{^OrhJlmdhWYT{CgDT5j9;X~F@>ky0$SFOmLcMWGZNpR&Ln`(SP0+yj7!Q=!kPZZ!+J?6=bEeC~QTz0TyW>13}-V6T7L4*|cU;FGM5 z#b}fmnio$j)T6LK9*<2~36NK=UV+Zxbp~rjNOxWZv8H)+$DAshXl9H<_dZU^2j?+> zO${2xrnZ5BK?=5*w=O*cRLvt3~*f>LheTcLFzF23eBM_Ar)Uuul{AeGD z90!Nc{m8H^YYHY%=om;)H_I6`bwx7$5Hqi&e-zbhe$?GGuqeZdtnd+2?Jt zJN2sZcl|`dgP_Q3Vvn1`gQocgqI{gIS;B5SdP$3l9BEbF*wLzFx8f=gj%*yUT{+yN zY%a==EX=J%`F!g-HK6;hYw@s$d$~<*?RCk`usItq(~|t>_aB8QKgG?3abSv52=6h> z@sb*GjyTy8jpQe8)}uY{LrByr5gQdFe(UN|mUh zP(ioahAFjGV-X@E$rk+>lu*D?LTyrHB2=7alYXya{SzuVninheeL$)26A(brJ_1r= z`vO&Z(0)fn&C%GQ0JRkDnB0DF&JL=U2Vr%o)_M{H*$l)@u)vZ_!r`1M{u5k12!1UX_SFFyX!{y9;14fNN+?V|JHgN^DfMh1-w8sX5*m?NU1*?n6duG5 z*H=8@!4gG|9nmq=5On~3!h;tSPq8Ck!@Kz`cOc(M z;)q@rRA>?e$Ncvw`vvs?Mfn>@{(+;RsC$XFN+X21H60}D{C?#TFa+F(KRQ=FwC{@N zLa>D)9#~f1N2!HF9^fED-JimEOdy{vQBAA|#^C@e8J?x?mQYc(HxLpiJa!)KFEI*K^?$Fm1tmc$!=7+P9hLd0MZ)=|P!f8m zp&y@Fh76RmW;p3+*{|xJBIg>0X3tdZXY)>~F`%~wRdn%0pYrG}uZpf5=q;b#@~dX* zZ`q<5YBT5~Bl^fl8&9vPFEvy(hF+V{YZL7ddM%*W0v0bdru5oW+k?iN(Q7lU7rka{ zZLW2t*A_J1LQ7qe)mYMNORbPz)Bo-XwLIaF3qdd|%&SIu&^kk`;54ih7=r67iXn+V zrV!SM@TXW4a*@$hL(rQk&MpUfkA`9AUcAlDHNU26k5oV|JgRxX_6dfliqBT5J3Zhl zTbMdsIcsqghec&6i^ngFHWsD&VxS#G10Gx9FMMqTy))E?(7;F=Km%i~4-HJT@a~oL zMWAg%15<5F8klJvX<)8xL<0-0H4QAa6j@)`3g3tmYN+_~WF+67(p@_31bQ{7_jhMG7bTMPI2Cg*n*h8=qJJYykFvW&n49 zC!ht`gFcFXf$N`w5 zi&+C20UQA>0c`*s0X~2LKnNfLFa$6P5DS+p&-!{O+j@nO6eGE?}+IV*4B7=GQ4}{e@+%+hVH%SnJYM z?1aEb4xZYo9q>eXYHGqpjig}=M$Q?re02lrOS22R#H*&#rk||!S;|^}WRtIX_+Heq4AGk@7aIT<8{8H`JnxTQ_K` zmZDfO?6x&uynKX~J}~c9a(`t|0?n}%B7Hs!=Q#cMoM8~-#pl5oh9*eEn>v3ZvWF-B=NuC z0bU8%($ZClrV^XCf%Enpy0Cn4#^EE7g#!WDAOfPmFd2(DH~PRyQ+$c5m?Y|#<;j!< z=pm4%e5A{kK7TJ;1|goBcZ8eJRZ0e}kP=j^1Qjbm#j04VS}UB$Qxu6<+;AAwE~=CN z7L5h+(XPeZ{Og>IbtJkXk!BYn!yupJa zVmBgi=Sl6@x6<^q1h`(y#XD(IA=yDs9nXqUk-2^=dKeal2`~t-!f6@sY4Dsne4sKk z&49DO!iC%t_z%N6Lc!`HIN2J^>-Wz4IiAd%3p~^+SHM5eMWe+*2t?>RFlRpn^NF8) z$lv2PzF{KV*~CT3r;aX7Ser}XPWS*hCEN*4@o)(kEaxy6q@i(f!nTHmouN&!p;!61 zN-=#e7PdK$|5~Xje;G&37TAKfLe~ZnI8p_OH`}c!?mRU)T)eUH5YVJGET$5Au3k#R z-Y74pYhhhf(R1zD5!CRE@+WWS33ek)<o!h_lnpErS6rU`pq1;b3si0_nimkIM`;JFf7D{!Sf)D=d`>t@#=Jq+=H{hvDV zc9_qVq$acsqE`3fM6gK_9y~=uBT+&9s&A;hDu=k~biO(iF~twVE3qa?o#_kIQJ)NX zBXl7qZ0Ls5EQrs*+(wBu6gdV34L;ouhq8xy@jj!ihf7WCIlpQ>=*$IX_^CVXnIo}l z0<#ZmUW`rNUL{Zx!g9Y9`?Yi>vhF5C@}AO-UU#K5T(JQDtRH~y`5|dbJEmu#swWSj z8`K4k9jUis9Ndax9zii-B6!P`_A$d)yoGmk!bPXB$iw%jB8rkBIzb5gtmb=t^7T{CLatvJ)b+(4cNAcN6 zG8*|;&8Oey@?XW~Pv-^{H^B>9ys?-YMD$S3f0n#4Ejb=LVLrh^@;qv>205`V5^n2k z=~>;_p$Hr8e{lw*Ytb65L%VZWjHo+M62?=*cQ90^{x(6vLqoBcDrtnR`axhWnkpmA z!+dTenkvO6yo+Hz*Ap#aaEJtMK|YrfpJB@j&8POqj)i&|HFSTzvp#)6BbI48q5|m6 z&FDTqRz#x7K`){+t5E6hqSiQmx~8aL*o|yxa=q9anHGl+M?b@v>P|T8qPv)7LkUHa&$Mq1!XOqNcAJxXmHgR(cah$a^R;uiA)`*k5 zp^l8<22SFLC1bLXdv4A>m%I}rSNjr0x$e|lBr-_u!k$im97bUO0;H{h)+X5gPLF-Z zrIo}NmynbPS~mjmDb!9Ox(kvkA&Lq}OW2Pjuc&?B9*-evxgdFi$HrQyuzZ)s(#|4EG12Iz?8s?SMsOooFXb6U5AAA7Z~? zZF(+omS=5Z%s{VDSZGEtBD+zeT5Q{IfNl?q?F8kZ@t8lOG$+78(VRRX&qGe^8(tUW z-iFvc#7;OgegORhI?^i`vcp$GxTh>@C`q1-X%*ivK?I#)-Q?aeNmDA|857rvv_^bM zC>xvRFC8b4OM%!4Pmt+Q6HOG2EEwvh=>j^N zdoIWc^hXa8Lt)(_=|0Jy^@dXnuI_sxYU_?{P@2{&)$ErCb&I$3(XXEh8d{s#8}MU) z7lQTez-4y(mj@7gvh_UqOROv_0is=QliVge`JW`CWVohIHkW%lpvjZz0cYH%(sPFhU>3>BB^Qn5_?I>%-OhaJxP{ zq7N_Y!~6R1cYXLpADZTK-*2K1Tk69O`p{1wM(D#a`Y=r&&en%p^f&XQL0B>BG+Y5L@op_8+JZ*&F;P_T$UIrTz@B2@Ki$e}=W`{~3yhbMO1lhytOg+eEbOR{-5b;?`fInk1{~?he;p&B%yZL?2nEg^ck%cjmONxrlDd- z`U9K8rZq=M)0qM2JC*>N9{d*jE;SCPd4p?W^C12ggY3g}{d0!z8?$~=l!P!0W5DC_ zYJu>~g`QsBdw}UEtA-CqyvF`$o^^rvBccxEfSsueMECRW2fW|t$Pe<}Kp*lD`n#UD zUuqTQ6VZ&2sJLsi8o&IkY&bkN(ubbca$mn6D3)>cb>SOiXN25}7bHC3^CN zu};y6<0MmJrzY2aWQ-&(E;i9AIX>PgX-f3u$+htlrY6TGPK}=I6q}eBpGZhdd}5*` zAvt!eQ&MtbZ1j}cbcwOkB(X`!PE+HPohD34m>f$pj~)B-dr8UB$+1X0C3Z@DV!9K) z5FZnrJRyFnQ(W|f$w;XGNv2Mk8lO6~_H$(Ngfa06k|d}0ZN1vIZ`0np&A6@|yvD?i z85`SX?1aSR^fqx5(&*dSnWl~q0rL8!W=&{Gc!|DQvv%kp+95xXY`MsVVxiUKm9(A-UjUsnM1Y=vAV4r61P}@c2lNH>0}KER0t^8R1B?Jf0!9O(0Wko|Ba8z~089c< z1|uFY4Uhzo08#n;3?oa;3c3EPzBHeUIX3&-U0pud;oj`)BwH$z5{ds>OGHcKr;jw0|Wpw zfCWGZum;oz*a8{?>;U!v2Y@5M8PFWi0^kauqHGTU^^e#NK=)NcOi4Ncx&XQYd;nB) z)dL^`^aKO}f&n3bP(V1KF97GFkpTcIP9Fjo1{eW|1dIk)pbmBbcYrTo5MUZ$9)RMU z;)&LYes>CB6W}Sp84O7*U2P`r}oxz1)2S`vj2P`Bm0MdoPi51!ju+AEEiyc`l>Z6?ihp|_) zz!r6a&0431z&Q+n-T`i-V#gbUrtCp?*d(TM0G(pbo!k-NgllKSHN&&!NaKREE$~iD zyz7cQfRIkDkf#UoZjEoW!MEDto1XY~dz7IA%HoAGbwb%Xql{fp)~+aXH4*ajV7Y4CZWxypzY$(hG@h=NoZ3E+BOw!oQ}56 z0G(!mW~Bf*t`&&O!?T%4Q-HKeyraUqbC3rJ;J^apxd?efe%5t4zO@qHT#aw9MHx1r zESpfKEht+N%D5e6-H9^qM%nkF4hK+|V$|sf>UIouJc+uhQRmaB`&r=N0&sB&IJpYk zTmz170#~0dV*TxU2w9p98lqfnyDDtp(0s1NYV3*oP{xeKwuJJ!nwG!x+Rz zeL!bacOn8kRf3LcKs(a0jaqwAkwa;M83+D81v1D-4ru&vfPX?!RDm$kU2^BuplSbSrBz&OEPn@ zC9$&=66sVOl$pO2(O_ zKX4`|i<=QOI^xW17h*TA1(6PDNz^@ENo5x|Qq#_z=sa2>y$3P&Y)$OE+mIfCZHatf zJ5o8`lcdYrlOC%(;IsrUe7_^Hv+PWCK3#}tsyC_J*p<{g=|-fE-HANLml$vNBX%`C z$jRO!!mJA*m0x?3^btYi!CzGZ#b#!HWKxZBt2G*CZfU7z<&&}D<4aAYvYJ0emtoRm`G~uCll$H zDePEX1UeNMg0?XBi7av50AmlvB6VBPmk9lEDB{p(dDlTM9?pu6a}Ob(Hbd{Ps}IoTjvqM$Q6Vs+(ekpyNO`z zQR3d{9FY&cNkq*nNKwvfB1r#A)TXA4sJk5_sOQcU&F#vlkM(Al6(bpU&v-^4&tc^A z7c=q++ZchFnsLv(%P^&H81+*lgQ5yY1HtL82BLWb4bzsS^^FYt-stM|>}i!zS$1xx?n z7g;$Nsc@le6+%>n1m@D;+)&2V$7s(bF3r5{B z7QJt5q7IBU5kwZ56b1ZgBKqzpkT0AiaJPCW5Tvy+Wp2q#nbvnq1^I2v+^c7p$tOQE z6B+uO7nQCt7wljx)R$u|M6E7c2(EUtEGk)JsW!C`G6yn+?&seK1>rGP^050>^0EQe zf+uCx?zw&HG3U$bsrL=2Uleh_zF=#Njp)={8}+tqTfro&21TZu8i*ElZz#9E+t7Vh zawCDItsS$j*pBHty0M^{z~0^EpuK!_Toch_JBOm*t~m(G3Y)42i5*2D0w=-#D^5jg z7dfl#qMI?TJe#{eG;k64+;frl+t*?N#?c_qs1ReOHO9#iLoi>=1|NvAxhoiqnBN() zXrBRbRP%_lgCW^=*N8ka5D?>iX5_*TOX8)gM_!gSB<z zGOSBD;q4hgI`y1PdbE@h(c*<9^yYSw<$RvFUickH0t%R$Bib=}uZJ;xTNW_=VvjQ` zEI%>f%FYIdLNX1~kDoW#FuDov_Rdt^=ynfy(UXGt+V{u!4kNo82KPK`C;MOs0uj3w)w@rmz33GQE4d!fe?Sjd{=a29}bK?S#^Y30CrzXRVc;JJdhE zcfZXz=e`ZxR~R&^FS=%Tru$+0IVC3?<{3Y8d@#tRndoSyR~iO zgm!D%6m(eUeyNkk>MmU)TU_ftGiI@Wi1DnzeHZqMW2c&j?>e=--=6iO2WPw;Iik(7 zg;5Qco5bDwbYzm^)Q)Kf?4GBdY!;dA&>Zt_IObihC%%v|U{ z$l$QYa|2tOS-fzQfqeC}9)_Z}1B|XLS!ldymO#)owb(Rd$X0X1ixrlU^(R`lcMZ3> zzIQ>x<1NMZn@S{(MT^{A?ya2S{^4-{c2my(-f7+DuD;QtkwH7wMD;oFX4p`>*&|~n zG|7&Cskoh~JC5-SyRqg>;*oc&jYyB?=0xplOXeJGPTqa%N_7235ax@L2qo9aw38i} z9?Odv^}{I!!m`o4X|q@Jbs4@!%)tRBb5?#ceHT5{vd5NL_0%oSG@KLLvFSUxO-tR^ zE$x_<-F$>&nuSc8Q8KJYVa^ow(C4{~C7;Z577**!^@-?B3nJ2J5$jKn2-7Qx zVdf7vux5&R^Ew?f6wTRUA~L#aZoPL!y?MgrjhV(NEtoLlj`LOz3ASFfJxa8Bp1gU# z2ILIpl7@#Tk!2st$)tj{jK%36298XGq4PZlf!AbD;qCPs8*Le9;_CiAt9!|F{?)5gOFP zyS-a82b+4}*|^`-vBL-U2^MwpYUAqE*amDI{iRGD7dbd0w3o=IQ#&`uhC*%*eqJy?gq0Y471;!>nJiaCV+FJ!x`W)bN4f!T4U=rp)$DtC!9x zP-LYfOdLCENIzQWHa6$=zq9INhxYE+x?%OQg>o?D`kw*t(1lDUdxxvy z^78nD3_BaS8oQfx6O1sOWp>%z-ZEYI(rR?Q-|J`CHfeaVQEuaqCU#B#biC_)y7}Q2 zhg^@jpYteh^R}I72ltMlo#VY1bvxSqnV*TMP0xs+_+VwohR_4yrG3x$J2&9OpdCXN z42vHT5NSI4Y*gA9hp~rZdyYFbzVpPDlRixLpPCiFBcW{C^Tbz4FOsiGcBkZ|iqn|% zUFrR%S58mN_%mZd=AF#0SqrmXX0^*s%-)gxG}}t*EgdPHE?p$uCOs~_BE2VlA^k)8 zUiwk`Mfy$pL#nIuS6lDekV@+5pB4LO3pq$5$Ud4u2f+Pcr1HzJ_C0s3NZm<){AG}h z$oSnFyx#No~lbsK%fEJE*KR)Def@g$5C!xM4C)me>}KQa?%AoUuYW*La7buapNRg0 zh721qa#Ylq*l`mkO`e*NC`nDv$jp|xHFWjr-ZP}{fFUDBMURb}FeyGMB|S4+u9&Hu zJ#XRSWh>Tf*j%)I=bnAdo3t1885T1sF)cecUo~&hvNfBw?cRU*c*(gdH_IPBeW`i- z;j684x4zK{Sp^GMZQgnC_^AtJ_nvG2{A%62$KZ(>%B7q396fXO&g0)-f3b7y5;|gn zL{>O|`MRw;_a8oS{@UHgFSXSlbXG2+VN+#u*X%fay6oPwSMR@=x%VC?TeAJ+^{4N2 z0*}5^3pX4od;ZbfH8>_??#6>>@4R^TUC_GUwAour?r3$!?T1U(o_zSp$ZPbB-!8o} zZZ{-jMe(iIMjfLIc3!JC>^gBtNu{Ar{OXH;8g?1C;An+m_i5{{e&&m&@2cSUlpe&W z#r-PuzqR-sK74rZ-mNoyThvBY<5yNzcJ}O{xdCo81^pq_VYOO)=+O2;|CTfb{UKEu z41?6_{c{4`XbSp6D*B@-MXodj{ZaFw3l}aNJGNUD;Kn83{x2;pEiT@n6uH(K7$Mb# zvuDp9J-TaFfLm>Oby%zakPi6qLAv={rL^YDEopeo5$U>5E2JMkq)L1L6((Kt-a-1I zT9Y06c1QMx*ORlqYF)DjzIu?gLsOV#TPjlR}S7o%p;sdxGpx z{&?quv&Ws@KQC_LK2@yk-Wg*{cTbN=+BI>E+s?t!PqzC;t=Q%~diZa;$fia2N50&$ zXT+|}is4zC`VWiP=rGiE!;8Vp`YnSh*2WDyy{6fKZL9D1TfAy^#LSiceKS_P4o_OX zBy8F;-_WF`&qC6c$i=yf9fIdCIvBKNVR+!l1VhseOgz|Jlbl=%$DvK zXUuU8objb z)?JabYsN~qor_mJ+5X$=728g&8UEXowM~nd^)I)y+^}nN-;G(DvNlC*+_~9x!}Bf7 z`X)scYlr=Idd>1}+g3l`zIav3oikS^?#ftEvO8&c{k_wcP1u*T^wj>eB~1_JE|wme zzvy}KmW92KoLsQ)=)?Ie(4D)tGZ>vtFD( zr5t~;p7QOb*uvB+Cs1RDg8A2E`A#>gX70T?WM&#cqF<%}(=BqgY3|Dhyjm!)(GHNWdu=E0{r0)+uj>7>)$a>s zA%Dfn{`$~Mw)RtNSy)YDw4|AA<2M6Y{~rum*g&>}F_R7F*`eiI%k~-dl8rWrm6@6r z$PSwAmqlAVmzfD0$q!f!kVn>AC^xjZC*N+{B4)c5T_x6rS}JmUXj-Zas}`A-5)EC>tSyzoHK z{6!AIIg4fDv?WhM5|?%ljbFAnENQtmeEJH%zB5*;A{MVI@Aunk=K-hI#14G2cJm-+ z{jH`ovDyRqM}tWApHJ2&qh@qEj@kxhzpk;8s-8ohj5_o&C)2SvBsIdM$luJo9a z-Fai{?^VT4*f%Hc)PB{trU&!KOAlpEcwRhpV(%lvCha>an%wlb%M{fKgQ>cc_opVS zcgFuwnw1cDI(XW%GZu+s&Rt1-aXvq3{6*j7x0l`|r(Rhh;g z%tt(}zHe4Sq2JDU;{RakG?Bs7Cjl-~!U6*(9|#&g$su^^M432y!jq8v@!dmbk6Roz zFHRe-ibd;=RYgpXDepINjProO(Xj)4qc#t69{p^vF4BJJ{gM5K?HQpMt{A?1ME_y; zMmh}DMZOs9GTek^^CtKamGEsjhd;nE(M z+nCYJSqMy~0HfuZq9$HhYZ?~<(+{!+HEfx^!*+7EvCWR`(e)~`4_Y}$&4ppoF%~J( zLuMFUMC5#Cf z58l*`$!FY1KG6^{mcU}Nm6$N&ne&VlNg&sVCo`AvBy+)eM&R(R2(p`4F$v5yrV&Yl zqa$x-DdSC+lFwu)Q_Kt{#iSAV$a02{_0nnL!sIe8B$vD(fy_oG5IioQ8Oxkz%(0@l zOj%yq^Ny2E?W zBQ0aPLJL>}P1Ip#81#8|OgeLqF@y&4476Z*Obh4|UqYX?i3uW`h#?cpoM9}WkGuk% zmj zDtOvj1-)G)bDW8U{?LKRVjeLDWDF@K&6pgf8Ob5fNC2~*2_Wl<0W*dvWlUk2bdj`T z3Yk`=NhW;NqSR>RzF6mx6{yIdq2`n4ZwU@|YOr6k`V6 z?5O=oR-d1E8I)&m=K78GEe4 z9zZv_g7JZd_barFN0|}O)!H)|%mc<4+U0Z5aL#00p(n0{u5t?#42`cbGmbgO2%&2( zgT8YP(+*nWx6oYfV){b=D`euCGNvIk&v&8yT*7pL4*3)Gn1`4l&;~bTQklDqBlOTu zpbK5Y^ngZL2d(BwW;Arej*OIfg8u7?9t<`H>ra35$F=B@P9y?-c{h49_Kl*SEksYV zA%W;M8_{oE$UyYC{pfQhuu&V2zIh&NRxx_(R`gdlSgYlumuev8=#E~v68*3V2}7US zj$YN8j6{Dth91d~_UK9T(3h;Cqo0Nzbe%Xu8!tzHcuFAC1HCT-#oI!2F9Dt3h6TVN zQ2qhXz5rVO3FspipqULpuPH*mafjx<0KKIO8e3oVhE?bf4$$&vp%*-YwLm2L#c}iu z16bCL0X3GwLu3Hxb3G`u8CKCbpvz~l!RrjVTMWu;Kzf0SHiM2@lEI+BgP=iE(hc;! z92DP}^al0*2KsG9hJ*5tfc7n6uQwGGb(J)QG$$L>_?WaMqd=c0K%qQXDa3#(PQlut zC+K7YD5W_p4-}w}=dg_Eg7#km3TOx`g;ccrUE&IRgqdi?O4v*IfbLd+^6X)|kO6vo z02_!ApunS`K_ToM;?c5Yu$SnIw%&!-ZU@_iIcVdzgrIG(nFnobf_C*r%Z^7&yP>Uz zqP6*Gn~rFmv1lz9wAVnimLU?~n*YYDs!2Id+7 zTfKm<6yVAp*a`){%zz^wU}+Mt<^jBo0OpKQ&n~FxIMmz~bsvJ-Lxv8#jRxijYC

?{OTS8Mq@SdJN#9A| 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Ν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 B;I9Ax literal 0 HcmV?d00001 diff --git a/disnake/disnake/bin/libopus-0.x86.dll b/disnake/disnake/bin/libopus-0.x86.dll new file mode 100644 index 0000000000000000000000000000000000000000..ee71317fa6291e8a015b57b5b42a968b49ceb7fd GIT binary patch literal 366080 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

ss8*YJa`<&!F4OE!PF< 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 None: + self.emoji: Optional[Union[Emoji, PartialEmoji]] = None + if emoji_data := data.get("emoji"): + emoji = state._get_emoji_from_data(emoji_data) + if isinstance(emoji, str): + emoji = PartialEmoji(name=emoji) + self.emoji = emoji + + self.animation_type = ( + try_enum(VoiceChannelEffectAnimationType, value) + if (value := data.get("animation_type")) is not None + else None + ) + self.animation_id: Optional[int] = utils._get_as_snowflake(data, "animation_id") + + self.sound: Optional[Union[GuildSoundboardSound, PartialSoundboardSound]] = None + if sound_id := utils._get_as_snowflake(data, "sound_id"): + if sound := state.get_soundboard_sound(sound_id): + self.sound = sound + else: + sound_data: PartialSoundboardSoundPayload = { + "sound_id": sound_id, + "volume": data.get("sound_volume"), # type: ignore # assume this exists if sound_id is set + } + self.sound = PartialSoundboardSound(data=sound_data, state=state) + + def __repr__(self) -> str: + return ( + f"" + ) + + +async def _single_delete_strategy(messages: Iterable[Message]) -> None: + for m in messages: + await m.delete() + + +class TextChannel(disnake.abc.Messageable, disnake.abc.GuildChannel, Hashable): + """Represents a Discord guild text channel. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel's ID. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it doesn't exist. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. + + A value of `0` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` permissions, bypass slowmode. + + See also :attr:`default_thread_slowmode_delay`. + + default_thread_slowmode_delay: :class:`int` + The default number of seconds a member must wait between sending messages + in newly created threads in this channel. + + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + .. versionadded:: 2.8 + + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + + .. versionadded:: 2.0 + + last_pin_timestamp: Optional[:class:`datetime.datetime`] + The time the most recent message was pinned, or ``None`` if no message is currently pinned. + + .. versionadded:: 2.5 + """ + + __slots__ = ( + "name", + "id", + "guild", + "topic", + "_state", + "nsfw", + "category_id", + "position", + "slowmode_delay", + "default_thread_slowmode_delay", + "last_message_id", + "default_auto_archive_duration", + "last_pin_timestamp", + "_overwrites", + "_flags", + "_type", + ) + + def __init__(self, *, state: ConnectionState, guild: Guild, data: TextChannelPayload) -> None: + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._type: Literal[0, 5] = data["type"] + self._update(guild, data) + + def __repr__(self) -> str: + attrs = ( + ("id", self.id), + ("name", self.name), + ("position", self.position), + ("nsfw", self.nsfw), + ("news", self.is_news()), + ("category_id", self.category_id), + ("default_auto_archive_duration", self.default_auto_archive_duration), + ("flags", self.flags), + ) + joined = " ".join(f"{k!s}={v!r}" for k, v in attrs) + return f"<{self.__class__.__name__} {joined}>" + + def _update(self, guild: Guild, data: TextChannelPayload) -> None: + self.guild: Guild = guild + # apparently this can be nullable in the case of a bad api deploy + self.name: str = data.get("name") or "" + self.category_id: Optional[int] = utils._get_as_snowflake(data, "parent_id") + self.topic: Optional[str] = data.get("topic") + self.position: int = data["position"] + self._flags = data.get("flags", 0) + self.nsfw: bool = data.get("nsfw", False) + # Does this need coercion into `int`? No idea yet. + self.slowmode_delay: int = data.get("rate_limit_per_user", 0) + self.default_thread_slowmode_delay: int = data.get("default_thread_rate_limit_per_user", 0) + self.default_auto_archive_duration: ThreadArchiveDurationLiteral = data.get( + "default_auto_archive_duration", 1440 + ) + self._type: Literal[0, 5] = data.get("type", self._type) + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, "last_message_id") + self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time( + data.get("last_pin_timestamp") + ) + self._fill_overwrites(data) + + async def _get_channel(self) -> Self: + return self + + @property + def type(self) -> Literal[ChannelType.text, ChannelType.news]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.text` or :attr:`ChannelType.news`. + """ + if self._type == ChannelType.text.value: + return ChannelType.text + return ChannelType.news + + @property + def _sorting_bucket(self) -> int: + return ChannelType.text.value + + @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) + + # text channels do not have voice related permissions + denied = Permissions.voice() + base.value &= ~denied.value + return base + + @property + def members(self) -> List[Member]: + """List[:class:`Member`]: Returns all members that can see this channel.""" + if isinstance(self.guild, Object): + return [] + return [m for m in self.guild.members if self.permissions_for(m).view_channel] + + @property + def threads(self) -> List[Thread]: + """List[:class:`Thread`]: Returns all the threads that you can see. + + .. versionadded:: 2.0 + """ + if isinstance(self.guild, Object): + return [] + return [thread for thread in self.guild._threads.values() if thread.parent_id == self.id] + + def is_nsfw(self) -> bool: + """Whether the channel is marked as NSFW. + + :return type: :class:`bool` + """ + return self.nsfw + + def is_news(self) -> bool: + """Whether the channel is a news channel. + + :return type: :class:`bool` + """ + return self._type == ChannelType.news.value + + @property + def last_message(self) -> Optional[Message]: + """Gets the last message in this channel from the cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + Returns + ------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return self._state._get_message(self.last_message_id) if self.last_message_id else None + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[TextChannel]: ... + + @overload + async def edit( + self, + *, + name: str = ..., + topic: Optional[str] = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + slowmode_delay: Optional[int] = ..., + default_thread_slowmode_delay: Optional[int] = ..., + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = ..., + type: ChannelType = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + flags: ChannelFlags = ..., + reason: Optional[str] = ..., + ) -> TextChannel: ... + + async def edit( + self, + *, + name: str = MISSING, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + type: ChannelType = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + flags: ChannelFlags = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[TextChannel]: + """|coro| + + Edits the channel. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 1.4 + The ``type`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + name: :class:`str` + The new channel's name. + topic: Optional[:class:`str`] + The new channel's topic. + position: :class:`int` + The new channel's position. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + default_thread_slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + This does not apply retroactively to existing threads. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.8 + type: :class:`ChannelType` + The new type of this text channel. Currently, only conversion between + :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This + is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + + .. versionadded:: 2.6 + + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. + + Returns + ------- + Optional[:class:`.TextChannel`] + The newly edited text channel. If the edit was only positional + then ``None`` is returned instead. + """ + payload = await self._edit( + name=name, + topic=topic, + position=position, + nsfw=nsfw, + sync_permissions=sync_permissions, + category=category, + slowmode_delay=slowmode_delay, + default_thread_slowmode_delay=default_thread_slowmode_delay, + default_auto_archive_duration=default_auto_archive_duration, + type=type, + overwrites=overwrites, + flags=flags, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def clone( + self, + *, + name: Optional[str] = None, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: int = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: AnyThreadArchiveDuration = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + news: bool = MISSING, + reason: Optional[str] = None, + ) -> TextChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.9 + Added ``topic``, ``position``, ``nsfw``, ``category``, ``slowmode_delay``, + ``default_thread_slowmode_delay``, ``default_auto_archive_duration``, ``news`` + and ``overwrites`` keyword-only parameters. + + .. note:: + The current :attr:`TextChannel.flags` value won't be cloned. + This is a Discord limitation. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + topic: Optional[:class:`str`] + The topic of the new channel. If not provided, defaults to this channel's topic. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + nsfw: :class:`bool` + Whether the new channel should be marked as NSFW. If not provided, defaults to this channel's NSFW value. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + slowmode_delay: :class:`int` + The slowmode of the new channel. If not provided, defaults to this channel's slowmode. + default_thread_slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + This does not apply retroactively to existing threads. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. If not provided, defaults + to this channel's default thread slowmode delay. + default_auto_archive_duration: Union[:class:`int`, :class:`ThreadArchiveDuration`] + The default auto archive duration of the new channel. If not provided, defaults to this channel's default auto archive duration. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. + news: :class:`bool` + Whether the new channel should be a news channel. News channels are text channels that can be followed. + This is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. If not provided, defaults to the current type of this channel. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`TextChannel` + The newly created text channel. + """ + if news is not MISSING: + # if news is True set the channel_type to News, otherwise if it's False set it to Text + channel_type = ChannelType.news if news else ChannelType.text + else: + # if news is not given falls back to the original TextChannel type + channel_type = self.type + + return await self._clone_impl( + { + "topic": topic if topic is not MISSING else self.topic, + "position": position if position is not MISSING else self.position, + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + "type": channel_type.value, + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + "default_thread_rate_limit_per_user": ( + default_thread_slowmode_delay + if default_thread_slowmode_delay is not MISSING + else self.default_thread_slowmode_delay + ), + "default_auto_archive_duration": ( + try_enum_to_int(default_auto_archive_duration) + if default_auto_archive_duration is not MISSING + else self.default_auto_archive_duration + ), + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, + ) + + async def delete_messages(self, messages: Iterable[Snowflake]) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days. + + You must have :attr:`~Permissions.manage_messages` permission to + do this. + + Parameters + ---------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id: int = messages[0].id + await self._state.http.delete_message(self.id, message_id) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids) + + async def purge( + self, + *, + limit: Optional[int] = 100, + check: Callable[[Message], bool] = MISSING, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + around: Optional[SnowflakeTime] = None, + oldest_first: Optional[bool] = False, + bulk: bool = True, + ) -> List[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + Examples + -------- + Deleting bot's messages :: + + def is_me(m): + return m.author == client.user + + deleted = await channel.purge(limit=100, check=is_me) + await channel.send(f'Deleted {len(deleted)} message(s)') + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Returns + ------- + List[:class:`.Message`] + A list of messages that were deleted. + """ + if check is MISSING: + check = lambda m: True + + iterator = self.history( + limit=limit, before=before, after=after, oldest_first=oldest_first, around=around + ) + ret: List[Message] = [] + count = 0 + + minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 + strategy = self.delete_messages if bulk else _single_delete_strategy + + async for message in iterator: + if count == 100: + to_delete = ret[-100:] + await strategy(to_delete) + count = 0 + await asyncio.sleep(1) + + if not check(message): + continue + + if message.id < minimum_time: + # older than 14 days old + if count == 1: + await ret[-1].delete() + elif count >= 2: + to_delete = ret[-count:] + await strategy(to_delete) + + count = 0 + strategy = _single_delete_strategy + + count += 1 + ret.append(message) + + # SOme messages remaining to poll + if count >= 2: + # more than 2 messages -> bulk delete + to_delete = ret[-count:] + await strategy(to_delete) + elif count == 1: + # delete a single message + await ret[-1].delete() + + return ret + + async def webhooks(self) -> List[Webhook]: + """|coro| + + Retrieves the list of webhooks this channel has. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + use this. + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + + Returns + ------- + List[:class:`Webhook`] + The list of webhooks this channel has. + """ + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: Optional[AssetBytes] = None, reason: Optional[str] = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + do this. + + .. versionchanged:: 1.1 + The ``reason`` keyword-only parameter was added. + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[|resource_type|] + The webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + + .. versionchanged:: 2.5 + Now accepts various resource types in addition to :class:`bytes`. + + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Raises + ------ + NotFound + The ``avatar`` asset couldn't be found. + Forbidden + You do not have permissions to create a webhook. + HTTPException + Creating the webhook failed. + TypeError + The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). + + Returns + ------- + :class:`Webhook` + The newly created webhook. + """ + from .webhook import Webhook + + avatar_data = await utils._assetbytes_to_base64_data(avatar) + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar_data, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + async def follow(self, *, destination: TextChannel, reason: Optional[str] = None) -> Webhook: + """|coro| + + Follows a channel using a webhook. + + Only news channels can be followed. + + .. note:: + + The webhook returned will not provide a token to do webhook + actions, as Discord does not provide it. + + .. versionadded:: 1.3 + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` instead of ``InvalidArgument``. + + Parameters + ---------- + destination: :class:`TextChannel` + The channel you would like to follow from. + reason: Optional[:class:`str`] + The reason for following the channel. Shows up on the destination guild's audit log. + + .. versionadded:: 1.4 + + Raises + ------ + HTTPException + Following the channel failed. + Forbidden + You do not have the permissions to create a webhook. + TypeError + The current or provided channel is not of the correct type. + + Returns + ------- + :class:`Webhook` + The newly created webhook. + """ + if not self.is_news(): + raise TypeError("This channel must be a news channel.") + + if not isinstance(destination, TextChannel): + raise TypeError(f"Expected TextChannel received {destination.__class__.__name__}") + + from .webhook import Webhook + + data = await self._state.http.follow_webhook( + self.id, webhook_channel_id=destination.id, reason=reason + ) + return Webhook._as_follower(data, channel=destination, user=self._state.user) + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the given message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + def get_thread(self, thread_id: int, /) -> Optional[Thread]: + """Returns a thread with the given ID. + + .. versionadded:: 2.0 + + Parameters + ---------- + thread_id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`Thread`] + The returned thread or ``None`` if not found. + """ + if isinstance(self.guild, Object): + return None + return self.guild.get_thread(thread_id) + + @overload + async def create_thread( + self, + *, + name: str, + message: Snowflake, + auto_archive_duration: Optional[AnyThreadArchiveDuration] = None, + slowmode_delay: Optional[int] = None, + reason: Optional[str] = None, + ) -> Thread: ... + + @overload + async def create_thread( + self, + *, + name: str, + type: ThreadType, + auto_archive_duration: Optional[AnyThreadArchiveDuration] = None, + invitable: Optional[bool] = None, + slowmode_delay: Optional[int] = None, + reason: Optional[str] = None, + ) -> Thread: ... + + async def create_thread( + self, + *, + name: str, + message: Optional[Snowflake] = None, + auto_archive_duration: Optional[AnyThreadArchiveDuration] = None, + type: Optional[ThreadType] = None, + invitable: Optional[bool] = None, + slowmode_delay: Optional[int] = None, + reason: Optional[str] = None, + ) -> Thread: + """|coro| + + Creates a thread in this text channel. + + To create a public thread, you must have :attr:`~disnake.Permissions.create_public_threads` permission. + For a private thread, :attr:`~disnake.Permissions.create_private_threads` permission is needed instead. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.5 + + - Only one of ``message`` and ``type`` may be provided. + - ``type`` is now required if ``message`` is not provided. + + + Parameters + ---------- + name: :class:`str` + The name of the thread. + message: :class:`abc.Snowflake` + A snowflake representing the message to create the thread with. + + .. versionchanged:: 2.5 + + Cannot be provided with ``type``. + + type: :class:`ChannelType` + The type of thread to create. + + .. versionchanged:: 2.5 + + Cannot be provided with ``message``. + Now required if message is not provided. + + auto_archive_duration: Union[:class:`int`, :class:`ThreadArchiveDuration`] + The duration in minutes before a thread is automatically archived for inactivity. + If not provided, the channel's default auto archive duration is used. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + invitable: :class:`bool` + Whether non-moderators can add other non-moderators to this thread. + Only available for private threads. + If a ``message`` is passed then this parameter is ignored, as a thread + created with a message is always a public thread. + Defaults to ``True``. + + .. versionadded:: 2.3 + + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this thread, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + If set to ``None`` or not provided, slowmode is inherited from the parent's + :attr:`~TextChannel.default_thread_slowmode_delay`. + + .. versionadded:: 2.3 + + reason: Optional[:class:`str`] + The reason for creating the thread. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to create a thread. + HTTPException + Starting the thread failed. + + Returns + ------- + :class:`Thread` + The newly created thread + """ + if not ((message is None) ^ (type is None)): + raise ValueError("Exactly one of message and type must be provided.") + + if auto_archive_duration is not None: + auto_archive_duration = cast( + "ThreadArchiveDurationLiteral", try_enum_to_int(auto_archive_duration) + ) + + if message is None: + data = await self._state.http.start_thread_without_message( + self.id, + name=name, + auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration, + type=type.value, # type: ignore + invitable=invitable if invitable is not None else True, + rate_limit_per_user=slowmode_delay, + reason=reason, + ) + else: + data = await self._state.http.start_thread_with_message( + self.id, + message.id, + name=name, + auto_archive_duration=auto_archive_duration or self.default_auto_archive_duration, + rate_limit_per_user=slowmode_delay, + reason=reason, + ) + + return Thread(guild=self.guild, state=self._state, data=data) + + def archived_threads( + self, + *, + private: bool = False, + joined: bool = False, + limit: Optional[int] = 50, + before: Optional[Union[Snowflake, datetime.datetime]] = None, + ) -> ArchivedThreadIterator: + """Returns an :class:`~disnake.AsyncIterator` that iterates over all archived threads in the channel. + + You must have :attr:`~Permissions.read_message_history` permission to use this. If iterating over private threads + then :attr:`~Permissions.manage_threads` permission is also required. + + .. versionadded:: 2.0 + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of threads to retrieve. + If ``None``, retrieves every archived thread in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve archived channels before the given date or ID. + private: :class:`bool` + Whether to retrieve private archived threads. + joined: :class:`bool` + Whether to retrieve private archived threads that you've joined. + You cannot set ``joined`` to ``True`` and ``private`` to ``False``. + + Raises + ------ + Forbidden + You do not have permissions to get archived threads. + HTTPException + The request to get the archived threads failed. + + Yields + ------ + :class:`Thread` + The archived threads. + """ + return ArchivedThreadIterator( + self.id, self.guild, limit=limit, joined=joined, private=private, before=before + ) + + +class VocalGuildChannel(disnake.abc.Connectable, disnake.abc.GuildChannel, Hashable): + __slots__ = ( + "name", + "id", + "guild", + "bitrate", + "user_limit", + "_state", + "position", + "_overwrites", + "category_id", + "rtc_region", + "video_quality_mode", + "_flags", + ) + + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: Union[VoiceChannelPayload, StageChannelPayload], + ) -> None: + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._update(guild, data) + + def _get_voice_client_key(self) -> Tuple[int, str]: + return self.guild.id, "guild_id" + + def _get_voice_state_pair(self) -> Tuple[int, int]: + return self.guild.id, self.id + + def _update(self, guild: Guild, data: Union[VoiceChannelPayload, StageChannelPayload]) -> None: + self.guild = guild + # apparently this can be nullable in the case of a bad api deploy + self.name: str = data.get("name") or "" + rtc = data.get("rtc_region") + self.rtc_region: Optional[str] = rtc + self.video_quality_mode: VideoQualityMode = try_enum( + VideoQualityMode, data.get("video_quality_mode", 1) + ) + self._flags = data.get("flags", 0) + self.category_id: Optional[int] = utils._get_as_snowflake(data, "parent_id") + self.position: int = data["position"] + # these don't exist in partial channel objects of slash command options + self.bitrate: int = data.get("bitrate", 0) + self.user_limit: int = data.get("user_limit", 0) + self._fill_overwrites(data) + + @property + def _sorting_bucket(self) -> int: + return ChannelType.voice.value + + @property + def members(self) -> List[Member]: + """List[:class:`Member`]: Returns all members that are currently inside this voice channel.""" + if isinstance(self.guild, Object): + return [] + + ret: List[Member] = [] + for user_id, state in self.guild._voice_states.items(): + if state.channel and state.channel.id == self.id: + member = self.guild.get_member(user_id) + if member is not None: + ret.append(member) + return ret + + @property + def voice_states(self) -> Dict[int, VoiceState]: + """Returns a mapping of member IDs who have voice states in this channel. + + .. versionadded:: 1.3 + + .. note:: + + This function is intentionally low level to replace :attr:`members` + when the member cache is unavailable. + + Returns + ------- + Mapping[:class:`int`, :class:`VoiceState`] + The mapping of member ID to a voice state. + """ + if isinstance(self.guild, Object): + return {} + + return { + key: value + for key, value in self.guild._voice_states.items() + if value.channel and value.channel.id == self.id + } + + @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) + + # voice channels cannot be edited by people who can't connect to them + # It also implicitly denies all other voice perms + if not base.connect: + denied = Permissions.voice() + # voice channels also deny all text related permissions + denied.value |= Permissions.text().value + # stage channels remove the stage permissions + denied.value |= Permissions.stage().value + + denied.update( + manage_channels=True, + manage_roles=True, + create_events=True, + manage_events=True, + manage_webhooks=True, + ) + base.value &= ~denied.value + return base + + +class VoiceChannel(disnake.abc.Messageable, VocalGuildChannel): + """Represents a Discord guild voice channel. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel's ID. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + user_limit: :class:`int` + The channel's limit for number of members that can be in a voice channel. + rtc_region: Optional[:class:`str`] + The region for the voice channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + + .. versionadded:: 1.7 + + .. versionchanged:: 2.5 + No longer a ``VoiceRegion`` instance. + + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the voice channel's participants. + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + + .. versionadded:: 2.3 + + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + .. versionadded:: 2.3 + + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + + .. versionadded:: 2.3 + """ + + __slots__ = ( + "nsfw", + "slowmode_delay", + "last_message_id", + ) + + def __repr__(self) -> str: + attrs = ( + ("id", self.id), + ("name", self.name), + ("rtc_region", self.rtc_region), + ("position", self.position), + ("bitrate", self.bitrate), + ("video_quality_mode", self.video_quality_mode), + ("user_limit", self.user_limit), + ("category_id", self.category_id), + ("nsfw", self.nsfw), + ("flags", self.flags), + ) + joined = " ".join(f"{k!s}={v!r}" for k, v in attrs) + return f"<{self.__class__.__name__} {joined}>" + + def _update(self, guild: Guild, data: VoiceChannelPayload) -> None: + super()._update(guild, data) + self.nsfw: bool = data.get("nsfw", False) + self.slowmode_delay: int = data.get("rate_limit_per_user", 0) + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, "last_message_id") + + async def _get_channel(self: Self) -> Self: + return self + + @property + def type(self) -> Literal[ChannelType.voice]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.voice`. + """ + return ChannelType.voice + + async def clone( + self, + *, + name: Optional[str] = None, + bitrate: int = MISSING, + user_limit: int = MISSING, + position: int = MISSING, + category: Optional[Snowflake] = MISSING, + rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + video_quality_mode: VideoQualityMode = MISSING, + nsfw: bool = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + slowmode_delay: Optional[int] = MISSING, + reason: Optional[str] = None, + ) -> VoiceChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.9 + Added ``bitrate``, ``user_limit``, ``position``, ``category``, + ``rtc_region``, ``video_quality_mode``, ``nsfw``, ``slowmode_delay`` + and ``overwrites`` keyword-only parameters. + + .. note:: + The current :attr:`VoiceChannel.flags` value won't be cloned. + This is a Discord limitation. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + bitrate: :class:`int` + The bitrate of the new channel. If not provided, defaults to this channel's bitrate. + user_limit: :class:`int` + The user limit of the new channel. If not provided, defaults to this channel's user limit. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The rtc region of the new channel. If not provided, defaults to this channel's rtc region. + video_quality_mode: :class:`VideoQualityMode` + The video quality mode of the new channel. If not provided, defaults to this channel's video quality mode. + nsfw: :class:`bool` + Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` to apply + to the channel. If not provided, defaults to this channel's overwrites. + slowmode_delay: Optional[:class:`int`] + The slowmode of the new channel. If not provided, defaults to this channel's slowmode. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`VoiceChannel` + The channel that was created. + """ + return await self._clone_impl( + { + "bitrate": bitrate if bitrate is not MISSING else self.bitrate, + "user_limit": user_limit if user_limit is not MISSING else self.user_limit, + "position": position if position is not MISSING else self.position, + "rtc_region": (str(rtc_region) if rtc_region is not MISSING else self.rtc_region), + "video_quality_mode": ( + int(video_quality_mode) + if video_quality_mode is not MISSING + else int(self.video_quality_mode) + ), + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, + ) + + def is_nsfw(self) -> bool: + """Whether the channel is marked as NSFW. + + .. versionadded:: 2.3 + + :return type: :class:`bool` + """ + return self.nsfw + + @property + def last_message(self) -> Optional[Message]: + """Gets the last message in this channel from the cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + .. versionadded:: 2.3 + + Returns + ------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return self._state._get_message(self.last_message_id) if self.last_message_id else None + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the given message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 2.3 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[VoiceChannel]: ... + + @overload + async def edit( + self, + *, + name: str = ..., + bitrate: int = ..., + user_limit: int = ..., + position: int = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + rtc_region: Optional[Union[str, VoiceRegion]] = ..., + video_quality_mode: VideoQualityMode = ..., + nsfw: bool = ..., + slowmode_delay: Optional[int] = ..., + flags: ChannelFlags = ..., + reason: Optional[str] = ..., + ) -> VoiceChannel: ... + + async def edit( + self, + *, + name: str = MISSING, + bitrate: int = MISSING, + user_limit: int = MISSING, + position: int = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + video_quality_mode: VideoQualityMode = MISSING, + nsfw: bool = MISSING, + slowmode_delay: Optional[int] = MISSING, + flags: ChannelFlags = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[VoiceChannel]: + """|coro| + + Edits the channel. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + name: :class:`str` + The channel's new name. + bitrate: :class:`int` + The channel's new bitrate. + user_limit: :class:`int` + The channel's new user limit. + position: :class:`int` + The channel's new position. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The new region for the voice channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + + .. versionadded:: 1.7 + + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the voice channel's participants. + + .. versionadded:: 2.0 + + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + + .. versionadded:: 2.3 + + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.3 + + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + + .. versionadded:: 2.6 + + Raises + ------ + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. + + Returns + ------- + Optional[:class:`.VoiceChannel`] + The newly edited voice channel. If the edit was only positional + then ``None`` is returned instead. + """ + payload = await self._edit( + name=name, + bitrate=bitrate, + user_limit=user_limit, + position=position, + sync_permissions=sync_permissions, + category=category, + overwrites=overwrites, + rtc_region=rtc_region, + video_quality_mode=video_quality_mode, + nsfw=nsfw, + slowmode_delay=slowmode_delay, + flags=flags, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def delete_messages(self, messages: Iterable[Snowflake]) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days. + + You must have :attr:`~Permissions.manage_messages` permission to + do this. + + .. versionadded:: 2.5 + + Parameters + ---------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id: int = messages[0].id + await self._state.http.delete_message(self.id, message_id) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids) + + async def purge( + self, + *, + limit: Optional[int] = 100, + check: Callable[[Message], bool] = MISSING, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + around: Optional[SnowflakeTime] = None, + oldest_first: Optional[bool] = False, + bulk: bool = True, + ) -> List[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + .. versionadded:: 2.5 + + .. note:: + + See :meth:`TextChannel.purge` for examples. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Returns + ------- + List[:class:`.Message`] + A list of messages that were deleted. + """ + if check is MISSING: + check = lambda m: True + + iterator = self.history( + limit=limit, before=before, after=after, oldest_first=oldest_first, around=around + ) + ret: List[Message] = [] + count = 0 + + minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 + strategy = self.delete_messages if bulk else _single_delete_strategy + + async for message in iterator: + if count == 100: + to_delete = ret[-100:] + await strategy(to_delete) + count = 0 + await asyncio.sleep(1) + + if not check(message): + continue + + if message.id < minimum_time: + # older than 14 days old + if count == 1: + await ret[-1].delete() + elif count >= 2: + to_delete = ret[-count:] + await strategy(to_delete) + + count = 0 + strategy = _single_delete_strategy + + count += 1 + ret.append(message) + + # SOme messages remaining to poll + if count >= 2: + # more than 2 messages -> bulk delete + to_delete = ret[-count:] + await strategy(to_delete) + elif count == 1: + # delete a single message + await ret[-1].delete() + + return ret + + async def webhooks(self) -> List[Webhook]: + """|coro| + + Retrieves the list of webhooks this channel has. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + use this. + + .. versionadded:: 2.5 + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + + Returns + ------- + List[:class:`Webhook`] + The list of webhooks this channel has. + """ + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: Optional[bytes] = None, reason: Optional[str] = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + do this. + + .. versionadded:: 2.5 + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + The webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Raises + ------ + NotFound + The ``avatar`` asset couldn't be found. + Forbidden + You do not have permissions to create a webhook. + HTTPException + Creating the webhook failed. + TypeError + The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). + + Returns + ------- + :class:`Webhook` + The newly created webhook. + """ + from .webhook import Webhook + + avatar_data = await utils._assetbytes_to_base64_data(avatar) + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar_data, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + async def send_soundboard_sound(self, sound: SoundboardSound, /) -> None: + """|coro| + + Sends a soundboard sound in this channel. + + You must have :attr:`~Permissions.speak` and :attr:`~Permissions.use_soundboard` + permissions to do this. For sounds from different guilds, you must also have + :attr:`~Permissions.use_external_sounds` permission. + Additionally, you may not be muted or deafened. + + Parameters + ---------- + sound: Union[:class:`SoundboardSound`, :class:`GuildSoundboardSound`] + The sound to send in the channel. + + Raises + ------ + Forbidden + You are not allowed to send soundboard sounds. + HTTPException + An error occurred sending the soundboard sound. + """ + if isinstance(sound, GuildSoundboardSound): + source_guild_id = sound.guild_id + else: + source_guild_id = None + + await self._state.http.send_soundboard_sound( + self.id, sound.id, source_guild_id=source_guild_id + ) + + +class StageChannel(disnake.abc.Messageable, VocalGuildChannel): + """Represents a Discord guild stage channel. + + .. versionadded:: 1.7 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel's ID. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it isn't set. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + user_limit: :class:`int` + The channel's limit for number of members that can be in a stage channel. + rtc_region: Optional[:class:`str`] + The region for the stage channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + + .. versionchanged:: 2.5 + No longer a ``VoiceRegion`` instance. + + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the stage channel's participants. + + .. versionadded:: 2.0 + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + + .. versionadded:: 2.9 + + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + .. versionadded:: 2.9 + + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + + .. versionadded:: 2.9 + """ + + __slots__ = ( + "topic", + "nsfw", + "slowmode_delay", + "last_message_id", + ) + + def __repr__(self) -> str: + attrs = ( + ("id", self.id), + ("name", self.name), + ("topic", self.topic), + ("rtc_region", self.rtc_region), + ("position", self.position), + ("bitrate", self.bitrate), + ("video_quality_mode", self.video_quality_mode), + ("user_limit", self.user_limit), + ("category_id", self.category_id), + ("nsfw", self.nsfw), + ("flags", self.flags), + ) + joined = " ".join(f"{k!s}={v!r}" for k, v in attrs) + return f"<{self.__class__.__name__} {joined}>" + + def _update(self, guild: Guild, data: StageChannelPayload) -> None: + super()._update(guild, data) + self.topic: Optional[str] = data.get("topic") + self.nsfw: bool = data.get("nsfw", False) + self.slowmode_delay: int = data.get("rate_limit_per_user", 0) + self.last_message_id: Optional[int] = utils._get_as_snowflake(data, "last_message_id") + + async def _get_channel(self) -> Self: + return self + + @property + def requesting_to_speak(self) -> List[Member]: + """List[:class:`Member`]: A list of members who are requesting to speak in the stage channel.""" + return [ + member + for member in self.members + if member.voice and member.voice.requested_to_speak_at is not None + ] + + @property + def speakers(self) -> List[Member]: + """List[:class:`Member`]: A list of members who have been permitted to speak in the stage channel. + + .. versionadded:: 2.0 + """ + return [ + member + for member in self.members + if member.voice + and not member.voice.suppress + and member.voice.requested_to_speak_at is None + ] + + @property + def listeners(self) -> List[Member]: + """List[:class:`Member`]: A list of members who are listening in the stage channel. + + .. versionadded:: 2.0 + """ + return [member for member in self.members if member.voice and member.voice.suppress] + + @property + def moderators(self) -> List[Member]: + """List[:class:`Member`]: A list of members who are moderating the stage channel. + + .. versionadded:: 2.0 + """ + required_permissions = Permissions.stage_moderator() + return [ + member + for member in self.members + if self.permissions_for(member) >= required_permissions + ] + + @property + def type(self) -> Literal[ChannelType.stage_voice]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.stage_voice`. + """ + return ChannelType.stage_voice + + async def clone( + self, + *, + name: Optional[str] = None, + bitrate: int = MISSING, + # user_limit: int = MISSING, + position: int = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: int = MISSING, + rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + video_quality_mode: VideoQualityMode = MISSING, + nsfw: bool = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> StageChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.9 + Added ``position``, ``category``, ``rtc_region``, + ``video_quality_mode``, ``bitrate``, ``nsfw``, ``slowmode_delay`` and ``overwrites`` keyword-only parameters. + + .. note:: + The current :attr:`StageChannel.flags` value won't be cloned. + This is a Discord limitation. + + .. warning:: + Currently the ``user_limit`` attribute is not cloned due to a Discord limitation. + You can directly edit the channel after its creation to set + a `user_limit`. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + bitrate: :class:`int` + The bitrate of the new channel. If not provided, defaults to this channel's bitrate. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + slowmode_delay: :class:`int` + The slowmode of the new channel. If not provided, defaults to this channel's slowmode. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The rtc region of the new channel. If not provided, defaults to this channel's rtc region. + video_quality_mode: :class:`VideoQualityMode` + The video quality mode of the new channel. If not provided, defaults to this channel's video quality mode. + nsfw: :class:`bool` + Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`StageChannel` + The channel that was created. + """ + # TODO + # - check if discord-api-docs#5962 is solved and clone the user_limit attribute + return await self._clone_impl( + { + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + "bitrate": bitrate if bitrate is not MISSING else self.bitrate, + # "user_limit": user_limit if user_limit is not MISSING else self.user_limit, + "position": position if position is not MISSING else self.position, + "rtc_region": (str(rtc_region) if rtc_region is not MISSING else self.rtc_region), + "video_quality_mode": ( + int(video_quality_mode) + if video_quality_mode is not MISSING + else int(self.video_quality_mode) + ), + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, + ) + + def is_nsfw(self) -> bool: + """Whether the channel is marked as NSFW. + + .. versionadded:: 2.9 + + :return type: :class:`bool` + """ + return self.nsfw + + @property + def last_message(self) -> Optional[Message]: + """Gets the last message in this channel from the cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + .. versionadded:: 2.9 + + Returns + ------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return self._state._get_message(self.last_message_id) if self.last_message_id else None + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the given message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 2.9 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + @property + def instance(self) -> Optional[StageInstance]: + """Optional[:class:`StageInstance`]: The running stage instance of the stage channel. + + .. versionadded:: 2.0 + """ + if isinstance(self.guild, Object): + return None + return utils.get(self.guild.stage_instances, channel_id=self.id) + + async def create_instance( + self, + *, + topic: str, + privacy_level: StagePrivacyLevel = MISSING, + notify_everyone: bool = False, + guild_scheduled_event: Snowflake = MISSING, + reason: Optional[str] = None, + ) -> StageInstance: + """|coro| + + Creates a stage instance. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` instead of ``InvalidArgument``. + + Parameters + ---------- + topic: :class:`str` + The stage instance's topic. + privacy_level: :class:`StagePrivacyLevel` + The stage instance's privacy level. Defaults to :attr:`StagePrivacyLevel.guild_only`. + notify_everyone: :class:`bool` + Whether to notify ``@everyone`` that the stage instance has started. + Requires the :attr:`~Permissions.mention_everyone` permission on the stage channel. + Defaults to ``False``. + + .. versionadded:: 2.5 + + guild_scheduled_event: :class:`abc.Snowflake` + The guild scheduled event associated with the stage instance. + Setting this will automatically start the event. + + .. versionadded:: 2.10 + + reason: Optional[:class:`str`] + The reason the stage instance was created. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to create a stage instance. + HTTPException + Creating a stage instance failed. + TypeError + If the ``privacy_level`` parameter is not the proper type. + + Returns + ------- + :class:`StageInstance` + The newly created stage instance. + """ + payload: Dict[str, Any] = { + "channel_id": self.id, + "topic": topic, + "send_start_notification": notify_everyone, + } + + if privacy_level is not MISSING: + if not isinstance(privacy_level, StagePrivacyLevel): + raise TypeError("privacy_level field must be of type PrivacyLevel") + if privacy_level is StagePrivacyLevel.public: + utils.warn_deprecated( + "Setting privacy_level to public is deprecated and will be removed in a future version.", + stacklevel=2, + ) + + payload["privacy_level"] = privacy_level.value + + if guild_scheduled_event is not MISSING: + payload["guild_scheduled_event_id"] = guild_scheduled_event.id + + data = await self._state.http.create_stage_instance(**payload, reason=reason) + return StageInstance(guild=self.guild, state=self._state, data=data) + + async def fetch_instance(self) -> StageInstance: + """|coro| + + Retrieves the running :class:`StageInstance`. + + .. versionadded:: 2.0 + + Raises + ------ + NotFound + The stage instance or channel could not be found. + HTTPException + Retrieving the stage instance failed. + + Returns + ------- + :class:`StageInstance` + The stage instance. + """ + data = await self._state.http.get_stage_instance(self.id) + return StageInstance(guild=self.guild, state=self._state, data=data) + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[StageChannel]: ... + + @overload + async def edit( + self, + *, + name: str = ..., + bitrate: int = ..., + user_limit: int = ..., + position: int = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + rtc_region: Optional[Union[str, VoiceRegion]] = ..., + video_quality_mode: VideoQualityMode = ..., + nsfw: bool = ..., + slowmode_delay: Optional[int] = ..., + flags: ChannelFlags = ..., + reason: Optional[str] = ..., + ) -> StageChannel: ... + + async def edit( + self, + *, + name: str = MISSING, + bitrate: int = MISSING, + user_limit: int = MISSING, + position: int = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + rtc_region: Optional[Union[str, VoiceRegion]] = MISSING, + video_quality_mode: VideoQualityMode = MISSING, + nsfw: bool = MISSING, + slowmode_delay: Optional[int] = MISSING, + flags: ChannelFlags = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[StageChannel]: + """|coro| + + Edits the channel. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.0 + The ``topic`` parameter must now be set via :attr:`create_instance`. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + .. versionchanged:: 2.9 + The ``user_limit``, ``nsfw``, and ``slowmode_delay`` + keyword-only parameters were added. + + Parameters + ---------- + name: :class:`str` + The channel's new name. + bitrate: :class:`int` + The channel's new bitrate. + + .. versionadded:: 2.6 + + user_limit: :class:`int` + The channel's new user limit. + + .. versionadded:: 2.9 + + position: :class:`int` + The channel's new position. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + rtc_region: Optional[Union[:class:`str`, :class:`VoiceRegion`]] + The new region for the stage channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the stage channel's participants. + + .. versionadded:: 2.9 + + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + + .. versionadded:: 2.9 + + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this channel, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.9 + + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + + .. versionadded:: 2.6 + + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. + + Returns + ------- + Optional[:class:`.StageChannel`] + The newly edited stage channel. If the edit was only positional + then ``None`` is returned instead. + """ + payload = await self._edit( + name=name, + bitrate=bitrate, + position=position, + user_limit=user_limit, + sync_permissions=sync_permissions, + category=category, + overwrites=overwrites, + rtc_region=rtc_region, + video_quality_mode=video_quality_mode, + nsfw=nsfw, + flags=flags, + slowmode_delay=slowmode_delay, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def delete_messages(self, messages: Iterable[Snowflake]) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days. + + You must have :attr:`~Permissions.manage_messages` permission to + do this. + + .. versionadded:: 2.9 + + Parameters + ---------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id: int = messages[0].id + await self._state.http.delete_message(self.id, message_id) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids) + + async def purge( + self, + *, + limit: Optional[int] = 100, + check: Callable[[Message], bool] = MISSING, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + around: Optional[SnowflakeTime] = None, + oldest_first: Optional[bool] = False, + bulk: bool = True, + ) -> List[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + .. versionadded:: 2.9 + + .. note:: + + See :meth:`TextChannel.purge` for examples. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Returns + ------- + List[:class:`.Message`] + A list of messages that were deleted. + """ + if check is MISSING: + check = lambda m: True + + iterator = self.history( + limit=limit, before=before, after=after, oldest_first=oldest_first, around=around + ) + ret: List[Message] = [] + count = 0 + + minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 + strategy = self.delete_messages if bulk else _single_delete_strategy + + async for message in iterator: + if count == 100: + to_delete = ret[-100:] + await strategy(to_delete) + count = 0 + await asyncio.sleep(1) + + if not check(message): + continue + + if message.id < minimum_time: + # older than 14 days old + if count == 1: + await ret[-1].delete() + elif count >= 2: + to_delete = ret[-count:] + await strategy(to_delete) + + count = 0 + strategy = _single_delete_strategy + + count += 1 + ret.append(message) + + # SOme messages remaining to poll + if count >= 2: + # more than 2 messages -> bulk delete + to_delete = ret[-count:] + await strategy(to_delete) + elif count == 1: + # delete a single message + await ret[-1].delete() + + return ret + + async def webhooks(self) -> List[Webhook]: + """|coro| + + Retrieves the list of webhooks this channel has. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + use this. + + .. versionadded:: 2.9 + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + + Returns + ------- + List[:class:`Webhook`] + The list of webhooks this channel has. + """ + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: Optional[bytes] = None, reason: Optional[str] = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + do this. + + .. versionadded:: 2.9 + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + The webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Raises + ------ + NotFound + The ``avatar`` asset couldn't be found. + Forbidden + You do not have permissions to create a webhook. + HTTPException + Creating the webhook failed. + TypeError + The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). + + Returns + ------- + :class:`Webhook` + The newly created webhook. + """ + from .webhook import Webhook + + avatar_data = await utils._assetbytes_to_base64_data(avatar) + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar_data, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + +class CategoryChannel(disnake.abc.GuildChannel, Hashable): + """Represents a Discord channel category. + + These are useful to group channels to logical compartments. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the category's hash. + + .. describe:: str(x) + + Returns the category's name. + + Attributes + ---------- + name: :class:`str` + The category name. + guild: :class:`Guild` + The guild the category belongs to. + id: :class:`int` + The category channel ID. + position: :class:`int` + The position in the category list. This is a number that starts at 0. e.g. the + top category is position 0. + nsfw: :class:`bool` + If the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + """ + + __slots__ = ( + "name", + "id", + "guild", + "nsfw", + "_state", + "position", + "_overwrites", + "category_id", + "_flags", + ) + + def __init__( + self, *, state: ConnectionState, guild: Guild, data: CategoryChannelPayload + ) -> None: + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._update(guild, data) + + def __repr__(self) -> str: + return f"" + + def _update(self, guild: Guild, data: CategoryChannelPayload) -> None: + self.guild: Guild = guild + # apparently this can be nullable in the case of a bad api deploy + self.name: str = data.get("name") or "" + self.category_id: Optional[int] = utils._get_as_snowflake(data, "parent_id") + self._flags = data.get("flags", 0) + self.nsfw: bool = data.get("nsfw", False) + self.position: int = data["position"] + self._fill_overwrites(data) + + @property + def _sorting_bucket(self) -> int: + return ChannelType.category.value + + @property + def type(self) -> Literal[ChannelType.category]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.category`. + """ + return ChannelType.category + + @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) + + return base + + def is_nsfw(self) -> bool: + """Whether the category is marked as NSFW. + + :return type: :class:`bool` + """ + return self.nsfw + + async def clone( + self, + *, + name: Optional[str] = None, + position: int = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> CategoryChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.9 + Added ``position``, ``nsfw`` and ``overwrites`` keyword-only parameters. + + .. note:: + The current :attr:`CategoryChannel.flags` value won't be cloned. + This is a Discord limitation. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`CategoryChannel` + The channel that was created. + """ + return await self._clone_impl( + { + "position": position if position is not MISSING else self.position, + }, + name=name, + reason=reason, + overwrites=overwrites, + ) + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def edit( + self, + *, + name: str = ..., + position: int = ..., + nsfw: bool = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + flags: ChannelFlags = ..., + reason: Optional[str] = ..., + ) -> CategoryChannel: ... + + async def edit( + self, + *, + name: str = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + flags: ChannelFlags = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[CategoryChannel]: + """|coro| + + Edits the category. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + name: :class:`str` + The new category's name. + position: :class:`int` + The new category's position. + nsfw: :class:`bool` + Whether to mark the category as NSFW. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the category. + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + + .. versionadded:: 2.6 + + reason: Optional[:class:`str`] + The reason for editing this category. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to edit the category. + HTTPException + Editing the category failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. + + Returns + ------- + Optional[:class:`.CategoryChannel`] + The newly edited category channel. If the edit was only positional + then ``None`` is returned instead. + """ + payload = await self._edit( + name=name, + position=position, + nsfw=nsfw, + overwrites=overwrites, + flags=flags, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + @overload + async def move( + self, + *, + beginning: bool, + offset: int = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def move( + self, + *, + end: bool, + offset: int = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def move( + self, + *, + before: Snowflake, + offset: int = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @overload + async def move( + self, + *, + after: Snowflake, + offset: int = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + @utils.copy_doc(disnake.abc.GuildChannel.move) + async def move(self, **kwargs: Any) -> None: + kwargs.pop("category", None) + return await super().move(**kwargs) + + @property + def channels(self) -> List[GuildChannelType]: + """List[:class:`abc.GuildChannel`]: Returns the channels that are under this category. + + These are sorted by the official Discord UI, which places voice channels below the text channels. + """ + if isinstance(self.guild, Object): + return [] + + def comparator(channel: GuildChannelType) -> Tuple[bool, int]: + return ( + not isinstance(channel, (TextChannel, ThreadOnlyGuildChannel)), + channel.position, + ) + + ret = [c for c in self.guild.channels if c.category_id == self.id] + ret.sort(key=comparator) + return ret + + @property + def text_channels(self) -> List[TextChannel]: + """List[:class:`TextChannel`]: Returns the text channels that are under this category.""" + if isinstance(self.guild, Object): + return [] + + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, TextChannel) + ] + ret.sort(key=lambda c: (c.position, c.id)) + return ret + + @property + def voice_channels(self) -> List[VoiceChannel]: + """List[:class:`VoiceChannel`]: Returns the voice channels that are under this category.""" + if isinstance(self.guild, Object): + return [] + + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, VoiceChannel) + ] + ret.sort(key=lambda c: (c.position, c.id)) + return ret + + @property + def stage_channels(self) -> List[StageChannel]: + """List[:class:`StageChannel`]: Returns the stage channels that are under this category. + + .. versionadded:: 1.7 + """ + if isinstance(self.guild, Object): + return [] + + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, StageChannel) + ] + ret.sort(key=lambda c: (c.position, c.id)) + return ret + + @property + def forum_channels(self) -> List[ForumChannel]: + """List[:class:`ForumChannel`]: Returns the forum channels that are under this category. + + .. versionadded:: 2.5 + """ + if isinstance(self.guild, Object): + return [] + + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, ForumChannel) + ] + ret.sort(key=lambda c: (c.position, c.id)) + return ret + + @property + def media_channels(self) -> List[MediaChannel]: + """List[:class:`MediaChannel`]: Returns the media channels that are under this category. + + .. versionadded:: 2.10 + """ + if isinstance(self.guild, Object): + return [] + + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, MediaChannel) + ] + ret.sort(key=lambda c: (c.position, c.id)) + return ret + + async def create_text_channel(self, name: str, **options: Any) -> TextChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_text_channel` to create a :class:`TextChannel` in the category. + + Returns + ------- + :class:`TextChannel` + The newly created text channel. + """ + if "category" in options: + raise TypeError("got an unexpected keyword argument 'category'") + return await self.guild.create_text_channel(name, category=self, **options) + + async def create_voice_channel(self, name: str, **options: Any) -> VoiceChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_voice_channel` to create a :class:`VoiceChannel` in the category. + + Returns + ------- + :class:`VoiceChannel` + The newly created voice channel. + """ + if "category" in options: + raise TypeError("got an unexpected keyword argument 'category'") + return await self.guild.create_voice_channel(name, category=self, **options) + + async def create_stage_channel(self, name: str, **options: Any) -> StageChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category. + + .. versionadded:: 1.7 + + Returns + ------- + :class:`StageChannel` + The newly created stage channel. + """ + if "category" in options: + raise TypeError("got an unexpected keyword argument 'category'") + return await self.guild.create_stage_channel(name, category=self, **options) + + async def create_forum_channel(self, name: str, **options: Any) -> ForumChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_forum_channel` to create a :class:`ForumChannel` in the category. + + .. versionadded:: 2.5 + + Returns + ------- + :class:`ForumChannel` + The newly created forum channel. + """ + if "category" in options: + raise TypeError("got an unexpected keyword argument 'category'") + return await self.guild.create_forum_channel(name, category=self, **options) + + async def create_media_channel(self, name: str, **options: Any) -> MediaChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_media_channel` to create a :class:`MediaChannel` in the category. + + .. versionadded:: 2.10 + + Returns + ------- + :class:`MediaChannel` + The newly created media channel. + """ + if "category" in options: + raise TypeError("got an unexpected keyword argument 'category'") + return await self.guild.create_media_channel(name, category=self, **options) + + +class NewsChannel(TextChannel): + """Represents a Discord news channel + + An exact 1:1 copy of :class:`TextChannel` meant for command annotations + """ + + type: ChannelType = ChannelType.news + + +class ThreadWithMessage(NamedTuple): + thread: Thread + message: Message + + +class ThreadOnlyGuildChannel(disnake.abc.GuildChannel, Hashable): + __slots__ = ( + "id", + "name", + "category_id", + "topic", + "position", + "nsfw", + "last_thread_id", + "_flags", + "default_auto_archive_duration", + "guild", + "slowmode_delay", + "default_thread_slowmode_delay", + "default_sort_order", + "_available_tags", + "_default_reaction_emoji_id", + "_default_reaction_emoji_name", + "_state", + "_type", + "_overwrites", + ) + + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: Union[ForumChannelPayload, MediaChannelPayload], + ) -> None: + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._type: int = data["type"] + self._update(guild, data) + + def __repr__(self) -> str: + attrs = ( + ("id", self.id), + ("name", self.name), + ("topic", self.topic), + ("position", self.position), + ("nsfw", self.nsfw), + ("category_id", self.category_id), + ("default_auto_archive_duration", self.default_auto_archive_duration), + ("flags", self.flags), + ) + joined = " ".join(f"{k!s}={v!r}" for k, v in attrs) + return f"<{type(self).__name__} {joined}>" + + def _update(self, guild: Guild, data: Union[ForumChannelPayload, MediaChannelPayload]) -> None: + self.guild: Guild = guild + # apparently this can be nullable in the case of a bad api deploy + self.name: str = data.get("name") or "" + self.category_id: Optional[int] = utils._get_as_snowflake(data, "parent_id") + self.topic: Optional[str] = data.get("topic") + self.position: int = data["position"] + self._flags = data.get("flags", 0) + self.nsfw: bool = data.get("nsfw", False) + self.last_thread_id: Optional[int] = utils._get_as_snowflake(data, "last_message_id") + self.default_auto_archive_duration: ThreadArchiveDurationLiteral = data.get( + "default_auto_archive_duration", 1440 + ) + self.slowmode_delay: int = data.get("rate_limit_per_user", 0) + self.default_thread_slowmode_delay: int = data.get("default_thread_rate_limit_per_user", 0) + + tags = [ + ForumTag._from_data(data=tag, state=self._state) + for tag in data.get("available_tags", []) + ] + self._available_tags: Dict[int, ForumTag] = {tag.id: tag for tag in tags} + + default_reaction_emoji = data.get("default_reaction_emoji") or {} + # emoji_id may be `0`, use `None` instead + self._default_reaction_emoji_id: Optional[int] = ( + utils._get_as_snowflake(default_reaction_emoji, "emoji_id") or None + ) + self._default_reaction_emoji_name: Optional[str] = default_reaction_emoji.get("emoji_name") + + self.default_sort_order: Optional[ThreadSortOrder] = ( + try_enum(ThreadSortOrder, order) + if (order := data.get("default_sort_order")) is not None + else None + ) + + self._fill_overwrites(data) + + async def _get_channel(self) -> Self: + return self + + @property + def _sorting_bucket(self) -> int: + return ChannelType.text.value + + @utils.copy_doc(disnake.abc.GuildChannel.permissions_for) + def permissions_for( + self, + obj: Union[Member, Role], + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + base = super().permissions_for(obj, ignore_timeout=ignore_timeout) + self._apply_implict_permissions(base) + + # thread-only channels do not have voice related permissions + denied = Permissions.voice() + base.value &= ~denied.value + return base + + @property + def members(self) -> List[Member]: + """List[:class:`Member`]: Returns all members that can see this channel.""" + if isinstance(self.guild, Object): + return [] + return [m for m in self.guild.members if self.permissions_for(m).view_channel] + + @property + def threads(self) -> List[Thread]: + """List[:class:`Thread`]: Returns all the threads that you can see.""" + if isinstance(self.guild, Object): + return [] + return [thread for thread in self.guild._threads.values() if thread.parent_id == self.id] + + def is_nsfw(self) -> bool: + """Whether the channel is marked as NSFW. + + :return type: :class:`bool` + """ + return self.nsfw + + def requires_tag(self) -> bool: + """Whether all newly created threads in this channel are required to have a tag. + + This is a shortcut to :attr:`self.flags.require_tag `. + + .. versionadded:: 2.6 + + :return type: :class:`bool` + """ + return self.flags.require_tag + + @property + def default_reaction(self) -> Optional[Union[Emoji, PartialEmoji]]: + """Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]]: + The default emoji shown for reacting to threads. + + Due to a Discord limitation, this will have an empty + :attr:`~PartialEmoji.name` if it is a custom :class:`PartialEmoji`. + + .. versionadded:: 2.6 + """ + return self._state._get_emoji_from_fields( + name=self._default_reaction_emoji_name, + id=self._default_reaction_emoji_id, + ) + + @property + def last_thread(self) -> Optional[Thread]: + """Gets the last created thread in this channel from the cache. + + The thread might not be valid or point to an existing thread. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last thread, use :meth:`Guild.fetch_channel` with the :attr:`last_thread_id` + attribute. + + Returns + ------- + Optional[:class:`Thread`] + The last created thread in this channel or ``None`` if not found. + """ + return self._state.get_channel(self.last_thread_id) if self.last_thread_id else None # type: ignore + + @property + def available_tags(self) -> List[ForumTag]: + """List[:class:`ForumTag`]: The available tags for threads in this channel. + + To create/edit/delete tags, use :func:`edit`. + + .. versionadded:: 2.6 + """ + return list(self._available_tags.values()) + + # both of these are re-implemented due to thread-only channels not being messageables + async def trigger_typing(self) -> None: + """|coro| + + Triggers a *typing* indicator to the destination. + + *Typing* indicator will go away after 10 seconds. + """ + channel = await self._get_channel() + await self._state.http.send_typing(channel.id) + + @utils.copy_doc(disnake.abc.Messageable.typing) + def typing(self) -> Typing: + return Typing(self) + + def get_thread(self, thread_id: int, /) -> Optional[Thread]: + """Returns a thread with the given ID. + + Parameters + ---------- + thread_id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`Thread`] + The returned thread of ``None`` if not found. + """ + if isinstance(self.guild, Object): + return None + return self.guild.get_thread(thread_id) + + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embed: Embed = ..., + file: File = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: MessageComponents = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... + + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embed: Embed = ..., + files: List[File] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: MessageComponents = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... + + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embeds: List[Embed] = ..., + file: File = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: MessageComponents = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... + + @overload + async def create_thread( + self, + *, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = ..., + slowmode_delay: Optional[int] = ..., + applied_tags: Sequence[Snowflake] = ..., + content: str = ..., + embeds: List[Embed] = ..., + files: List[File] = ..., + suppress_embeds: bool = ..., + flags: MessageFlags = ..., + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ..., + allowed_mentions: AllowedMentions = ..., + view: View = ..., + components: MessageComponents = ..., + reason: Optional[str] = None, + ) -> ThreadWithMessage: ... + + async def create_thread( + self, + *, + name: str, + auto_archive_duration: AnyThreadArchiveDuration = MISSING, + slowmode_delay: Optional[int] = MISSING, + applied_tags: Sequence[Snowflake] = MISSING, + content: str = MISSING, + embed: Embed = MISSING, + embeds: List[Embed] = MISSING, + file: File = MISSING, + files: List[File] = MISSING, + suppress_embeds: bool = MISSING, + flags: MessageFlags = MISSING, + stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING, + allowed_mentions: AllowedMentions = MISSING, + view: View = MISSING, + components: MessageComponents = MISSING, + reason: Optional[str] = None, + ) -> ThreadWithMessage: + """|coro| + + Creates a thread (with an initial message) in this channel. + + You must have the :attr:`~Permissions.create_forum_threads` permission to do this. + + At least one of ``content``, ``embed``/``embeds``, ``file``/``files``, + ``stickers``, ``components``, or ``view`` must be provided. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + .. versionchanged:: 2.6 + The ``content`` parameter is no longer required. + + .. note:: + Unlike :meth:`TextChannel.create_thread`, + this **returns a tuple** with both the created **thread and message**. + + Parameters + ---------- + name: :class:`str` + The name of the thread. + auto_archive_duration: Union[:class:`int`, :class:`ThreadArchiveDuration`] + The duration in minutes before the thread is automatically archived for inactivity. + If not provided, the channel's default auto archive duration is used. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this thread, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + If set to ``None`` or not provided, slowmode is inherited from the parent's + :attr:`default_thread_slowmode_delay`. + applied_tags: Sequence[:class:`abc.Snowflake`] + The tags to apply to the new thread. Maximum of 5. + + .. versionadded:: 2.6 + + content: :class:`str` + The content of the message to send. + embed: :class:`.Embed` + The rich embed for the content to send. This cannot be mixed with the + ``embeds`` parameter. + embeds: List[:class:`.Embed`] + A list of embeds to send with the content. Must be a maximum of 10. + This cannot be mixed with the ``embed`` parameter. + suppress_embeds: :class:`bool` + Whether to suppress embeds for the message. This hides + all the embeds from the UI if set to ``True``. + flags: :class:`MessageFlags` + The flags to set for this message. + Only :attr:`~MessageFlags.suppress_embeds` and :attr:`~MessageFlags.is_components_v2` + are supported. + + If parameter ``suppress_embeds`` is provided, + that will override the setting of :attr:`MessageFlags.suppress_embeds`. + + .. versionadded:: 2.9 + + file: :class:`~disnake.File` + The file to upload. This cannot be mixed with the ``files`` parameter. + files: List[:class:`~disnake.File`] + A list of files to upload. Must be a maximum of 10. + This cannot be mixed with the ``file`` parameter. + stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StandardSticker`, :class:`.StickerItem`]] + A list of stickers to upload. Must be a maximum of 3. + allowed_mentions: :class:`.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`.Client.allowed_mentions` + are used instead. + view: :class:`.ui.View` + A Discord UI View to add to the message. This cannot be mixed with ``components``. + components: |components_type| + A list of components to include in the message. This cannot be mixed with ``view``. + + .. note:: + Passing v2 components here automatically sets the :attr:`~MessageFlags.is_components_v2` flag. + Setting this flag cannot be reverted. Note that this also disables the + ``content``, ``embeds``, and ``stickers`` fields. + + reason: Optional[:class:`str`] + The reason for creating the thread. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to create a thread. + HTTPException + Starting the thread failed. + TypeError + Specified both ``file`` and ``files``, + or you specified both ``embed`` and ``embeds``, + or you specified both ``view`` and ``components``, + or you have passed an object that is not :class:`File` to ``file`` or ``files``. + ValueError + Specified more than 10 embeds, or more than 10 files, or + you tried to send v2 components together with ``content``, ``embeds``, or ``stickers``. + + Returns + ------- + :class:`ThreadWithMessage` + A :class:`~typing.NamedTuple` with the newly created thread and the message sent in it. + """ + from .message import Message + from .webhook.async_ import handle_message_parameters_dict + + params = handle_message_parameters_dict( + content, + embed=embed, + embeds=embeds, + file=file, + files=files, + suppress_embeds=suppress_embeds, + flags=flags, + view=view, + components=components, + allowed_mentions=allowed_mentions, + stickers=stickers, + ) + + if auto_archive_duration not in (MISSING, None): + auto_archive_duration = cast( + "ThreadArchiveDurationLiteral", try_enum_to_int(auto_archive_duration) + ) + + tag_ids = [t.id for t in applied_tags] if applied_tags else [] + + if params.files and len(params.files) > 10: + raise ValueError("files parameter must be a list of up to 10 elements") + elif params.files and not all(isinstance(file, File) for file in params.files): + raise TypeError("files parameter must be a list of File") + + channel_data = { + "name": name, + "auto_archive_duration": auto_archive_duration or self.default_auto_archive_duration, + "applied_tags": tag_ids, + } + + if slowmode_delay not in (MISSING, None): + channel_data["rate_limit_per_user"] = slowmode_delay + + try: + data = await self._state.http.start_thread_in_forum_channel( + self.id, + **channel_data, + files=params.files, + reason=reason, + **params.payload, + ) + finally: + if params.files: + for f in params.files: + f.close() + + thread = Thread(guild=self.guild, data=data, state=self._state) + message = Message(channel=thread, data=data["message"], state=self._state) + + if view: + self._state.store_view(view, message.id) + + return ThreadWithMessage(thread, message) + + def archived_threads( + self, + *, + limit: Optional[int] = 50, + before: Optional[Union[Snowflake, datetime.datetime]] = None, + ) -> ArchivedThreadIterator: + """Returns an :class:`~disnake.AsyncIterator` that iterates over all archived threads in the channel. + + You must have :attr:`~Permissions.read_message_history` permission to use this. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of threads to retrieve. + If ``None``, retrieves every archived thread in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve archived channels before the given date or ID. + + Raises + ------ + Forbidden + You do not have permissions to get archived threads. + HTTPException + The request to get the archived threads failed. + + Yields + ------ + :class:`Thread` + The archived threads. + """ + return ArchivedThreadIterator( + self.id, self.guild, limit=limit, joined=False, private=False, before=before + ) + + async def webhooks(self) -> List[Webhook]: + """|coro| + + Retrieves the list of webhooks this channel has. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + use this. + + .. versionadded:: 2.6 + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + + Returns + ------- + List[:class:`Webhook`] + The list of webhooks this channel has. + """ + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: Optional[bytes] = None, reason: Optional[str] = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + You must have :attr:`~.Permissions.manage_webhooks` permission to + do this. + + .. versionadded:: 2.6 + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + The webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Raises + ------ + NotFound + The ``avatar`` asset couldn't be found. + Forbidden + You do not have permissions to create a webhook. + HTTPException + Creating the webhook failed. + TypeError + The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). + + Returns + ------- + :class:`Webhook` + The newly created webhook. + """ + from .webhook import Webhook + + avatar_data = await utils._assetbytes_to_base64_data(avatar) + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar_data, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + def get_tag(self, tag_id: int, /) -> Optional[ForumTag]: + """Returns a thread tag with the given ID. + + .. versionadded:: 2.6 + + Parameters + ---------- + tag_id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`ForumTag`] + The tag with the given ID, or ``None`` if not found. + """ + return self._available_tags.get(tag_id) + + def get_tag_by_name(self, name: str, /) -> Optional[ForumTag]: + """Returns a thread tag with the given name. + + Tags can be uniquely identified based on the name, as tag names + in a channel must be unique. + + .. versionadded:: 2.6 + + Parameters + ---------- + name: :class:`str` + The name to search for. + + Returns + ------- + Optional[:class:`ForumTag`] + The tag with the given name, or ``None`` if not found. + """ + return utils.get(self._available_tags.values(), name=name) + + +class ForumChannel(ThreadOnlyGuildChannel): + """Represents a Discord guild forum channel. + + .. versionadded:: 2.5 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it isn't set. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + last_thread_id: Optional[:class:`int`] + The ID of the last created thread in this channel. It may + *not* point to an existing or valid thread. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + slowmode_delay: :class:`int` + The number of seconds a member must wait between creating threads + in this channel. + + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + See also :attr:`default_thread_slowmode_delay`. + + default_thread_slowmode_delay: :class:`int` + The default number of seconds a member must wait between sending messages + in newly created threads in this channel. + + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + .. versionadded:: 2.6 + + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of threads in this channel. + Members will still be able to change this locally. + + .. versionadded:: 2.6 + + default_layout: :class:`ThreadLayout` + The default layout of threads in this channel. + Members will still be able to change this locally. + + .. versionadded:: 2.8 + """ + + __slots__ = ("default_layout",) + + def _update(self, guild: Guild, data: ForumChannelPayload) -> None: + super()._update(guild=guild, data=data) + self.default_layout: ThreadLayout = ( + try_enum(ThreadLayout, layout) + if (layout := data.get("default_forum_layout")) is not None + else ThreadLayout.not_set + ) + + @property + def type(self) -> Literal[ChannelType.forum]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.forum`. + """ + return ChannelType.forum + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[ForumChannel]: ... + + @overload + async def edit( + self, + *, + name: str = ..., + topic: Optional[str] = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + slowmode_delay: Optional[int] = ..., + default_thread_slowmode_delay: Optional[int] = ..., + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + flags: ChannelFlags = ..., + require_tag: bool = ..., + available_tags: Sequence[ForumTag] = ..., + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., + default_sort_order: Optional[ThreadSortOrder] = ..., + default_layout: ThreadLayout = ..., + reason: Optional[str] = ..., + ) -> ForumChannel: ... + + async def edit( + self, + *, + name: str = MISSING, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + flags: ChannelFlags = MISSING, + require_tag: bool = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + default_layout: ThreadLayout = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[ForumChannel]: + """|coro| + + Edits the channel. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` or :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + name: :class:`str` + The channel's new name. + topic: Optional[:class:`str`] + The channel's new topic. + position: :class:`int` + The channel's new position. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can create + threads in this channel, in seconds. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + default_thread_slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + This does not apply retroactively to existing threads. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + + .. versionadded:: 2.6 + + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + If parameter ``require_tag`` is provided, that will override the setting of :attr:`ChannelFlags.require_tag`. + + .. versionadded:: 2.6 + + require_tag: :class:`bool` + Whether all newly created threads are required to have a tag. + + .. versionadded:: 2.6 + + available_tags: Sequence[:class:`ForumTag`] + The new :class:`ForumTag`\\s available for threads in this channel. + Can be used to create new tags and edit/reorder/delete existing tags. + Maximum of 20. + + Note that this overwrites all tags, removing existing tags unless they're passed as well. + + See :class:`ForumTag` for examples regarding creating/editing tags. + + .. versionadded:: 2.6 + + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The new default emoji shown for reacting to threads. + + .. versionadded:: 2.6 + + default_sort_order: Optional[:class:`ThreadSortOrder`] + The new default sort order of threads in this channel. + + .. versionadded:: 2.6 + + default_layout: :class:`ThreadLayout` + The new default layout of threads in this channel. + + .. versionadded:: 2.8 + + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. + + Returns + ------- + Optional[:class:`ForumChannel`] + The newly edited forum channel. If the edit was only positional + then ``None`` is returned instead. + """ + if require_tag is not MISSING: + # create base flags if flags are provided, otherwise use the internal flags. + flags = ChannelFlags._from_value(self._flags if flags is MISSING else flags.value) + flags.require_tag = require_tag + + payload = await self._edit( + name=name, + topic=topic, + position=position, + nsfw=nsfw, + sync_permissions=sync_permissions, + category=category, + slowmode_delay=slowmode_delay, + default_thread_slowmode_delay=default_thread_slowmode_delay, + default_auto_archive_duration=default_auto_archive_duration, + overwrites=overwrites, + flags=flags, + available_tags=available_tags, + default_reaction=default_reaction, + default_sort_order=default_sort_order, + default_layout=default_layout, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def clone( + self, + *, + name: Optional[str] = None, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + default_layout: ThreadLayout = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> ForumChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. versionchanged:: 2.9 + Added new ``topic``, ``position``, ``nsfw``, ``category``, ``slowmode_delay``, + ``default_thread_slowmode_delay``, ``default_auto_archive_duration``, + ``available_tags``, ``default_reaction``, ``default_sort_order`` + and ``overwrites`` keyword-only parameters. + + .. versionchanged:: 2.10 + Added ``default_layout`` parameter. + + .. note:: + The current :attr:`ForumChannel.flags` value won't be cloned. + This is a Discord limitation. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + topic: Optional[:class:`str`] + The topic of the new channel. If not provided, defaults to this channel's topic. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + nsfw: :class:`bool` + Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + slowmode_delay: Optional[:class:`int`] + The slowmode delay of the new channel. If not provided, defaults to this channel's slowmode delay. + default_thread_slowmode_delay: Optional[:class:`int`] + The default thread slowmode delay of the new channel. If not provided, defaults to this channel's default thread slowmode delay. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The default auto archive duration of the new channel. If not provided, defaults to this channel's default auto archive duration. + available_tags: Sequence[:class:`ForumTag`] + The applicable tags of the new channel. If not provided, defaults to this channel's available tags. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The default reaction of the new channel. If not provided, defaults to this channel's default reaction. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of the new channel. If not provided, defaults to this channel's default sort order. + default_layout: :class:`ThreadLayout` + The default layout of threads in the new channel. If not provided, defaults to this channel's default layout. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`MediaChannel` + The channel that was created. + """ + default_reaction_emoji_payload: Optional[DefaultReactionPayload] = MISSING + if default_reaction is MISSING: + default_reaction = self.default_reaction + + if default_reaction is not None: + emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) + default_reaction_emoji_payload = { + "emoji_name": emoji_name, + "emoji_id": emoji_id, + } + else: + default_reaction_emoji_payload = None + + return await self._clone_impl( + { + "topic": topic if topic is not MISSING else self.topic, + "position": position if position is not MISSING else self.position, + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + "default_thread_rate_limit_per_user": ( + default_thread_slowmode_delay + if default_thread_slowmode_delay is not MISSING + else self.default_thread_slowmode_delay + ), + "default_auto_archive_duration": ( + try_enum_to_int(default_auto_archive_duration) + if default_auto_archive_duration is not MISSING + else self.default_auto_archive_duration + ), + "available_tags": ( + [tag.to_dict() for tag in available_tags] + if available_tags is not MISSING + else [tag.to_dict() for tag in self.available_tags] + ), + "default_reaction_emoji": default_reaction_emoji_payload, + "default_sort_order": ( + try_enum_to_int(default_sort_order) + if default_sort_order is not MISSING + else try_enum_to_int(self.default_sort_order) + ), + "default_forum_layout": ( + try_enum_to_int(default_layout) + if default_layout is not MISSING + else try_enum_to_int(self.default_layout) + ), + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, + ) + + +class MediaChannel(ThreadOnlyGuildChannel): + """Represents a Discord guild media channel. + + Media channels are very similar to forum channels - only threads can be created in them, + with only minor differences in functionality. + + .. versionadded:: 2.10 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + id: :class:`int` + The channel's ID. + name: :class:`str` + The channel's name. + guild: :class:`Guild` + The guild the channel belongs to. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it isn't set. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. + nsfw: :class:`bool` + Whether the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + last_thread_id: Optional[:class:`int`] + The ID of the last created thread in this channel. It may + *not* point to an existing or valid thread. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + slowmode_delay: :class:`int` + The number of seconds a member must wait between creating threads + in this channel. + + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + See also :attr:`default_thread_slowmode_delay`. + + default_thread_slowmode_delay: :class:`int` + The default number of seconds a member must wait between sending messages + in newly created threads in this channel. + + A value of ``0`` denotes that it is disabled. + Bots, and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages`, bypass slowmode. + + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of threads in this channel. + Members will still be able to change this locally. + """ + + __slots__ = () + + @property + def type(self) -> Literal[ChannelType.media]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.media`. + """ + return ChannelType.media + + def hides_media_download_options(self) -> bool: + """Whether the channel hides the embedded media download options. + + This is a shortcut to :attr:`self.flags.hide_media_download_options `. + + :return type: :class:`bool` + """ + return self.flags.hide_media_download_options + + # if only these parameters are passed, `_move` is called and no channel will be returned + @overload + async def edit( + self, + *, + position: int, + category: Optional[Snowflake] = ..., + sync_permissions: bool = ..., + reason: Optional[str] = ..., + ) -> None: ... + + # only passing `sync_permissions` may or may not return a channel, + # depending on whether the channel is in a category + @overload + async def edit( + self, + *, + sync_permissions: bool, + reason: Optional[str] = ..., + ) -> Optional[MediaChannel]: ... + + @overload + async def edit( + self, + *, + name: str = ..., + topic: Optional[str] = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: Optional[Snowflake] = ..., + slowmode_delay: Optional[int] = ..., + default_thread_slowmode_delay: Optional[int] = ..., + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = ..., + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ..., + flags: ChannelFlags = ..., + require_tag: bool = ..., + available_tags: Sequence[ForumTag] = ..., + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = ..., + default_sort_order: Optional[ThreadSortOrder] = ..., + reason: Optional[str] = ..., + ) -> MediaChannel: ... + + async def edit( + self, + *, + name: str = MISSING, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + sync_permissions: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + flags: ChannelFlags = MISSING, + require_tag: bool = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + reason: Optional[str] = None, + **kwargs: Never, + ) -> Optional[MediaChannel]: + """|coro| + + Edits the channel. + + You must have :attr:`~Permissions.manage_channels` permission to + do this. + + Parameters + ---------- + name: :class:`str` + The channel's new name. + topic: Optional[:class:`str`] + The channel's new topic. + position: :class:`int` + The channel's new position. + nsfw: :class:`bool` + Whether to mark the channel as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`abc.Snowflake`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can create + threads in this channel, in seconds. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + default_thread_slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit at which users can send messages + in newly created threads in this channel, in seconds. + This does not apply retroactively to existing threads. + A value of ``0`` or ``None`` disables slowmode. The maximum value possible is ``21600``. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to + :class:`PermissionOverwrite` to apply to the channel. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + flags: :class:`ChannelFlags` + The new flags to set for this channel. This will overwrite any existing flags set on this channel. + If parameter ``require_tag`` is provided, that will override the setting of :attr:`ChannelFlags.require_tag`. + require_tag: :class:`bool` + Whether all newly created threads are required to have a tag. + available_tags: Sequence[:class:`ForumTag`] + The new :class:`ForumTag`\\s available for threads in this channel. + Can be used to create new tags and edit/reorder/delete existing tags. + Maximum of 20. + + Note that this overwrites all tags, removing existing tags unless they're passed as well. + + See :class:`ForumTag` for examples regarding creating/editing tags. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The new default emoji shown for reacting to threads. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The new default sort order of threads in this channel. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + TypeError + The permission overwrite information is not in proper form. + ValueError + The position is less than 0. + + Returns + ------- + Optional[:class:`MediaChannel`] + The newly edited media channel. If the edit was only positional + then ``None`` is returned instead. + """ + if require_tag is not MISSING: + # create base flags if flags are provided, otherwise use the internal flags. + flags = ChannelFlags._from_value(self._flags if flags is MISSING else flags.value) + flags.require_tag = require_tag + + payload = await self._edit( + name=name, + topic=topic, + position=position, + nsfw=nsfw, + sync_permissions=sync_permissions, + category=category, + slowmode_delay=slowmode_delay, + default_thread_slowmode_delay=default_thread_slowmode_delay, + default_auto_archive_duration=default_auto_archive_duration, + overwrites=overwrites, + flags=flags, + available_tags=available_tags, + default_reaction=default_reaction, + default_sort_order=default_sort_order, + reason=reason, + **kwargs, # type: ignore + ) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def clone( + self, + *, + name: Optional[str] = None, + topic: Optional[str] = MISSING, + position: int = MISSING, + nsfw: bool = MISSING, + category: Optional[Snowflake] = MISSING, + slowmode_delay: Optional[int] = MISSING, + default_thread_slowmode_delay: Optional[int] = MISSING, + default_auto_archive_duration: Optional[AnyThreadArchiveDuration] = MISSING, + available_tags: Sequence[ForumTag] = MISSING, + default_reaction: Optional[Union[str, Emoji, PartialEmoji]] = MISSING, + default_sort_order: Optional[ThreadSortOrder] = MISSING, + overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING, + reason: Optional[str] = None, + ) -> MediaChannel: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have :attr:`.Permissions.manage_channels` permission to + do this. + + .. note:: + The current :attr:`MediaChannel.flags` value won't be cloned. + This is a Discord limitation. + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this channel's name. + topic: Optional[:class:`str`] + The topic of the new channel. If not provided, defaults to this channel's topic. + position: :class:`int` + The position of the new channel. If not provided, defaults to this channel's position. + nsfw: :class:`bool` + Whether the new channel should be nsfw or not. If not provided, defaults to this channel's NSFW value. + category: Optional[:class:`abc.Snowflake`] + The category where the new channel should be grouped. If not provided, defaults to this channel's category. + slowmode_delay: Optional[:class:`int`] + The slowmode delay of the new channel. If not provided, defaults to this channel's slowmode delay. + default_thread_slowmode_delay: Optional[:class:`int`] + The default thread slowmode delay of the new channel. If not provided, defaults to this channel's default thread slowmode delay. + default_auto_archive_duration: Optional[Union[:class:`int`, :class:`ThreadArchiveDuration`]] + The default auto archive duration of the new channel. If not provided, defaults to this channel's default auto archive duration. + available_tags: Sequence[:class:`ForumTag`] + The applicable tags of the new channel. If not provided, defaults to this channel's available tags. + default_reaction: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The default reaction of the new channel. If not provided, defaults to this channel's default reaction. + default_sort_order: Optional[:class:`ThreadSortOrder`] + The default sort order of the new channel. If not provided, defaults to this channel's default sort order. + overwrites: :class:`Mapping` + A :class:`Mapping` of target (either a role or a member) to :class:`PermissionOverwrite` + to apply to the channel. If not provided, defaults to this channel's overwrites. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have the proper permissions to create this channel. + HTTPException + Creating the channel failed. + + Returns + ------- + :class:`MediaChannel` + The channel that was created. + """ + default_reaction_emoji_payload: Optional[DefaultReactionPayload] = MISSING + if default_reaction is MISSING: + default_reaction = self.default_reaction + + if default_reaction is not None: + emoji_name, emoji_id = PartialEmoji._emoji_to_name_id(default_reaction) + default_reaction_emoji_payload = { + "emoji_name": emoji_name, + "emoji_id": emoji_id, + } + else: + default_reaction_emoji_payload = None + + if default_sort_order is MISSING: + default_sort_order = self.default_sort_order + + return await self._clone_impl( + { + "topic": topic if topic is not MISSING else self.topic, + "position": position if position is not MISSING else self.position, + "nsfw": nsfw if nsfw is not MISSING else self.nsfw, + "rate_limit_per_user": ( + slowmode_delay if slowmode_delay is not MISSING else self.slowmode_delay + ), + "default_thread_rate_limit_per_user": ( + default_thread_slowmode_delay + if default_thread_slowmode_delay is not MISSING + else self.default_thread_slowmode_delay + ), + "default_auto_archive_duration": ( + try_enum_to_int(default_auto_archive_duration) + if default_auto_archive_duration is not MISSING + else self.default_auto_archive_duration + ), + "available_tags": ( + [tag.to_dict() for tag in available_tags] + if available_tags is not MISSING + else [tag.to_dict() for tag in self.available_tags] + ), + "default_reaction_emoji": default_reaction_emoji_payload, + "default_sort_order": ( + try_enum_to_int(default_sort_order) if default_sort_order is not None else None + ), + }, + name=name, + category=category, + reason=reason, + overwrites=overwrites, + ) + + +class DMChannel(disnake.abc.Messageable, Hashable): + """Represents a Discord direct message channel. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns a string representation of the channel + + Attributes + ---------- + recipient: Optional[:class:`User`] + The user you are participating with in the direct message channel. + If this channel is received through the gateway, the recipient information + may not be always available. + me: :class:`ClientUser` + The user presenting yourself. + id: :class:`int` + The direct message channel ID. + last_pin_timestamp: Optional[:class:`datetime.datetime`] + The time the most recent message was pinned, or ``None`` if no message is currently pinned. + + .. versionadded:: 2.5 + """ + + __slots__ = ( + "id", + "recipient", + "me", + "last_pin_timestamp", + "_state", + "_flags", + ) + + def __init__(self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload) -> None: + self._state: ConnectionState = state + self.recipient: Optional[User] = None + if recipients := data.get("recipients"): + self.recipient = state.store_user(recipients[0]) # type: ignore + + self.me: ClientUser = me + self.id: int = int(data["id"]) + self.last_pin_timestamp: Optional[datetime.datetime] = utils.parse_time( + data.get("last_pin_timestamp") + ) + self._flags: int = data.get("flags", 0) + + async def _get_channel(self) -> Self: + return self + + def __str__(self) -> str: + if self.recipient: + return f"Direct Message with {self.recipient}" + return "Direct Message with Unknown User" + + def __repr__(self) -> str: + return f"" + + @classmethod + def _from_message(cls, state: ConnectionState, channel_id: int, user_id: int) -> Self: + self = cls.__new__(cls) + self._state = state + self.id = channel_id + # state.user won't be None here + self.me = state.user + self.recipient = state.get_user(user_id) if user_id != self.me.id else None + self.last_pin_timestamp = None + self._flags = 0 + return self + + @property + def type(self) -> Literal[ChannelType.private]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.private`. + """ + return ChannelType.private + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the direct message channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def jump_url(self) -> str: + """A URL that can be used to jump to this channel. + + .. versionadded:: 2.4 + """ + return f"https://discord.com/channels/@me/{self.id}" + + @property + def flags(self) -> ChannelFlags: + """:class:`.ChannelFlags`: The channel flags for this channel. + + .. versionadded:: 2.6 + """ + return ChannelFlags._from_value(self._flags) + + def permissions_for( + self, + obj: Any = None, + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + """Handles permission resolution for a :class:`User`. + + This function is there for compatibility with other channel types. + + Actual direct messages do not really have the concept of permissions. + + This returns all the :meth:`Permissions.private_channel` permissions set to ``True``. + + Parameters + ---------- + obj: :class:`User` + The user to check permissions for. This parameter is ignored + but kept for compatibility with other ``permissions_for`` methods. + + ignore_timeout: :class:`bool` + Whether to ignore the guild timeout when checking permsisions. + This parameter is ignored but kept for compatibility with other ``permissions_for`` methods. + + Returns + ------- + :class:`Permissions` + The resolved permissions. + """ + return Permissions.private_channel() + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the given message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + +class GroupChannel(disnake.abc.Messageable, Hashable): + """Represents a Discord group channel. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns a string representation of the channel + + Attributes + ---------- + recipients: List[:class:`User`] + The users you are participating with in the group channel. + If this channel is received through the gateway, the recipient information + may not be always available. + me: :class:`ClientUser` + The user representing yourself. + id: :class:`int` + The group channel ID. + owner: Optional[:class:`User`] + The user that owns the group channel. + owner_id: :class:`int` + The owner ID that owns the group channel. + + .. versionadded:: 2.0 + + name: Optional[:class:`str`] + The group channel's name if provided. + """ + + __slots__ = ("id", "recipients", "owner_id", "owner", "_icon", "name", "me", "_state") + + def __init__( + self, *, me: ClientUser, state: ConnectionState, data: GroupChannelPayload + ) -> None: + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self.me: ClientUser = me + self._update_group(data) + + def _update_group(self, data: GroupChannelPayload) -> None: + self.owner_id: Optional[int] = utils._get_as_snowflake(data, "owner_id") + self._icon: Optional[str] = data.get("icon") + self.name: Optional[str] = data.get("name") + self.recipients: List[User] = [ + self._state.store_user(u) for u in data.get("recipients", []) + ] + + self.owner: Optional[BaseUser] + if self.owner_id == self.me.id: + self.owner = self.me + else: + self.owner = utils.find(lambda u: u.id == self.owner_id, self.recipients) + + async def _get_channel(self) -> Self: + return self + + def __str__(self) -> str: + if self.name: + return self.name + + if len(self.recipients) == 0: + return "Unnamed" + + return ", ".join([x.name for x in self.recipients]) + + def __repr__(self) -> str: + return f"" + + @property + def type(self) -> Literal[ChannelType.group]: + """:class:`ChannelType`: The channel's Discord type. + + This always returns :attr:`ChannelType.group`. + """ + return ChannelType.group + + @property + def icon(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the channel's icon asset if available.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path="channel") + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + def permissions_for( + self, + obj: Snowflake, + /, + *, + ignore_timeout: bool = MISSING, + ) -> Permissions: + """Handles permission resolution for a :class:`User`. + + This function is there for compatibility with other channel types. + + Actual direct messages do not really have the concept of permissions. + + This returns all the :meth:`Permissions.private_channel` permissions set to ``True``. + + This also checks the kick_members permission if the user is the owner. + + Parameters + ---------- + obj: :class:`~disnake.abc.Snowflake` + The user to check permissions for. + + ignore_timeout: :class:`bool` + Whether to ignore the guild timeout when checking permsisions. + This parameter is ignored but kept for compatibility with other ``permissions_for`` methods. + + Returns + ------- + :class:`Permissions` + The resolved permissions for the user. + """ + base = Permissions.private_channel() + + if obj.id == self.owner_id: + base.kick_members = True + + return base + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the given message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 2.10 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + async def leave(self) -> None: + """|coro| + + Leaves the group. + + If you are the only one in the group, this deletes it as well. + + Raises + ------ + HTTPException + Leaving the group failed. + """ + await self._state.http.leave_group(self.id) + + +class PartialMessageable(disnake.abc.Messageable, Hashable): + """Represents a partial messageable to aid with working messageable channels when + only a channel ID is present. + + The only way to construct this class is through :meth:`Client.get_partial_messageable`. + + Note that this class is trimmed down and has no rich attributes. + + .. versionadded:: 2.0 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two partial messageables are equal. + + .. describe:: x != y + + Checks if two partial messageables are not equal. + + .. describe:: hash(x) + + Returns the partial messageable's hash. + + Attributes + ---------- + id: :class:`int` + The channel ID associated with this partial messageable. + type: Optional[:class:`ChannelType`] + The channel type associated with this partial messageable, if given. + """ + + def __init__(self, state: ConnectionState, id: int, type: Optional[ChannelType] = None) -> None: + self._state: ConnectionState = state + self.id: int = id + self.type: Optional[ChannelType] = type + + async def _get_channel(self) -> PartialMessageable: + return self + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the given message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message object. + """ + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + +def _guild_channel_factory( + channel_type: int, +) -> Tuple[Optional[Type[GuildChannelType]], ChannelType]: + value = try_enum(ChannelType, channel_type) + if value is ChannelType.text: + return TextChannel, value + elif value is ChannelType.voice: + return VoiceChannel, value + elif value is ChannelType.category: + return CategoryChannel, value + elif value is ChannelType.news: + return TextChannel, value + elif value is ChannelType.stage_voice: + return StageChannel, value + elif value is ChannelType.forum: + return ForumChannel, value + elif value is ChannelType.media: + return MediaChannel, value + else: + return None, value + + +def _channel_factory( + channel_type: int, +) -> Tuple[ + Optional[Union[Type[GuildChannelType], Type[DMChannel], Type[GroupChannel]]], ChannelType +]: + cls, value = _guild_channel_factory(channel_type) + if value is ChannelType.private: + return DMChannel, value + elif value is ChannelType.group: + return GroupChannel, value + else: + return cls, value + + +def _threaded_channel_factory( + channel_type: int, +) -> Tuple[ + Optional[Union[Type[GuildChannelType], Type[DMChannel], Type[GroupChannel], Type[Thread]]], + ChannelType, +]: + cls, value = _channel_factory(channel_type) + if value in (ChannelType.private_thread, ChannelType.public_thread, ChannelType.news_thread): + return Thread, value + return cls, value + + +def _threaded_guild_channel_factory( + channel_type: int, +) -> Tuple[Optional[Union[Type[GuildChannelType], Type[Thread]]], ChannelType]: + cls, value = _guild_channel_factory(channel_type) + if value in (ChannelType.private_thread, ChannelType.public_thread, ChannelType.news_thread): + return Thread, value + return cls, value + + +def _channel_type_factory( + cls: Union[Type[disnake.abc.GuildChannel], Type[Thread]], +) -> List[ChannelType]: + return { + # FIXME: this includes private channels; improve this once there's a common base type for all channels + disnake.abc.GuildChannel: list(ChannelType.__members__.values()), + VocalGuildChannel: [ChannelType.voice, ChannelType.stage_voice], + disnake.abc.PrivateChannel: [ChannelType.private, ChannelType.group], + TextChannel: [ChannelType.text, ChannelType.news], + DMChannel: [ChannelType.private], + VoiceChannel: [ChannelType.voice], + GroupChannel: [ChannelType.group], + CategoryChannel: [ChannelType.category], + NewsChannel: [ChannelType.news], + Thread: [ChannelType.news_thread, ChannelType.public_thread, ChannelType.private_thread], + StageChannel: [ChannelType.stage_voice], + ForumChannel: [ChannelType.forum], + MediaChannel: [ChannelType.media], + }.get(cls, []) diff --git a/disnake/disnake/client.py b/disnake/disnake/client.py new file mode 100644 index 0000000000..baa0d2f751 --- /dev/null +++ b/disnake/disnake/client.py @@ -0,0 +1,3308 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +import logging +import signal +import sys +import traceback +import types +from datetime import datetime, timedelta +from errno import ECONNRESET +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Coroutine, + Dict, + Generator, + List, + Literal, + Mapping, + NamedTuple, + Optional, + Sequence, + Tuple, + TypedDict, + TypeVar, + Union, + overload, +) + +import aiohttp + +from . import abc, utils +from .activity import ActivityTypes, BaseActivity, create_activity +from .app_commands import ( + APIMessageCommand, + APISlashCommand, + APIUserCommand, + ApplicationCommand, + GuildApplicationCommandPermissions, +) +from .appinfo import AppInfo +from .application_role_connection import ApplicationRoleConnectionMetadata +from .backoff import ExponentialBackoff +from .channel import PartialMessageable, _threaded_channel_factory +from .emoji import Emoji +from .entitlement import Entitlement +from .enums import ApplicationCommandType, ChannelType, Event, Status +from .errors import ( + ConnectionClosed, + GatewayNotFound, + HTTPException, + InvalidData, + PrivilegedIntentsRequired, + SessionStartLimitReached, +) +from .flags import ApplicationFlags, Intents, MemberCacheFlags +from .gateway import DiscordWebSocket, ReconnectWebSocket +from .guild import Guild, GuildBuilder +from .guild_preview import GuildPreview +from .http import HTTPClient +from .i18n import LocalizationProtocol, LocalizationStore +from .invite import Invite +from .iterators import EntitlementIterator, GuildIterator +from .mentions import AllowedMentions +from .object import Object +from .sku import SKU +from .soundboard import GuildSoundboardSound, SoundboardSound +from .stage_instance import StageInstance +from .state import ConnectionState +from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory +from .template import Template +from .threads import Thread +from .ui.view import View +from .user import ClientUser, User +from .utils import MISSING, deprecated +from .voice_client import VoiceClient +from .voice_region import VoiceRegion +from .webhook import Webhook +from .widget import Widget + +if TYPE_CHECKING: + from typing_extensions import NotRequired + + from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime + from .app_commands import APIApplicationCommand, MessageCommand, SlashCommand, UserCommand + from .asset import AssetBytes + from .channel import DMChannel + from .member import Member + from .message import Message + from .types.application_role_connection import ( + ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload, + ) + from .types.gateway import SessionStartLimit as SessionStartLimitPayload + from .voice_client import VoiceProtocol + + +__all__ = ( + "Client", + "SessionStartLimit", + "GatewayParams", +) + +T = TypeVar("T") + +Coro = Coroutine[Any, Any, T] +CoroFunc = Callable[..., Coro[Any]] + +CoroT = TypeVar("CoroT", bound=Callable[..., Coroutine[Any, Any, Any]]) + +_log = logging.getLogger(__name__) + + +def _cancel_tasks(loop: asyncio.AbstractEventLoop) -> None: + tasks = {t for t in asyncio.all_tasks(loop=loop) if not t.done()} + + if not tasks: + return + + _log.info("Cleaning up after %d tasks.", len(tasks)) + for task in tasks: + task.cancel() + + loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) + _log.info("All tasks finished cancelling.") + + for task in tasks: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "Unhandled exception during Client.run shutdown.", + "exception": task.exception(), + "task": task, + } + ) + + +def _cleanup_loop(loop: asyncio.AbstractEventLoop) -> None: + try: + _cancel_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + finally: + _log.info("Closing the event loop.") + loop.close() + + +class SessionStartLimit: + """A class that contains information about the current session start limit, + at the time when the client connected for the first time. + + .. versionadded:: 2.5 + + Attributes + ---------- + total: :class:`int` + The total number of allowed session starts. + remaining: :class:`int` + The remaining number of allowed session starts. + reset_after: :class:`int` + The number of milliseconds after which the :attr:`.remaining` limit resets, + relative to when the client connected. + See also :attr:`reset_time`. + max_concurrency: :class:`int` + The number of allowed ``IDENTIFY`` requests per 5 seconds. + reset_time: :class:`datetime.datetime` + The approximate time at which which the :attr:`.remaining` limit resets. + """ + + __slots__: Tuple[str, ...] = ( + "total", + "remaining", + "reset_after", + "max_concurrency", + "reset_time", + ) + + def __init__(self, data: SessionStartLimitPayload) -> None: + self.total: int = data["total"] + self.remaining: int = data["remaining"] + self.reset_after: int = data["reset_after"] + self.max_concurrency: int = data["max_concurrency"] + + self.reset_time: datetime = utils.utcnow() + timedelta(milliseconds=self.reset_after) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class GatewayParams(NamedTuple): + """Container type for configuring gateway connections. + + .. versionadded:: 2.6 + + Parameters + ---------- + encoding: :class:`str` + The payload encoding (``json`` is currently the only supported encoding). + Defaults to ``"json"``. + zlib: :class:`bool` + Whether to enable transport compression. + Defaults to ``True``. + """ + + encoding: Literal["json"] = "json" + zlib: bool = True + + +# used for typing the ws parameter dict in the connect() loop +class _WebSocketParams(TypedDict): + initial: bool + shard_id: Optional[int] + gateway: Optional[str] + + sequence: NotRequired[Optional[int]] + resume: NotRequired[bool] + session: NotRequired[Optional[str]] + + +class Client: + """Represents a client connection that connects to Discord. + This class is used to interact with the Discord WebSocket and API. + + A number of options can be passed to the :class:`Client`. + + Parameters + ---------- + max_messages: Optional[:class:`int`] + The maximum number of messages to store in the internal message cache. + This defaults to ``1000``. Passing in ``None`` disables the message cache. + + .. versionchanged:: 1.3 + Allow disabling the message cache and change the default size to ``1000``. + loop: Optional[:class:`asyncio.AbstractEventLoop`] + The :class:`asyncio.AbstractEventLoop` to use for asynchronous operations. + Defaults to ``None``, in which case the current event loop is + used, or a new loop is created if there is none. + asyncio_debug: :class:`bool` + Whether to enable asyncio debugging when the client starts. + Defaults to False. + connector: Optional[:class:`aiohttp.BaseConnector`] + The connector to use for connection pooling. + proxy: Optional[:class:`str`] + Proxy URL. + proxy_auth: Optional[:class:`aiohttp.BasicAuth`] + An object that represents proxy HTTP Basic Authorization. + shard_id: Optional[:class:`int`] + Integer starting at ``0`` and less than :attr:`.shard_count`. + shard_count: Optional[:class:`int`] + The total number of shards. + application_id: :class:`int` + The client's application ID. + intents: Optional[:class:`Intents`] + The intents that you want to enable for the session. This is a way of + disabling and enabling certain gateway events from triggering and being sent. + If not given, defaults to a regularly constructed :class:`Intents` class. + + .. versionadded:: 1.5 + + member_cache_flags: :class:`MemberCacheFlags` + Allows for finer control over how the library caches members. + If not given, defaults to cache as much as possible with the + currently selected intents. + + .. versionadded:: 1.5 + + chunk_guilds_at_startup: :class:`bool` + Indicates if :func:`.on_ready` should be delayed to chunk all guilds + at start-up if necessary. This operation is incredibly slow for large + amounts of guilds. The default is ``True`` if :attr:`Intents.members` + is ``True``. + + .. versionadded:: 1.5 + + status: Optional[Union[class:`str`, :class:`.Status`]] + A status to start your presence with upon logging on to Discord. + activity: Optional[:class:`.BaseActivity`] + An activity to start your presence with upon logging on to Discord. + allowed_mentions: Optional[:class:`AllowedMentions`] + Control how the client handles mentions by default on every message sent. + + .. versionadded:: 1.4 + + heartbeat_timeout: :class:`float` + The maximum numbers of seconds before timing out and restarting the + WebSocket in the case of not receiving a HEARTBEAT_ACK. Useful if + processing the initial packets take too long to the point of disconnecting + you. The default timeout is 60 seconds. + guild_ready_timeout: :class:`float` + The maximum number of seconds to wait for the GUILD_CREATE stream to end before + preparing the member cache and firing READY. The default timeout is 2 seconds. + + .. versionadded:: 1.4 + + assume_unsync_clock: :class:`bool` + Whether to assume the system clock is unsynced. This applies to the ratelimit handling + code. If this is set to ``True``, the default, then the library uses the time to reset + a rate limit bucket given by Discord. If this is ``False`` then your system clock is + used to calculate how long to sleep for. If this is set to ``False`` it is recommended to + sync your system clock to Google's NTP server. + + .. versionadded:: 1.3 + + enable_debug_events: :class:`bool` + Whether to enable events that are useful only for debugging gateway related information. + + Right now this involves :func:`on_socket_raw_receive` and :func:`on_socket_raw_send`. If + this is ``False`` then those events will not be dispatched (due to performance considerations). + To enable these events, this must be set to ``True``. Defaults to ``False``. + + .. versionadded:: 2.0 + + enable_gateway_error_handler: :class:`bool` + Whether to enable the :func:`disnake.on_gateway_error` event. + Defaults to ``True``. + + If this is disabled, exceptions that occur while parsing (known) gateway events + won't be handled and the pre-v2.6 behavior of letting the exception propagate up to + the :func:`connect`/:func:`start`/:func:`run` call is used instead. + + .. versionadded:: 2.6 + + localization_provider: :class:`.LocalizationProtocol` + An implementation of :class:`.LocalizationProtocol` to use for localization of + application commands. + If not provided, the default :class:`.LocalizationStore` implementation is used. + + .. versionadded:: 2.5 + + .. versionchanged:: 2.6 + Can no longer be provided together with ``strict_localization``, as it does + not apply to the custom localization provider entered in this parameter. + + strict_localization: :class:`bool` + Whether to raise an exception when localizations for a specific key couldn't be found. + This is mainly useful for testing/debugging, consider disabling this eventually + as missing localized names will automatically fall back to the default/base name without it. + Only applicable if the ``localization_provider`` parameter is not provided. + Defaults to ``False``. + + .. versionadded:: 2.5 + + .. versionchanged:: 2.6 + Can no longer be provided together with ``localization_provider``, as this parameter is + ignored for custom localization providers. + + gateway_params: :class:`.GatewayParams` + Allows configuring parameters used for establishing gateway connections, + notably enabling/disabling compression (enabled by default). + Encodings other than JSON are not supported. + + .. versionadded:: 2.6 + + Attributes + ---------- + ws + The websocket gateway the client is currently connected to. Could be ``None``. + loop: :class:`asyncio.AbstractEventLoop` + The event loop that the client uses for asynchronous operations. + session_start_limit: Optional[:class:`SessionStartLimit`] + Information about the current session start limit. + Only available after initiating the connection. + + .. versionadded:: 2.5 + i18n: :class:`.LocalizationProtocol` + An implementation of :class:`.LocalizationProtocol` used for localization of + application commands. + + .. versionadded:: 2.5 + """ + + def __init__( + self, + *, + asyncio_debug: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, + shard_id: Optional[int] = None, + shard_count: Optional[int] = None, + enable_debug_events: bool = False, + enable_gateway_error_handler: bool = True, + localization_provider: Optional[LocalizationProtocol] = None, + strict_localization: bool = False, + gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, + proxy: Optional[str] = None, + proxy_auth: Optional[aiohttp.BasicAuth] = None, + assume_unsync_clock: bool = True, + max_messages: Optional[int] = 1000, + application_id: Optional[int] = None, + heartbeat_timeout: float = 60.0, + guild_ready_timeout: float = 2.0, + allowed_mentions: Optional[AllowedMentions] = None, + activity: Optional[BaseActivity] = None, + status: Optional[Union[Status, str]] = None, + intents: Optional[Intents] = None, + chunk_guilds_at_startup: Optional[bool] = None, + member_cache_flags: Optional[MemberCacheFlags] = None, + ) -> None: + # self.ws is set in the connect method + self.ws: DiscordWebSocket = None # type: ignore + + if loop is None: + self.loop: asyncio.AbstractEventLoop = utils.get_event_loop() + else: + self.loop: asyncio.AbstractEventLoop = loop + + self.loop.set_debug(asyncio_debug) + self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {} + self.session_start_limit: Optional[SessionStartLimit] = None + + self.http: HTTPClient = HTTPClient( + connector, + proxy=proxy, + proxy_auth=proxy_auth, + unsync_clock=assume_unsync_clock, + loop=self.loop, + ) + + self._handlers: Dict[str, Callable] = { + "ready": self._handle_ready, + "connect_internal": self._handle_first_connect, + } + + self._hooks: Dict[str, Callable] = {"before_identify": self._call_before_identify_hook} + + self._enable_debug_events: bool = enable_debug_events + self._enable_gateway_error_handler: bool = enable_gateway_error_handler + self._connection: ConnectionState = self._get_state( + max_messages=max_messages, + application_id=application_id, + heartbeat_timeout=heartbeat_timeout, + guild_ready_timeout=guild_ready_timeout, + allowed_mentions=allowed_mentions, + activity=activity, + status=status, + intents=intents, + chunk_guilds_at_startup=chunk_guilds_at_startup, + member_cache_flags=member_cache_flags, + ) + self.shard_id: Optional[int] = shard_id + self.shard_count: Optional[int] = shard_count + self._connection.shard_count = shard_count + + self._closed: bool = False + self._ready: asyncio.Event = asyncio.Event() + self._first_connect: asyncio.Event = asyncio.Event() + self._connection._get_websocket = self._get_websocket + self._connection._get_client = lambda: self + + if VoiceClient.warn_nacl: + VoiceClient.warn_nacl = False + _log.warning("PyNaCl is not installed, voice will NOT be supported") + + if strict_localization and localization_provider is not None: + raise ValueError( + "Providing both `localization_provider` and `strict_localization` is not supported." + " If strict localization is desired for a customized localization provider, this" + " should be implemented by that custom provider." + ) + + self.i18n: LocalizationProtocol = ( + LocalizationStore(strict=strict_localization) + if localization_provider is None + else localization_provider + ) + + self.gateway_params: GatewayParams = gateway_params or GatewayParams() + if self.gateway_params.encoding != "json": + raise ValueError("Gateway encodings other than `json` are currently not supported.") + + self.extra_events: Dict[str, List[CoroFunc]] = {} + + # internals + + def _get_websocket( + self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None + ) -> DiscordWebSocket: + return self.ws + + def _get_state( + self, + *, + max_messages: Optional[int], + application_id: Optional[int], + heartbeat_timeout: float, + guild_ready_timeout: float, + allowed_mentions: Optional[AllowedMentions], + activity: Optional[BaseActivity], + status: Optional[Union[str, Status]], + intents: Optional[Intents], + chunk_guilds_at_startup: Optional[bool], + member_cache_flags: Optional[MemberCacheFlags], + ) -> ConnectionState: + return ConnectionState( + dispatch=self.dispatch, + handlers=self._handlers, + hooks=self._hooks, + http=self.http, + loop=self.loop, + max_messages=max_messages, + application_id=application_id, + heartbeat_timeout=heartbeat_timeout, + guild_ready_timeout=guild_ready_timeout, + allowed_mentions=allowed_mentions, + activity=activity, + status=status, + intents=intents, + chunk_guilds_at_startup=chunk_guilds_at_startup, + member_cache_flags=member_cache_flags, + ) + + def _handle_ready(self) -> None: + self._ready.set() + + def _handle_first_connect(self) -> None: + if self._first_connect.is_set(): + return + self._first_connect.set() + + @property + def latency(self) -> float: + """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. + + This could be referred to as the Discord WebSocket protocol latency. + """ + ws = self.ws + return float("nan") if not ws else ws.latency + + def is_ws_ratelimited(self) -> bool: + """Whether the websocket is currently rate limited. + + This can be useful to know when deciding whether you should query members + using HTTP or via the gateway. + + .. versionadded:: 1.6 + + :return type: :class:`bool` + """ + if self.ws: + return self.ws.is_ratelimited() + return False + + @property + def user(self) -> ClientUser: + """Optional[:class:`.ClientUser`]: Represents the connected client. ``None`` if not logged in.""" + return self._connection.user + + @property + def guilds(self) -> List[Guild]: + """List[:class:`.Guild`]: The guilds that the connected client is a member of.""" + return self._connection.guilds + + @property + def emojis(self) -> List[Emoji]: + """List[:class:`.Emoji`]: The emojis that the connected client has.""" + return self._connection.emojis + + @property + def stickers(self) -> List[GuildSticker]: + """List[:class:`.GuildSticker`]: The stickers that the connected client has. + + .. versionadded:: 2.0 + """ + return self._connection.stickers + + @property + def soundboard_sounds(self) -> List[GuildSoundboardSound]: + """List[:class:`.GuildSoundboardSound`]: The soundboard sounds that the connected client has. + + .. versionadded:: 2.10 + """ + return self._connection.soundboard_sounds + + @property + def cached_messages(self) -> Sequence[Message]: + """Sequence[:class:`.Message`]: Read-only list of messages the connected client has cached. + + .. versionadded:: 1.1 + """ + return utils.SequenceProxy(self._connection._messages or []) + + @property + def private_channels(self) -> List[PrivateChannel]: + """List[:class:`.abc.PrivateChannel`]: The private channels that the connected client is participating on. + + .. note:: + + This returns only up to 128 most recent private channels due to an internal working + on how Discord deals with private channels. + """ + return self._connection.private_channels + + @property + def voice_clients(self) -> List[VoiceProtocol]: + """List[:class:`.VoiceProtocol`]: Represents a list of voice connections. + + These are usually :class:`.VoiceClient` instances. + """ + return self._connection.voice_clients + + @property + def application_id(self) -> int: + """Optional[:class:`int`]: The client's application ID. + + If this is not passed via ``__init__`` then this is retrieved + through the gateway when an event contains the data. Usually + after :func:`~disnake.on_connect` is called. + + .. versionadded:: 2.0 + """ + return self._connection.application_id # type: ignore + + @property + def application_flags(self) -> ApplicationFlags: + """:class:`~disnake.ApplicationFlags`: The client's application flags. + + .. versionadded:: 2.0 + """ + return self._connection.application_flags + + @property + def global_application_commands(self) -> List[APIApplicationCommand]: + """List[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]: The client's global application commands.""" + return list(self._connection._global_application_commands.values()) + + @property + def global_slash_commands(self) -> List[APISlashCommand]: + """List[:class:`.APISlashCommand`]: The client's global slash commands.""" + return [ + cmd + for cmd in self._connection._global_application_commands.values() + if isinstance(cmd, APISlashCommand) + ] + + @property + def global_user_commands(self) -> List[APIUserCommand]: + """List[:class:`.APIUserCommand`]: The client's global user commands.""" + return [ + cmd + for cmd in self._connection._global_application_commands.values() + if isinstance(cmd, APIUserCommand) + ] + + @property + def global_message_commands(self) -> List[APIMessageCommand]: + """List[:class:`.APIMessageCommand`]: The client's global message commands.""" + return [ + cmd + for cmd in self._connection._global_application_commands.values() + if isinstance(cmd, APIMessageCommand) + ] + + def get_message(self, id: int) -> Optional[Message]: + """Gets the message with the given ID from the bot's message cache. + + Parameters + ---------- + id: :class:`int` + The ID of the message to look for. + + Returns + ------- + Optional[:class:`.Message`] + The corresponding message. + """ + return utils.get(self.cached_messages, id=id) + + @overload + async def get_or_fetch_user( + self, user_id: int, *, strict: Literal[False] = ... + ) -> Optional[User]: ... + + @overload + async def get_or_fetch_user(self, user_id: int, *, strict: Literal[True]) -> User: ... + + async def get_or_fetch_user(self, user_id: int, *, strict: bool = False) -> Optional[User]: + """|coro| + + Tries to get the user from the cache. If it fails, + fetches the user from the API. + + This only propagates exceptions when the ``strict`` parameter is enabled. + + Parameters + ---------- + user_id: :class:`int` + The ID to search for. + strict: :class:`bool` + Whether to propagate exceptions from :func:`fetch_user` + instead of returning ``None`` in case of failure + (e.g. if the user wasn't found). + Defaults to ``False``. + + Returns + ------- + Optional[:class:`~disnake.User`] + The user with the given ID, or ``None`` if not found and ``strict`` is set to ``False``. + """ + user = self.get_user(user_id) + if user is not None: + return user + try: + user = await self.fetch_user(user_id) + except Exception: + if strict: + raise + return None + return user + + getch_user = get_or_fetch_user + + def is_ready(self) -> bool: + """Whether the client's internal cache is ready for use. + + :return type: :class:`bool` + """ + return self._ready.is_set() + + async def _run_event( + self, + coro: Callable[..., Coroutine[Any, Any, Any]], + event_name: str, + *args: Any, + **kwargs: Any, + ) -> None: + try: + await coro(*args, **kwargs) + except asyncio.CancelledError: + pass + except Exception: + try: + await self.on_error(event_name, *args, **kwargs) + except asyncio.CancelledError: + pass + + def _schedule_event( + self, + coro: Callable[..., Coroutine[Any, Any, Any]], + event_name: str, + *args: Any, + **kwargs: Any, + ) -> asyncio.Task: + wrapped = self._run_event(coro, event_name, *args, **kwargs) + # Schedules the task + return asyncio.create_task(wrapped, name=f"disnake: {event_name}") + + def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None: + _log.debug("Dispatching event %s", event) + method = "on_" + event + + listeners = self._listeners.get(event) + if listeners: + removed = [] + for i, (future, condition) in enumerate(listeners): + if future.cancelled(): + removed.append(i) + continue + + try: + result = condition(*args) + except Exception as exc: + future.set_exception(exc) + removed.append(i) + else: + if result: + if len(args) == 0: + future.set_result(None) + elif len(args) == 1: + future.set_result(args[0]) + else: + future.set_result(args) + removed.append(i) + + if len(removed) == len(listeners): + self._listeners.pop(event) + else: + for idx in reversed(removed): + del listeners[idx] + + try: + coro = getattr(self, method) + except AttributeError: + pass + else: + self._schedule_event(coro, method, *args, **kwargs) + + for event_ in self.extra_events.get(method, []): + self._schedule_event(event_, method, *args, **kwargs) + + def add_listener(self, func: CoroFunc, name: Union[str, Event] = MISSING) -> None: + """The non decorator alternative to :meth:`.listen`. + + .. versionchanged:: 2.10 + The definition of this method was moved from :class:`.ext.commands.Bot` + to the :class:`.Client` class. + + Parameters + ---------- + func: :ref:`coroutine ` + The function to call. + name: Union[:class:`str`, :class:`.Event`] + The name of the event to listen for. Defaults to ``func.__name__``. + + Example + -------- + + .. code-block:: python + + async def on_ready(): pass + async def my_message(message): pass + async def another_message(message): pass + + client.add_listener(on_ready) + client.add_listener(my_message, 'on_message') + client.add_listener(another_message, Event.message) + + Raises + ------ + TypeError + The function is not a coroutine or a string or an :class:`.Event` was not passed + as the name. + """ + if name is not MISSING and not isinstance(name, (str, Event)): + raise TypeError( + f"add_listener expected str or Enum but received {name.__class__.__name__!r} instead." + ) + + name_ = ( + func.__name__ + if name is MISSING + else (name if isinstance(name, str) else f"on_{name.value}") + ) + + if not utils.iscoroutinefunction(func): + raise TypeError("Listeners must be coroutines") + + if name_ in self.extra_events: + self.extra_events[name_].append(func) + else: + self.extra_events[name_] = [func] + + def remove_listener(self, func: CoroFunc, name: Union[str, Event] = MISSING) -> None: + """Removes a listener from the pool of listeners. + + .. versionchanged:: 2.10 + The definition of this method was moved from :class:`.ext.commands.Bot` + to the :class:`.Client` class. + + Parameters + ---------- + func + The function that was used as a listener to remove. + name: Union[:class:`str`, :class:`.Event`] + The name of the event we want to remove. Defaults to + ``func.__name__``. + + Raises + ------ + TypeError + The name passed was not a string or an :class:`.Event`. + """ + if name is not MISSING and not isinstance(name, (str, Event)): + raise TypeError( + f"remove_listener expected str or Enum but received {name.__class__.__name__!r} instead." + ) + name = ( + func.__name__ + if name is MISSING + else (name if isinstance(name, str) else f"on_{name.value}") + ) + + if name in self.extra_events: + try: + self.extra_events[name].remove(func) + except ValueError: + pass + + def listen(self, name: Union[str, Event] = MISSING) -> Callable[[CoroT], CoroT]: + """A decorator that registers another function as an external + event listener. Basically this allows you to listen to multiple + events from different places e.g. such as :func:`.on_ready` + + The functions being listened to must be a :ref:`coroutine `. + + .. versionchanged:: 2.10 + The definition of this method was moved from :class:`.ext.commands.Bot` + to the :class:`.Client` class. + + Example + ------- + .. code-block:: python3 + + @client.listen() + async def on_message(message): + print('one') + + # in some other file... + + @client.listen('on_message') + async def my_message(message): + print('two') + + # in yet another file + @client.listen(Event.message) + async def another_message(message): + print('three') + + Would print one, two and three in an unspecified order. + + Raises + ------ + TypeError + The function being listened to is not a coroutine or a string or an :class:`.Event` was not passed + as the name. + """ + if name is not MISSING and not isinstance(name, (str, Event)): + raise TypeError( + f"listen expected str or Enum but received {name.__class__.__name__!r} instead." + ) + + def decorator(func: CoroT) -> CoroT: + self.add_listener(func, name) + return func + + return decorator + + def get_listeners(self) -> Mapping[str, List[CoroFunc]]: + """Mapping[:class:`str`, List[Callable]]: A read-only mapping of event names to listeners. + + .. note:: + To add or remove a listener you should use :meth:`.add_listener` and + :meth:`.remove_listener`. + + .. versionchanged:: 2.10 + The definition of this method was moved from :class:`.ext.commands.Bot` + to the :class:`.Client` class. + """ + return types.MappingProxyType(self.extra_events) + + async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None: + """|coro| + + The default error handler provided by the client. + + By default this prints to :data:`sys.stderr` however it could be + overridden to have a different implementation. + Check :func:`~disnake.on_error` for more details. + """ + print(f"Ignoring exception in {event_method}", file=sys.stderr) + traceback.print_exc() + + async def _dispatch_gateway_error( + self, event: str, data: Any, shard_id: Optional[int], exc: Exception, / + ) -> None: + # This is an internal hook that calls the public one, + # enabling additional handling while still allowing users to + # overwrite `on_gateway_error`. + # Even though this is always meant to be an async func, we use `maybe_coroutine` + # just in case the client gets subclassed and the method is overwritten with a sync one. + await utils.maybe_coroutine(self.on_gateway_error, event, data, shard_id, exc) + + async def on_gateway_error( + self, event: str, data: Any, shard_id: Optional[int], exc: Exception, / + ) -> None: + """|coro| + + The default gateway error handler provided by the client. + + By default this prints to :data:`sys.stderr` however it could be + overridden to have a different implementation. + Check :func:`~disnake.on_gateway_error` for more details. + + .. versionadded:: 2.6 + + .. note:: + Unlike :func:`on_error`, the exception is available as the ``exc`` + parameter and cannot be obtained through :func:`sys.exc_info`. + """ + print( + f"Ignoring exception in {event} gateway event handler (shard ID {shard_id})", + file=sys.stderr, + ) + traceback.print_exception(type(exc), exc, exc.__traceback__) + + # hooks + + async def _call_before_identify_hook( + self, shard_id: Optional[int], *, initial: bool = False + ) -> None: + # This hook is an internal hook that actually calls the public one. + # It allows the library to have its own hook without stepping on the + # toes of those who need to override their own hook. + await self.before_identify_hook(shard_id, initial=initial) + + async def before_identify_hook(self, shard_id: Optional[int], *, initial: bool = False) -> None: + """|coro| + + A hook that is called before IDENTIFYing a session. This is useful + if you wish to have more control over the synchronization of multiple + IDENTIFYing clients. + + The default implementation sleeps for 5 seconds. + + .. versionadded:: 1.4 + + Parameters + ---------- + shard_id: :class:`int` + The shard ID that requested being IDENTIFY'd + initial: :class:`bool` + Whether this IDENTIFY is the first initial IDENTIFY. + """ + if not initial: + await asyncio.sleep(5.0) + + # login state management + + async def login(self, token: str) -> None: + """|coro| + + Logs in the client with the specified credentials. + + Parameters + ---------- + token: :class:`str` + The authentication token. Do not prefix this token with + anything as the library will do it for you. + + Raises + ------ + LoginFailure + The wrong credentials are passed. + HTTPException + An unknown HTTP related error occurred, + usually when it isn't 200 or the known incorrect credentials + passing status code. + """ + _log.info("logging in using static token") + if not isinstance(token, str): + raise TypeError(f"token must be of type str, got {type(token).__name__} instead") + + data = await self.http.static_login(token.strip()) + self._connection.user = ClientUser(state=self._connection, data=data) + + async def connect( + self, *, reconnect: bool = True, ignore_session_start_limit: bool = False + ) -> None: + """|coro| + + Creates a websocket connection and lets the websocket listen + to messages from Discord. This is a loop that runs the entire + event system and miscellaneous aspects of the library. Control + is not resumed until the WebSocket connection is terminated. + + .. versionchanged:: 2.6 + Added usage of :class:`.SessionStartLimit` when connecting to the API. + Added the ``ignore_session_start_limit`` parameter. + + + Parameters + ---------- + reconnect: :class:`bool` + Whether reconnecting should be attempted, either due to internet + failure or a specific failure on Discord's part. Certain + disconnects that lead to bad state will not be handled (such as + invalid sharding payloads or bad tokens). + + ignore_session_start_limit: :class:`bool` + Whether the API provided session start limit should be ignored when + connecting to the API. + + .. versionadded:: 2.6 + + Raises + ------ + GatewayNotFound + If the gateway to connect to Discord is not found. Usually if this + is thrown then there is a Discord API outage. + ConnectionClosed + The websocket connection has been terminated. + SessionStartLimitReached + If the client doesn't have enough connects remaining in the current 24-hour window + and ``ignore_session_start_limit`` is ``False`` this will be raised rather than + connecting to the gateawy and Discord resetting the token. + However, if ``ignore_session_start_limit`` is ``True``, the client will connect regardless + and this exception will not be raised. + """ + _, initial_gateway, session_start_limit = await self.http.get_bot_gateway( + encoding=self.gateway_params.encoding, + zlib=self.gateway_params.zlib, + ) + self.session_start_limit = SessionStartLimit(session_start_limit) + + if not ignore_session_start_limit and self.session_start_limit.remaining == 0: + raise SessionStartLimitReached(self.session_start_limit) + + ws_params: _WebSocketParams = { + "initial": True, + "shard_id": self.shard_id, + "gateway": initial_gateway, + } + + backoff = ExponentialBackoff() + while not self.is_closed(): + # "connecting" in this case means "waiting for HELLO" + connecting = True + + try: + coro = DiscordWebSocket.from_client(self, **ws_params) + self.ws = await asyncio.wait_for(coro, timeout=60.0) + + # If we got to this point: + # - connection was established + # - received a HELLO + # - and sent an IDENTIFY or RESUME + connecting = False + ws_params["initial"] = False + + while True: + await self.ws.poll_event() + + except ReconnectWebSocket as e: + _log.info("Got a request to %s the websocket.", e.op) + self.dispatch("disconnect") + ws_params.update( + sequence=self.ws.sequence, + resume=e.resume, + session=self.ws.session_id, + # use current (possibly new) gateway if resuming, + # reset to default if not + gateway=self.ws.resume_gateway if e.resume else initial_gateway, + ) + continue + + except ( + OSError, + HTTPException, + GatewayNotFound, + ConnectionClosed, + aiohttp.ClientError, + asyncio.TimeoutError, + ) as exc: + self.dispatch("disconnect") + if not reconnect: + await self.close() + if isinstance(exc, ConnectionClosed) and exc.code == 1000: + # clean close, don't re-raise this + return + raise + + if self.is_closed(): + return + + # If we get connection reset by peer then try to RESUME + if isinstance(exc, OSError) and exc.errno == ECONNRESET: + ws_params.update( + sequence=self.ws.sequence, + initial=False, + resume=True, + session=self.ws.session_id, + gateway=self.ws.resume_gateway, + ) + continue + + # We should only get this when an unhandled close code happens, + # such as a clean disconnect (1000) or a bad state (bad token, no sharding, etc) + # sometimes, Discord sends us 1000 for unknown reasons so we should reconnect + # regardless and rely on is_closed instead + if isinstance(exc, ConnectionClosed): + if exc.code == 4014: + raise PrivilegedIntentsRequired(exc.shard_id) from None + if exc.code != 1000: + await self.close() + raise + + retry = backoff.delay() + _log.exception("Attempting a reconnect in %.2fs", retry) + await asyncio.sleep(retry) + + if connecting: + # Always identify back to the initial gateway if we failed while connecting. + # This is in case we fail to connect to the resume_gateway instance. + ws_params.update( + resume=False, + gateway=initial_gateway, + ) + else: + # Just try to resume the session. + # If it's not RESUME-able then the gateway will invalidate the session. + # This is apparently what the official Discord client does. + ws_params.update( + sequence=self.ws.sequence, + resume=True, + session=self.ws.session_id, + gateway=self.ws.resume_gateway, + ) + + async def close(self) -> None: + """|coro| + + Closes the connection to Discord. + """ + if self._closed: + return + + self._closed = True + + for voice in self.voice_clients: + try: + await voice.disconnect(force=True) + except Exception: + # if an error happens during disconnects, disregard it. + pass + + # can be None if not connected + if self.ws is not None and self.ws.open: # pyright: ignore[reportUnnecessaryComparison] + await self.ws.close(code=1000) + + await self.http.close() + self._ready.clear() + + def clear(self) -> None: + """Clears the internal state of the bot. + + After this, the bot can be considered "re-opened", i.e. :meth:`is_closed` + and :meth:`is_ready` both return ``False`` along with the bot's internal + cache cleared. + """ + self._closed = False + self._ready.clear() + self._connection.clear() + self.http.recreate() + + async def start( + self, token: str, *, reconnect: bool = True, ignore_session_start_limit: bool = False + ) -> None: + """|coro| + + A shorthand coroutine for :meth:`login` + :meth:`connect`. + + Raises + ------ + TypeError + An unexpected keyword argument was received. + """ + await self.login(token) + await self.connect( + reconnect=reconnect, ignore_session_start_limit=ignore_session_start_limit + ) + + def run(self, *args: Any, **kwargs: Any) -> None: + """A blocking call that abstracts away the event loop + initialisation from you. + + If you want more control over the event loop then this + function should not be used. Use :meth:`start` coroutine + or :meth:`connect` + :meth:`login`. + + Roughly Equivalent to: :: + + try: + loop.run_until_complete(start(*args, **kwargs)) + except KeyboardInterrupt: + loop.run_until_complete(close()) + # cancel all tasks lingering + finally: + loop.close() + + .. warning:: + + This function must be the last function to call due to the fact that it + is blocking. That means that registration of events or anything being + called after this function call will not execute until it returns + + Parameters + ---------- + token: :class:`str` + The discord token of the bot that is being ran. + """ + loop = self.loop + + try: + loop.add_signal_handler(signal.SIGINT, lambda: loop.stop()) + loop.add_signal_handler(signal.SIGTERM, lambda: loop.stop()) + except NotImplementedError: + pass + + async def runner() -> None: + try: + await self.start(*args, **kwargs) + finally: + if not self.is_closed(): + await self.close() + + def stop_loop_on_completion(f) -> None: + loop.stop() + + future = asyncio.ensure_future(runner(), loop=loop) + future.add_done_callback(stop_loop_on_completion) + try: + loop.run_forever() + except KeyboardInterrupt: + _log.info("Received signal to terminate bot and event loop.") + finally: + future.remove_done_callback(stop_loop_on_completion) + _log.info("Cleaning up tasks.") + _cleanup_loop(loop) + + if not future.cancelled(): + try: + return future.result() + except KeyboardInterrupt: + # I am unsure why this gets raised here but suppress it anyway + return None + + # properties + + def is_closed(self) -> bool: + """Whether the websocket connection is closed. + + :return type: :class:`bool` + """ + return self._closed + + @property + def activity(self) -> Optional[ActivityTypes]: + """Optional[:class:`.BaseActivity`]: The activity being used upon logging in.""" + return create_activity(self._connection._activity, state=self._connection) + + @activity.setter + def activity(self, value: Optional[ActivityTypes]) -> None: + if value is None: + self._connection._activity = None + elif isinstance(value, BaseActivity): + # ConnectionState._activity is typehinted as ActivityPayload, we're passing Dict[str, Any] + self._connection._activity = value.to_dict() # type: ignore + else: + raise TypeError("activity must derive from BaseActivity.") + + @property + def status(self) -> Status: + """:class:`.Status`: The status being used upon logging on to Discord. + + .. versionadded:: 2.0 + """ + if self._connection._status in {state.value for state in Status}: + return Status(self._connection._status) + return Status.online + + @status.setter + def status(self, value) -> None: + if value is Status.offline: + self._connection._status = "invisible" + elif isinstance(value, Status): + self._connection._status = str(value) + else: + raise TypeError("status must derive from Status.") + + @property + def allowed_mentions(self) -> Optional[AllowedMentions]: + """Optional[:class:`~disnake.AllowedMentions`]: The allowed mention configuration. + + .. versionadded:: 1.4 + """ + return self._connection.allowed_mentions + + @allowed_mentions.setter + def allowed_mentions(self, value: Optional[AllowedMentions]) -> None: + if value is None or isinstance(value, AllowedMentions): + self._connection.allowed_mentions = value + else: + raise TypeError(f"allowed_mentions must be AllowedMentions not {value.__class__!r}") + + @property + def intents(self) -> Intents: + """:class:`~disnake.Intents`: The intents configured for this connection. + + .. versionadded:: 1.5 + """ + return self._connection.intents + + # helpers/getters + + @property + def users(self) -> List[User]: + """List[:class:`~disnake.User`]: Returns a list of all the users the bot can see.""" + return list(self._connection._users.values()) + + def get_channel(self, id: int, /) -> Optional[Union[GuildChannel, Thread, PrivateChannel]]: + """Returns a channel or thread with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.abc.PrivateChannel`]] + The returned channel or ``None`` if not found. + """ + return self._connection.get_channel(id) + + def get_partial_messageable( + self, id: int, *, type: Optional[ChannelType] = None + ) -> PartialMessageable: + """Returns a partial messageable with the given channel ID. + + This is useful if you have a channel_id but don't want to do an API call + to send messages to it. + + .. versionadded:: 2.0 + + Parameters + ---------- + id: :class:`int` + The channel ID to create a partial messageable for. + type: Optional[:class:`.ChannelType`] + The underlying channel type for the partial messageable. + + Returns + ------- + :class:`.PartialMessageable` + The partial messageable + """ + return PartialMessageable(state=self._connection, id=id, type=type) + + def get_stage_instance(self, id: int, /) -> Optional[StageInstance]: + """Returns a stage instance with the given stage channel ID. + + .. versionadded:: 2.0 + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.StageInstance`] + The returns stage instance or ``None`` if not found. + """ + from .channel import StageChannel + + channel = self._connection.get_channel(id) + + if isinstance(channel, StageChannel): + return channel.instance + + def get_guild(self, id: int, /) -> Optional[Guild]: + """Returns a guild with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.Guild`] + The guild or ``None`` if not found. + """ + return self._connection._get_guild(id) + + def get_user(self, id: int, /) -> Optional[User]: + """Returns a user with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`~disnake.User`] + The user or ``None`` if not found. + """ + return self._connection.get_user(id) + + def get_emoji(self, id: int, /) -> Optional[Emoji]: + """Returns an emoji with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.Emoji`] + The custom emoji or ``None`` if not found. + """ + return self._connection.get_emoji(id) + + def get_sticker(self, id: int, /) -> Optional[GuildSticker]: + """Returns a guild sticker with the given ID. + + .. versionadded:: 2.0 + + .. note:: + + To retrieve standard stickers, use :meth:`.fetch_sticker` + or :meth:`.fetch_sticker_packs`. + + Returns + ------- + Optional[:class:`.GuildSticker`] + The sticker or ``None`` if not found. + """ + return self._connection.get_sticker(id) + + def get_soundboard_sound(self, id: int, /) -> Optional[GuildSoundboardSound]: + """Returns a guild soundboard sound with the given ID. + + .. versionadded:: 2.10 + + .. note:: + + To retrieve standard soundboard sounds, use :meth:`.fetch_default_soundboard_sounds`. + + Returns + ------- + Optional[:class:`.GuildSoundboardSound`] + The soundboard sound or ``None`` if not found. + """ + return self._connection.get_soundboard_sound(id) + + def get_all_channels(self) -> Generator[GuildChannel, None, None]: + """A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'. + + This is equivalent to: :: + + for guild in client.guilds: + for channel in guild.channels: + yield channel + + .. note:: + + Just because you receive a :class:`.abc.GuildChannel` does not mean that + you can communicate in said channel. :meth:`.abc.GuildChannel.permissions_for` should + be used for that. + + Yields + ------ + :class:`.abc.GuildChannel` + A channel the client can 'access'. + """ + for guild in self.guilds: + yield from guild.channels + + def get_all_members(self) -> Generator[Member, None, None]: + """Returns a generator with every :class:`.Member` the client can see. + + This is equivalent to: :: + + for guild in client.guilds: + for member in guild.members: + yield member + + Yields + ------ + :class:`.Member` + A member the client can see. + """ + for guild in self.guilds: + yield from guild.members + + def get_guild_application_commands(self, guild_id: int) -> List[APIApplicationCommand]: + """Returns a list of all application commands in the guild with the given ID. + + Parameters + ---------- + guild_id: :class:`int` + The ID to search for. + + Returns + ------- + List[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + The list of application commands. + """ + data = self._connection._guild_application_commands.get(guild_id, {}) + return list(data.values()) + + def get_guild_slash_commands(self, guild_id: int) -> List[APISlashCommand]: + """Returns a list of all slash commands in the guild with the given ID. + + Parameters + ---------- + guild_id: :class:`int` + The ID to search for. + + Returns + ------- + List[:class:`.APISlashCommand`] + The list of slash commands. + """ + data = self._connection._guild_application_commands.get(guild_id, {}) + return [cmd for cmd in data.values() if isinstance(cmd, APISlashCommand)] + + def get_guild_user_commands(self, guild_id: int) -> List[APIUserCommand]: + """Returns a list of all user commands in the guild with the given ID. + + Parameters + ---------- + guild_id: :class:`int` + The ID to search for. + + Returns + ------- + List[:class:`.APIUserCommand`] + The list of user commands. + """ + data = self._connection._guild_application_commands.get(guild_id, {}) + return [cmd for cmd in data.values() if isinstance(cmd, APIUserCommand)] + + def get_guild_message_commands(self, guild_id: int) -> List[APIMessageCommand]: + """Returns a list of all message commands in the guild with the given ID. + + Parameters + ---------- + guild_id: :class:`int` + The ID to search for. + + Returns + ------- + List[:class:`.APIMessageCommand`] + The list of message commands. + """ + data = self._connection._guild_application_commands.get(guild_id, {}) + return [cmd for cmd in data.values() if isinstance(cmd, APIMessageCommand)] + + def get_global_command(self, id: int) -> Optional[APIApplicationCommand]: + """Returns a global application command with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + The application command. + """ + return self._connection._get_global_application_command(id) + + def get_guild_command(self, guild_id: int, id: int) -> Optional[APIApplicationCommand]: + """Returns a guild application command with the given guild ID and application command ID. + + Parameters + ---------- + guild_id: :class:`int` + The guild ID to search for. + id: :class:`int` + The command ID to search for. + + Returns + ------- + Optional[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + The application command. + """ + return self._connection._get_guild_application_command(guild_id, id) + + def get_global_command_named( + self, name: str, cmd_type: Optional[ApplicationCommandType] = None + ) -> Optional[APIApplicationCommand]: + """Returns a global application command matching the given name. + + Parameters + ---------- + name: :class:`str` + The name to look for. + cmd_type: :class:`.ApplicationCommandType` + The type to look for. By default, no types are checked. + + Returns + ------- + Optional[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + The application command. + """ + return self._connection._get_global_command_named(name, cmd_type) + + def get_guild_command_named( + self, guild_id: int, name: str, cmd_type: Optional[ApplicationCommandType] = None + ) -> Optional[APIApplicationCommand]: + """Returns a guild application command matching the given name. + + Parameters + ---------- + guild_id: :class:`int` + The guild ID to search for. + name: :class:`str` + The command name to search for. + cmd_type: :class:`.ApplicationCommandType` + The type to look for. By default, no types are checked. + + Returns + ------- + Optional[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + The application command. + """ + return self._connection._get_guild_command_named(guild_id, name, cmd_type) + + # listeners/waiters + + async def wait_until_ready(self) -> None: + """|coro| + + Waits until the client's internal cache is all ready. + """ + await self._ready.wait() + + async def wait_until_first_connect(self) -> None: + """|coro| + + Waits until the first connect. + """ + await self._first_connect.wait() + + def wait_for( + self, + event: Union[str, Event], + *, + check: Optional[Callable[..., bool]] = None, + timeout: Optional[float] = None, + ) -> Any: + """|coro| + + Waits for a WebSocket event to be dispatched. + + This could be used to wait for a user to reply to a message, + or to react to a message, or to edit a message in a self-contained + way. + + The ``timeout`` parameter is passed onto :func:`asyncio.wait_for`. By default, + it does not timeout. Note that this does propagate the + :exc:`asyncio.TimeoutError` for you in case of timeout and is provided for + ease of use. + + In case the event returns multiple arguments, a :class:`tuple` containing those + arguments is returned instead. Please check the + :ref:`documentation ` for a list of events and their + parameters. + + This function returns the **first event that meets the requirements**. + + Examples + -------- + Waiting for a user reply: :: + + @client.event + async def on_message(message): + if message.content.startswith('$greet'): + channel = message.channel + await channel.send('Say hello!') + + def check(m): + return m.content == 'hello' and m.channel == channel + + msg = await client.wait_for('message', check=check) + await channel.send(f'Hello {msg.author}!') + + # using events enums: + @client.event + async def on_message(message): + if message.content.startswith('$greet'): + channel = message.channel + await channel.send('Say hello!') + + def check(m): + return m.content == 'hello' and m.channel == channel + + msg = await client.wait_for(Event.message, check=check) + await channel.send(f'Hello {msg.author}!') + + Waiting for a thumbs up reaction from the message author: :: + + @client.event + async def on_message(message): + if message.content.startswith('$thumb'): + channel = message.channel + await channel.send('Send me that \N{THUMBS UP SIGN} reaction, mate') + + def check(reaction, user): + return user == message.author and str(reaction.emoji) == '\N{THUMBS UP SIGN}' + + try: + reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check) + except asyncio.TimeoutError: + await channel.send('\N{THUMBS DOWN SIGN}') + else: + await channel.send('\N{THUMBS UP SIGN}') + + + Parameters + ---------- + event: Union[:class:`str`, :class:`.Event`] + The event name, similar to the :ref:`event reference `, + but without the ``on_`` prefix, to wait for. It's recommended + to use :class:`.Event`. + check: Optional[Callable[..., :class:`bool`]] + A predicate to check what to wait for. The arguments must meet the + parameters of the event being waited for. + timeout: Optional[:class:`float`] + The number of seconds to wait before timing out and raising + :exc:`asyncio.TimeoutError`. + + Raises + ------ + asyncio.TimeoutError + If a timeout is provided and it was reached. + + Returns + ------- + Any + Returns no arguments, a single argument, or a :class:`tuple` of multiple + arguments that mirrors the parameters passed in the + :ref:`event `. + """ + future = self.loop.create_future() + if check is None: + + def _check(*args) -> bool: + return True + + check = _check + + ev = event.lower() if isinstance(event, str) else event.value + try: + listeners = self._listeners[ev] + except KeyError: + listeners = [] + self._listeners[ev] = listeners + + listeners.append((future, check)) + return asyncio.wait_for(future, timeout) + + # event registration + + def event(self, coro: CoroT) -> CoroT: + """A decorator that registers an event to listen to. + + You can find more info about the events in the :ref:`documentation `. + + The events must be a :ref:`coroutine `, if not, :exc:`TypeError` is raised. + + Example + ------- + .. code-block:: python3 + + @client.event + async def on_ready(): + print('Ready!') + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not utils.iscoroutinefunction(coro): + raise TypeError("event registered must be a coroutine function") + + setattr(self, coro.__name__, coro) + _log.debug("%s has successfully been registered as an event", coro.__name__) + return coro + + async def change_presence( + self, + *, + activity: Optional[BaseActivity] = None, + status: Optional[Status] = None, + ) -> None: + """|coro| + + Changes the client's presence. + + .. versionchanged:: 2.0 + Removed the ``afk`` keyword-only parameter. + + .. versionchanged:: 2.6 + Raises :exc:`TypeError` instead of ``InvalidArgument``. + + Example + --------- + + .. code-block:: python3 + + game = disnake.Game("with the API") + await client.change_presence(status=disnake.Status.idle, activity=game) + + Parameters + ---------- + activity: Optional[:class:`.BaseActivity`] + The activity being done. ``None`` if no currently active activity is done. + status: Optional[:class:`.Status`] + Indicates what status to change to. If ``None``, then + :attr:`.Status.online` is used. + + Raises + ------ + TypeError + If the ``activity`` parameter is not the proper type. + """ + if status is None: + status_str = "online" + status = Status.online + elif status is Status.offline: + status_str = "invisible" + status = Status.offline + else: + status_str = str(status) + + await self.ws.change_presence(activity=activity, status=status_str) + + activities = () if activity is None else (activity,) + for guild in self._connection.guilds: + me = guild.me + if me is None: # pyright: ignore[reportUnnecessaryComparison] + # may happen if guild is unavailable + continue + + # Member.activities is typehinted as Tuple[ActivityType, ...], we may be setting it as Tuple[BaseActivity, ...] + me.activities = activities # type: ignore + me.status = status + + # Guild stuff + + def fetch_guilds( + self, + *, + limit: Optional[int] = 100, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + with_counts: bool = True, + ) -> GuildIterator: + """Retrieves an :class:`.AsyncIterator` that enables receiving your guilds. + + .. note:: + + Using this, you will only receive :attr:`.Guild.id`, :attr:`.Guild.name`, + :attr:`.Guild.features`, :attr:`.Guild.icon`, and :attr:`.Guild.banner` per :class:`.Guild`. + + .. note:: + + This method is an API call. For general usage, consider :attr:`guilds` instead. + + Examples + -------- + Usage :: + + async for guild in client.fetch_guilds(limit=150): + print(guild.name) + + Flattening into a list :: + + guilds = await client.fetch_guilds(limit=150).flatten() + # guilds is now a list of Guild... + + All parameters are optional. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of guilds to retrieve. + If ``None``, it retrieves every guild you have access to. Note, however, + that this would make it a slow operation. + Defaults to ``100``. + before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieves guilds before this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieve guilds after this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + with_counts: :class:`bool` + Whether to include approximate member and presence counts for the guilds. + Defaults to ``True``. + + .. versionadded:: 2.10 + + Raises + ------ + HTTPException + Retrieving the guilds failed. + + Yields + ------ + :class:`.Guild` + The guild with the guild data parsed. + """ + return GuildIterator(self, limit=limit, before=before, after=after, with_counts=with_counts) + + async def fetch_template(self, code: Union[Template, str]) -> Template: + """|coro| + + Retrieves a :class:`.Template` from a discord.new URL or code. + + Parameters + ---------- + code: Union[:class:`.Template`, :class:`str`] + The Discord Template Code or URL (must be a discord.new URL). + + Raises + ------ + NotFound + The template is invalid. + HTTPException + Retrieving the template failed. + + Returns + ------- + :class:`.Template` + The template from the URL/code. + """ + code = utils.resolve_template(code) + data = await self.http.get_template(code) + return Template(data=data, state=self._connection) + + async def fetch_guild(self, guild_id: int, /, *, with_counts: bool = True) -> Guild: + """|coro| + + Retrieves a :class:`.Guild` from the given ID. + + .. note:: + + Using this, you will **not** receive :attr:`.Guild.channels`, :attr:`.Guild.members`, + :attr:`.Member.activity` and :attr:`.Member.voice` per :class:`.Member`. + + .. note:: + + This method is an API call. For general usage, consider :meth:`get_guild` instead. + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild to retrieve. + with_counts: :class:`bool` + Whether to include approximate member and presence counts for the guild. + Defaults to ``True``. + + .. versionadded:: 2.10 + + Raises + ------ + Forbidden + You do not have access to the guild. + HTTPException + Retrieving the guild failed. + + Returns + ------- + :class:`.Guild` + The guild from the given ID. + """ + data = await self.http.get_guild(guild_id, with_counts=with_counts) + return Guild(data=data, state=self._connection) + + async def fetch_guild_preview( + self, + guild_id: int, + /, + ) -> GuildPreview: + """|coro| + + Retrieves a :class:`.GuildPreview` from the given ID. Your bot does not have to be in this guild. + + .. note:: + + This method may fetch any guild that has ``DISCOVERABLE`` in :attr:`.Guild.features`, + but this information can not be known ahead of time. + + This will work for any guild that you are in. + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild to to retrieve a preview object. + + Raises + ------ + NotFound + Retrieving the guild preview failed. + + Returns + ------- + :class:`.GuildPreview` + The guild preview from the given ID. + """ + data = await self.http.get_guild_preview(guild_id) + return GuildPreview(data=data, state=self._connection) + + async def create_guild( + self, + *, + name: str, + icon: AssetBytes = MISSING, + code: str = MISSING, + ) -> Guild: + """|coro| + + Creates a :class:`.Guild`. + + See :func:`guild_builder` for a more comprehensive alternative. + + Bot accounts in 10 or more guilds are not allowed to create guilds. + + .. note:: + + Using this, you will **not** receive :attr:`.Guild.channels`, :attr:`.Guild.members`, + :attr:`.Member.activity` and :attr:`.Member.voice` per :class:`.Member`. + + .. versionchanged:: 2.5 + Removed the ``region`` parameter. + + .. versionchanged:: 2.6 + Raises :exc:`ValueError` instead of ``InvalidArgument``. + + Parameters + ---------- + name: :class:`str` + The name of the guild. + icon: |resource_type| + The icon of the guild. + See :meth:`.ClientUser.edit` for more details on what is expected. + + .. versionchanged:: 2.5 + Now accepts various resource types in addition to :class:`bytes`. + + code: :class:`str` + The code for a template to create the guild with. + + .. versionadded:: 1.4 + + Raises + ------ + NotFound + The ``icon`` asset couldn't be found. + HTTPException + Guild creation failed. + ValueError + Invalid icon image format given. Must be PNG or JPG. + TypeError + The ``icon`` asset is a lottie sticker (see :func:`Sticker.read `). + + Returns + ------- + :class:`.Guild` + The created guild. This is not the same guild that is added to cache. + """ + if icon is not MISSING: + icon_base64 = await utils._assetbytes_to_base64_data(icon) + else: + icon_base64 = None + + if code: + data = await self.http.create_from_template(code, name, icon_base64) + else: + data = await self.http.create_guild(name, icon_base64) + return Guild(data=data, state=self._connection) + + def guild_builder(self, name: str) -> GuildBuilder: + """Creates a builder object that can be used to create more complex guilds. + + This is a more comprehensive alternative to :func:`create_guild`. + See :class:`.GuildBuilder` for details and examples. + + Bot accounts in 10 or more guilds are not allowed to create guilds. + + .. note:: + + Using this, you will **not** receive :attr:`.Guild.channels`, :attr:`.Guild.members`, + :attr:`.Member.activity` and :attr:`.Member.voice` per :class:`.Member`. + + .. versionadded:: 2.8 + + Parameters + ---------- + name: :class:`str` + The name of the guild. + + Returns + ------- + :class:`.GuildBuilder` + The guild builder object for configuring and creating a new guild. + """ + return GuildBuilder(name=name, state=self._connection) + + async def fetch_stage_instance(self, channel_id: int, /) -> StageInstance: + """|coro| + + Retrieves a :class:`.StageInstance` with the given ID. + + .. note:: + + This method is an API call. For general usage, consider :meth:`get_stage_instance` instead. + + .. versionadded:: 2.0 + + Parameters + ---------- + channel_id: :class:`int` + The stage channel ID. + + Raises + ------ + NotFound + The stage instance or channel could not be found. + HTTPException + Retrieving the stage instance failed. + + Returns + ------- + :class:`.StageInstance` + The stage instance from the given ID. + """ + data = await self.http.get_stage_instance(channel_id) + guild = self.get_guild(int(data["guild_id"])) + return StageInstance(guild=guild, state=self._connection, data=data) # type: ignore + + # Invite management + + async def fetch_invite( + self, + url: Union[Invite, str], + *, + with_counts: bool = True, + guild_scheduled_event_id: Optional[int] = None, + with_expiration: bool = False, + ) -> Invite: + """|coro| + + Retrieves an :class:`.Invite` from a discord.gg URL or ID. + + .. note:: + + If the invite is for a guild you have not joined, the guild and channel + attributes of the returned :class:`.Invite` will be :class:`.PartialInviteGuild` and + :class:`.PartialInviteChannel` respectively. + + Parameters + ---------- + url: Union[:class:`.Invite`, :class:`str`] + The Discord invite ID or URL (must be a discord.gg URL). + with_counts: :class:`bool` + Whether to include count information in the invite. This fills the + :attr:`.Invite.approximate_member_count` and :attr:`.Invite.approximate_presence_count` + fields. + guild_scheduled_event_id: :class:`int` + The ID of the scheduled event to include in the invite. + If not provided, defaults to the ``event`` parameter in the URL if it exists, + or the ID of the scheduled event contained in the provided invite object. + + .. versionadded:: 2.3 + + Raises + ------ + NotFound + The invite has expired or is invalid. + HTTPException + Retrieving the invite failed. + + Returns + ------- + :class:`.Invite` + The invite from the URL/ID. + """ + if with_expiration: + utils.warn_deprecated( + "Using the `with_expiration` argument is deprecated and will " + "result in an error in future versions. " + "The `expires_at` field is always included now.", + stacklevel=2, + ) + + invite_id, params = utils.resolve_invite(url, with_params=True) + + if not guild_scheduled_event_id: + # keep scheduled event ID from invite url/object + if "event" in params: + guild_scheduled_event_id = int(params["event"]) + elif isinstance(url, Invite) and url.guild_scheduled_event: + guild_scheduled_event_id = url.guild_scheduled_event.id + + data = await self.http.get_invite( + invite_id, + with_counts=with_counts, + guild_scheduled_event_id=guild_scheduled_event_id, + ) + return Invite.from_incomplete(state=self._connection, data=data) + + async def delete_invite(self, invite: Union[Invite, str]) -> None: + """|coro| + + Revokes an :class:`.Invite`, URL, or ID to an invite. + + You must have :attr:`~.Permissions.manage_channels` permission in + the associated guild to do this. + + Parameters + ---------- + invite: Union[:class:`.Invite`, :class:`str`] + The invite to revoke. + + Raises + ------ + Forbidden + You do not have permissions to revoke invites. + NotFound + The invite is invalid or expired. + HTTPException + Revoking the invite failed. + """ + invite_id = utils.resolve_invite(invite) + await self.http.delete_invite(invite_id) + + # Voice region stuff + + async def fetch_voice_regions(self, guild_id: Optional[int] = None) -> List[VoiceRegion]: + """Retrieves a list of :class:`.VoiceRegion`\\s. + + Retrieves voice regions for the user, or a guild if provided. + + .. versionadded:: 2.5 + + Parameters + ---------- + guild_id: Optional[:class:`int`] + The guild to get regions for, if provided. + + Raises + ------ + HTTPException + Retrieving voice regions failed. + NotFound + The provided ``guild_id`` could not be found. + """ + if guild_id: + regions = await self.http.get_guild_voice_regions(guild_id) + else: + regions = await self.http.get_voice_regions() + return [VoiceRegion(data=data) for data in regions] + + # Miscellaneous stuff + + async def fetch_widget(self, guild_id: int, /) -> Widget: + """|coro| + + Retrieves a :class:`.Widget` for the given guild ID. + + .. note:: + + The guild must have the widget enabled to get this information. + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild. + + Raises + ------ + Forbidden + The widget for this guild is disabled. + HTTPException + Retrieving the widget failed. + + Returns + ------- + :class:`.Widget` + The guild's widget. + """ + data = await self.http.get_widget(guild_id) + return Widget(state=self._connection, data=data) + + async def fetch_default_soundboard_sounds(self) -> List[SoundboardSound]: + """|coro| + + Retrieves the list of default :class:`.SoundboardSound`\\s provided by Discord. + + .. versionadded:: 2.10 + + Raises + ------ + HTTPException + Retrieving the soundboard sounds failed. + + Returns + ------- + List[:class:`.SoundboardSound`] + The default soundboard sounds. + """ + data = await self.http.get_default_soundboard_sounds() + return [SoundboardSound(data=d, state=self._connection) for d in data] + + async def application_info(self) -> AppInfo: + """|coro| + + Retrieves the bot's application information. + + Raises + ------ + HTTPException + Retrieving the information failed somehow. + + Returns + ------- + :class:`.AppInfo` + The bot's application information. + """ + data = await self.http.application_info() + return AppInfo(self._connection, data) + + async def fetch_user(self, user_id: int, /) -> User: + """|coro| + + Retrieves a :class:`~disnake.User` based on their ID. + You do not have to share any guilds with the user to get this information, + however many operations do require that you do. + + .. note:: + + This method is an API call. If you have :attr:`disnake.Intents.members` and member cache enabled, consider :meth:`get_user` instead. + + Parameters + ---------- + user_id: :class:`int` + The ID of the user to retrieve. + + Raises + ------ + NotFound + A user with this ID does not exist. + HTTPException + Retrieving the user failed. + + Returns + ------- + :class:`~disnake.User` + The user you requested. + """ + data = await self.http.get_user(user_id) + return User(state=self._connection, data=data) + + async def fetch_channel( + self, + channel_id: int, + /, + ) -> Union[GuildChannel, PrivateChannel, Thread]: + """|coro| + + Retrieves a :class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`, or :class:`.Thread` with the specified ID. + + .. note:: + + This method is an API call. For general usage, consider :meth:`get_channel` instead. + + .. versionadded:: 1.2 + + Parameters + ---------- + channel_id: :class:`int` + The ID of the channel to retrieve. + + Raises + ------ + InvalidData + An unknown channel type was received from Discord. + HTTPException + Retrieving the channel failed. + NotFound + Invalid Channel ID. + Forbidden + You do not have permission to fetch this channel. + + Returns + ------- + Union[:class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`, :class:`.Thread`] + The channel from the ID. + """ + data = await self.http.get_channel(channel_id) + + factory, ch_type = _threaded_channel_factory(data["type"]) + if factory is None: + raise InvalidData("Unknown channel type {type} for channel ID {id}.".format_map(data)) + + if ch_type in (ChannelType.group, ChannelType.private): + # the factory will be a DMChannel or GroupChannel here + channel = factory(me=self.user, data=data, state=self._connection) # type: ignore + else: + # the factory can't be a DMChannel or GroupChannel here + guild_id = int(data["guild_id"]) # type: ignore + guild = self.get_guild(guild_id) or Object(id=guild_id) + # GuildChannels expect a Guild, we may be passing an Object + channel = factory(guild=guild, state=self._connection, data=data) # type: ignore + + return channel + + async def fetch_webhook(self, webhook_id: int, /) -> Webhook: + """|coro| + + Retrieves a :class:`.Webhook` with the given ID. + + Parameters + ---------- + webhook_id: :class:`int` + The ID of the webhook to retrieve. + + Raises + ------ + HTTPException + Retrieving the webhook failed. + NotFound + Invalid webhook ID. + Forbidden + You do not have permission to fetch this webhook. + + Returns + ------- + :class:`.Webhook` + The webhook you requested. + """ + data = await self.http.get_webhook(webhook_id) + return Webhook.from_state(data, state=self._connection) + + async def fetch_sticker(self, sticker_id: int, /) -> Union[StandardSticker, GuildSticker]: + """|coro| + + Retrieves a :class:`.Sticker` with the given ID. + + .. versionadded:: 2.0 + + Parameters + ---------- + sticker_id: :class:`int` + The ID of the sticker to retrieve. + + Raises + ------ + HTTPException + Retrieving the sticker failed. + NotFound + Invalid sticker ID. + + Returns + ------- + Union[:class:`.StandardSticker`, :class:`.GuildSticker`] + The sticker you requested. + """ + data = await self.http.get_sticker(sticker_id) + cls, _ = _sticker_factory(data["type"]) # type: ignore + return cls(state=self._connection, data=data) # type: ignore + + async def fetch_sticker_pack(self, pack_id: int, /) -> StickerPack: + """|coro| + + Retrieves a :class:`.StickerPack` with the given ID. + + .. versionadded:: 2.10 + + Parameters + ---------- + pack_id: :class:`int` + The ID of the sticker pack to retrieve. + + Raises + ------ + HTTPException + Retrieving the sticker pack failed. + NotFound + Invalid sticker pack ID. + + Returns + ------- + :class:`.StickerPack` + The sticker pack you requested. + """ + data = await self.http.get_sticker_pack(pack_id) + return StickerPack(state=self._connection, data=data) + + async def fetch_sticker_packs(self) -> List[StickerPack]: + """|coro| + + Retrieves all available sticker packs. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.10 + Renamed from ``fetch_premium_sticker_packs``. + + Raises + ------ + HTTPException + Retrieving the sticker packs failed. + + Returns + ------- + List[:class:`.StickerPack`] + All available sticker packs. + """ + data = await self.http.list_sticker_packs() + return [StickerPack(state=self._connection, data=pack) for pack in data["sticker_packs"]] + + @deprecated("fetch_sticker_packs") + async def fetch_premium_sticker_packs(self) -> List[StickerPack]: + """An alias of :meth:`fetch_sticker_packs`. + + .. deprecated:: 2.10 + """ + return await self.fetch_sticker_packs() + + async def create_dm(self, user: Snowflake) -> DMChannel: + """|coro| + + Creates a :class:`.DMChannel` with the given user. + + This should be rarely called, as this is done transparently for most + people. + + .. versionadded:: 2.0 + + Parameters + ---------- + user: :class:`~disnake.abc.Snowflake` + The user to create a DM with. + + Returns + ------- + :class:`.DMChannel` + The channel that was created. + """ + state = self._connection + found = state._get_private_channel_by_user(user.id) + if found: + return found + + data = await state.http.start_private_message(user.id) + return state.add_dm_channel(data) + + def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: + """Registers a :class:`~disnake.ui.View` for persistent listening. + + This method should be used for when a view is comprised of components + that last longer than the lifecycle of the program. + + .. versionadded:: 2.0 + + Parameters + ---------- + view: :class:`disnake.ui.View` + The view to register for dispatching. + message_id: Optional[:class:`int`] + The message ID that the view is attached to. This is currently used to + refresh the view's state during message update events. If not given + then message update events are not propagated for the view. + + Raises + ------ + TypeError + A view was not passed. + ValueError + The view is not persistent. A persistent view has no timeout + and all their components have an explicitly provided custom_id. + """ + if not isinstance(view, View): + raise TypeError(f"expected an instance of View not {view.__class__!r}") + + if not view.is_persistent(): + raise ValueError( + "View is not persistent. Items need to have a custom_id set and View must have no timeout" + ) + + self._connection.store_view(view, message_id) + + @property + def persistent_views(self) -> Sequence[View]: + """Sequence[:class:`.View`]: A sequence of persistent views added to the client. + + .. versionadded:: 2.0 + """ + return self._connection.persistent_views + + # Application commands (global) + + async def fetch_global_commands( + self, + *, + with_localizations: bool = True, + ) -> List[APIApplicationCommand]: + """|coro| + + Retrieves a list of global application commands. + + .. versionadded:: 2.1 + + Parameters + ---------- + with_localizations: :class:`bool` + Whether to include localizations in the response. Defaults to ``True``. + + .. versionadded:: 2.5 + + Returns + ------- + List[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + A list of application commands. + """ + return await self._connection.fetch_global_commands(with_localizations=with_localizations) + + async def fetch_global_command(self, command_id: int) -> APIApplicationCommand: + """|coro| + + Retrieves a global application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + command_id: :class:`int` + The ID of the command to retrieve. + + Returns + ------- + Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`] + The requested application command. + """ + return await self._connection.fetch_global_command(command_id) + + @overload + async def create_global_command(self, application_command: SlashCommand) -> APISlashCommand: ... + + @overload + async def create_global_command(self, application_command: UserCommand) -> APIUserCommand: ... + + @overload + async def create_global_command( + self, application_command: MessageCommand + ) -> APIMessageCommand: ... + + @overload + async def create_global_command( + self, application_command: ApplicationCommand + ) -> APIApplicationCommand: ... + + async def create_global_command( + self, application_command: ApplicationCommand + ) -> APIApplicationCommand: + """|coro| + + Creates a global application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + application_command: :class:`.ApplicationCommand` + An object representing the application command to create. + + Returns + ------- + Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`] + The application command that was created. + """ + application_command.localize(self.i18n) + return await self._connection.create_global_command(application_command) + + @overload + async def edit_global_command( + self, command_id: int, new_command: SlashCommand + ) -> APISlashCommand: ... + + @overload + async def edit_global_command( + self, command_id: int, new_command: UserCommand + ) -> APIUserCommand: ... + + @overload + async def edit_global_command( + self, command_id: int, new_command: MessageCommand + ) -> APIMessageCommand: ... + + @overload + async def edit_global_command( + self, command_id: int, new_command: ApplicationCommand + ) -> APIApplicationCommand: ... + + async def edit_global_command( + self, command_id: int, new_command: ApplicationCommand + ) -> APIApplicationCommand: + """|coro| + + Edits a global application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + command_id: :class:`int` + The ID of the application command to edit. + new_command: :class:`.ApplicationCommand` + An object representing the edited application command. + + Returns + ------- + Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`] + The edited application command. + """ + new_command.localize(self.i18n) + return await self._connection.edit_global_command(command_id, new_command) + + async def delete_global_command(self, command_id: int) -> None: + """|coro| + + Deletes a global application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + command_id: :class:`int` + The ID of the application command to delete. + """ + await self._connection.delete_global_command(command_id) + + async def bulk_overwrite_global_commands( + self, application_commands: List[ApplicationCommand] + ) -> List[APIApplicationCommand]: + """|coro| + + Overwrites several global application commands in one API request. + + .. versionadded:: 2.1 + + Parameters + ---------- + application_commands: List[:class:`.ApplicationCommand`] + A list of application commands to insert instead of the existing commands. + + Returns + ------- + List[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + A list of registered application commands. + """ + for cmd in application_commands: + cmd.localize(self.i18n) + return await self._connection.bulk_overwrite_global_commands(application_commands) + + # Application commands (guild) + + async def fetch_guild_commands( + self, + guild_id: int, + *, + with_localizations: bool = True, + ) -> List[APIApplicationCommand]: + """|coro| + + Retrieves a list of guild application commands. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild to fetch commands from. + with_localizations: :class:`bool` + Whether to include localizations in the response. Defaults to ``True``. + + .. versionadded:: 2.5 + + Returns + ------- + List[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + A list of application commands. + """ + return await self._connection.fetch_guild_commands( + guild_id, with_localizations=with_localizations + ) + + async def fetch_guild_command(self, guild_id: int, command_id: int) -> APIApplicationCommand: + """|coro| + + Retrieves a guild application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild to fetch command from. + command_id: :class:`int` + The ID of the application command to retrieve. + + Returns + ------- + Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`] + The requested application command. + """ + return await self._connection.fetch_guild_command(guild_id, command_id) + + @overload + async def create_guild_command( + self, guild_id: int, application_command: SlashCommand + ) -> APISlashCommand: ... + + @overload + async def create_guild_command( + self, guild_id: int, application_command: UserCommand + ) -> APIUserCommand: ... + + @overload + async def create_guild_command( + self, guild_id: int, application_command: MessageCommand + ) -> APIMessageCommand: ... + + @overload + async def create_guild_command( + self, guild_id: int, application_command: ApplicationCommand + ) -> APIApplicationCommand: ... + + async def create_guild_command( + self, guild_id: int, application_command: ApplicationCommand + ) -> APIApplicationCommand: + """|coro| + + Creates a guild application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild where the application command should be created. + application_command: :class:`.ApplicationCommand` + The application command. + + Returns + ------- + Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`] + The newly created application command. + """ + application_command.localize(self.i18n) + return await self._connection.create_guild_command(guild_id, application_command) + + @overload + async def edit_guild_command( + self, guild_id: int, command_id: int, new_command: SlashCommand + ) -> APISlashCommand: ... + + @overload + async def edit_guild_command( + self, guild_id: int, command_id: int, new_command: UserCommand + ) -> APIUserCommand: ... + + @overload + async def edit_guild_command( + self, guild_id: int, command_id: int, new_command: MessageCommand + ) -> APIMessageCommand: ... + + @overload + async def edit_guild_command( + self, guild_id: int, command_id: int, new_command: ApplicationCommand + ) -> APIApplicationCommand: ... + + async def edit_guild_command( + self, guild_id: int, command_id: int, new_command: ApplicationCommand + ) -> APIApplicationCommand: + """|coro| + + Edits a guild application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild where the application command should be edited. + command_id: :class:`int` + The ID of the application command to edit. + new_command: :class:`.ApplicationCommand` + An object representing the edited application command. + + Returns + ------- + Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`] + The newly edited application command. + """ + new_command.localize(self.i18n) + return await self._connection.edit_guild_command(guild_id, command_id, new_command) + + async def delete_guild_command(self, guild_id: int, command_id: int) -> None: + """|coro| + + Deletes a guild application command. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild where the application command should be deleted. + command_id: :class:`int` + The ID of the application command to delete. + """ + await self._connection.delete_guild_command(guild_id, command_id) + + async def bulk_overwrite_guild_commands( + self, guild_id: int, application_commands: List[ApplicationCommand] + ) -> List[APIApplicationCommand]: + """|coro| + + Overwrites several guild application commands in one API request. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild where the application commands should be overwritten. + application_commands: List[:class:`.ApplicationCommand`] + A list of application commands to insert instead of the existing commands. + + Returns + ------- + List[Union[:class:`.APIUserCommand`, :class:`.APIMessageCommand`, :class:`.APISlashCommand`]] + A list of registered application commands. + """ + for cmd in application_commands: + cmd.localize(self.i18n) + return await self._connection.bulk_overwrite_guild_commands(guild_id, application_commands) + + # Application command permissions + + async def bulk_fetch_command_permissions( + self, guild_id: int + ) -> List[GuildApplicationCommandPermissions]: + """|coro| + + Retrieves a list of :class:`.GuildApplicationCommandPermissions` configured for the guild with the given ID. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild to inspect. + """ + return await self._connection.bulk_fetch_command_permissions(guild_id) + + async def fetch_command_permissions( + self, guild_id: int, command_id: int + ) -> GuildApplicationCommandPermissions: + """|coro| + + Retrieves :class:`.GuildApplicationCommandPermissions` for a specific application command in the guild with the given ID. + + .. versionadded:: 2.1 + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild to inspect. + command_id: :class:`int` + The ID of the application command, or the application ID to fetch application-wide permissions. + + .. versionchanged:: 2.5 + Can now also fetch application-wide permissions. + + Returns + ------- + :class:`.GuildApplicationCommandPermissions` + The permissions configured for the specified application command. + """ + return await self._connection.fetch_command_permissions(guild_id, command_id) + + async def fetch_role_connection_metadata(self) -> List[ApplicationRoleConnectionMetadata]: + """|coro| + + Retrieves the :class:`.ApplicationRoleConnectionMetadata` records for the application. + + .. versionadded:: 2.8 + + Raises + ------ + HTTPException + Retrieving the metadata records failed. + + Returns + ------- + List[:class:`.ApplicationRoleConnectionMetadata`] + The list of metadata records. + """ + data = await self.http.get_application_role_connection_metadata_records(self.application_id) + return [ApplicationRoleConnectionMetadata._from_data(record) for record in data] + + async def edit_role_connection_metadata( + self, records: Sequence[ApplicationRoleConnectionMetadata] + ) -> List[ApplicationRoleConnectionMetadata]: + """|coro| + + Edits the :class:`.ApplicationRoleConnectionMetadata` records for the application. + + An application can have up to 5 metadata records. + + .. warning:: + This will overwrite all existing metadata records. + Consider :meth:`fetching ` them first, + and constructing the new list of metadata records based off of the returned list. + + .. versionadded:: 2.8 + + Parameters + ---------- + records: Sequence[:class:`.ApplicationRoleConnectionMetadata`] + The new metadata records. + + Raises + ------ + HTTPException + Editing the metadata records failed. + + Returns + ------- + List[:class:`.ApplicationRoleConnectionMetadata`] + The list of newly edited metadata records. + """ + payload: List[ApplicationRoleConnectionMetadataPayload] = [] + for record in records: + record._localize(self.i18n) + payload.append(record.to_dict()) + + data = await self.http.edit_application_role_connection_metadata_records( + self.application_id, payload + ) + return [ApplicationRoleConnectionMetadata._from_data(record) for record in data] + + async def skus(self) -> List[SKU]: + """|coro| + + Retrieves the :class:`.SKU`\\s for the application. + + To manage application subscription entitlements, you should use the SKU + with :attr:`.SKUType.subscription` (not the :attr:`~.SKUType.subscription_group` one). + + .. versionadded:: 2.10 + + Raises + ------ + HTTPException + Retrieving the SKUs failed. + + Returns + ------- + List[:class:`.SKU`] + The list of SKUs. + """ + data = await self.http.get_skus(self.application_id) + return [SKU(data=d, state=self._connection) for d in data] + + def entitlements( + self, + *, + limit: Optional[int] = 100, + before: Optional[SnowflakeTime] = None, + after: Optional[SnowflakeTime] = None, + user: Optional[Snowflake] = None, + guild: Optional[Snowflake] = None, + skus: Optional[Sequence[Snowflake]] = None, + exclude_ended: bool = False, + exclude_deleted: bool = True, + oldest_first: bool = False, + ) -> EntitlementIterator: + """Retrieves an :class:`.AsyncIterator` that enables receiving entitlements for the application. + + .. note:: + + This method is an API call. To get the entitlements of the invoking user/guild + in interactions, consider using :attr:`.Interaction.entitlements`. + + Entries are returned in order from newest to oldest by default; + pass ``oldest_first=True`` to reverse the iteration order. + + All parameters are optional. + + .. versionadded:: 2.10 + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of entitlements to retrieve. + If ``None``, retrieves every entitlement. + Note, however, that this would make it a slow operation. + Defaults to ``100``. + before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieves entitlements created before this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieve entitlements created after this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + user: Optional[:class:`.abc.Snowflake`] + The user to retrieve entitlements for. + guild: Optional[:class:`.abc.Snowflake`] + The guild to retrieve entitlements for. + skus: Optional[Sequence[:class:`.abc.Snowflake`]] + The SKUs for which entitlements are retrieved. + exclude_ended: :class:`bool` + Whether to exclude ended/expired entitlements. Defaults to ``False``. + exclude_deleted: :class:`bool` + Whether to exclude deleted entitlements. Defaults to ``True``. + oldest_first: :class:`bool` + If set to ``True``, return entries in oldest->newest order. Defaults to ``False``. + + Raises + ------ + HTTPException + Retrieving the entitlements failed. + + Yields + ------ + :class:`.Entitlement` + The entitlements for the given parameters. + """ + return EntitlementIterator( + self.application_id, + state=self._connection, + limit=limit, + before=before, + after=after, + user_id=user.id if user is not None else None, + guild_id=guild.id if guild is not None else None, + sku_ids=[sku.id for sku in skus] if skus else None, + exclude_ended=exclude_ended, + exclude_deleted=exclude_deleted, + oldest_first=oldest_first, + ) + + async def fetch_entitlement(self, entitlement_id: int, /) -> Entitlement: + """|coro| + + Retrieves a :class:`.Entitlement` for the given ID. + + .. note:: + + This method is an API call. To get the entitlements of the invoking user/guild + in interactions, consider using :attr:`.Interaction.entitlements`. + + .. versionadded:: 2.10 + + Parameters + ---------- + entitlement_id: :class:`int` + The ID of the entitlement to retrieve. + + Raises + ------ + HTTPException + Retrieving the entitlement failed. + + Returns + ------- + :class:`.Entitlement` + The retrieved entitlement. + """ + data = await self.http.get_entitlement(self.application_id, entitlement_id=entitlement_id) + return Entitlement(data=data, state=self._connection) + + async def create_entitlement( + self, sku: Snowflake, owner: Union[abc.User, Guild] + ) -> Entitlement: + """|coro| + + Creates a new test :class:`.Entitlement` for the given user or guild, with no expiry. + + .. note:: + This is only meant to be used with subscription SKUs. To test one-time purchases, + use Application Test Mode. + + Parameters + ---------- + sku: :class:`.abc.Snowflake` + The :class:`.SKU` to grant the entitlement for. + owner: Union[:class:`.abc.User`, :class:`.Guild`] + The user or guild to grant the entitlement to. + + Raises + ------ + HTTPException + Creating the entitlement failed. + + Returns + ------- + :class:`.Entitlement` + The newly created entitlement. + """ + data = await self.http.create_test_entitlement( + self.application_id, + sku_id=sku.id, + owner_id=owner.id, + owner_type=2 if isinstance(owner, abc.User) else 1, + ) + return Entitlement(data=data, state=self._connection) diff --git a/disnake/disnake/colour.py b/disnake/disnake/colour.py new file mode 100644 index 0000000000..a203f64063 --- /dev/null +++ b/disnake/disnake/colour.py @@ -0,0 +1,336 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import colorsys +import random +from typing import TYPE_CHECKING, Any, Optional, Tuple, Union + +if TYPE_CHECKING: + from typing_extensions import Self + + +__all__ = ( + "Colour", + "Color", +) + + +class Colour: + """Represents a Discord role colour. This class is similar + to a (red, green, blue) :class:`tuple`. + + There is an alias for this called Color. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two colours are equal. + + .. describe:: x != y + + Checks if two colours are not equal. + + .. describe:: hash(x) + + Return the colour's hash. + + .. describe:: str(x) + + Returns the hex format for the colour. + + .. describe:: int(x) + + Returns the raw colour value. + + Attributes + ---------- + value: :class:`int` + The raw integer colour value. + """ + + __slots__ = ("value",) + + def __init__(self, value: int) -> None: + if not isinstance(value, int): + raise TypeError(f"Expected int parameter, received {value.__class__.__name__} instead.") + + self.value: int = value + + def _get_byte(self, byte: int) -> int: + return (self.value >> (8 * byte)) & 0xFF + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Colour) and self.value == other.value + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __str__(self) -> str: + return f"#{self.value:0>6x}" + + def __int__(self) -> int: + return self.value + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash(self.value) + + @property + def r(self) -> int: + """:class:`int`: Returns the red component of the colour.""" + return self._get_byte(2) + + @property + def g(self) -> int: + """:class:`int`: Returns the green component of the colour.""" + return self._get_byte(1) + + @property + def b(self) -> int: + """:class:`int`: Returns the blue component of the colour.""" + return self._get_byte(0) + + def to_rgb(self) -> Tuple[int, int, int]: + """Tuple[:class:`int`, :class:`int`, :class:`int`]: Returns an (r, g, b) tuple representing the colour.""" + return (self.r, self.g, self.b) + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> Self: + """Constructs a :class:`Colour` from an RGB tuple.""" + return cls((r << 16) + (g << 8) + b) + + @classmethod + def from_hsv(cls, h: float, s: float, v: float) -> Self: + """Constructs a :class:`Colour` from an HSV tuple.""" + rgb = colorsys.hsv_to_rgb(h, s, v) + return cls.from_rgb(*(int(x * 255) for x in rgb)) + + @classmethod + def default(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0``.""" + return cls(0) + + @classmethod + def random(cls, *, seed: Optional[Union[int, str, float, bytes, bytearray]] = None) -> Self: + """A factory method that returns a :class:`Colour` with a random hue. + + .. note:: + + The random algorithm works by choosing a colour with a random hue but + with maxed out saturation and value. + + .. versionadded:: 1.6 + + Parameters + ---------- + seed: Optional[Union[:class:`int`, :class:`str`, :class:`float`, :class:`bytes`, :class:`bytearray`]] + The seed to initialize the RNG with. If ``None`` is passed the default RNG is used. + + .. versionadded:: 1.7 + """ + rand = random if seed is None else random.Random(seed) + return cls.from_hsv(rand.random(), 1, 1) + + @classmethod + def teal(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``.""" + return cls(0x1ABC9C) + + @classmethod + def dark_teal(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x11806a``.""" + return cls(0x11806A) + + @classmethod + def brand_green(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x57F287``. + + .. versionadded:: 2.0 + """ + return cls(0x57F287) + + @classmethod + def green(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``.""" + return cls(0x2ECC71) + + @classmethod + def dark_green(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``.""" + return cls(0x1F8B4C) + + @classmethod + def blue(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x3498db``.""" + return cls(0x3498DB) + + @classmethod + def dark_blue(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x206694``.""" + return cls(0x206694) + + @classmethod + def purple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``.""" + return cls(0x9B59B6) + + @classmethod + def dark_purple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x71368a``.""" + return cls(0x71368A) + + @classmethod + def magenta(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xe91e63``.""" + return cls(0xE91E63) + + @classmethod + def dark_magenta(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xad1457``.""" + return cls(0xAD1457) + + @classmethod + def gold(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``.""" + return cls(0xF1C40F) + + @classmethod + def dark_gold(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``.""" + return cls(0xC27C0E) + + @classmethod + def orange(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xe67e22``.""" + return cls(0xE67E22) + + @classmethod + def dark_orange(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xa84300``.""" + return cls(0xA84300) + + @classmethod + def brand_red(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xED4245``. + + .. versionadded:: 2.0 + """ + return cls(0xED4245) + + @classmethod + def red(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``.""" + return cls(0xE74C3C) + + @classmethod + def dark_red(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x992d22``.""" + return cls(0x992D22) + + @classmethod + def lighter_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``.""" + return cls(0x95A5A6) + + lighter_gray = lighter_grey + + @classmethod + def dark_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x607d8b``.""" + return cls(0x607D8B) + + dark_gray = dark_grey + + @classmethod + def light_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x979c9f``.""" + return cls(0x979C9F) + + light_gray = light_grey + + @classmethod + def darker_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x546e7a``.""" + return cls(0x546E7A) + + darker_gray = darker_grey + + @classmethod + def og_blurple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x7289da``.""" + return cls(0x7289DA) + + old_blurple = og_blurple + + @classmethod + def blurple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x5865F2``.""" + return cls(0x5865F2) + + @classmethod + def greyple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x99aab5``.""" + return cls(0x99AAB5) + + @classmethod + def dark_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x313338``. + This will appear transparent on Discord's dark theme. + + .. versionadded:: 1.5 + """ + return cls(0x313338) + + @classmethod + def fuchsia(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xEB459E``. + + .. versionadded:: 2.0 + """ + return cls(0xEB459E) + + @classmethod + def yellow(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``. + + .. versionadded:: 2.0 + """ + return cls(0xFEE75C) + + @classmethod + def light_embed(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xF2F3F5``. + This matches the embed background colour on Discord's light theme. + + .. versionadded:: 2.10 + """ + return cls(0xF2F3F5) + + @classmethod + def dark_embed(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x2B2D31``. + This matches the embed background colour on Discord's dark theme. + + .. versionadded:: 2.10 + """ + return cls(0x2B2D31) + + @classmethod + def holographic_style(cls) -> Tuple[Self, Self, Self]: + """A factory method that returns a tuple of :class:`Colour` with values of + ``0xA9C9FF``, ``0xFFBBEC``, ``0xFFC3A0``. This matches the holographic colour style + for roles. + + The first value represents the ``colour`` (``primary_color``), the second and the third + represents the ``secondary_colour`` and ``tertiary_colour`` respectively. + + .. versionadded:: 2.11 + """ + return cls(0xA9C9FF), cls(0xFFBBEC), cls(0xFFC3A0) + + +Color = Colour diff --git a/disnake/disnake/components.py b/disnake/disnake/components.py new file mode 100644 index 0000000000..09646865ae --- /dev/null +++ b/disnake/disnake/components.py @@ -0,0 +1,1624 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + Final, + Generic, + List, + Literal, + Mapping, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +from .asset import AssetMixin +from .colour import Colour +from .enums import ( + ButtonStyle, + ChannelType, + ComponentType, + SelectDefaultValueType, + SeparatorSpacing, + TextInputStyle, + try_enum, +) +from .partial_emoji import PartialEmoji, _EmojiTag +from .utils import MISSING, _get_as_snowflake, assert_never, get_slots + +if TYPE_CHECKING: + from typing_extensions import Self, TypeAlias + + from .emoji import Emoji + from .message import Attachment + from .types.components import ( + ActionRow as ActionRowPayload, + AnySelectMenu as AnySelectMenuPayload, + BaseSelectMenu as BaseSelectMenuPayload, + ButtonComponent as ButtonComponentPayload, + ChannelSelectMenu as ChannelSelectMenuPayload, + Component as ComponentPayload, + ComponentType as ComponentTypeLiteral, + ContainerComponent as ContainerComponentPayload, + FileComponent as FileComponentPayload, + LabelComponent as LabelComponentPayload, + MediaGalleryComponent as MediaGalleryComponentPayload, + MediaGalleryItem as MediaGalleryItemPayload, + MentionableSelectMenu as MentionableSelectMenuPayload, + MessageTopLevelComponent as MessageTopLevelComponentPayload, + RoleSelectMenu as RoleSelectMenuPayload, + SectionComponent as SectionComponentPayload, + SelectDefaultValue as SelectDefaultValuePayload, + SelectOption as SelectOptionPayload, + SeparatorComponent as SeparatorComponentPayload, + StringSelectMenu as StringSelectMenuPayload, + TextDisplayComponent as TextDisplayComponentPayload, + TextInput as TextInputPayload, + ThumbnailComponent as ThumbnailComponentPayload, + UnfurledMediaItem as UnfurledMediaItemPayload, + UserSelectMenu as UserSelectMenuPayload, + ) + +__all__ = ( + "Component", + "ActionRow", + "Button", + "BaseSelectMenu", + "StringSelectMenu", + "SelectMenu", + "UserSelectMenu", + "RoleSelectMenu", + "MentionableSelectMenu", + "ChannelSelectMenu", + "SelectOption", + "SelectDefaultValue", + "TextInput", + "Section", + "TextDisplay", + "UnfurledMediaItem", + "Thumbnail", + "MediaGallery", + "MediaGalleryItem", + "FileComponent", + "Separator", + "Container", + "Label", +) + +# miscellaneous components-related type aliases + +LocalMediaItemInput = Union[str, "UnfurledMediaItem"] +MediaItemInput = Union[LocalMediaItemInput, "AssetMixin", "Attachment"] + +AnySelectMenu = Union[ + "StringSelectMenu", + "UserSelectMenu", + "RoleSelectMenu", + "MentionableSelectMenu", + "ChannelSelectMenu", +] + +SelectMenuType = Literal[ + ComponentType.string_select, + ComponentType.user_select, + ComponentType.role_select, + ComponentType.mentionable_select, + ComponentType.channel_select, +] + +# valid `ActionRow.components` item types in a message/modal +ActionRowMessageComponent = Union["Button", "AnySelectMenu"] +ActionRowModalComponent: TypeAlias = "TextInput" + +# any child component type of action rows +ActionRowChildComponent = Union[ActionRowMessageComponent, ActionRowModalComponent] +ActionRowChildComponentT = TypeVar("ActionRowChildComponentT", bound=ActionRowChildComponent) + +# valid `Section.accessory` types +SectionAccessoryComponent = Union["Thumbnail", "Button"] +# valid `Section.components` item types +SectionChildComponent: TypeAlias = "TextDisplay" + +# valid `Container.components` item types +ContainerChildComponent = Union[ + "ActionRow[ActionRowMessageComponent]", + "Section", + "TextDisplay", + "MediaGallery", + "FileComponent", + "Separator", +] + +# valid `Label.component` types +LabelChildComponent = Union[ + "TextInput", + "AnySelectMenu", +] + +# valid `Message.components` item types (v1/v2) +MessageTopLevelComponentV1: TypeAlias = "ActionRow[ActionRowMessageComponent]" +MessageTopLevelComponentV2 = Union[ + "Section", + "TextDisplay", + "MediaGallery", + "FileComponent", + "Separator", + "Container", +] +MessageTopLevelComponent = Union[MessageTopLevelComponentV1, MessageTopLevelComponentV2] + + +_SELECT_COMPONENT_TYPES = frozenset( + ( + ComponentType.string_select, + ComponentType.user_select, + ComponentType.role_select, + ComponentType.mentionable_select, + ComponentType.channel_select, + ) +) + +# not using `_SELECT_COMPONENT_TYPES` since pyright wouldn't infer the literal properly +_SELECT_COMPONENT_TYPE_VALUES = frozenset( + ( + ComponentType.string_select.value, + ComponentType.user_select.value, + ComponentType.role_select.value, + ComponentType.mentionable_select.value, + ComponentType.channel_select.value, + ) +) + + +class Component: + """Represents the base component that all other components inherit from. + + The components supported by Discord are: + + - :class:`ActionRow` + - :class:`Button` + - subtypes of :class:`BaseSelectMenu` (:class:`ChannelSelectMenu`, :class:`MentionableSelectMenu`, :class:`RoleSelectMenu`, :class:`StringSelectMenu`, :class:`UserSelectMenu`) + - :class:`TextInput` + - :class:`Section` + - :class:`TextDisplay` + - :class:`Thumbnail` + - :class:`MediaGallery` + - :class:`FileComponent` + - :class:`Separator` + - :class:`Container` + - :class:`Label` + + This class is abstract and cannot be instantiated. + + .. versionadded:: 2.0 + + Attributes + ---------- + type: :class:`ComponentType` + The type of component. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + If set to ``0`` (the default) when sending a component, the API will assign sequential + identifiers to the components in the message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("type", "id") + + __repr_attributes__: ClassVar[Tuple[str, ...]] + + # subclasses are expected to overwrite this if they're only usable with `MessageFlags.is_components_v2` + is_v2: ClassVar[bool] = False + + type: ComponentType + id: int + + def __repr__(self) -> str: + attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_attributes__) + return f"<{self.__class__.__name__} {attrs}>" + + @classmethod + def _raw_construct(cls, **kwargs) -> Self: + self = cls.__new__(cls) + for slot in get_slots(cls): + try: + value = kwargs[slot] + except KeyError: + pass + else: + setattr(self, slot, value) + return self + + def to_dict(self) -> Dict[str, Any]: + raise NotImplementedError + + +class ActionRow(Component, Generic[ActionRowChildComponentT]): + """Represents an action row. + + This is a component that holds up to 5 children components in a row. + + This inherits from :class:`Component`. + + .. versionadded:: 2.0 + + Attributes + ---------- + children: List[Union[:class:`Button`, :class:`BaseSelectMenu`, :class:`TextInput`]] + The children components that this holds, if any. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("children",) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + def __init__(self, data: ActionRowPayload) -> None: + self.type: Literal[ComponentType.action_row] = ComponentType.action_row + self.id = data.get("id", 0) + + children = [_component_factory(d) for d in data.get("components", [])] + self.children: List[ActionRowChildComponentT] = children # type: ignore + + def to_dict(self) -> ActionRowPayload: + return { + "type": self.type.value, + "id": self.id, + "components": [child.to_dict() for child in self.children], + } + + +class Button(Component): + """Represents a button from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + The user constructible and usable type to create a button is :class:`disnake.ui.Button`, + not this one. + + .. versionadded:: 2.0 + + Attributes + ---------- + style: :class:`.ButtonStyle` + The style of the button. + custom_id: Optional[:class:`str`] + The ID of the button that gets received during an interaction. + If this button is for a URL or an SKU, it does not have a custom ID. + url: Optional[:class:`str`] + The URL this button sends you to. + disabled: :class:`bool` + Whether the button is disabled or not. + label: Optional[:class:`str`] + The label of the button, if any. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the button, if available. + sku_id: Optional[:class:`int`] + The ID of a purchasable SKU, for premium buttons. + Premium buttons additionally cannot have a ``label``, ``url``, or ``emoji``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ( + "style", + "custom_id", + "url", + "disabled", + "label", + "emoji", + "sku_id", + ) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + def __init__(self, data: ButtonComponentPayload) -> None: + self.type: Literal[ComponentType.button] = ComponentType.button + self.id = data.get("id", 0) + + self.style: ButtonStyle = try_enum(ButtonStyle, data["style"]) + self.custom_id: Optional[str] = data.get("custom_id") + self.url: Optional[str] = data.get("url") + self.disabled: bool = data.get("disabled", False) + self.label: Optional[str] = data.get("label") + self.emoji: Optional[PartialEmoji] + try: + self.emoji = PartialEmoji.from_dict(data["emoji"]) + except KeyError: + self.emoji = None + + self.sku_id: Optional[int] = _get_as_snowflake(data, "sku_id") + + def to_dict(self) -> ButtonComponentPayload: + payload: ButtonComponentPayload = { + "type": self.type.value, + "id": self.id, + "style": self.style.value, + "disabled": self.disabled, + } + + if self.label: + payload["label"] = self.label + + if self.custom_id: + payload["custom_id"] = self.custom_id + + if self.url: + payload["url"] = self.url + + if self.emoji: + payload["emoji"] = self.emoji.to_dict() + + if self.sku_id: + payload["sku_id"] = self.sku_id + + return payload + + +class BaseSelectMenu(Component): + """Represents an abstract select menu from the Discord Bot UI Kit. + + A select menu is functionally the same as a dropdown, however + on mobile it renders a bit differently. + + The currently supported select menus are: + + - :class:`~disnake.StringSelectMenu` + - :class:`~disnake.UserSelectMenu` + - :class:`~disnake.RoleSelectMenu` + - :class:`~disnake.MentionableSelectMenu` + - :class:`~disnake.ChannelSelectMenu` + + .. versionadded:: 2.7 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + options: List[:class:`SelectOption`] + A list of options that can be selected in this select menu. + disabled: :class:`bool` + Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + The list of values (users/roles/channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + Only available for auto-populated select menus. + + .. versionadded:: 2.10 + required: :class:`bool` + Whether the select menu is required. Only applies to components in modals. + Defaults to ``True``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ( + "custom_id", + "placeholder", + "min_values", + "max_values", + "disabled", + "default_values", + "required", + ) + + # FIXME: this isn't pretty; we should decouple __repr__ from slots + __repr_attributes__: ClassVar[Tuple[str, ...]] = tuple( + s for s in __slots__ if s != "default_values" + ) + + # n.b: ideally this would be `BaseSelectMenuPayload`, + # but pyright made TypedDict keys invariant and doesn't + # fully support readonly items yet (which would help avoid this) + def __init__(self, data: AnySelectMenuPayload) -> None: + component_type = try_enum(ComponentType, data["type"]) + self.type: SelectMenuType = component_type # type: ignore + self.id = data.get("id", 0) + + self.custom_id: str = data["custom_id"] + self.placeholder: Optional[str] = data.get("placeholder") + self.min_values: int = data.get("min_values", 1) + self.max_values: int = data.get("max_values", 1) + self.disabled: bool = data.get("disabled", False) + self.default_values: List[SelectDefaultValue] = [ + SelectDefaultValue._from_dict(d) for d in (data.get("default_values") or []) + ] + self.required: bool = data.get("required", True) + + def to_dict(self) -> BaseSelectMenuPayload: + payload: BaseSelectMenuPayload = { + "type": self.type.value, + "id": self.id, + "custom_id": self.custom_id, + "min_values": self.min_values, + "max_values": self.max_values, + "disabled": self.disabled, + "required": self.required, + } + + if self.placeholder: + payload["placeholder"] = self.placeholder + + if self.default_values: + payload["default_values"] = [v.to_dict() for v in self.default_values] + + return payload + + +class StringSelectMenu(BaseSelectMenu): + """Represents a string select menu from the Discord Bot UI Kit. + + .. note:: + The user constructible and usable type to create a + string select menu is :class:`disnake.ui.StringSelect`. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.7 + Renamed from ``SelectMenu`` to ``StringSelectMenu``. + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select menu is disabled or not. + options: List[:class:`SelectOption`] + A list of options that can be selected in this select menu. + required: :class:`bool` + Whether the select menu is required. Only applies to components in modals. + Defaults to ``True``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("options",) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = ( + *BaseSelectMenu.__repr_attributes__, + *__slots__, + ) + type: Literal[ComponentType.string_select] + + def __init__(self, data: StringSelectMenuPayload) -> None: + super().__init__(data) + self.options: List[SelectOption] = [ + SelectOption.from_dict(option) for option in data.get("options", []) + ] + + def to_dict(self) -> StringSelectMenuPayload: + payload = cast("StringSelectMenuPayload", super().to_dict()) + payload["options"] = [op.to_dict() for op in self.options] + return payload + + +SelectMenu = StringSelectMenu # backwards compatibility + + +class UserSelectMenu(BaseSelectMenu): + """Represents a user select menu from the Discord Bot UI Kit. + + .. note:: + The user constructible and usable type to create a + user select menu is :class:`disnake.ui.UserSelect`. + + .. versionadded:: 2.7 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + The list of values (users/members) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 + required: :class:`bool` + Whether the select menu is required. Only applies to components in modals. + Defaults to ``True``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = () + + type: Literal[ComponentType.user_select] + + if TYPE_CHECKING: + + def to_dict(self) -> UserSelectMenuPayload: + return cast("UserSelectMenuPayload", super().to_dict()) + + +class RoleSelectMenu(BaseSelectMenu): + """Represents a role select menu from the Discord Bot UI Kit. + + .. note:: + The user constructible and usable type to create a + role select menu is :class:`disnake.ui.RoleSelect`. + + .. versionadded:: 2.7 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + The list of values (roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 + required: :class:`bool` + Whether the select menu is required. Only applies to components in modals. + Defaults to ``True``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = () + + type: Literal[ComponentType.role_select] + + if TYPE_CHECKING: + + def to_dict(self) -> RoleSelectMenuPayload: + return cast("RoleSelectMenuPayload", super().to_dict()) + + +class MentionableSelectMenu(BaseSelectMenu): + """Represents a mentionable (user/member/role) select menu from the Discord Bot UI Kit. + + .. note:: + The user constructible and usable type to create a + mentionable select menu is :class:`disnake.ui.MentionableSelect`. + + .. versionadded:: 2.7 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select menu is disabled or not. + default_values: List[:class:`SelectDefaultValue`] + The list of values (users/roles) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 + required: :class:`bool` + Whether the select menu is required. Only applies to components in modals. + Defaults to ``True``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = () + + type: Literal[ComponentType.mentionable_select] + + if TYPE_CHECKING: + + def to_dict(self) -> MentionableSelectMenuPayload: + return cast("MentionableSelectMenuPayload", super().to_dict()) + + +class ChannelSelectMenu(BaseSelectMenu): + """Represents a channel select menu from the Discord Bot UI Kit. + + .. note:: + The user constructible and usable type to create a + channel select menu is :class:`disnake.ui.ChannelSelect`. + + .. versionadded:: 2.7 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + disabled: :class:`bool` + Whether the select menu is disabled or not. + channel_types: Optional[List[:class:`ChannelType`]] + A list of channel types that can be selected in this select menu. + If ``None``, channels of all types may be selected. + default_values: List[:class:`SelectDefaultValue`] + The list of values (channels) that are selected by default. + If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``. + + .. versionadded:: 2.10 + required: :class:`bool` + Whether the select menu is required. Only applies to components in modals. + Defaults to ``True``. + + .. versionadded:: 2.11 + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("channel_types",) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = ( + *BaseSelectMenu.__repr_attributes__, + *__slots__, + ) + type: Literal[ComponentType.channel_select] + + def __init__(self, data: ChannelSelectMenuPayload) -> None: + super().__init__(data) + # on the API side, an empty list is (currently) equivalent to no value + channel_types = data.get("channel_types") + self.channel_types: Optional[List[ChannelType]] = ( + [try_enum(ChannelType, t) for t in channel_types] if channel_types else None + ) + + def to_dict(self) -> ChannelSelectMenuPayload: + payload = cast("ChannelSelectMenuPayload", super().to_dict()) + if self.channel_types: + payload["channel_types"] = [t.value for t in self.channel_types] + return payload + + +class SelectOption: + """Represents a string select menu's option. + + These can be created by users. + + .. versionadded:: 2.0 + + Attributes + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not provided when constructed then it defaults to the + label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + emoji: Optional[Union[:class:`str`, :class:`Emoji`, :class:`PartialEmoji`]] + The emoji of the option, if available. + default: :class:`bool` + Whether this option is selected by default. + """ + + __slots__: Tuple[str, ...] = ( + "label", + "value", + "description", + "emoji", + "default", + ) + + def __init__( + self, + *, + label: str, + value: str = MISSING, + description: Optional[str] = None, + emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + default: bool = False, + ) -> None: + self.label = label + self.value = label if value is MISSING else value + self.description = description + + if emoji is not None: + if isinstance(emoji, str): + emoji = PartialEmoji.from_str(emoji) + elif isinstance(emoji, _EmojiTag): + emoji = emoji._to_partial() + else: + raise TypeError( + f"expected emoji to be str, Emoji, or PartialEmoji not {emoji.__class__}" + ) + + self.emoji = emoji + self.default = default + + def __repr__(self) -> str: + return ( + f"" + ) + + def __str__(self) -> str: + if self.emoji: + base = f"{self.emoji} {self.label}" + else: + base = self.label + + if self.description: + return f"{base}\n{self.description}" + return base + + @classmethod + def from_dict(cls, data: SelectOptionPayload) -> SelectOption: + try: + emoji = PartialEmoji.from_dict(data["emoji"]) + except KeyError: + emoji = None + + return cls( + label=data["label"], + value=data["value"], + description=data.get("description"), + emoji=emoji, + default=data.get("default", False), + ) + + def to_dict(self) -> SelectOptionPayload: + payload: SelectOptionPayload = { + "label": self.label, + "value": self.value, + "default": self.default, + } + + if self.emoji: + payload["emoji"] = self.emoji.to_dict() + + if self.description: + payload["description"] = self.description + + return payload + + +class SelectDefaultValue: + """Represents a default value of an auto-populated select menu (currently all + select menu types except :class:`StringSelectMenu`). + + Depending on the :attr:`type` attribute, this can represent different types of objects. + + .. versionadded:: 2.10 + + Attributes + ---------- + id: :class:`int` + The ID of the target object. + type: :class:`SelectDefaultValueType` + The type of the target object. + """ + + __slots__: Tuple[str, ...] = ("id", "type") + + def __init__(self, id: int, type: SelectDefaultValueType) -> None: + self.id: int = id + self.type: SelectDefaultValueType = type + + @classmethod + def _from_dict(cls, data: SelectDefaultValuePayload) -> Self: + return cls(int(data["id"]), try_enum(SelectDefaultValueType, data["type"])) + + def to_dict(self) -> SelectDefaultValuePayload: + return { + "id": self.id, + "type": self.type.value, + } + + def __repr__(self) -> str: + return f"" + + +class TextInput(Component): + """Represents a text input from the Discord Bot UI Kit. + + .. versionadded:: 2.4 + + .. note:: + + The user constructible and usable type to create a text input is + :class:`disnake.ui.TextInput`, not this one. + + Attributes + ---------- + style: :class:`TextInputStyle` + The style of the text input. + label: Optional[:class:`str`] + The label of the text input. + + .. deprecated:: 2.11 + Deprecated in favor of :class:`Label`. + + custom_id: :class:`str` + The ID of the text input that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is entered. + value: Optional[:class:`str`] + The pre-filled text of the text input. + required: :class:`bool` + Whether the text input is required. Defaults to ``True``. + min_length: Optional[:class:`int`] + The minimum length of the text input. + max_length: Optional[:class:`int`] + The maximum length of the text input. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ( + "style", + "custom_id", + "label", + "placeholder", + "value", + "required", + "max_length", + "min_length", + ) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + def __init__(self, data: TextInputPayload) -> None: + self.type: Literal[ComponentType.text_input] = ComponentType.text_input + self.id = data.get("id", 0) + + self.custom_id: str = data["custom_id"] + self.style: TextInputStyle = try_enum( + TextInputStyle, data.get("style", TextInputStyle.short.value) + ) + self.label: Optional[str] = data.get("label") # deprecated + self.placeholder: Optional[str] = data.get("placeholder") + self.value: Optional[str] = data.get("value") + self.required: bool = data.get("required", True) + self.min_length: Optional[int] = data.get("min_length") + self.max_length: Optional[int] = data.get("max_length") + + def to_dict(self) -> TextInputPayload: + payload: TextInputPayload = { + "type": self.type.value, + "id": self.id, + "style": self.style.value, + "label": self.label, + "custom_id": self.custom_id, + "required": self.required, + } + + if self.placeholder is not None: + payload["placeholder"] = self.placeholder + + if self.value is not None: + payload["value"] = self.value + + if self.min_length is not None: + payload["min_length"] = self.min_length + + if self.max_length is not None: + payload["max_length"] = self.max_length + + return payload + + +class Section(Component): + """Represents a section from the Discord Bot UI Kit (v2). + + This allows displaying an accessory (thumbnail or button) next to a block of text. + + .. note:: + The user constructible and usable type to create a + section is :class:`disnake.ui.Section`. + + .. versionadded:: 2.11 + + Attributes + ---------- + children: List[:class:`TextDisplay`] + The text items in this section. + accessory: Union[:class:`Thumbnail`, :class:`Button`] + The accessory component displayed next to the section text. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("children", "accessory") + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: SectionComponentPayload) -> None: + self.type: Literal[ComponentType.section] = ComponentType.section + self.id = data.get("id", 0) + + self.children: List[SectionChildComponent] = [ + _component_factory(d, type=SectionChildComponent) for d in data.get("components", []) + ] + + accessory = _component_factory(data["accessory"]) + self.accessory: SectionAccessoryComponent = accessory # type: ignore + + def to_dict(self) -> SectionComponentPayload: + return { + "type": self.type.value, + "id": self.id, + "accessory": self.accessory.to_dict(), + "components": [child.to_dict() for child in self.children], + } + + +class TextDisplay(Component): + """Represents a text display from the Discord Bot UI Kit (v2). + + .. note:: + The user constructible and usable type to create a + text display is :class:`disnake.ui.TextDisplay`. + + .. versionadded:: 2.11 + + Attributes + ---------- + content: :class:`str` + The text displayed by this component. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("content",) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: TextDisplayComponentPayload) -> None: + self.type: Literal[ComponentType.text_display] = ComponentType.text_display + self.id = data.get("id", 0) + + self.content: str = data["content"] + + def to_dict(self) -> TextDisplayComponentPayload: + return { + "type": self.type.value, + "id": self.id, + "content": self.content, + } + + +class UnfurledMediaItem: + """Represents an unfurled/resolved media item within a component. + + .. versionadded:: 2.11 + + Attributes + ---------- + url: :class:`str` + The URL of this media item. + proxy_url: :class:`str` + The proxied URL of this media item. This is a cached version of + the :attr:`url` in the case of images. + height: Optional[:class:`int`] + The height of this media item, if applicable. + width: Optional[:class:`int`] + The width of this media item, if applicable. + content_type: Optional[:class:`str`] + The `media type `_ of this media item. + attachment_id: Optional[:class:`int`] + The ID of the uploaded attachment. Only present if the media item was + uploaded as an attachment. + """ + + __slots__: Tuple[str, ...] = ( + "url", + "proxy_url", + "height", + "width", + "content_type", + "attachment_id", + ) + + # generally, users should also be able to pass a plain url where applicable instead of + # an UnfurledMediaItem instance; this is largely for internal use + def __init__(self, url: str) -> None: + self.url: str = url + self.proxy_url: Optional[str] = None + self.height: Optional[int] = None + self.width: Optional[int] = None + self.content_type: Optional[str] = None + self.attachment_id: Optional[int] = None + + @classmethod + def from_dict(cls, data: UnfurledMediaItemPayload) -> Self: + self = cls(data["url"]) + self.proxy_url = data.get("proxy_url") + self.height = _get_as_snowflake(data, "height") + self.width = _get_as_snowflake(data, "width") + self.content_type = data.get("content_type") + self.attachment_id = _get_as_snowflake(data, "attachment_id") + return self + + def to_dict(self) -> UnfurledMediaItemPayload: + # for sending, only `url` is required, and other fields are ignored regardless + return {"url": self.url} + + def __repr__(self) -> str: + return f"" + + +class Thumbnail(Component): + """Represents a thumbnail from the Discord Bot UI Kit (v2). + + This is only supported as the :attr:`~Section.accessory` of a section component. + + .. note:: + The user constructible and usable type to create a + thumbnail is :class:`disnake.ui.Thumbnail`. + + .. versionadded:: 2.11 + + Attributes + ---------- + media: :class:`UnfurledMediaItem` + The media item to display. Can be an arbitrary URL or attachment + reference (``attachment://``). + description: Optional[:class:`str`] + The thumbnail's description ("alt text"), if any. + spoiler: :class:`bool` + Whether the thumbnail is marked as a spoiler. Defaults to ``False``. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ( + "media", + "description", + "spoiler", + ) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: ThumbnailComponentPayload) -> None: + self.type: Literal[ComponentType.thumbnail] = ComponentType.thumbnail + self.id = data.get("id", 0) + + self.media: UnfurledMediaItem = UnfurledMediaItem.from_dict(data["media"]) + self.description: Optional[str] = data.get("description") + self.spoiler: bool = data.get("spoiler", False) + + def to_dict(self) -> ThumbnailComponentPayload: + payload: ThumbnailComponentPayload = { + "type": self.type.value, + "id": self.id, + "media": self.media.to_dict(), + "spoiler": self.spoiler, + } + + if self.description: + payload["description"] = self.description + + return payload + + +class MediaGallery(Component): + """Represents a media gallery from the Discord Bot UI Kit (v2). + + This allows displaying up to 10 images in a gallery. + + .. note:: + The user constructible and usable type to create a + media gallery is :class:`disnake.ui.MediaGallery`. + + .. versionadded:: 2.11 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The images in this gallery. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("items",) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: MediaGalleryComponentPayload) -> None: + self.type: Literal[ComponentType.media_gallery] = ComponentType.media_gallery + self.id = data.get("id", 0) + + self.items: List[MediaGalleryItem] = [MediaGalleryItem.from_dict(i) for i in data["items"]] + + def to_dict(self) -> MediaGalleryComponentPayload: + return { + "type": self.type.value, + "id": self.id, + "items": [i.to_dict() for i in self.items], + } + + +class MediaGalleryItem: + """Represents an item inside a :class:`MediaGallery`. + + .. versionadded:: 2.11 + + Parameters + ---------- + media: Union[:class:`str`, :class:`.Asset`, :class:`.Attachment`, :class:`.UnfurledMediaItem`] + The media item to display. Can be an arbitrary URL or attachment + reference (``attachment://``). + description: Optional[:class:`str`] + The item's description ("alt text"), if any. + spoiler: :class:`bool` + Whether the item is marked as a spoiler. Defaults to ``False``. + """ + + __slots__: Tuple[str, ...] = ( + "media", + "description", + "spoiler", + ) + + def __init__( + self, + media: MediaItemInput, + description: Optional[str] = None, + *, + spoiler: bool = False, + ) -> None: + self.media: UnfurledMediaItem = handle_media_item_input(media) + self.description: Optional[str] = description + self.spoiler: bool = spoiler + + @classmethod + def from_dict(cls, data: MediaGalleryItemPayload) -> Self: + return cls( + media=UnfurledMediaItem.from_dict(data["media"]), + description=data.get("description"), + spoiler=data.get("spoiler", False), + ) + + def to_dict(self) -> MediaGalleryItemPayload: + payload: MediaGalleryItemPayload = { + "media": self.media.to_dict(), + "spoiler": self.spoiler, + } + + if self.description: + payload["description"] = self.description + + return payload + + def __repr__(self) -> str: + return f"" + + +class FileComponent(Component): + """Represents a file component from the Discord Bot UI Kit (v2). + + This allows displaying attached files. + + .. note:: + The user constructible and usable type to create a + file component is :class:`disnake.ui.File`. + + .. versionadded:: 2.11 + + Attributes + ---------- + file: :class:`UnfurledMediaItem` + The file to display. This **only** supports attachment references (i.e. + using the ``attachment://`` syntax), not arbitrary URLs. + spoiler: :class:`bool` + Whether the file is marked as a spoiler. Defaults to ``False``. + name: Optional[:class:`str`] + The name of the file. + This is available in objects from the API, and ignored when sending. + size: Optional[:class:`int`] + The size of the file. + This is available in objects from the API, and ignored when sending. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("file", "spoiler", "name", "size") + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: FileComponentPayload) -> None: + self.type: Literal[ComponentType.file] = ComponentType.file + self.id = data.get("id", 0) + + self.file: UnfurledMediaItem = UnfurledMediaItem.from_dict(data["file"]) + self.spoiler: bool = data.get("spoiler", False) + + self.name: Optional[str] = data.get("name") + self.size: Optional[int] = data.get("size") + + def to_dict(self) -> FileComponentPayload: + return { + "type": self.type.value, + "id": self.id, + "file": self.file.to_dict(), + "spoiler": self.spoiler, + } + + +class Separator(Component): + """Represents a separator from the Discord Bot UI Kit (v2). + + This allows vertically separating components. + + .. note:: + The user constructible and usable type to create a + separator is :class:`disnake.ui.Separator`. + + .. versionadded:: 2.11 + + Attributes + ---------- + divider: :class:`bool` + Whether the separator should be visible, instead of just being vertical padding/spacing. + Defaults to ``True``. + spacing: :class:`SeparatorSpacing` + The size of the separator padding. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ("divider", "spacing") + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: SeparatorComponentPayload) -> None: + self.type: Literal[ComponentType.separator] = ComponentType.separator + self.id = data.get("id", 0) + + self.divider: bool = data.get("divider", True) + self.spacing: SeparatorSpacing = try_enum(SeparatorSpacing, data.get("spacing", 1)) + + def to_dict(self) -> SeparatorComponentPayload: + return { + "type": self.type.value, + "id": self.id, + "divider": self.divider, + "spacing": self.spacing.value, + } + + +class Container(Component): + """Represents a container from the Discord Bot UI Kit (v2). + + This is visually similar to :class:`Embed`\\s, and contains other components. + + .. note:: + The user constructible and usable type to create a + container is :class:`disnake.ui.Container`. + + .. versionadded:: 2.11 + + Attributes + ---------- + children: List[Union[:class:`ActionRow`, :class:`Section`, :class:`TextDisplay`, :class:`MediaGallery`, :class:`FileComponent`, :class:`Separator`]] + The child components in this container. + accent_colour: Optional[:class:`Colour`] + The accent colour of the container. An alias exists under ``accent_color``. + spoiler: :class:`bool` + Whether the container is marked as a spoiler. Defaults to ``False``. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + + .. versionadded:: 2.11 + """ + + __slots__: Tuple[str, ...] = ( + "children", + "accent_colour", + "spoiler", + ) + + __repr_attributes__: ClassVar[Tuple[str, ...]] = __slots__ + + is_v2 = True + + def __init__(self, data: ContainerComponentPayload) -> None: + self.type: Literal[ComponentType.container] = ComponentType.container + self.id = data.get("id", 0) + + components = [_component_factory(d) for d in data.get("components", [])] + self.children: List[ContainerChildComponent] = components # type: ignore + + self.accent_colour: Optional[Colour] = ( + Colour(accent_color) if (accent_color := data.get("accent_color")) is not None else None + ) + self.spoiler: bool = data.get("spoiler", False) + + def to_dict(self) -> ContainerComponentPayload: + payload: ContainerComponentPayload = { + "type": self.type.value, + "id": self.id, + "spoiler": self.spoiler, + "components": [child.to_dict() for child in self.children], + } + + if self.accent_colour is not None: + payload["accent_color"] = self.accent_colour.value + + return payload + + @property + def accent_color(self) -> Optional[Colour]: + """Optional[:class:`Colour`]: The accent color of the container. + An alias exists under ``accent_colour``. + """ + return self.accent_colour + + +class Label(Component): + """Represents a label from the Discord Bot UI Kit. + + This wraps other components with a label and an optional description, + and can only be used in modals. + + .. versionadded:: 2.11 + + .. note:: + + The user constructible and usable type to create a label is + :class:`disnake.ui.Label`, not this one. + + Attributes + ---------- + text: :class:`str` + The label text. + description: Optional[:class:`str`] + The description text for the label. + component: Union[:class:`TextInput`, :class:`StringSelectMenu`] + The component within the label. + id: :class:`int` + The numeric identifier for the component. + This is always present in components received from the API, + and unique within a message. + """ + + __slots__: Tuple[str, ...] = ( + "text", + "description", + "component", + ) + + __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ + + def __init__(self, data: LabelComponentPayload) -> None: + self.type: Literal[ComponentType.label] = ComponentType.label + self.id = data.get("id", 0) + + self.text: str = data["label"] + self.description: Optional[str] = data.get("description") + + component = _component_factory(data["component"]) + self.component: LabelChildComponent = component # type: ignore + + def to_dict(self) -> LabelComponentPayload: + payload: LabelComponentPayload = { + "type": self.type.value, + "id": self.id, + "label": self.text, + "component": self.component.to_dict(), + } + + if self.description is not None: + payload["description"] = self.description + + return payload + + +# types of components that are allowed in a message's action rows; +# see also `ActionRowMessageComponent` type alias +VALID_ACTION_ROW_MESSAGE_COMPONENT_TYPES: Final = ( + Button, + StringSelectMenu, + UserSelectMenu, + RoleSelectMenu, + MentionableSelectMenu, + ChannelSelectMenu, +) + + +def handle_media_item_input(value: MediaItemInput) -> UnfurledMediaItem: + if isinstance(value, UnfurledMediaItem): + return value + elif isinstance(value, str): + return UnfurledMediaItem(value) + + # circular import + from .message import Attachment + + if isinstance(value, (AssetMixin, Attachment)): + return UnfurledMediaItem(value.url) + + assert_never(value) + raise TypeError(f"{type(value).__name__} cannot be converted to UnfurledMediaItem") + + +C = TypeVar("C", bound="Component") + + +COMPONENT_LOOKUP: Mapping[ComponentTypeLiteral, Type[Component]] = { + ComponentType.action_row.value: ActionRow, + ComponentType.button.value: Button, + ComponentType.string_select.value: StringSelectMenu, + ComponentType.text_input.value: TextInput, + ComponentType.user_select.value: UserSelectMenu, + ComponentType.role_select.value: RoleSelectMenu, + ComponentType.mentionable_select.value: MentionableSelectMenu, + ComponentType.channel_select.value: ChannelSelectMenu, + ComponentType.section.value: Section, + ComponentType.text_display.value: TextDisplay, + ComponentType.thumbnail.value: Thumbnail, + ComponentType.media_gallery.value: MediaGallery, + ComponentType.file.value: FileComponent, + ComponentType.separator.value: Separator, + ComponentType.container.value: Container, + ComponentType.label.value: Label, +} + + +# NOTE: The type param is purely for type-checking, it has no implications on runtime behavior. +# FIXME: could be improved with https://peps.python.org/pep-0747/ +def _component_factory(data: ComponentPayload, *, type: Type[C] = Component) -> C: + component_type = data["type"] + + try: + component_cls = COMPONENT_LOOKUP[component_type] + except KeyError: + # if we encounter an unknown component type, just construct a placeholder component for it + as_enum = try_enum(ComponentType, component_type) + return Component._raw_construct(type=as_enum) # type: ignore + else: + return component_cls(data) # type: ignore + + +# this is just a rebranded _component_factory, as a workaround to Python not supporting typescript-like mapped types +if TYPE_CHECKING: + + def _message_component_factory( + data: MessageTopLevelComponentPayload, + ) -> MessageTopLevelComponent: ... + +else: + _message_component_factory = _component_factory diff --git a/disnake/disnake/context_managers.py b/disnake/disnake/context_managers.py new file mode 100644 index 0000000000..120c75ffa9 --- /dev/null +++ b/disnake/disnake/context_managers.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING, Optional, Type, Union + +if TYPE_CHECKING: + from types import TracebackType + + from typing_extensions import Self + + from .abc import Messageable + from .channel import ThreadOnlyGuildChannel + +__all__ = ("Typing",) + + +def _typing_done_callback(fut: asyncio.Future) -> None: + # just retrieve any exception and call it a day + try: + fut.exception() + except (asyncio.CancelledError, Exception): + pass + + +class Typing: + def __init__(self, messageable: Union[Messageable, ThreadOnlyGuildChannel]) -> None: + self.loop: asyncio.AbstractEventLoop = messageable._state.loop + self.messageable: Union[Messageable, ThreadOnlyGuildChannel] = messageable + + async def do_typing(self) -> None: + try: + channel = self._channel + except AttributeError: + channel = await self.messageable._get_channel() + + typing = channel._state.http.send_typing + + while True: + await typing(channel.id) + await asyncio.sleep(5) + + def __enter__(self) -> Self: + self.task: asyncio.Task = self.loop.create_task(self.do_typing()) + self.task.add_done_callback(_typing_done_callback) + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.task.cancel() + + async def __aenter__(self) -> Self: + self._channel = channel = await self.messageable._get_channel() + await channel._state.http.send_typing(channel.id) + return self.__enter__() + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.task.cancel() diff --git a/disnake/disnake/custom_warnings.py b/disnake/disnake/custom_warnings.py new file mode 100644 index 0000000000..7b21498747 --- /dev/null +++ b/disnake/disnake/custom_warnings.py @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +__all__ = ( + "DiscordWarning", + "ConfigWarning", + "SyncWarning", + "LocalizationWarning", +) + + +class DiscordWarning(Warning): + """Base warning class for disnake. + + .. versionadded:: 2.3 + """ + + pass + + +class ConfigWarning(DiscordWarning): + """Warning class related to configuration issues. + + .. versionadded:: 2.3 + """ + + pass + + +class SyncWarning(DiscordWarning): + """Warning class for application command synchronization issues. + + .. versionadded:: 2.3 + """ + + pass + + +class LocalizationWarning(DiscordWarning): + """Warning class for localization issues. + + .. versionadded:: 2.5 + """ + + pass diff --git a/disnake/disnake/embeds.py b/disnake/disnake/embeds.py new file mode 100644 index 0000000000..35cb714660 --- /dev/null +++ b/disnake/disnake/embeds.py @@ -0,0 +1,952 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import datetime +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + List, + Literal, + Mapping, + Optional, + Protocol, + Sized, + Union, + cast, + overload, +) + +from . import utils +from .colour import Colour +from .file import File +from .utils import MISSING, classproperty, warn_deprecated + +__all__ = ("Embed",) + + +# backwards compatibility, hidden from type-checkers to have them show errors when accessed +if not TYPE_CHECKING: + + def __getattr__(name: str) -> None: + if name == "EmptyEmbed": + warn_deprecated( + "`EmptyEmbed` is deprecated and will be removed in a future version. Use `None` instead.", + stacklevel=2, + ) + return None + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +class EmbedProxy: + def __init__(self, layer: Optional[Mapping[str, Any]]) -> None: + if layer is not None: + self.__dict__.update(layer) + + def __len__(self) -> int: + return len(self.__dict__) + + def __repr__(self) -> str: + inner = ", ".join((f"{k}={v!r}" for k, v in self.__dict__.items() if not k.startswith("_"))) + return f"EmbedProxy({inner})" + + def __getattr__(self, attr: str) -> None: + return None + + def __eq__(self, other: Any) -> bool: + return isinstance(other, EmbedProxy) and self.__dict__ == other.__dict__ + + +if TYPE_CHECKING: + from typing_extensions import Self + + from disnake.types.embed import ( + Embed as EmbedData, + EmbedAuthor as EmbedAuthorPayload, + EmbedField as EmbedFieldPayload, + EmbedFooter as EmbedFooterPayload, + EmbedImage as EmbedImagePayload, + EmbedProvider as EmbedProviderPayload, + EmbedThumbnail as EmbedThumbnailPayload, + EmbedType, + EmbedVideo as EmbedVideoPayload, + ) + + class _EmbedFooterProxy(Sized, Protocol): + text: Optional[str] + icon_url: Optional[str] + proxy_icon_url: Optional[str] + + class _EmbedFieldProxy(Sized, Protocol): + name: Optional[str] + value: Optional[str] + inline: Optional[bool] + + class _EmbedMediaProxy(Sized, Protocol): + url: Optional[str] + proxy_url: Optional[str] + height: Optional[int] + width: Optional[int] + + class _EmbedVideoProxy(Sized, Protocol): + url: Optional[str] + proxy_url: Optional[str] + height: Optional[int] + width: Optional[int] + + class _EmbedProviderProxy(Sized, Protocol): + name: Optional[str] + url: Optional[str] + + class _EmbedAuthorProxy(Sized, Protocol): + name: Optional[str] + url: Optional[str] + icon_url: Optional[str] + proxy_icon_url: Optional[str] + + _FileKey = Literal["image", "thumbnail", "footer", "author"] + + +class Embed: + """Represents a Discord embed. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two embeds are equal. + + .. versionadded:: 2.6 + + .. describe:: x != y + + Checks if two embeds are not equal. + + .. versionadded:: 2.6 + + .. describe:: len(x) + + Returns the total size of the embed. + Useful for checking if it's within the 6000 character limit. + Check if all aspects of the embed are within the limits with :func:`Embed.check_limits`. + + .. describe:: bool(b) + + Returns whether the embed has any data set. + + .. versionadded:: 2.0 + + Certain properties return an ``EmbedProxy``, a type + that acts similar to a regular :class:`dict` except using dotted access, + e.g. ``embed.author.icon_url``. + + For ease of use, all parameters that expect a :class:`str` are implicitly + cast to :class:`str` for you. + + Attributes + ---------- + title: Optional[:class:`str`] + The title of the embed. + type: Optional[:class:`str`] + The type of embed. Usually "rich". + Possible strings for embed types can be found on Discord's + :ddocs:`api-docs `. + description: Optional[:class:`str`] + The description of the embed. + url: Optional[:class:`str`] + The URL of the embed. + timestamp: Optional[:class:`datetime.datetime`] + The timestamp of the embed content. This is an aware datetime. + If a naive datetime is passed, it is converted to an aware + datetime with the local timezone. + colour: Optional[:class:`Colour`] + The colour code of the embed. Aliased to ``color`` as well. + In addition to :class:`Colour`, :class:`int` can also be assigned to it, + in which case the value will be converted to a :class:`Colour` object. + """ + + __slots__ = ( + "title", + "url", + "type", + "_timestamp", + "_colour", + "_footer", + "_image", + "_thumbnail", + "_video", + "_provider", + "_author", + "_fields", + "description", + "_files", + ) + + _default_colour: ClassVar[Optional[Colour]] = None + _colour: Optional[Colour] + + def __init__( + self, + *, + title: Optional[Any] = None, + type: Optional[EmbedType] = "rich", + description: Optional[Any] = None, + url: Optional[Any] = None, + timestamp: Optional[datetime.datetime] = None, + colour: Optional[Union[int, Colour]] = MISSING, + color: Optional[Union[int, Colour]] = MISSING, + ) -> None: + self.title: Optional[str] = str(title) if title is not None else None + self.type: Optional[EmbedType] = type + self.description: Optional[str] = str(description) if description is not None else None + self.url: Optional[str] = str(url) if url is not None else None + + self.timestamp = timestamp + + # possible values: + # - MISSING: embed color will be _default_color + # - None: embed color will not be set + # - Color: embed color will be set to specified color + if colour is not MISSING: + color = colour + self.colour = color + + self._thumbnail: Optional[EmbedThumbnailPayload] = None + self._video: Optional[EmbedVideoPayload] = None + self._provider: Optional[EmbedProviderPayload] = None + self._author: Optional[EmbedAuthorPayload] = None + self._image: Optional[EmbedImagePayload] = None + self._footer: Optional[EmbedFooterPayload] = None + self._fields: Optional[List[EmbedFieldPayload]] = None + + self._files: Dict[_FileKey, File] = {} + + # see `EmptyEmbed` above + if not TYPE_CHECKING: + + @classproperty + def Empty(self) -> None: + warn_deprecated( + "`Embed.Empty` is deprecated and will be removed in a future version. Use `None` instead.", + stacklevel=3, + ) + return None + + @classmethod + def from_dict(cls, data: EmbedData) -> Self: + """Converts a :class:`dict` to a :class:`Embed` provided it is in the + format that Discord expects it to be in. + + You can find out about this format in the + :ddocs:`official Discord documentation `. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into an embed. + """ + # we are bypassing __init__ here since it doesn't apply here + self = cls.__new__(cls) + + # fill in the basic fields + + self.title = str(title) if (title := data.get("title")) is not None else None + self.type = data.get("type") + self.description = ( + str(description) if (description := data.get("description")) is not None else None + ) + self.url = str(url) if (url := data.get("url")) is not None else None + + self._files = {} + + # try to fill in the more rich fields + + self.colour = data.get("color") + self.timestamp = utils.parse_time(data.get("timestamp")) + + self._thumbnail = data.get("thumbnail") + self._video = data.get("video") + self._provider = data.get("provider") + self._author = data.get("author") + self._image = data.get("image") + self._footer = data.get("footer") + self._fields = data.get("fields") + + return self + + def copy(self) -> Self: + """Returns a shallow copy of the embed.""" + embed = type(self).from_dict(self.to_dict()) + + # assign manually to keep behavior of default colors + embed._colour = self._colour + + # copy files and fields collections + embed._files = self._files.copy() + if self._fields is not None: + embed._fields = self._fields.copy() + + return embed + + def __len__(self) -> int: + total = len((self.title or "").strip()) + len((self.description or "").strip()) + if self._fields: + for field in self._fields: + total += len(field["name"].strip()) + len(field["value"].strip()) + + if self._footer and (footer_text := self._footer.get("text")): + total += len(footer_text.strip()) + + if self._author and (author_name := self._author.get("name")): + total += len(author_name.strip()) + + return total + + def __bool__(self) -> bool: + return any( + ( + self.title, + self.url, + self.description, + self._colour, + self._fields, + self._timestamp, + self._author, + self._thumbnail, + self._footer, + self._image, + self._provider, + self._video, + ) + ) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Embed): + return False + for slot in self.__slots__: + if slot == "_colour": + slot = "color" + if (getattr(self, slot) or None) != (getattr(other, slot) or None): + return False + return True + + @property + def colour(self) -> Optional[Colour]: + col = self._colour + return col if col is not MISSING else type(self)._default_colour + + @colour.setter + def colour(self, value: Optional[Union[int, Colour]]) -> None: + if isinstance(value, int): + self._colour = Colour(value=value) + elif value is MISSING or value is None or isinstance(value, Colour): + self._colour = value + else: + raise TypeError( + f"Expected disnake.Colour, int, or None but received {type(value).__name__} instead." + ) + + @colour.deleter + def colour(self) -> None: + self._colour = MISSING + + color = colour + + @property + def timestamp(self) -> Optional[datetime.datetime]: + return self._timestamp + + @timestamp.setter + def timestamp(self, value: Optional[datetime.datetime]) -> None: + if isinstance(value, datetime.datetime): + if value.tzinfo is None: + value = value.astimezone() + self._timestamp = value + elif value is None: + self._timestamp = value + else: + raise TypeError( + f"Expected datetime.datetime or None received {type(value).__name__} instead" + ) + + @property + def footer(self) -> _EmbedFooterProxy: + """Returns an ``EmbedProxy`` denoting the footer contents. + + Possible attributes you can access are: + + - ``text`` + - ``icon_url`` + - ``proxy_icon_url`` + + If an attribute is not set, it will be ``None``. + """ + return cast("_EmbedFooterProxy", EmbedProxy(self._footer)) + + @overload + def set_footer(self, *, text: Any, icon_url: Optional[Any] = ...) -> Self: ... + + @overload + def set_footer(self, *, text: Any, icon_file: File = ...) -> Self: ... + + def set_footer( + self, *, text: Any, icon_url: Optional[Any] = MISSING, icon_file: File = MISSING + ) -> Self: + """Sets the footer for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + At most one of ``icon_url`` or ``icon_file`` may be passed. + + .. warning:: + Passing a :class:`disnake.File` object will make the embed not + reusable. + + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + + Parameters + ---------- + text: :class:`str` + The footer text. + + .. versionchanged:: 2.6 + No longer optional, must be set to a valid string. + + icon_url: Optional[:class:`str`] + The URL of the footer icon. Only HTTP(S) is supported. + icon_file: :class:`File` + The file to use as the footer icon. + + .. versionadded:: 2.10 + """ + self._footer = { + "text": str(text), + } + + result = self._handle_resource(icon_url, icon_file, key="footer", required=False) + if result is not None: + self._footer["icon_url"] = result + + return self + + def remove_footer(self) -> Self: + """Clears embed's footer information. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 2.0 + """ + self._footer = None + return self + + @property + def image(self) -> _EmbedMediaProxy: + """Returns an ``EmbedProxy`` denoting the image contents. + + Possible attributes you can access are: + + - ``url`` + - ``proxy_url`` + - ``width`` + - ``height`` + + If an attribute is not set, it will be ``None``. + """ + return cast("_EmbedMediaProxy", EmbedProxy(self._image)) + + @overload + def set_image(self, url: Optional[Any]) -> Self: ... + + @overload + def set_image(self, *, file: File) -> Self: ... + + def set_image(self, url: Optional[Any] = MISSING, *, file: File = MISSING) -> Self: + """Sets the image for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + Exactly one of ``url`` or ``file`` must be passed. + + .. warning:: + Passing a :class:`disnake.File` object will make the embed not + reusable. + + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + + .. versionchanged:: 1.4 + Passing ``None`` removes the image. + + Parameters + ---------- + url: Optional[:class:`str`] + The source URL for the image. Only HTTP(S) is supported. + file: :class:`File` + The file to use as the image. + + .. versionadded:: 2.2 + """ + result = self._handle_resource(url, file, key="image") + self._image = {"url": result} if result is not None else None + return self + + @property + def thumbnail(self) -> _EmbedMediaProxy: + """Returns an ``EmbedProxy`` denoting the thumbnail contents. + + Possible attributes you can access are: + + - ``url`` + - ``proxy_url`` + - ``width`` + - ``height`` + + If an attribute is not set, it will be ``None``. + """ + return cast("_EmbedMediaProxy", EmbedProxy(self._thumbnail)) + + @overload + def set_thumbnail(self, url: Optional[Any]) -> Self: ... + + @overload + def set_thumbnail(self, *, file: File) -> Self: ... + + def set_thumbnail(self, url: Optional[Any] = MISSING, *, file: File = MISSING) -> Self: + """Sets the thumbnail for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + Exactly one of ``url`` or ``file`` must be passed. + + .. warning:: + Passing a :class:`disnake.File` object will make the embed not + reusable. + + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + + .. versionchanged:: 1.4 + Passing ``None`` removes the thumbnail. + + Parameters + ---------- + url: Optional[:class:`str`] + The source URL for the thumbnail. Only HTTP(S) is supported. + file: :class:`File` + The file to use as the image. + + .. versionadded:: 2.2 + """ + result = self._handle_resource(url, file, key="thumbnail") + self._thumbnail = {"url": result} if result is not None else None + return self + + @property + def video(self) -> _EmbedVideoProxy: + """Returns an ``EmbedProxy`` denoting the video contents. + + Possible attributes include: + + - ``url`` for the video URL. + - ``proxy_url`` for the proxied video URL. + - ``height`` for the video height. + - ``width`` for the video width. + + If an attribute is not set, it will be ``None``. + """ + return cast("_EmbedVideoProxy", EmbedProxy(self._video)) + + @property + def provider(self) -> _EmbedProviderProxy: + """Returns an ``EmbedProxy`` denoting the provider contents. + + The only attributes that might be accessed are ``name`` and ``url``. + + If an attribute is not set, it will be ``None``. + """ + return cast("_EmbedProviderProxy", EmbedProxy(self._provider)) + + @property + def author(self) -> _EmbedAuthorProxy: + """Returns an ``EmbedProxy`` denoting the author contents. + + See :meth:`set_author` for possible values you can access. + + If an attribute is not set, it will be ``None``. + """ + return cast("_EmbedAuthorProxy", EmbedProxy(self._author)) + + @overload + def set_author( + self, *, name: Any, url: Optional[Any] = ..., icon_url: Optional[Any] = ... + ) -> Self: ... + + @overload + def set_author(self, *, name: Any, url: Optional[Any] = ..., icon_file: File = ...) -> Self: ... + + def set_author( + self, + *, + name: Any, + url: Optional[Any] = None, + icon_url: Optional[Any] = MISSING, + icon_file: File = MISSING, + ) -> Self: + """Sets the author for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + At most one of ``icon_url`` or ``icon_file`` may be passed. + + .. warning:: + Passing a :class:`disnake.File` object will make the embed not + reusable. + + .. warning:: + If used with the other ``set_*`` methods, you must ensure + that the :attr:`.File.filename` is unique to avoid duplication. + + Parameters + ---------- + name: :class:`str` + The name of the author. + url: Optional[:class:`str`] + The URL for the author. + icon_url: Optional[:class:`str`] + The URL of the author icon. Only HTTP(S) is supported. + icon_file: :class:`File` + The file to use as the author icon. + + .. versionadded:: 2.10 + """ + self._author = { + "name": str(name), + } + + if url is not None: + self._author["url"] = str(url) + + result = self._handle_resource(icon_url, icon_file, key="author", required=False) + if result is not None: + self._author["icon_url"] = result + + return self + + def remove_author(self) -> Self: + """Clears embed's author information. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 1.4 + """ + self._author = None + return self + + @property + def fields(self) -> List[_EmbedFieldProxy]: + """List[``EmbedProxy``]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents. + + See :meth:`add_field` for possible values you can access. + + If an attribute is not set, it will be ``None``. + """ + return cast("List[_EmbedFieldProxy]", [EmbedProxy(d) for d in (self._fields or [])]) + + def add_field(self, name: Any, value: Any, *, inline: bool = True) -> Self: + """Adds a field to the embed object. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + name: :class:`str` + The name of the field. + value: :class:`str` + The value of the field. + inline: :class:`bool` + Whether the field should be displayed inline. + Defaults to ``True``. + """ + field: EmbedFieldPayload = { + "inline": inline, + "name": str(name), + "value": str(value), + } + + if self._fields is not None: + self._fields.append(field) + else: + self._fields = [field] + + return self + + def insert_field_at(self, index: int, name: Any, value: Any, *, inline: bool = True) -> Self: + """Inserts a field before a specified index to the embed. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 1.2 + + Parameters + ---------- + index: :class:`int` + The index of where to insert the field. + name: :class:`str` + The name of the field. + value: :class:`str` + The value of the field. + inline: :class:`bool` + Whether the field should be displayed inline. + Defaults to ``True``. + """ + field: EmbedFieldPayload = { + "inline": inline, + "name": str(name), + "value": str(value), + } + + if self._fields is not None: + self._fields.insert(index, field) + else: + self._fields = [field] + + return self + + def clear_fields(self) -> None: + """Removes all fields from this embed.""" + self._fields = None + + def remove_field(self, index: int) -> None: + """Removes a field at a specified index. + + If the index is invalid or out of bounds then the error is + silently swallowed. + + .. note:: + + When deleting a field by index, the index of the other fields + shift to fill the gap just like a regular list. + + Parameters + ---------- + index: :class:`int` + The index of the field to remove. + """ + if self._fields is not None: + try: + del self._fields[index] + except IndexError: + pass + + def set_field_at(self, index: int, name: Any, value: Any, *, inline: bool = True) -> Self: + """Modifies a field to the embed object. + + The index must point to a valid pre-existing field. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + index: :class:`int` + The index of the field to modify. + name: :class:`str` + The name of the field. + value: :class:`str` + The value of the field. + inline: :class:`bool` + Whether the field should be displayed inline. + Defaults to ``True``. + + Raises + ------ + IndexError + An invalid index was provided. + """ + if not self._fields: + raise IndexError("field index out of range") + try: + self._fields[index] + except IndexError: + raise IndexError("field index out of range") from None + + field: EmbedFieldPayload = { + "inline": inline, + "name": str(name), + "value": str(value), + } + self._fields[index] = field + return self + + def to_dict(self) -> EmbedData: + """Converts this embed object into a dict.""" + # add in the raw data into the dict + result: EmbedData = {} + if self._footer is not None: + result["footer"] = self._footer + if self._image is not None: + result["image"] = self._image + if self._thumbnail is not None: + result["thumbnail"] = self._thumbnail + if self._video is not None: + result["video"] = self._video + if self._provider is not None: + result["provider"] = self._provider + if self._author is not None: + result["author"] = self._author + if self._fields is not None: + result["fields"] = self._fields + + # deal with basic convenience wrappers + if self.colour: + result["color"] = self.colour.value + + if self._timestamp: + result["timestamp"] = utils.isoformat_utc(self._timestamp) + + # add in the non raw attribute ones + if self.type: + result["type"] = self.type + + if self.description: + result["description"] = self.description + + if self.url: + result["url"] = self.url + + if self.title: + result["title"] = self.title + + return result + + @classmethod + def set_default_colour(cls, value: Optional[Union[int, Colour]]) -> Optional[Colour]: + """Set the default colour of all new embeds. + + .. versionadded:: 2.4 + + Returns + ------- + Optional[:class:`Colour`] + The colour that was set. + """ + if value is None or isinstance(value, Colour): + cls._default_colour = value + elif isinstance(value, int): + cls._default_colour = Colour(value=value) + else: + raise TypeError( + f"Expected disnake.Colour, int, or None but received {type(value).__name__} instead." + ) + return cls._default_colour + + set_default_color = set_default_colour + + @classmethod + def get_default_colour(cls) -> Optional[Colour]: + """Get the default colour of all new embeds. + + .. versionadded:: 2.4 + + Returns + ------- + Optional[:class:`Colour`] + The default colour. + + """ + return cls._default_colour + + get_default_color = get_default_colour + + def _handle_resource( + self, url: Optional[Any], file: Optional[File], *, key: _FileKey, required: bool = True + ) -> Optional[str]: + if required: + if not (url is MISSING) ^ (file is MISSING): + raise TypeError("Exactly one of url or file must be provided") + else: + if url is not MISSING and file is not MISSING: + raise TypeError("At most one of url or file may be provided, not both.") + + if file: + if file.filename is None: + raise TypeError("File must have a filename") + self._files[key] = file + return f"attachment://{file.filename}" + else: + self._files.pop(key, None) + return str(url) if url else None + + def check_limits(self) -> None: + """Checks if this embed fits within the limits dictated by Discord. + There is also a 6000 character limit across all embeds in a message. + + Returns nothing on success, raises :exc:`ValueError` if an attribute exceeds the limits. + + +--------------------------+------------------------------------+ + | Field | Limit | + +--------------------------+------------------------------------+ + | title | 256 characters | + +--------------------------+------------------------------------+ + | description | 4096 characters | + +--------------------------+------------------------------------+ + | fields | Up to 25 field objects | + +--------------------------+------------------------------------+ + | field.name | 256 characters | + +--------------------------+------------------------------------+ + | field.value | 1024 characters | + +--------------------------+------------------------------------+ + | footer.text | 2048 characters | + +--------------------------+------------------------------------+ + | author.name | 256 characters | + +--------------------------+------------------------------------+ + + .. versionadded:: 2.6 + + Raises + ------ + ValueError + One or more of the embed attributes are too long. + """ + if self.title and len(self.title.strip()) > 256: + raise ValueError("Embed title cannot be longer than 256 characters") + + if self.description and len(self.description.strip()) > 4096: + raise ValueError("Embed description cannot be longer than 4096 characters") + + if self._footer and len(self._footer.get("text", "").strip()) > 2048: + raise ValueError("Embed footer text cannot be longer than 2048 characters") + + if self._author and len(self._author.get("name", "").strip()) > 256: + raise ValueError("Embed author name cannot be longer than 256 characters") + + if self._fields: + if len(self._fields) > 25: + raise ValueError("Embeds cannot have more than 25 fields") + + for field_index, field in enumerate(self._fields): + if len(field["name"].strip()) > 256: + raise ValueError( + f"Embed field {field_index} name cannot be longer than 256 characters" + ) + if len(field["value"].strip()) > 1024: + raise ValueError( + f"Embed field {field_index} value cannot be longer than 1024 characters" + ) + + if len(self) > 6000: + raise ValueError("Embed total size cannot be longer than 6000 characters") diff --git a/disnake/disnake/emoji.py b/disnake/disnake/emoji.py new file mode 100644 index 0000000000..badedbce86 --- /dev/null +++ b/disnake/disnake/emoji.py @@ -0,0 +1,248 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Union + +from .asset import Asset, AssetMixin +from .partial_emoji import PartialEmoji, _EmojiTag +from .user import User +from .utils import MISSING, SnowflakeList, snowflake_time + +__all__ = ("Emoji",) + +if TYPE_CHECKING: + from datetime import datetime + + from .abc import Snowflake + from .guild import Guild + from .guild_preview import GuildPreview + from .role import Role + from .state import ConnectionState + from .types.emoji import Emoji as EmojiPayload + + +class Emoji(_EmojiTag, AssetMixin): + """Represents a custom emoji. + + Depending on the way this object was created, some of the attributes can + have a value of ``None``. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two emoji are the same. + + .. describe:: x != y + + Checks if two emoji are not the same. + + .. describe:: hash(x) + + Return the emoji's hash. + + .. describe:: iter(x) + + Returns an iterator of ``(field, value)`` pairs. This allows this class + to be used as an iterable in list/dict/etc constructions. + + .. describe:: str(x) + + Returns the emoji rendered for Discord. + + Attributes + ---------- + name: :class:`str` + The emoji's name. + id: :class:`int` + The emoji's ID. + require_colons: :class:`bool` + Whether colons are required to use this emoji in the client (:PJSalt: vs PJSalt). + animated: :class:`bool` + Whether the emoji is animated or not. + managed: :class:`bool` + Whether the emoji is managed by a Twitch integration. + guild_id: :class:`int` + The guild ID the emoji belongs to. + available: :class:`bool` + Whether the emoji is available for use. + user: Optional[:class:`User`] + The user that created this emoji. This can only be retrieved using + :meth:`Guild.fetch_emoji`/:meth:`Guild.fetch_emojis` while + having the :attr:`~Permissions.manage_guild_expressions` permission. + """ + + __slots__: Tuple[str, ...] = ( + "require_colons", + "animated", + "managed", + "id", + "name", + "_roles", + "guild_id", + "user", + "available", + ) + + def __init__( + self, *, guild: Union[Guild, GuildPreview], state: ConnectionState, data: EmojiPayload + ) -> None: + self.guild_id: int = guild.id + self._state: ConnectionState = state + self._from_data(data) + + def _from_data(self, emoji: EmojiPayload) -> None: + self.require_colons: bool = emoji.get("require_colons", False) + self.managed: bool = emoji.get("managed", False) + self.id: int = int(emoji["id"]) # type: ignore + self.name: str = emoji["name"] # type: ignore + self.animated: bool = emoji.get("animated", False) + self.available: bool = emoji.get("available", True) + self._roles: SnowflakeList = SnowflakeList(map(int, emoji.get("roles", []))) + user = emoji.get("user") + self.user: Optional[User] = User(state=self._state, data=user) if user else None + + def _to_partial(self) -> PartialEmoji: + return PartialEmoji(name=self.name, animated=self.animated, id=self.id) + + def __iter__(self) -> Iterator[Tuple[str, Any]]: + for attr in self.__slots__: + if attr[0] != "_": + value = getattr(self, attr, None) + if value is not None: + yield (attr, value) + + def __str__(self) -> str: + if self.animated: + return f"" + return f"<:{self.name}:{self.id}>" + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: Any) -> bool: + return isinstance(other, _EmojiTag) and self.id == other.id + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def __hash__(self) -> int: + return self.id >> 22 + + @property + def created_at(self) -> datetime: + """:class:`datetime.datetime`: Returns the emoji's creation time in UTC.""" + return snowflake_time(self.id) + + @property + def url(self) -> str: + """:class:`str`: Returns the URL of the emoji.""" + fmt = "gif" if self.animated else "png" + return f"{Asset.BASE}/emojis/{self.id}.{fmt}" + + @property + def roles(self) -> List[Role]: + """List[:class:`Role`]: A :class:`list` of roles that are allowed to use this emoji. + + If roles is empty, the emoji is unrestricted. + + Emojis with :attr:`subscription roles ` are considered premium emojis, + and count towards a separate limit of 25 emojis. + """ + guild = self.guild + if guild is None: # pyright: ignore[reportUnnecessaryComparison] + return [] + + return [role for role in guild.roles if self._roles.has(role.id)] + + @property + def guild(self) -> Guild: + """:class:`Guild`: The guild this emoji belongs to.""" + # this will most likely never return None but there's a possibility + return self._state._get_guild(self.guild_id) # type: ignore + + def is_usable(self) -> bool: + """Whether the bot can use this emoji. + + .. versionadded:: 1.3 + + :return type: :class:`bool` + """ + if not self.available: + return False + if not self._roles: + return True + emoji_roles, my_roles = self._roles, self.guild.me._roles + return any(my_roles.has(role_id) for role_id in emoji_roles) + + async def delete(self, *, reason: Optional[str] = None) -> None: + """|coro| + + Deletes the custom emoji. + + You must have :attr:`~Permissions.manage_guild_expressions` permission to + do this. + + Parameters + ---------- + reason: Optional[:class:`str`] + The reason for deleting this emoji. Shows up on the audit log. + + Raises + ------ + Forbidden + You are not allowed to delete this emoji. + HTTPException + An error occurred deleting the emoji. + """ + await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason) + + async def edit( + self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None + ) -> Emoji: + """|coro| + + Edits the custom emoji. + + You must have :attr:`~Permissions.manage_guild_expressions` permission to + do this. + + .. versionchanged:: 2.0 + The newly updated emoji is returned. + + Parameters + ---------- + name: :class:`str` + The new emoji name. + roles: Optional[List[:class:`~disnake.abc.Snowflake`]] + A list of roles that can use this emoji. An empty list can be passed to make it available to everyone. + + An emoji cannot have both subscription roles (see :attr:`RoleTags.integration_id`) and + non-subscription roles, and emojis can't be converted between premium and non-premium + after creation. + reason: Optional[:class:`str`] + The reason for editing this emoji. Shows up on the audit log. + + Raises + ------ + Forbidden + You are not allowed to edit this emoji. + HTTPException + An error occurred editing the emoji. + + Returns + ------- + :class:`Emoji` + The newly updated emoji. + """ + payload = {} + if name is not MISSING: + payload["name"] = name + if roles is not MISSING: + payload["roles"] = [role.id for role in roles] + + data = await self._state.http.edit_custom_emoji( + self.guild.id, self.id, payload=payload, reason=reason + ) + return Emoji(guild=self.guild, data=data, state=self._state) diff --git a/disnake/disnake/entitlement.py b/disnake/disnake/entitlement.py new file mode 100644 index 0000000000..8dfda2c745 --- /dev/null +++ b/disnake/disnake/entitlement.py @@ -0,0 +1,193 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Optional + +from .enums import EntitlementType, try_enum +from .mixins import Hashable +from .utils import _get_as_snowflake, parse_time, snowflake_time, utcnow + +if TYPE_CHECKING: + from .guild import Guild + from .state import ConnectionState + from .types.entitlement import Entitlement as EntitlementPayload + from .user import User + + +__all__ = ("Entitlement",) + + +class Entitlement(Hashable): + """Represents an entitlement. + + This is usually retrieved using :meth:`Client.entitlements`, from + :attr:`Interaction.entitlements` when using interactions, or provided by + events (e.g. :func:`on_entitlement_create`). + + Note that some entitlements may have ended already; consider using + :meth:`is_active` to check whether a given entitlement is considered active at the current time, + or use ``exclude_ended=True`` when fetching entitlements using :meth:`Client.entitlements`. + + You may create new entitlements for testing purposes using :meth:`Client.create_entitlement`. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two :class:`Entitlement`\\s are equal. + + .. describe:: x != y + + Checks if two :class:`Entitlement`\\s are not equal. + + .. describe:: hash(x) + + Returns the entitlement's hash. + + .. versionadded:: 2.10 + + Attributes + ---------- + id: :class:`int` + The entitlement's ID. + type: :class:`EntitlementType` + The entitlement's type. + sku_id: :class:`int` + The ID of the associated SKU. + user_id: Optional[:class:`int`] + The ID of the user that is granted access to the entitlement's SKU. + + See also :attr:`user`. + guild_id: Optional[:class:`int`] + The ID of the guild that is granted access to the entitlement's SKU. + + See also :attr:`guild`. + application_id: :class:`int` + The parent application's ID. + deleted: :class:`bool` + Whether the entitlement has been deleted. + consumed: :class:`bool` + Whether the entitlement has been consumed. Only applies to consumable items, + i.e. those associated with a :attr:`~SKUType.consumable` SKU. + starts_at: Optional[:class:`datetime.datetime`] + The time at which the entitlement starts being active. + Set to ``None`` when this is a test entitlement. + ends_at: Optional[:class:`datetime.datetime`] + The time at which the entitlement stops being active. + + You can use :meth:`is_active` to check whether this entitlement is still active. + """ + + __slots__ = ( + "_state", + "id", + "sku_id", + "user_id", + "guild_id", + "application_id", + "type", + "deleted", + "consumed", + "starts_at", + "ends_at", + ) + + def __init__(self, *, data: EntitlementPayload, state: ConnectionState) -> None: + self._state: ConnectionState = state + + self.id: int = int(data["id"]) + self.sku_id: int = int(data["sku_id"]) + self.user_id: Optional[int] = _get_as_snowflake(data, "user_id") + self.guild_id: Optional[int] = _get_as_snowflake(data, "guild_id") + self.application_id: int = int(data["application_id"]) + self.type: EntitlementType = try_enum(EntitlementType, data["type"]) + self.deleted: bool = data.get("deleted", False) + self.consumed: bool = data.get("consumed", False) + self.starts_at: Optional[datetime.datetime] = parse_time(data.get("starts_at")) + self.ends_at: Optional[datetime.datetime] = parse_time(data.get("ends_at")) + + def __repr__(self) -> str: + # presumably one of these is set + if self.user_id: + grant_repr = f"user_id={self.user_id!r}" + else: + grant_repr = f"guild_id={self.guild_id!r}" + return ( + f"" + ) + + @property + def created_at(self) -> datetime.datetime: + """:class:`datetime.datetime`: Returns the entitlement's creation time in UTC.""" + return snowflake_time(self.id) + + @property + def guild(self) -> Optional[Guild]: + """Optional[:class:`Guild`]: The guild that is granted access to + this entitlement's SKU, if applicable. + """ + return self._state._get_guild(self.guild_id) + + @property + def user(self) -> Optional[User]: + """Optional[:class:`User`]: The user that is granted access to + this entitlement's SKU, if applicable. + + Requires the user to be cached. + See also :attr:`user_id`. + """ + return self._state.get_user(self.user_id) + + def is_active(self) -> bool: + """Whether the entitlement is currently active, + based on :attr:`starts_at` and :attr:`ends_at`. + + Always returns ``True`` for test entitlements. + + :return type: :class:`bool` + """ + if self.deleted: + return False + + now = utcnow() + if self.starts_at is not None and now < self.starts_at: + return False + if self.ends_at is not None and now >= self.ends_at: + return False + + return True + + async def consume(self) -> None: + """|coro| + + Marks the entitlement as consumed. + + This is only valid for consumable one-time entitlements; see :attr:`consumed`. + + Raises + ------ + NotFound + The entitlement does not exist. + HTTPException + Consuming the entitlement failed. + """ + await self._state.http.consume_entitlement(self.application_id, self.id) + + async def delete(self) -> None: + """|coro| + + Deletes the entitlement. + + This is only valid for test entitlements; you cannot use this to + delete entitlements that users purchased. + + Raises + ------ + NotFound + The entitlement does not exist. + HTTPException + Deleting the entitlement failed. + """ + await self._state.http.delete_test_entitlement(self.application_id, self.id) diff --git a/disnake/disnake/enums.py b/disnake/disnake/enums.py new file mode 100644 index 0000000000..69068db724 --- /dev/null +++ b/disnake/disnake/enums.py @@ -0,0 +1,2485 @@ +# SPDX-License-Identifier: MIT +from __future__ import annotations + +import types +from functools import total_ordering +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + Iterator, + List, + NamedTuple, + NoReturn, + Optional, + Type, + TypeVar, +) + +if TYPE_CHECKING: + from typing_extensions import Self + +__all__ = ( + "Enum", + "ChannelType", + "MessageType", + "SpeakingState", + "VerificationLevel", + "ContentFilter", + "Status", + "StatusDisplayType", + "DefaultAvatar", + "AuditLogAction", + "AuditLogActionCategory", + "UserFlags", + "ActivityType", + "NotificationLevel", + "TeamMembershipState", + "TeamMemberRole", + "WebhookType", + "ExpireBehaviour", + "ExpireBehavior", + "StickerType", + "StickerFormatType", + "InviteType", + "InviteTarget", + "VideoQualityMode", + "ComponentType", + "ButtonStyle", + "TextInputStyle", + "SelectDefaultValueType", + "StagePrivacyLevel", + "InteractionType", + "InteractionResponseType", + "NSFWLevel", + "OptionType", + "ApplicationCommandType", + "ApplicationCommandPermissionType", + "PartyType", + "GuildScheduledEventEntityType", + "GuildScheduledEventStatus", + "GuildScheduledEventPrivacyLevel", + "ThreadArchiveDuration", + "WidgetStyle", + "Locale", + "AutoModTriggerType", + "AutoModEventType", + "AutoModActionType", + "ThreadSortOrder", + "ThreadLayout", + "Event", + "ApplicationRoleConnectionMetadataType", + "ApplicationEventWebhookStatus", + "OnboardingPromptType", + "SKUType", + "EntitlementType", + "SubscriptionStatus", + "PollLayoutType", + "VoiceChannelEffectAnimationType", + "MessageReferenceType", + "SeparatorSpacing", + "NameplatePalette", +) + +EnumMetaT = TypeVar("EnumMetaT", bound="Type[EnumMeta]") + + +class _EnumValueBase(NamedTuple): + if TYPE_CHECKING: + _cls_name: ClassVar[str] + + name: str + value: Any + + def __repr__(self) -> str: + return f"<{self._cls_name}.{self.name}: {self.value!r}>" + + def __str__(self) -> str: + return f"{self._cls_name}.{self.name}" + + +@total_ordering +class _EnumValueComparable(_EnumValueBase): + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.value == other.value + + def __lt__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.value < other.value + + +def _create_value_cls(name: str, comparable: bool) -> Type[_EnumValueBase]: + parent = _EnumValueComparable if comparable else _EnumValueBase + return type(f"{parent.__name__}_{name}", (parent,), {"_cls_name": name}) # type: ignore + + +def _is_descriptor(obj) -> bool: + return hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") + + +class EnumMeta(type): + if TYPE_CHECKING: + __name__: ClassVar[str] + _enum_member_names_: ClassVar[List[str]] + _enum_member_map_: ClassVar[Dict[str, Any]] + _enum_value_map_: ClassVar[Dict[Any, Any]] + _enum_value_cls_: ClassVar[Type[_EnumValueBase]] + + def __new__(cls: EnumMetaT, name: str, bases, attrs, *, comparable: bool = False) -> EnumMetaT: + value_mapping = {} + member_mapping = {} + member_names = [] + + value_cls = _create_value_cls(name, comparable) + for key, value in list(attrs.items()): + is_descriptor = _is_descriptor(value) + if key[0] == "_" and not is_descriptor: + continue + + # Special case classmethod to just pass through + if isinstance(value, classmethod): + continue + + if is_descriptor: + setattr(value_cls, key, value) + del attrs[key] + continue + + try: + new_value = value_mapping[value] + except KeyError: + new_value = value_cls(name=key, value=value) + value_mapping[value] = new_value + member_names.append(key) + + member_mapping[key] = new_value + attrs[key] = new_value + + attrs["_enum_value_map_"] = value_mapping + attrs["_enum_member_map_"] = member_mapping + attrs["_enum_member_names_"] = member_names + attrs["_enum_value_cls_"] = value_cls + actual_cls = super().__new__(cls, name, bases, attrs) + value_cls._actual_enum_cls_ = actual_cls # type: ignore + return actual_cls + + def __iter__(cls) -> Iterator[Self]: + return (cls._enum_member_map_[name] for name in cls._enum_member_names_) + + def __reversed__(cls) -> Iterator[Self]: + return (cls._enum_member_map_[name] for name in reversed(cls._enum_member_names_)) + + def __len__(cls) -> int: + return len(cls._enum_member_names_) + + def __repr__(cls) -> str: + return f"" + + @property + def __members__(cls): + return types.MappingProxyType(cls._enum_member_map_) + + def __call__(cls, value: Any) -> Any: + try: + return cls._enum_value_map_[value] + except (KeyError, TypeError): + raise ValueError(f"{value!r} is not a valid {cls.__name__}") from None + + def __getitem__(cls, key: str) -> Any: + return cls._enum_member_map_[key] + + def __setattr__(cls, name: str, value: Any) -> NoReturn: + raise TypeError("Enums are immutable.") + + def __delattr__(cls, attr) -> NoReturn: + raise TypeError("Enums are immutable") + + def __instancecheck__(self, instance) -> bool: + # isinstance(x, Y) + # -> __instancecheck__(Y, x) + try: + return instance._actual_enum_cls_ is self + except AttributeError: + return False + + +if TYPE_CHECKING: + from enum import Enum +else: + + class Enum(metaclass=EnumMeta): + @classmethod + def try_value(cls, value: Any) -> Self: + try: + return cls._enum_value_map_[value] + except (KeyError, TypeError): + return value + + +class ChannelType(Enum): + """Specifies the type of channel.""" + + text = 0 + """A text channel.""" + private = 1 + """A private text channel. Also called a direct message.""" + voice = 2 + """A voice channel.""" + group = 3 + """A private group text channel.""" + category = 4 + """A category channel.""" + news = 5 + """A guild news channel.""" + news_thread = 10 + """A news thread. + + .. versionadded:: 2.0 + """ + public_thread = 11 + """A public thread. + + .. versionadded:: 2.0 + """ + private_thread = 12 + """A private thread. + + .. versionadded:: 2.0 + """ + stage_voice = 13 + """A guild stage voice channel. + + .. versionadded:: 1.7 + """ + guild_directory = 14 + """A student hub channel. + + .. versionadded:: 2.1 + """ + forum = 15 + """A channel of only threads. + + .. versionadded:: 2.5 + """ + media = 16 + """A channel of only threads but with a focus on media, similar to forum channels. + + .. versionadded:: 2.10 + """ + + def __str__(self) -> str: + return self.name + + +class MessageType(Enum): + """Specifies the type of :class:`Message`. This is used to denote if a message + is to be interpreted as a system message or a regular message. + """ + + default = 0 + """The default message type. This is the same as regular messages.""" + recipient_add = 1 + """The system message when a user is added to a group private message or a thread.""" + recipient_remove = 2 + """The system message when a user is removed from a group private message or a thread.""" + call = 3 + """The system message denoting call state, e.g. missed call, started call, etc.""" + channel_name_change = 4 + """The system message denoting that a channel's name has been changed.""" + channel_icon_change = 5 + """The system message denoting that a channel's icon has been changed.""" + pins_add = 6 + """The system message denoting that a pinned message has been added to a channel.""" + new_member = 7 + """The system message denoting that a new member has joined a Guild.""" + premium_guild_subscription = 8 + """The system message denoting that a member has "nitro boosted" a guild.""" + premium_guild_tier_1 = 9 + """The system message denoting that a member has "nitro boosted" a guild and it achieved level 1.""" + premium_guild_tier_2 = 10 + """The system message denoting that a member has "nitro boosted" a guild and it achieved level 2.""" + premium_guild_tier_3 = 11 + """The system message denoting that a member has "nitro boosted" a guild and it achieved level 3.""" + channel_follow_add = 12 + """The system message denoting that an announcement channel has been followed. + + .. versionadded:: 1.3 + """ + guild_stream = 13 + """The system message denoting that a member is streaming in the guild. + + .. versionadded:: 1.7 + """ + guild_discovery_disqualified = 14 + """The system message denoting that the guild is no longer eligible for Server Discovery. + + .. versionadded:: 1.7 + """ + guild_discovery_requalified = 15 + """The system message denoting that the guild has become eligible again for Server Discovery. + + .. versionadded:: 1.7 + """ + guild_discovery_grace_period_initial_warning = 16 + """The system message denoting that the guild has failed to meet the Server + Discovery requirements for one week. + + .. versionadded:: 1.7 + """ + guild_discovery_grace_period_final_warning = 17 + """The system message denoting that the guild has failed to meet the Server + Discovery requirements for 3 weeks in a row. + + .. versionadded:: 1.7 + """ + thread_created = 18 + """The system message denoting that a thread has been created. This is only + sent if the thread has been created from an older message. The period of time + required for a message to be considered old cannot be relied upon and is up to + Discord. + + .. versionadded:: 2.0 + """ + reply = 19 + """The system message denoting that the author is replying to a message. + + .. versionadded:: 2.0 + """ + application_command = 20 + """The system message denoting that an application (or "slash") command was executed. + + .. versionadded:: 2.0 + """ + thread_starter_message = 21 + """The system message denoting the message in the thread that is the one that started the + thread's conversation topic. + + .. versionadded:: 2.0 + """ + guild_invite_reminder = 22 + """The system message sent as a reminder to invite people to the guild. + + .. versionadded:: 2.0 + """ + context_menu_command = 23 + """The system message denoting that a context menu command was executed. + + .. versionadded:: 2.3 + """ + auto_moderation_action = 24 + """The system message denoting that an auto moderation action was executed. + + .. versionadded:: 2.5 + """ + role_subscription_purchase = 25 + """The system message denoting that a role subscription was purchased. + + .. versionadded:: 2.9 + """ + interaction_premium_upsell = 26 + """The system message for an application premium subscription upsell. + + .. versionadded:: 2.8 + """ + stage_start = 27 + """The system message denoting the stage has been started. + + .. versionadded:: 2.9 + """ + stage_end = 28 + """The system message denoting the stage has ended. + + .. versionadded:: 2.9 + """ + stage_speaker = 29 + """The system message denoting a user has become a speaker. + + .. versionadded:: 2.9 + """ + stage_topic = 31 + """The system message denoting the stage topic has been changed. + + .. versionadded:: 2.9 + """ + guild_application_premium_subscription = 32 + """The system message denoting that a guild member has subscribed to an application. + + .. versionadded:: 2.8 + """ + guild_incident_alert_mode_enabled = 36 + """The system message denoting that an admin enabled security actions. + + .. versionadded:: 2.10 + """ + guild_incident_alert_mode_disabled = 37 + """The system message denoting that an admin disabled security actions. + + .. versionadded:: 2.10 + """ + guild_incident_report_raid = 38 + """The system message denoting that an admin reported a raid. + + .. versionadded:: 2.10 + """ + guild_incident_report_false_alarm = 39 + """The system message denoting that a raid report was a false alarm. + + .. versionadded:: 2.10 + """ + poll_result = 46 + """The system message denoting that a poll expired, announcing the most voted answer. + + .. versionadded:: 2.10 + """ + emoji_added = 63 + """The system message denoting that an emoji was added to the server. + + .. versionadded: 2.11 + """ + + +class PartyType(Enum): + """Represents the type of a voice channel activity/application. + + .. deprecated:: 2.9 + """ + + poker = 755827207812677713 + """The "Poker Night" activity.""" + betrayal = 773336526917861400 + """The "Betrayal.io" activity.""" + fishing = 814288819477020702 + """The "Fishington.io" activity.""" + chess = 832012774040141894 + """The "Chess In The Park" activity.""" + letter_tile = 879863686565621790 + """The "Letter Tile" activity.""" + word_snack = 879863976006127627 + """The "Word Snacks" activity.""" + doodle_crew = 878067389634314250 + """The "Doodle Crew" activity.""" + checkers = 832013003968348200 + """The "Checkers In The Park" activity. + + .. versionadded:: 2.3 + """ + spellcast = 852509694341283871 + """The "SpellCast" activity. + + .. versionadded:: 2.3 + """ + watch_together = 880218394199220334 + """The "Watch Together" activity, a Youtube application. + + .. versionadded:: 2.3 + """ + sketch_heads = 902271654783242291 + """The "Sketch Heads" activity. + + .. versionadded:: 2.4 + """ + ocho = 832025144389533716 + """The "Ocho" activity. + + .. versionadded:: 2.4 + """ + gartic_phone = 1007373802981822582 + """The "Gartic Phone" activity. + + .. versionadded:: 2.9 + """ + + +# undocumented/internal +class SpeakingState(Enum): + none = 0 + voice = 1 << 0 + soundshare = 1 << 1 + priority = 1 << 2 + + def __str__(self) -> str: + return self.name + + def __int__(self) -> int: + return self.value + + +class VerificationLevel(Enum, comparable=True): + """Specifies a :class:`Guild`\\'s verification level, which is the criteria in + which a member must meet before being able to send messages to the guild. + + .. collapse:: operations + + .. versionadded:: 2.0 + + .. describe:: x == y + + Checks if two verification levels are equal. + .. describe:: x != y + + Checks if two verification levels are not equal. + .. describe:: x > y + + Checks if a verification level is higher than another. + .. describe:: x < y + + Checks if a verification level is lower than another. + .. describe:: x >= y + + Checks if a verification level is higher or equal to another. + .. describe:: x <= y + + Checks if a verification level is lower or equal to another. + """ + + none = 0 + """No criteria set.""" + low = 1 + """Member must have a verified email on their Discord account.""" + medium = 2 + """Member must have a verified email and be registered on Discord for more than five minutes.""" + high = 3 + """Member must have a verified email, be registered on Discord for more + than five minutes, and be a member of the guild itself for more than ten minutes. + """ + highest = 4 + """Member must have a verified phone on their Discord account.""" + + def __str__(self) -> str: + return self.name + + +class ContentFilter(Enum, comparable=True): + """Specifies a :class:`Guild`\\'s explicit content filter, which is the machine + learning algorithms that Discord uses to detect if an image contains NSFW content. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two content filter levels are equal. + .. describe:: x != y + + Checks if two content filter levels are not equal. + .. describe:: x > y + + Checks if a content filter level is higher than another. + .. describe:: x < y + + Checks if a content filter level is lower than another. + .. describe:: x >= y + + Checks if a content filter level is higher or equal to another. + .. describe:: x <= y + + Checks if a content filter level is lower or equal to another. + """ + + disabled = 0 + """The guild does not have the content filter enabled.""" + no_role = 1 + """The guild has the content filter enabled for members without a role.""" + all_members = 2 + """The guild has the content filter enabled for every member.""" + + def __str__(self) -> str: + return self.name + + +class Status(Enum): + """Specifies a :class:`Member`\\'s status.""" + + online = "online" + """The member is online.""" + offline = "offline" + """The member is offline.""" + idle = "idle" + """The member is idle.""" + dnd = "dnd" + """The member is "Do Not Disturb".""" + do_not_disturb = "dnd" + """An alias for :attr:`dnd`.""" + invisible = "invisible" + """The member is "invisible". In reality, this is only used in sending + a presence a la :meth:`Client.change_presence`. When you receive a + user's presence this will be :attr:`offline` instead. + """ + streaming = "streaming" + """The member is live streaming to Twitch or YouTube. + + .. versionadded:: 2.3 + """ + + def __str__(self) -> str: + return self.value + + +class StatusDisplayType(Enum): + """Specifies an :class:`Activity` display status. + + .. versionadded:: 2.11 + """ + + name = 0 # type: ignore[reportAssignmentType] + """The name of the activity is displayed, e.g: ``Listening to Spotify``.""" + state = 1 + """The state of the activity is displayed, e.g: ``Listening to Rick Astley``.""" + details = 2 + """The details of the activity are displayed, e.g: ``Listening to Never Gonna Give You Up``.""" + + def __int__(self) -> int: + return self.value + + +class DefaultAvatar(Enum): + """Represents the default avatar of a Discord :class:`User`.""" + + blurple = 0 + """Represents the default avatar with the color blurple. See also :attr:`Colour.blurple`.""" + grey = 1 + """Represents the default avatar with the color grey. See also :attr:`Colour.greyple`.""" + gray = 1 + """An alias for :attr:`grey`.""" + green = 2 + """Represents the default avatar with the color green. See also :attr:`Colour.green`.""" + orange = 3 + """Represents the default avatar with the color orange. See also :attr:`Colour.orange`.""" + red = 4 + """Represents the default avatar with the color red. See also :attr:`Colour.red`.""" + fuchsia = 5 + """Represents the default avatar with the color fuchsia. See also :attr:`Colour.fuchsia`. + + .. versionadded:: 2.9 + """ + + def __str__(self) -> str: + return self.name + + +class NotificationLevel(Enum, comparable=True): + """Specifies whether a :class:`Guild` has notifications on for all messages or mentions only by default. + + .. collapse:: operations + + .. describe:: x == y + + Checks if two notification levels are equal. + .. describe:: x != y + + Checks if two notification levels are not equal. + .. describe:: x > y + + Checks if a notification level is higher than another. + .. describe:: x < y + + Checks if a notification level is lower than another. + .. describe:: x >= y + + Checks if a notification level is higher or equal to another. + .. describe:: x <= y + + Checks if a notification level is lower or equal to another. + """ + + all_messages = 0 + """Members receive notifications for every message regardless of them being mentioned.""" + only_mentions = 1 + """Members receive notifications for messages they are mentioned in.""" + + +class AuditLogActionCategory(Enum): + """Represents the category that the :class:`AuditLogAction` belongs to. + + This can be retrieved via :attr:`AuditLogEntry.category`. + """ + + create = 1 + """The action is the creation of something.""" + delete = 2 + """The action is the deletion of something.""" + update = 3 + """The action is the update of something.""" + + +# NOTE: these fields are only fully documented in audit_logs.rst, +# as the docstrings alone would be ~1000-1500 additional lines +class AuditLogAction(Enum): + """Represents the type of action being done for a :class:`AuditLogEntry`\\, + which is retrievable via :meth:`Guild.audit_logs` or via the :func:`on_audit_log_entry_create` event. + """ + + # fmt: off + guild_update = 1 + channel_create = 10 + channel_update = 11 + channel_delete = 12 + overwrite_create = 13 + overwrite_update = 14 + overwrite_delete = 15 + kick = 20 + member_prune = 21 + ban = 22 + unban = 23 + member_update = 24 + member_role_update = 25 + member_move = 26 + member_disconnect = 27 + bot_add = 28 + role_create = 30 + role_update = 31 + role_delete = 32 + invite_create = 40 + invite_update = 41 + invite_delete = 42 + webhook_create = 50 + webhook_update = 51 + webhook_delete = 52 + emoji_create = 60 + emoji_update = 61 + emoji_delete = 62 + message_delete = 72 + message_bulk_delete = 73 + message_pin = 74 + message_unpin = 75 + integration_create = 80 + integration_update = 81 + integration_delete = 82 + stage_instance_create = 83 + stage_instance_update = 84 + stage_instance_delete = 85 + sticker_create = 90 + sticker_update = 91 + sticker_delete = 92 + guild_scheduled_event_create = 100 + guild_scheduled_event_update = 101 + guild_scheduled_event_delete = 102 + thread_create = 110 + thread_update = 111 + thread_delete = 112 + application_command_permission_update = 121 + soundboard_sound_create = 130 + soundboard_sound_update = 131 + soundboard_sound_delete = 132 + automod_rule_create = 140 + automod_rule_update = 141 + automod_rule_delete = 142 + automod_block_message = 143 + automod_send_alert_message = 144 + automod_timeout = 145 + automod_quarantine_user = 146 + creator_monetization_request_created = 150 + creator_monetization_terms_accepted = 151 + # fmt: on + + @property + def category(self) -> Optional[AuditLogActionCategory]: + # fmt: off + lookup: Dict[AuditLogAction, Optional[AuditLogActionCategory]] = { + AuditLogAction.guild_update: AuditLogActionCategory.update, + AuditLogAction.channel_create: AuditLogActionCategory.create, + AuditLogAction.channel_update: AuditLogActionCategory.update, + AuditLogAction.channel_delete: AuditLogActionCategory.delete, + AuditLogAction.overwrite_create: AuditLogActionCategory.create, + AuditLogAction.overwrite_update: AuditLogActionCategory.update, + AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, + AuditLogAction.kick: None, + AuditLogAction.member_prune: None, + AuditLogAction.ban: None, + AuditLogAction.unban: None, + AuditLogAction.member_update: AuditLogActionCategory.update, + AuditLogAction.member_role_update: AuditLogActionCategory.update, + AuditLogAction.member_move: None, + AuditLogAction.member_disconnect: None, + AuditLogAction.bot_add: None, + AuditLogAction.role_create: AuditLogActionCategory.create, + AuditLogAction.role_update: AuditLogActionCategory.update, + AuditLogAction.role_delete: AuditLogActionCategory.delete, + AuditLogAction.invite_create: AuditLogActionCategory.create, + AuditLogAction.invite_update: AuditLogActionCategory.update, + AuditLogAction.invite_delete: AuditLogActionCategory.delete, + AuditLogAction.webhook_create: AuditLogActionCategory.create, + AuditLogAction.webhook_update: AuditLogActionCategory.update, + AuditLogAction.webhook_delete: AuditLogActionCategory.delete, + AuditLogAction.emoji_create: AuditLogActionCategory.create, + AuditLogAction.emoji_update: AuditLogActionCategory.update, + AuditLogAction.emoji_delete: AuditLogActionCategory.delete, + AuditLogAction.message_delete: AuditLogActionCategory.delete, + AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete, + AuditLogAction.message_pin: None, + AuditLogAction.message_unpin: None, + AuditLogAction.integration_create: AuditLogActionCategory.create, + AuditLogAction.integration_update: AuditLogActionCategory.update, + AuditLogAction.integration_delete: AuditLogActionCategory.delete, + AuditLogAction.stage_instance_create: AuditLogActionCategory.create, + AuditLogAction.stage_instance_update: AuditLogActionCategory.update, + AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete, + AuditLogAction.sticker_create: AuditLogActionCategory.create, + AuditLogAction.sticker_update: AuditLogActionCategory.update, + AuditLogAction.sticker_delete: AuditLogActionCategory.delete, + AuditLogAction.thread_create: AuditLogActionCategory.create, + AuditLogAction.thread_update: AuditLogActionCategory.update, + AuditLogAction.thread_delete: AuditLogActionCategory.delete, + AuditLogAction.guild_scheduled_event_create: AuditLogActionCategory.create, + AuditLogAction.guild_scheduled_event_update: AuditLogActionCategory.update, + AuditLogAction.guild_scheduled_event_delete: AuditLogActionCategory.delete, + AuditLogAction.application_command_permission_update: AuditLogActionCategory.update, + AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create, + AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update, + AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete, + AuditLogAction.automod_rule_create: AuditLogActionCategory.create, + AuditLogAction.automod_rule_update: AuditLogActionCategory.update, + AuditLogAction.automod_rule_delete: AuditLogActionCategory.delete, + AuditLogAction.automod_block_message: None, + AuditLogAction.automod_send_alert_message: None, + AuditLogAction.automod_timeout: None, + AuditLogAction.automod_quarantine_user: None, + AuditLogAction.creator_monetization_request_created: None, + AuditLogAction.creator_monetization_terms_accepted: None, + } + # fmt: on + return lookup[self] + + @property + def target_type(self) -> Optional[str]: + v = self.value + if v == -1: # pyright: ignore[reportUnnecessaryComparison] + return "all" + elif v < 10: + return "guild" + elif v < 20: + return "channel" + elif v < 30: + return "user" + elif v < 40: + return "role" + elif v < 50: + return "invite" + elif v < 60: + return "webhook" + elif v < 70: + return "emoji" + elif v == 73: + return "channel" + elif v < 80: + return "message" + elif v < 83: + return "integration" + elif v < 90: + return "stage_instance" + elif v < 93: + return "sticker" + elif v < 103: + return "guild_scheduled_event" + elif v < 113: + return "thread" + elif v < 122: + return "application_command_or_integration" + elif v < 140: + return None + elif v < 143: + return "automod_rule" + elif v < 147: + return "user" + elif v < 152: + return None + else: + return None + + +class UserFlags(Enum): + """Represents Discord user flags.""" + + staff = 1 << 0 + """The user is a Discord Employee.""" + partner = 1 << 1 + """The user is a Discord Partner.""" + hypesquad = 1 << 2 + """The user is a HypeSquad Events member.""" + bug_hunter = 1 << 3 + """The user is a Bug Hunter.""" + mfa_sms = 1 << 4 + """The user has SMS recovery for Multi Factor Authentication enabled.""" + premium_promo_dismissed = 1 << 5 + """The user has dismissed the Discord Nitro promotion.""" + hypesquad_bravery = 1 << 6 + """The user is a HypeSquad Bravery member.""" + hypesquad_brilliance = 1 << 7 + """The user is a HypeSquad Brilliance member.""" + hypesquad_balance = 1 << 8 + """The user is a HypeSquad Balance member.""" + early_supporter = 1 << 9 + """The user is an Early Supporter.""" + team_user = 1 << 10 + """The user is a Team User.""" + system = 1 << 12 + """The user is a system user (i.e. represents Discord officially).""" + has_unread_urgent_messages = 1 << 13 + """The user has an unread system message.""" + bug_hunter_level_2 = 1 << 14 + """The user is a Bug Hunter Level 2.""" + verified_bot = 1 << 16 + """The user is a Verified Bot.""" + verified_bot_developer = 1 << 17 + """The user is an Early Verified Bot Developer.""" + discord_certified_moderator = 1 << 18 + """The user is a Discord Certified Moderator.""" + http_interactions_bot = 1 << 19 + """The user is a bot that only uses HTTP interactions. + + .. versionadded:: 2.3 + """ + spammer = 1 << 20 + """The user is marked as a spammer. + + .. versionadded:: 2.3 + """ + active_developer = 1 << 22 + """The user is an Active Developer. + + .. versionadded:: 2.8 + """ + + +class ActivityType(Enum): + """Specifies the type of :class:`Activity`. This is used to check how to + interpret the activity itself. + """ + + unknown = -1 + """An unknown activity type. This should generally not happen.""" + playing = 0 + """A "Playing" activity type.""" + streaming = 1 + """A "Streaming" activity type.""" + listening = 2 + """A "Listening" activity type.""" + watching = 3 + """A "Watching" activity type.""" + custom = 4 + """A custom activity type.""" + competing = 5 + """A competing activity type. + + .. versionadded:: 1.5 + """ + + def __int__(self) -> int: + return self.value + + +class TeamMembershipState(Enum): + """Represents the membership state of a team member retrieved through :func:`Client.application_info`. + + .. versionadded:: 1.3 + """ + + invited = 1 + """Represents an invited member.""" + accepted = 2 + """Represents a member currently in the team.""" + + +class TeamMemberRole(Enum): + """Represents the role of a team member retrieved through :func:`Client.application_info`. + + .. versionadded:: 2.10 + """ + + admin = "admin" + """Admins have the most permissions. An admin can only take destructive actions + on the team or team-owned apps if they are the team owner. + """ + developer = "developer" + """Developers can access information about a team and team-owned applications, + and take limited actions on them, like configuring interaction + endpoints or resetting the bot token. + """ + read_only = "read_only" + """Read-only members can access information about a team and team-owned applications.""" + + def __str__(self) -> str: + return self.name + + +class WebhookType(Enum): + """Represents the type of webhook that can be received. + + .. versionadded:: 1.3 + """ + + incoming = 1 + """Represents a webhook that can post messages to channels with a token.""" + channel_follower = 2 + """Represents a webhook that is internally managed by Discord, used for following channels.""" + application = 3 + """Represents a webhook that is used for interactions or applications. + + .. versionadded:: 2.0 + """ + + +class ExpireBehaviour(Enum): + """Represents the behaviour the :class:`Integration` should perform + when a user's subscription has finished. + + There is an alias for this called ``ExpireBehavior``. + + .. versionadded:: 1.4 + """ + + remove_role = 0 + """This will remove the :attr:`StreamIntegration.role` from the user + when their subscription is finished. + """ + kick = 1 + """This will kick the user when their subscription is finished.""" + + +ExpireBehavior = ExpireBehaviour + + +class StickerType(Enum): + """Represents the type of sticker. + + .. versionadded:: 2.0 + """ + + standard = 1 + """Represents a standard sticker that all users can use.""" + guild = 2 + """Represents a custom sticker created in a guild.""" + + +class StickerFormatType(Enum): + """Represents the type of sticker images. + + .. versionadded:: 1.6 + """ + + png = 1 + """Represents a sticker with a png image.""" + apng = 2 + """Represents a sticker with an apng image.""" + lottie = 3 + """Represents a sticker with a lottie image.""" + gif = 4 + """Represents a sticker with a gif image. + + .. versionadded:: 2.8 + """ + + @property + def file_extension(self) -> str: + return STICKER_FORMAT_LOOKUP[self] + + +STICKER_FORMAT_LOOKUP: Dict[StickerFormatType, str] = { + StickerFormatType.png: "png", + StickerFormatType.apng: "png", + StickerFormatType.lottie: "json", + StickerFormatType.gif: "gif", +} + + +class InviteType(Enum): + """Represents the type of an invite. + + .. versionadded:: 2.10 + """ + + guild = 0 + """Represents an invite to a guild.""" + group_dm = 1 + """Represents an invite to a group channel.""" + friend = 2 + """Represents a friend invite.""" + + +class InviteTarget(Enum): + """Represents the invite type for voice channel invites. + + .. versionadded:: 2.0 + """ + + unknown = 0 + """The invite doesn't target anyone or anything.""" + stream = 1 + """A stream invite that targets a user.""" + embedded_application = 2 + """A stream invite that targets an embedded application.""" + + +class InteractionType(Enum): + """Specifies the type of :class:`Interaction`. + + .. versionadded:: 2.0 + """ + + ping = 1 + """Represents Discord pinging to see if the interaction response server is alive.""" + application_command = 2 + """Represents an application command interaction.""" + component = 3 + """Represents a component based interaction, i.e. using the Discord Bot UI Kit.""" + application_command_autocomplete = 4 + """Represents an application command autocomplete interaction.""" + modal_submit = 5 + """Represents a modal submit interaction.""" + + +class InteractionResponseType(Enum): + """Specifies the response type for the interaction. + + .. versionadded:: 2.0 + """ + + pong = 1 + """Pongs the interaction when given a ping. + + See also :meth:`InteractionResponse.pong`. + """ + channel_message = 4 + """Responds to the interaction with a message. + + See also :meth:`InteractionResponse.send_message`. + """ + deferred_channel_message = 5 + """Responds to the interaction with a message at a later time. + + See also :meth:`InteractionResponse.defer`. + """ + deferred_message_update = 6 + """Acknowledges the component interaction with a promise that + the message will update later (though there is no need to actually update the message). + + See also :meth:`InteractionResponse.defer`. + """ + message_update = 7 + """Responds to the interaction by editing the message. + + See also :meth:`InteractionResponse.edit_message`. + """ + application_command_autocomplete_result = 8 + """Responds to the autocomplete interaction with suggested choices. + + See also :meth:`InteractionResponse.autocomplete`. + """ + modal = 9 + """Responds to the interaction by displaying a modal. + + See also :meth:`InteractionResponse.send_modal`. + + .. versionadded:: 2.4 + """ + premium_required = 10 + """Responds to the interaction with a message containing an upgrade button. + Only available for applications with monetization enabled. + + See also :meth:`InteractionResponse.require_premium`. + + .. versionadded:: 2.10 + + .. deprecated:: 2.11 + Use premium buttons (:class:`ui.Button` with :attr:`~ui.Button.sku_id`) instead. + """ + + +class VideoQualityMode(Enum): + """Represents the camera video quality mode for voice channel participants. + + .. versionadded:: 2.0 + """ + + auto = 1 + """Represents auto camera video quality.""" + full = 2 + """Represents full camera video quality.""" + + def __int__(self) -> int: + return self.value + + +class ComponentType(Enum): + """Represents the type of component. + + .. versionadded:: 2.0 + """ + + action_row = 1 + """Represents the group component which holds different components in a row.""" + button = 2 + """Represents a button component.""" + string_select = 3 + """Represents a string select component. + + .. versionadded:: 2.7 + """ + select = 3 # backwards compatibility + """An alias of :attr:`string_select`.""" + text_input = 4 + """Represents a text input component.""" + user_select = 5 + """Represents a user select component. + + .. versionadded:: 2.7 + """ + role_select = 6 + """Represents a role select component. + + .. versionadded:: 2.7 + """ + mentionable_select = 7 + """Represents a mentionable (user/member/role) select component. + + .. versionadded:: 2.7 + """ + channel_select = 8 + """Represents a channel select component. + + .. versionadded:: 2.7 + """ + section = 9 + """Represents a Components V2 section component. + + .. versionadded:: 2.11 + """ + text_display = 10 + """Represents a Components V2 text display component. + + .. versionadded:: 2.11 + """ + thumbnail = 11 + """Represents a Components V2 thumbnail component. + + .. versionadded:: 2.11 + """ + media_gallery = 12 + """Represents a Components V2 media gallery component. + + .. versionadded:: 2.11 + """ + file = 13 + """Represents a Components V2 file component. + + .. versionadded:: 2.11 + """ + separator = 14 + """Represents a Components V2 separator component. + + .. versionadded:: 2.11 + """ + container = 17 + """Represents a Components V2 container component. + + .. versionadded:: 2.11 + """ + label = 18 + """Represents a label component. + + .. versionadded:: 2.11 + """ + + def __int__(self) -> int: + return self.value + + +class ButtonStyle(Enum): + """Represents the style of the button component. + + .. versionadded:: 2.0 + """ + + primary = 1 + """Represents a blurple button for the primary action.""" + secondary = 2 + """Represents a grey button for the secondary action.""" + success = 3 + """Represents a green button for a successful action.""" + danger = 4 + """Represents a red button for a dangerous action.""" + link = 5 + """Represents a link button.""" + premium = 6 + """Represents a premium/SKU button. + + .. versionadded:: 2.11 + """ + + # Aliases + blurple = 1 + """An alias for :attr:`primary`.""" + grey = 2 + """An alias for :attr:`secondary`.""" + gray = 2 + """An alias for :attr:`secondary`.""" + green = 3 + """An alias for :attr:`success`.""" + red = 4 + """An alias for :attr:`danger`.""" + url = 5 + """An alias for :attr:`link`.""" + sku = 6 + """An alias for :attr:`premium`. + + .. versionadded:: 2.11 + """ + + def __int__(self) -> int: + return self.value + + +class TextInputStyle(Enum): + """Represents a style of the text input component. + + .. versionadded:: 2.4 + """ + + short = 1 + """Represents a single-line text input component.""" + paragraph = 2 + """Represents a multi-line text input component.""" + + # Aliases + single_line = 1 + """An alias for :attr:`short`.""" + multi_line = 2 + """An alias for :attr:`paragraph`.""" + long = 2 + """An alias for :attr:`paragraph`.""" + + def __int__(self) -> int: + return self.value + + +class SelectDefaultValueType(Enum): + """Represents the type of a :class:`SelectDefaultValue`. + + .. versionadded:: 2.10 + """ + + user = "user" + """Represents a user/member.""" + role = "role" + """Represents a role.""" + channel = "channel" + """Represents a channel.""" + + def __str__(self) -> str: + return self.value + + +class ApplicationCommandType(Enum): + """Represents the type of an application command. + + .. versionadded:: 2.1 + """ + + chat_input = 1 + """Represents a slash command.""" + user = 2 + """Represents a user command from the context menu.""" + message = 3 + """Represents a message command from the context menu.""" + + +class ApplicationCommandPermissionType(Enum): + """Represents the type of a permission of an application command. + + .. versionadded:: 2.5 + """ + + role = 1 + """Represents a permission that affects roles.""" + user = 2 + """Represents a permission that affects users.""" + channel = 3 + """Represents a permission that affects channels.""" + + def __int__(self) -> int: + return self.value + + +class OptionType(Enum): + """Represents the type of an option. + + .. versionadded:: 2.1 + """ + + sub_command = 1 + """Represents a sub command of the main command or group.""" + sub_command_group = 2 + """Represents a sub command group of the main command.""" + string = 3 + """Represents a string option.""" + integer = 4 + """Represents an integer option.""" + boolean = 5 + """Represents a boolean option.""" + user = 6 + """Represents a user option.""" + channel = 7 + """Represents a channel option.""" + role = 8 + """Represents a role option.""" + mentionable = 9 + """Represents a role + user option.""" + number = 10 + """Represents a float option.""" + attachment = 11 + """Represents an attachment option. + + .. versionadded:: 2.4 + """ + + +class StagePrivacyLevel(Enum): + """Represents a stage instance's privacy level. + + .. versionadded:: 2.0 + """ + + public = 1 + """The stage instance can be joined by external users. + + .. deprecated:: 2.5 + Public stages are no longer supported by discord. + """ + closed = 2 + """The stage instance can only be joined by members of the guild.""" + guild_only = 2 + """Alias for :attr:`.closed`""" + + +class NSFWLevel(Enum, comparable=True): + """Represents the NSFW level of a guild. + + .. versionadded:: 2.0 + + .. collapse:: operations + + .. describe:: x == y + + Checks if two NSFW levels are equal. + .. describe:: x != y + + Checks if two NSFW levels are not equal. + .. describe:: x > y + + Checks if an NSFW level is higher than another. + .. describe:: x < y + + Checks if an NSFW level is lower than another. + .. describe:: x >= y + + Checks if an NSFW level is higher or equal to another. + .. describe:: x <= y + + Checks if an NSFW level is lower or equal to another. + """ + + default = 0 + """The guild has not been categorised yet.""" + explicit = 1 + """The guild contains NSFW content.""" + safe = 2 + """The guild does not contain any NSFW content.""" + age_restricted = 3 + """The guild may contain NSFW content.""" + + +class GuildScheduledEventEntityType(Enum): + """Represents the type of a guild scheduled event entity. + + .. versionadded:: 2.3 + """ + + stage_instance = 1 + """The guild scheduled event will take place in a stage channel.""" + voice = 2 + """The guild scheduled event will take place in a voice channel.""" + external = 3 + """The guild scheduled event will take place in a custom location.""" + + +class GuildScheduledEventStatus(Enum): + """Represents the status of a guild scheduled event. + + .. versionadded:: 2.3 + """ + + scheduled = 1 + """Represents a scheduled event.""" + active = 2 + """Represents an active event.""" + completed = 3 + """Represents a completed event.""" + canceled = 4 + """Represents a canceled event.""" + cancelled = 4 + """An alias for :attr:`canceled`. + + .. versionadded:: 2.6 + """ + + +class GuildScheduledEventPrivacyLevel(Enum): + """Represents the privacy level of a guild scheduled event. + + .. versionadded:: 2.3 + """ + + guild_only = 2 + """The guild scheduled event is only for a specific guild.""" + + +class ThreadArchiveDuration(Enum): + """Represents the automatic archive duration of a thread in minutes. + + .. versionadded:: 2.3 + """ + + hour = 60 + """The thread will archive after an hour of inactivity.""" + day = 1440 + """The thread will archive after a day of inactivity.""" + three_days = 4320 + """The thread will archive after three days of inactivity.""" + week = 10080 + """The thread will archive after a week of inactivity.""" + + def __int__(self) -> int: + return self.value + + +class WidgetStyle(Enum): + """Represents the supported widget image styles. + + .. versionadded:: 2.5 + """ + + shield = "shield" + """A shield style image with a Discord icon and the online member count.""" + banner1 = "banner1" + """A large image with guild icon, name and online member count and a footer.""" + banner2 = "banner2" + """A small image with guild icon, name and online member count.""" + banner3 = "banner3" + """A large image with guild icon, name and online member count and a footer, + with a "Chat Now" label on the right. + """ + banner4 = "banner4" + """A large image with a large Discord logo, guild icon, name and online member count, + with a "Join My Server" label at the bottom. + """ + + def __str__(self) -> str: + return self.value + + +# reference: https://discord.com/developers/docs/reference#locales +class Locale(Enum): + """Represents supported locales by Discord. + + .. versionadded:: 2.5 + """ + + bg = "bg" + """The ``bg`` (Bulgarian) locale.""" + cs = "cs" + """The ``cs`` (Czech) locale.""" + da = "da" + """The ``da`` (Danish) locale.""" + de = "de" + """The ``de`` (German) locale.""" + el = "el" + """The ``el`` (Greek) locale.""" + en_GB = "en-GB" + """The ``en-GB`` (English, UK) locale.""" + en_US = "en-US" + """The ``en-US`` (English, US) locale.""" + es_ES = "es-ES" + """The ``es-ES`` (Spanish) locale.""" + es_LATAM = "es-419" + """The ``es-419`` (Spanish, LATAM) locale. + + .. versionadded:: 2.10 + """ + fi = "fi" + """The ``fi`` (Finnish) locale.""" + fr = "fr" + """The ``fr`` (French) locale.""" + hi = "hi" + """The ``hi`` (Hindi) locale.""" + hr = "hr" + """The ``hr`` (Croatian) locale.""" + hu = "hu" + """The ``hu`` (Hungarian) locale.""" + id = "id" + """The ``id`` (Indonesian) locale. + + .. versionadded:: 2.8 + """ + it = "it" + """The ``it`` (Italian) locale.""" + ja = "ja" + """The ``ja`` (Japanese) locale.""" + ko = "ko" + """The ``ko`` (Korean) locale.""" + lt = "lt" + """The ``lt`` (Lithuanian) locale.""" + nl = "nl" + """The ``nl`` (Dutch) locale.""" + no = "no" + """The ``no`` (Norwegian) locale.""" + pl = "pl" + """The ``pl`` (Polish) locale.""" + pt_BR = "pt-BR" + """The ``pt-BR`` (Portuguese) locale.""" + ro = "ro" + """The ``ro`` (Romanian) locale.""" + ru = "ru" + """The ``ru`` (Russian) locale.""" + sv_SE = "sv-SE" + """The ``sv-SE`` (Swedish) locale.""" + th = "th" + """The ``th`` (Thai) locale.""" + tr = "tr" + """The ``tr`` (Turkish) locale.""" + uk = "uk" + """The ``uk`` (Ukrainian) locale.""" + vi = "vi" + """The ``vi`` (Vietnamese) locale.""" + zh_CN = "zh-CN" + """The ``zh-CN`` (Chinese, China) locale.""" + zh_TW = "zh-TW" + """The ``zh-TW`` (Chinese, Taiwan) locale.""" + + def __str__(self) -> str: + return self.value + + +class AutoModActionType(Enum): + """Represents the type of action an auto moderation rule will take upon execution. + + .. versionadded:: 2.6 + """ + + block_message = 1 + """The rule will prevent matching messages from being posted.""" + send_alert_message = 2 + """The rule will send an alert to a specified channel.""" + timeout = 3 + """The rule will timeout the user that sent the message. + + .. note:: + This action type is only available for rules with trigger type + :attr:`~AutoModTriggerType.keyword` or :attr:`~AutoModTriggerType.mention_spam`, + and :attr:`~Permissions.moderate_members` permissions are required to use it. + """ + + +class AutoModEventType(Enum): + """Represents the type of event/context an auto moderation rule will be checked in. + + .. versionadded:: 2.6 + """ + + message_send = 1 + """The rule will apply when a member sends or edits a message in the guild.""" + + +class AutoModTriggerType(Enum): + """Represents the type of content that can trigger an auto moderation rule. + + .. versionadded:: 2.6 + + .. versionchanged:: 2.9 + Removed obsolete ``harmful_link`` type. + """ + + keyword = 1 + """The rule will filter messages based on a custom keyword list. + + This trigger type requires additional :class:`metadata `. + """ + + if not TYPE_CHECKING: + harmful_link = 2 # obsolete/deprecated + + spam = 3 + """The rule will filter messages suspected of being spam.""" + keyword_preset = 4 + """The rule will filter messages based on predefined lists containing commonly flagged words. + + This trigger type requires additional :class:`metadata `. + """ + mention_spam = 5 + """The rule will filter messages based on the number of member/role mentions they contain. + + This trigger type requires additional :class:`metadata `. + """ + + +class ThreadSortOrder(Enum): + """Represents the sort order of threads in a :class:`ForumChannel` or :class:`MediaChannel`. + + .. versionadded:: 2.6 + """ + + latest_activity = 0 + """Sort forum threads by activity.""" + creation_date = 1 + """Sort forum threads by creation date/time (from newest to oldest).""" + + +class ThreadLayout(Enum): + """Represents the layout of threads in :class:`ForumChannel`\\s. + + .. versionadded:: 2.8 + """ + + not_set = 0 + """No preferred layout has been set.""" + list_view = 1 + """Display forum threads in a text-focused list.""" + gallery_view = 2 + """Display forum threads in a media-focused collection of tiles.""" + + +class Event(Enum): + """ + Represents all the events of the library. + + These offer to register listeners/events in a more pythonic way; additionally autocompletion and documentation are both supported. + + .. versionadded:: 2.8 + """ + + connect = "connect" + """Called when the client has successfully connected to Discord. + Represents the :func:`on_connect` event. + """ + disconnect = "disconnect" + """Called when the client has disconnected from Discord, or a connection attempt to Discord has failed. + Represents the :func:`on_disconnect` event. + """ + error = "error" + """Called when an uncaught exception occurred. + Represents the :func:`on_error` event. + """ + gateway_error = "gateway_error" + """Called when a known gateway event cannot be parsed. + Represents the :func:`on_gateway_error` event. + """ + ready = "ready" + """Called when the client is done preparing the data received from Discord. + Represents the :func:`on_ready` event. + """ + resumed = "resumed" + """Called when the client has resumed a session. + Represents the :func:`on_resumed` event. + """ + shard_connect = "shard_connect" + """Called when a shard has successfully connected to Discord. + Represents the :func:`on_shard_connect` event. + """ + shard_disconnect = "shard_disconnect" + """Called when a shard has disconnected from Discord. + Represents the :func:`on_shard_disconnect` event. + """ + shard_ready = "shard_ready" + """Called when a shard has become ready. + Represents the :func:`on_shard_ready` event. + """ + shard_resumed = "shard_resumed" + """Called when a shard has resumed a session. + Represents the :func:`on_shard_resumed` event. + """ + socket_event_type = "socket_event_type" + """Called whenever a websocket event is received from the WebSocket. + Represents the :func:`on_socket_event_type` event. + """ + socket_raw_receive = "socket_raw_receive" + """Called whenever a message is completely received from the WebSocket, before it's processed and parsed. + Represents the :func:`on_socket_raw_receive` event. + """ + socket_raw_send = "socket_raw_send" + """Called whenever a send operation is done on the WebSocket before the message is sent. + Represents the :func:`on_socket_raw_send` event. + """ + guild_channel_create = "guild_channel_create" + """Called whenever a guild channel is created. + Represents the :func:`on_guild_channel_create` event. + """ + guild_channel_update = "guild_channel_update" + """Called whenever a guild channel is updated. + Represents the :func:`on_guild_channel_update` event. + """ + guild_channel_delete = "guild_channel_delete" + """Called whenever a guild channel is deleted. + Represents the :func:`on_guild_channel_delete` event. + """ + guild_channel_pins_update = "guild_channel_pins_update" + """Called whenever a message is pinned or unpinned from a guild channel. + Represents the :func:`on_guild_channel_pins_update` event. + """ + invite_create = "invite_create" + """Called when an :class:`Invite` is created. + Represents the :func:`.on_invite_create` event. + """ + invite_delete = "invite_delete" + """Called when an Invite is deleted. + Represents the :func:`.on_invite_delete` event. + """ + private_channel_update = "private_channel_update" + """Called whenever a private group DM is updated. + Represents the :func:`on_private_channel_update` event. + """ + private_channel_pins_update = "private_channel_pins_update" + """Called whenever a message is pinned or unpinned from a private channel. + Represents the :func:`on_private_channel_pins_update` event. + """ + webhooks_update = "webhooks_update" + """Called whenever a webhook is created, modified, or removed from a guild channel. + Represents the :func:`on_webhooks_update` event. + """ + thread_create = "thread_create" + """Called whenever a thread is created. + Represents the :func:`on_thread_create` event. + """ + thread_update = "thread_update" + """Called when a thread is updated. + Represents the :func:`on_thread_update` event. + """ + thread_delete = "thread_delete" + """Called when a thread is deleted. + Represents the :func:`on_thread_delete` event. + """ + thread_join = "thread_join" + """Called whenever the bot joins a thread or gets access to a thread. + Represents the :func:`on_thread_join` event. + """ + thread_remove = "thread_remove" + """Called whenever a thread is removed. This is different from a thread being deleted. + Represents the :func:`on_thread_remove` event. + """ + thread_member_join = "thread_member_join" + """Called when a `ThreadMember` joins a `Thread`. + Represents the :func:`on_thread_member_join` event. + """ + thread_member_remove = "thread_member_remove" + """Called when a `ThreadMember` leaves a `Thread`. + Represents the :func:`on_thread_member_remove` event. + """ + raw_thread_member_remove = "raw_thread_member_remove" + """Called when a `ThreadMember` leaves `Thread` regardless of the thread member cache. + Represents the :func:`on_raw_thread_member_remove` event. + """ + raw_thread_update = "raw_thread_update" + """Called whenever a thread is updated regardless of the state of the internal thread cache. + Represents the :func:`on_raw_thread_update` event. + """ + raw_thread_delete = "raw_thread_delete" + """Called whenever a thread is deleted regardless of the state of the internal thread cache. + Represents the :func:`on_raw_thread_delete` event. + """ + guild_join = "guild_join" + """Called when a `Guild` is either created by the `Client` or when the Client joins a guild. + Represents the :func:`on_guild_join` event. + """ + guild_remove = "guild_remove" + """Called when a `Guild` is removed from the :class:`Client`. + Represents the :func:`on_guild_remove` event. + """ + guild_update = "guild_update" + """Called when a `Guild` updates. + Represents the :func:`on_guild_update` event. + """ + guild_available = "guild_available" + """Called when a guild becomes available. + Represents the :func:`on_guild_available` event. + """ + guild_unavailable = "guild_unavailable" + """Called when a guild becomes unavailable. + Represents the :func:`on_guild_unavailable` event. + """ + guild_role_create = "guild_role_create" + """Called when a `Guild` creates a new `Role`. + Represents the :func:`on_guild_role_create` event. + """ + guild_role_delete = "guild_role_delete" + """Called when a `Guild` deletes a `Role`. + Represents the :func:`on_guild_role_delete` event. + """ + guild_role_update = "guild_role_update" + """Called when a `Guild` updates a `Role`. + Represents the :func:`on_guild_role_update` event. + """ + guild_emojis_update = "guild_emojis_update" + """Called when a `Guild` adds or removes `Emoji`. + Represents the :func:`on_guild_emojis_update` event. + """ + guild_stickers_update = "guild_stickers_update" + """Called when a `Guild` updates its stickers. + Represents the :func:`on_guild_stickers_update` event. + """ + guild_soundboard_sounds_update = "guild_soundboard_sounds_update" + """Called when a `Guild` updates its soundboard sounds. + Represents the :func:`on_guild_soundboard_sounds_update` event. + + .. versionadded:: 2.10 + """ + guild_integrations_update = "guild_integrations_update" + """Called whenever an integration is created, modified, or removed from a guild. + Represents the :func:`on_guild_integrations_update` event. + """ + guild_scheduled_event_create = "guild_scheduled_event_create" + """Called when a guild scheduled event is created. + Represents the :func:`on_guild_scheduled_event_create` event. + """ + guild_scheduled_event_update = "guild_scheduled_event_update" + """Called when a guild scheduled event is updated. + Represents the :func:`on_guild_scheduled_event_update` event. + """ + guild_scheduled_event_delete = "guild_scheduled_event_delete" + """Called when a guild scheduled event is deleted. + Represents the :func:`on_guild_scheduled_event_delete` event. + """ + guild_scheduled_event_subscribe = "guild_scheduled_event_subscribe" + """Called when a user subscribes from a guild scheduled event. + Represents the :func:`on_guild_scheduled_event_subscribe` event. + """ + guild_scheduled_event_unsubscribe = "guild_scheduled_event_unsubscribe" + """Called when a user unsubscribes from a guild scheduled event. + Represents the :func:`on_guild_scheduled_event_unsubscribe` event. + """ + raw_guild_scheduled_event_subscribe = "raw_guild_scheduled_event_subscribe" + """Called when a user subscribes from a guild scheduled event regardless of the guild scheduled event cache. + Represents the :func:`on_raw_guild_scheduled_event_subscribe` event. + """ + raw_guild_scheduled_event_unsubscribe = "raw_guild_scheduled_event_unsubscribe" + """Called when a user subscribes to or unsubscribes from a guild scheduled event regardless of the guild scheduled event cache. + Represents the :func:`on_raw_guild_scheduled_event_unsubscribe` event. + """ + application_command_permissions_update = "application_command_permissions_update" + """Called when the permissions of an application command or the application-wide command permissions are updated. + Represents the :func:`on_application_command_permissions_update` event. + """ + automod_action_execution = "automod_action_execution" + """Called when an auto moderation action is executed due to a rule triggering for a particular event. + Represents the :func:`on_automod_action_execution` event. + """ + automod_rule_create = "automod_rule_create" + """Called when an `AutoModRule` is created. + Represents the :func:`on_automod_rule_create` event. + """ + automod_rule_update = "automod_rule_update" + """Called when an `AutoModRule` is updated. + Represents the :func:`on_automod_rule_update` event. + """ + automod_rule_delete = "automod_rule_delete" + """Called when an `AutoModRule` is deleted. + Represents the :func:`on_automod_rule_delete` event. + """ + audit_log_entry_create = "audit_log_entry_create" + """Called when an audit log entry is created. + Represents the :func:`on_audit_log_entry_create` event. + """ + integration_create = "integration_create" + """Called when an integration is created. + Represents the :func:`on_integration_create` event. + """ + integration_update = "integration_update" + """Called when an integration is updated. + Represents the :func:`on_integration_update` event. + """ + raw_integration_delete = "raw_integration_delete" + """Called when an integration is deleted. + Represents the :func:`on_raw_integration_delete` event. + """ + member_join = "member_join" + """Called when a `Member` joins a `Guild`. + Represents the :func:`on_member_join` event. + """ + member_remove = "member_remove" + """Called when a `Member` leaves a `Guild`. + Represents the :func:`on_member_remove` event. + """ + member_update = "member_update" + """Called when a `Member` is updated in a `Guild`. + Represents the :func:`on_member_update` event. + """ + raw_member_remove = "raw_member_remove" + """Called when a member leaves a `Guild` regardless of the member cache. + Represents the :func:`on_raw_member_remove` event. + """ + raw_member_update = "raw_member_update" + """Called when a `Member` is updated in a `Guild` regardless of the member cache. + Represents the :func:`on_raw_member_update` event. + """ + member_ban = "member_ban" + """Called when user gets banned from a `Guild`. + Represents the :func:`on_member_ban` event. + """ + member_unban = "member_unban" + """Called when a `User` gets unbanned from a `Guild`. + Represents the :func:`on_member_unban` event. + """ + presence_update = "presence_update" + """Called when a `Member` updates their presence. + Represents the :func:`on_presence_update` event. + """ + user_update = "user_update" + """Called when a `User` is updated. + Represents the :func:`on_user_update` event. + """ + voice_state_update = "voice_state_update" + """Called when a `Member` changes their `VoiceState`. + Represents the :func:`on_voice_state_update` event. + """ + voice_channel_effect = "voice_channel_effect" + """Called when a `Member` sends an effect in a voice channel the bot is connected to. + Represents the :func:`on_voice_channel_effect` event. + + .. versionadded:: 2.10 + """ + raw_voice_channel_effect = "raw_voice_channel_effect" + """Called when a `Member` sends an effect in a voice channel the bot is connected to, + regardless of the member cache. + Represents the :func:`on_raw_voice_channel_effect` event. + + .. versionadded:: 2.10 + """ + stage_instance_create = "stage_instance_create" + """Called when a `StageInstance` is created for a `StageChannel`. + Represents the :func:`on_stage_instance_create` event. + """ + stage_instance_delete = "stage_instance_delete" + """Called when a `StageInstance` is deleted for a `StageChannel`. + Represents the :func:`on_stage_instance_delete` event. + """ + stage_instance_update = "stage_instance_update" + """Called when a `StageInstance` is updated. + Represents the :func:`on_stage_instance_update` event. + """ + application_command = "application_command" + """Called when an application command is invoked. + Represents the :func:`on_application_command` event. + """ + application_command_autocomplete = "application_command_autocomplete" + """Called when an application command autocomplete is called. + Represents the :func:`on_application_command_autocomplete` event. + """ + button_click = "button_click" + """Called when a button is clicked. + Represents the :func:`on_button_click` event. + """ + dropdown = "dropdown" + """Called when a select menu is clicked. + Represents the :func:`on_dropdown` event. + """ + interaction = "interaction" + """Called when an interaction happened. + Represents the :func:`on_interaction` event. + """ + message_interaction = "message_interaction" + """Called when a message interaction happened. + Represents the :func:`on_message_interaction` event. + """ + modal_submit = "modal_submit" + """Called when a modal is submitted. + Represents the :func:`on_modal_submit` event. + """ + message = "message" + """Called when a `Message` is created and sent. + Represents the :func:`on_message` event. + """ + message_edit = "message_edit" + """Called when a `Message` receives an update event. + Represents the :func:`on_message_edit` event. + """ + message_delete = "message_delete" + """Called when a message is deleted. + Represents the :func:`on_message_delete` event. + """ + bulk_message_delete = "bulk_message_delete" + """Called when messages are bulk deleted. + Represents the :func:`on_bulk_message_delete` event. + """ + poll_vote_add = "poll_vote_add" + """Called when a vote is added on a `Poll`. + Represents the :func:`on_poll_vote_add` event. + """ + poll_vote_remove = "poll_vote_remove" + """Called when a vote is removed from a `Poll`. + Represents the :func:`on_poll_vote_remove` event. + """ + raw_message_edit = "raw_message_edit" + """Called when a message is edited regardless of the state of the internal message cache. + Represents the :func:`on_raw_message_edit` event. + """ + raw_message_delete = "raw_message_delete" + """Called when a message is deleted regardless of the message being in the internal message cache or not. + Represents the :func:`on_raw_message_delete` event. + """ + raw_bulk_message_delete = "raw_bulk_message_delete" + """Called when a bulk delete is triggered regardless of the messages being in the internal message cache or not. + Represents the :func:`on_raw_bulk_message_delete` event. + """ + raw_poll_vote_add = "raw_poll_vote_add" + """Called when a vote is added on a `Poll` regardless of the internal message cache. + Represents the :func:`on_raw_poll_vote_add` event. + """ + raw_poll_vote_remove = "raw_poll_vote_remove" + """Called when a vote is removed from a `Poll` regardless of the internal message cache. + Represents the :func:`on_raw_poll_vote_remove` event. + """ + reaction_add = "reaction_add" + """Called when a message has a reaction added to it. + Represents the :func:`on_reaction_add` event. + """ + reaction_remove = "reaction_remove" + """Called when a message has a reaction removed from it. + Represents the :func:`on_reaction_remove` event. + """ + reaction_clear = "reaction_clear" + """Called when a message has all its reactions removed from it. + Represents the :func:`on_reaction_clear` event. + """ + reaction_clear_emoji = "reaction_clear_emoji" + """Called when a message has a specific reaction removed from it. + Represents the :func:`on_reaction_clear_emoji` event. + """ + raw_presence_update = "raw_presence_update" + """Called when a user's presence changes regardless of the state of the internal member cache. + Represents the :func:`on_raw_presence_update` event. + """ + raw_reaction_add = "raw_reaction_add" + """Called when a message has a reaction added regardless of the state of the internal message cache. + Represents the :func:`on_raw_reaction_add` event. + """ + raw_reaction_remove = "raw_reaction_remove" + """Called when a message has a reaction removed regardless of the state of the internal message cache. + Represents the :func:`on_raw_reaction_remove` event. + """ + raw_reaction_clear = "raw_reaction_clear" + """Called when a message has all its reactions removed regardless of the state of the internal message cache. + Represents the :func:`on_raw_reaction_clear` event. + """ + raw_reaction_clear_emoji = "raw_reaction_clear_emoji" + """Called when a message has a specific reaction removed from it regardless of the state of the internal message cache. + Represents the :func:`on_raw_reaction_clear_emoji` event. + """ + typing = "typing" + """Called when someone begins typing a message. + Represents the :func:`on_typing` event. + """ + raw_typing = "raw_typing" + """Called when someone begins typing a message regardless of whether `Intents.members` and `Intents.guilds` are enabled. + Represents the :func:`on_raw_typing` event. + """ + entitlement_create = "entitlement_create" + """Called when a user subscribes to an SKU, creating a new :class:`Entitlement`. + Represents the :func:`on_entitlement_create` event. + + .. versionadded:: 2.10 + """ + entitlement_update = "entitlement_update" + """Called when a user's subscription renews. + Represents the :func:`on_entitlement_update` event. + + .. versionadded:: 2.10 + """ + entitlement_delete = "entitlement_delete" + """Called when a user's entitlement is deleted. + Represents the :func:`on_entitlement_delete` event.""" + subscription_create = "subscription_create" + """Called when a subscription for a premium app is created. + Represents the :func:`on_subscription_create` event. + + .. versionadded:: 2.10 + """ + subscription_update = "subscription_update" + """Called when a subscription for a premium app is updated. + Represents the :func:`on_subscription_update` event. + + .. versionadded:: 2.10 + """ + subscription_delete = "subscription_delete" + """Called when a subscription for a premium app is deleted. + Represents the :func:`on_subscription_delete` event. + + .. versionadded:: 2.10 + """ + # ext.commands events + command = "command" + """Called when a command is found and is about to be invoked. + Represents the :func:`.on_command` event. + """ + command_completion = "command_completion" + """Called when a command has completed its invocation. + Represents the :func:`.on_command_completion` event. + """ + command_error = "command_error" + """Called when an error is raised inside a command either through user input error, check failure, or an error in your own code. + Represents the :func:`.on_command_error` event. + """ + slash_command = "slash_command" + """Called when a slash command is found and is about to be invoked. + Represents the :func:`.on_slash_command` event. + """ + slash_command_completion = "slash_command_completion" + """Called when a slash command has completed its invocation. + Represents the :func:`.on_slash_command_completion` event. + """ + slash_command_error = "slash_command_error" + """Called when an error is raised inside a slash command either through user input error, check failure, or an error in your own code. + Represents the :func:`.on_slash_command_error` event. + """ + user_command = "user_command" + """Called when a user command is found and is about to be invoked. + Represents the :func:`.on_user_command` event. + """ + user_command_completion = "user_command_completion" + """Called when a user command has completed its invocation. + Represents the :func:`.on_user_command_completion` event. + """ + user_command_error = "user_command_error" + """Called when an error is raised inside a user command either through check failure, or an error in your own code. + Represents the :func:`.on_user_command_error` event. + """ + message_command = "message_command" + """Called when a message command is found and is about to be invoked. + Represents the :func:`.on_message_command` event. + """ + message_command_completion = "message_command_completion" + """Called when a message command has completed its invocation. + Represents the :func:`.on_message_command_completion` event. + """ + message_command_error = "message_command_error" + """Called when an error is raised inside a message command either through check failure, or an error in your own code. + Represents the :func:`.on_message_command_error` event. + """ + + +class ApplicationRoleConnectionMetadataType(Enum): + """Represents the type of a role connection metadata value. + + These offer comparison operations, which allow guilds to configure role requirements + based on the metadata value for each user and a guild-specified configured value. + + .. versionadded:: 2.8 + """ + + integer_less_than_or_equal = 1 + """The metadata value (``integer``) is less than or equal to the guild's configured value.""" + integer_greater_than_or_equal = 2 + """The metadata value (``integer``) is greater than or equal to the guild's configured value.""" + integer_equal = 3 + """The metadata value (``integer``) is equal to the guild's configured value.""" + integer_not_equal = 4 + """The metadata value (``integer``) is not equal to the guild's configured value.""" + datetime_less_than_or_equal = 5 + """The metadata value (``ISO8601 string``) is less than or equal to the guild's configured value (``integer``; days before current date).""" + datetime_greater_than_or_equal = 6 + """The metadata value (``ISO8601 string``) is greater than or equal to the guild's configured value (``integer``; days before current date).""" + boolean_equal = 7 + """The metadata value (``integer``) is equal to the guild's configured value.""" + boolean_not_equal = 8 + """The metadata value (``integer``) is not equal to the guild's configured value.""" + + +class ApplicationEventWebhookStatus(Enum): + """Represents the status of an application event webhook. + + .. versionadded:: 2.11 + """ + + disabled = 1 + """Webhook events are disabled by developer.""" + enabled = 2 + """Webhook events are enabled by developer.""" + disabled_by_discord = 3 + """Webhook events are disabled by Discord, usually due to inactivity.""" + + +class OnboardingPromptType(Enum): + """Represents the type of onboarding prompt. + + .. versionadded:: 2.9 + """ + + multiple_choice = 0 + """The prompt is a multiple choice prompt.""" + dropdown = 1 + """The prompt is a dropdown prompt.""" + + +class SKUType(Enum): + """Represents the type of an SKU. + + .. versionadded:: 2.10 + """ + + durable = 2 + """Represents a durable one-time purchase.""" + consumable = 3 + """Represents a consumable one-time purchase.""" + subscription = 5 + """Represents a recurring subscription.""" + subscription_group = 6 + """Represents a system-generated group for each :attr:`subscription` SKU.""" + + +class EntitlementType(Enum): + """Represents the type of an entitlement. + + .. versionadded:: 2.10 + """ + + purchase = 1 + """Represents an entitlement purchased by a user.""" + premium_subscription = 2 + """Represents an entitlement for a Discord Nitro subscription.""" + developer_gift = 3 + """Represents an entitlement gifted by the application developer.""" + test_mode_purchase = 4 + """Represents an entitlement purchased by a developer in application test mode.""" + free_purchase = 5 + """Represents an entitlement granted when the SKU was free.""" + user_gift = 6 + """Represents an entitlement gifted by another user.""" + premium_purchase = 7 + """Represents an entitlement claimed by a user for free as a Discord Nitro subscriber.""" + application_subscription = 8 + """Represents an entitlement for an application subscription.""" + + +class SubscriptionStatus(Enum): + """Represents the status of a subscription. + + .. versionadded:: 2.10 + """ + + active = 0 + """Represents an active Subscription which is scheduled to renew.""" + ending = 1 + """Represents an active Subscription which will not renew.""" + inactive = 2 + """Represents an inactive Subscription which is not being charged.""" + + +class PollLayoutType(Enum): + """Specifies the layout of a :class:`Poll`. + + .. versionadded:: 2.10 + """ + + default = 1 + """The default poll layout type.""" + + +class VoiceChannelEffectAnimationType(Enum): + """The type of an emoji reaction effect animation in a voice channel. + + .. versionadded:: 2.10 + """ + + premium = 0 + """A fun animation, sent by a Nitro subscriber.""" + basic = 1 + """A standard animation.""" + + +class MessageReferenceType(Enum): + """Specifies the type of :class:`MessageReference`. This can be used to determine + if a message is e.g. a reply or a forwarded message. + + .. versionadded:: 2.10 + """ + + default = 0 + """A standard message reference used in message replies.""" + forward = 1 + """Reference used to point to a message at a point in time (forward).""" + + +class SeparatorSpacing(Enum): + """Specifies the size of a :class:`Separator` component's padding. + + .. versionadded:: 2.11 + """ + + small = 1 + """Small spacing.""" + large = 2 + """Large spacing.""" + + +class NameplatePalette(Enum): + """Specifies the palette of a :class:`Nameplate`. + + .. versionadded:: 2.11 + """ + + crimson = "crimson" + """Crimson color palette.""" + berry = "berry" + """Berry color palette.""" + sky = "sky" + """Sky color palette.""" + teal = "teal" + """Teal color palette.""" + forest = "forest" + """Forest color palette.""" + bubble_gum = "bubble_gum" + """Bubble gum color palette.""" + violet = "violet" + """Violet color palette.""" + cobalt = "cobalt" + """Cobalt color palette.""" + clover = "clover" + """Clover color palette.""" + lemon = "lemon" + """Lemon color palette.""" + white = "white" + """White color palette.""" + + +T = TypeVar("T") + + +def create_unknown_value(cls: Type[T], val: Any) -> T: + value_cls = cls._enum_value_cls_ # type: ignore + name = f"unknown_{val}" + return value_cls(name=name, value=val) + + +def try_enum(cls: Type[T], val: Any) -> T: + """A function that tries to turn the value into enum ``cls``. + + If it fails it returns a proxy invalid value instead. + """ + try: + return cls._enum_value_map_[val] # type: ignore + except (KeyError, TypeError, AttributeError): + return create_unknown_value(cls, val) + + +def enum_if_int(cls: Type[T], val: Any) -> T: + """A function that tries to turn the value into enum ``cls``. + + If it fails it returns a proxy invalid value instead. + """ + if not isinstance(val, int): + return val + return try_enum(cls, val) + + +def try_enum_to_int(val: Any) -> Any: + if isinstance(val, int): + return val + try: + return val.value + except Exception: + return val diff --git a/disnake/disnake/errors.py b/disnake/disnake/errors.py new file mode 100644 index 0000000000..1bab2cf8ba --- /dev/null +++ b/disnake/disnake/errors.py @@ -0,0 +1,435 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Union + +if TYPE_CHECKING: + from aiohttp import ClientResponse, ClientWebSocketResponse + from requests import Response + + from .client import SessionStartLimit + from .interactions import Interaction, ModalInteraction + + _ResponseType = Union[ClientResponse, Response] + +__all__ = ( + "DiscordException", + "ClientException", + "NoMoreItems", + "GatewayNotFound", + "HTTPException", + "Forbidden", + "NotFound", + "DiscordServerError", + "InvalidData", + "WebhookTokenMissing", + "LoginFailure", + "SessionStartLimitReached", + "ConnectionClosed", + "PrivilegedIntentsRequired", + "InteractionException", + "InteractionTimedOut", + "InteractionResponded", + "InteractionNotResponded", + "ModalChainNotSupported", + "InteractionNotEditable", + "LocalizationKeyError", +) + + +class DiscordException(Exception): + """Base exception class for disnake. + + Ideally speaking, this could be caught to handle any exceptions raised from this library. + """ + + pass + + +class ClientException(DiscordException): + """Exception that's raised when an operation in the :class:`Client` fails. + + These are usually for exceptions that happened due to user input. + """ + + pass + + +class NoMoreItems(DiscordException): + """Exception that is raised when an async iteration operation has no more items.""" + + pass + + +class GatewayNotFound(DiscordException): + """An exception that is raised when the gateway for Discord could not be found""" + + def __init__(self) -> None: + message = "The gateway to connect to Discord was not found." + super().__init__(message) + + +def _flatten_error_dict(d: Dict[str, Any], key: str = "") -> Dict[str, str]: + items: List[Tuple[str, str]] = [] + for k, v in d.items(): + new_key = f"{key}.{k}" if key else k + + if isinstance(v, dict): + try: + _errors: List[Dict[str, Any]] = v["_errors"] + except KeyError: + items.extend(_flatten_error_dict(v, new_key).items()) + else: + items.append((new_key, " ".join(x.get("message", "") for x in _errors))) + else: + items.append((new_key, v)) + + return dict(items) + + +class HTTPException(DiscordException): + """Exception that's raised when an HTTP request operation fails. + + Attributes + ---------- + response: :class:`aiohttp.ClientResponse` + The response of the failed HTTP request. This is an + instance of :class:`aiohttp.ClientResponse`. In some cases + this could also be a :class:`requests.Response`. + + text: :class:`str` + The text of the error. Could be an empty string. + status: :class:`int` + The status code of the HTTP request. + code: :class:`int` + The Discord specific error code for the failure. + """ + + def __init__( + self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]] + ) -> None: + self.response: _ResponseType = response + self.status: int = response.status # type: ignore + self.code: int + self.text: str + if isinstance(message, dict): + self.code = message.get("code", 0) + base = message.get("message", "") + errors = message.get("errors") + if errors: + errors = _flatten_error_dict(errors) + helpful = "\n".join(f"In {k}: {m}" for k, m in errors.items()) + self.text = base + "\n" + helpful + else: + self.text = base + else: + self.text = message or "" + self.code = 0 + + fmt = "{0.status} {0.reason} (error code: {1})" + if len(self.text): + fmt += ": {2}" + + super().__init__(fmt.format(self.response, self.code, self.text)) + + +class Forbidden(HTTPException): + """Exception that's raised for when status code 403 occurs. + + Subclass of :exc:`HTTPException`. + """ + + pass + + +class NotFound(HTTPException): + """Exception that's raised for when status code 404 occurs. + + Subclass of :exc:`HTTPException`. + """ + + pass + + +class DiscordServerError(HTTPException): + """Exception that's raised for when a 500 range status code occurs. + + Subclass of :exc:`HTTPException`. + + .. versionadded:: 1.5 + """ + + pass + + +class InvalidData(ClientException): + """Exception that's raised when the library encounters unknown + or invalid data from Discord. + """ + + pass + + +class WebhookTokenMissing(DiscordException): + """Exception that's raised when a :class:`Webhook` or :class:`SyncWebhook` is missing a token to make requests with. + + .. versionadded:: 2.6 + """ + + pass + + +class LoginFailure(ClientException): + """Exception that's raised when the :meth:`Client.login` function + fails to log you in from improper credentials or some other misc. + failure. + """ + + pass + + +class SessionStartLimitReached(ClientException): + """Exception that's raised when :meth:`Client.connect` function + fails to connect to Discord due to the session start limit being reached. + + .. versionadded:: 2.6 + + Attributes + ---------- + session_start_limit: :class:`.SessionStartLimit` + The current state of the session start limit. + + """ + + def __init__(self, session_start_limit: SessionStartLimit, requested: int = 1) -> None: + self.session_start_limit: SessionStartLimit = session_start_limit + super().__init__( + f"Daily session start limit has been reached, resets at {self.session_start_limit.reset_time} " + f"Requested {requested} shards, have only {session_start_limit.remaining} remaining." + ) + + +class ConnectionClosed(ClientException): + """Exception that's raised when the gateway connection is + closed for reasons that could not be handled internally. + + Attributes + ---------- + code: :class:`int` + The close code of the websocket. + reason: :class:`str` + The reason provided for the closure. + shard_id: Optional[:class:`int`] + The shard ID that got closed if applicable. + """ + + # https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes + GATEWAY_CLOSE_EVENT_REASONS: ClassVar[Mapping[int, str]] = { + 4000: "Unknown error", + 4001: "Unknown opcode", + 4002: "Decode error", + 4003: "Not authenticated", + 4004: "Authentication failed", + 4005: "Already authenticated", + 4007: "Invalid sequence", + 4008: "Rate limited", + 4009: "Session timed out", + 4010: "Invalid Shard", + 4011: "Sharding required - you are required to shard your connection in order to connect.", + 4012: "Invalid API version", + 4013: "Invalid intents", + 4014: "Disallowed intents - you tried to specify an intent that you have not enabled or are not approved for.", + } + + # https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes + GATEWAY_VOICE_CLOSE_EVENT_REASONS: ClassVar[Mapping[int, str]] = { + **GATEWAY_CLOSE_EVENT_REASONS, + 4002: "Failed to decode payload", + 4006: "Session no longer valid", + 4011: "Server not found", + 4012: "Unknown protocol", + 4014: "Disconnected (you were kicked, the main gateway session was dropped, etc.)", + 4015: "Voice server crashed", + 4016: "Unknown encryption mode", + 4020: "Bad request - you sent a malformed request", + 4021: "Disconnected: Rate Limited", + 4022: "Disconnected: Call Terminated (channel deleted, voice server changed, etc.)", + } + + def __init__( + self, + socket: ClientWebSocketResponse, + *, + shard_id: Optional[int], + code: Optional[int] = None, + voice: bool = False, + ) -> None: + # This exception is just the same exception except + # reconfigured to subclass ClientException for users + self.code: int = code or socket.close_code or -1 + # aiohttp doesn't seem to consistently provide close reason + self.reason: str = self.GATEWAY_CLOSE_EVENT_REASONS.get(self.code, "Unknown reason") + if voice: + self.reason = self.GATEWAY_VOICE_CLOSE_EVENT_REASONS.get(self.code, "Unknown reason") + + self.shard_id: Optional[int] = shard_id + super().__init__( + f"Shard ID {self.shard_id} WebSocket closed with {self.code}: {self.reason}" + ) + + +class PrivilegedIntentsRequired(ClientException): + """Exception that's raised when the gateway is requesting privileged intents + but they're not ticked in the developer page yet. + + Go to https://discord.com/developers/applications/ and enable the intents + that are required. Currently these are as follows: + + - :attr:`Intents.members` + - :attr:`Intents.presences` + - :attr:`Intents.message_content` + + Attributes + ---------- + shard_id: Optional[:class:`int`] + The shard ID that got closed if applicable. + """ + + def __init__(self, shard_id: Optional[int]) -> None: + self.shard_id: Optional[int] = shard_id + msg = ( + f"Shard ID {shard_id} is requesting privileged intents that have not been explicitly enabled in the " + "developer portal. It is recommended to go to https://discord.com/developers/applications/ " + "and explicitly enable the privileged intents within your application's page. If this is not " + "possible, then consider disabling the privileged intents instead." + ) + super().__init__(msg) + + +class InteractionException(ClientException): + """Exception that's raised when an interaction operation fails + + .. versionadded:: 2.0 + + Attributes + ---------- + interaction: :class:`Interaction` + The interaction that was responded to. + """ + + interaction: Interaction + + +class InteractionTimedOut(InteractionException): + """Exception that's raised when an interaction takes more than 3 seconds + to respond but is not deferred. + + .. versionadded:: 2.0 + + Attributes + ---------- + interaction: :class:`Interaction` + The interaction that was responded to. + """ + + def __init__(self, interaction: Interaction) -> None: + self.interaction: Interaction = interaction + + msg = ( + "Interaction took more than 3 seconds to be responded to. " + 'Please defer it using "interaction.response.defer" on the start of your command. ' + "Later you may send a response by editing the deferred message " + 'using "interaction.edit_original_response"' + "\n" + "Note: This might also be caused by a misconfiguration in the components " + "make sure you do not respond twice in case this is a component." + ) + super().__init__(msg) + + +class InteractionResponded(InteractionException): + """Exception that's raised when sending another interaction response using + :class:`InteractionResponse` when one has already been done before. + + An interaction can only be responded to once. + + .. versionadded:: 2.0 + + Attributes + ---------- + interaction: :class:`Interaction` + The interaction that's already been responded to. + """ + + def __init__(self, interaction: Interaction) -> None: + self.interaction: Interaction = interaction + super().__init__("This interaction has already been responded to before") + + +class InteractionNotResponded(InteractionException): + """Exception that's raised when editing an interaction response without + sending a response message first. + + An interaction must be responded to exactly once. + + .. versionadded:: 2.0 + + Attributes + ---------- + interaction: :class:`Interaction` + The interaction that hasn't been responded to. + """ + + def __init__(self, interaction: Interaction) -> None: + self.interaction: Interaction = interaction + super().__init__("This interaction hasn't been responded to yet") + + +class ModalChainNotSupported(InteractionException): + """Exception that's raised when responding to a modal with another modal. + + .. versionadded:: 2.4 + + Attributes + ---------- + interaction: :class:`ModalInteraction` + The interaction that was responded to. + """ + + def __init__(self, interaction: ModalInteraction) -> None: + self.interaction: ModalInteraction = interaction + super().__init__("You cannot respond to a modal with another modal.") + + +class InteractionNotEditable(InteractionException): + """Exception that's raised when trying to use :func:`InteractionResponse.edit_message` + on an interaction without an associated message (which is thus non-editable). + + .. versionadded:: 2.5 + + Attributes + ---------- + interaction: :class:`Interaction` + The interaction that was responded to. + """ + + def __init__(self, interaction: Interaction) -> None: + self.interaction: Interaction = interaction + super().__init__("This interaction does not have a message to edit.") + + +class LocalizationKeyError(DiscordException): + """Exception that's raised when a localization key lookup fails. + + .. versionadded:: 2.5 + + Attributes + ---------- + key: :class:`str` + The localization key that couldn't be found. + """ + + def __init__(self, key: str) -> None: + self.key: str = key + super().__init__(f"No localizations were found for the key '{key}'.") diff --git a/disnake/disnake/ext/commands/__init__.py b/disnake/disnake/ext/commands/__init__.py new file mode 100644 index 0000000000..ea897b384e --- /dev/null +++ b/disnake/disnake/ext/commands/__init__.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT + +"""disnake.ext.commands +~~~~~~~~~~~~~~~~~~~~~ + +An extension module to facilitate creation of bot commands. + +:copyright: (c) 2015-2021 Rapptz, 2021-present Disnake Development +:license: MIT, see LICENSE for more details. +""" + +from .base_core import * +from .bot import * +from .cog import * +from .context import * +from .converter import * +from .cooldowns import * +from .core import * +from .ctx_menus_core import * +from .custom_warnings import * +from .errors import * +from .flag_converter import * +from .flags import * +from .help import * +from .params import * +from .slash_core import * diff --git a/disnake/disnake/ext/commands/_types.py b/disnake/disnake/ext/commands/_types.py new file mode 100644 index 0000000000..6f23a6e11c --- /dev/null +++ b/disnake/disnake/ext/commands/_types.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MIT + +from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, Union + +if TYPE_CHECKING: + from disnake import ApplicationCommandInteraction + + from .cog import Cog + from .context import Context + from .errors import CommandError + +T = TypeVar("T") + +FuncT = TypeVar("FuncT", bound=Callable[..., Any]) + +Coro = Coroutine[Any, Any, T] +MaybeCoro = Union[T, Coro[T]] +CoroFunc = Callable[..., Coro[Any]] + +Check = Union[ + Callable[["Cog", "Context[Any]"], MaybeCoro[bool]], Callable[["Context[Any]"], MaybeCoro[bool]] +] +AppCheck = Union[ + Callable[["Cog", "ApplicationCommandInteraction"], MaybeCoro[bool]], + Callable[["ApplicationCommandInteraction"], MaybeCoro[bool]], +] +Hook = Union[Callable[["Cog", "Context[Any]"], Coro[Any]], Callable[["Context[Any]"], Coro[Any]]] +Error = Union[ + Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]], + Callable[["Context[Any]", "CommandError"], Coro[Any]], +] + + +# This is merely a tag type to avoid circular import issues. +# Yes, this is a terrible solution but ultimately it is the only solution. +class _BaseCommand: + __slots__ = () diff --git a/disnake/disnake/ext/commands/base_core.py b/disnake/disnake/ext/commands/base_core.py new file mode 100644 index 0000000000..b655a1f4c8 --- /dev/null +++ b/disnake/disnake/ext/commands/base_core.py @@ -0,0 +1,914 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +import datetime +import functools +from abc import ABC +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + TypeVar, + Union, + cast, + overload, +) + +from disnake.app_commands import ApplicationCommand +from disnake.enums import ApplicationCommandType +from disnake.flags import ApplicationInstallTypes, InteractionContextTypes +from disnake.permissions import Permissions +from disnake.utils import ( + _generated, + _overload_with_permissions, + async_all, + iscoroutinefunction, + maybe_coroutine, +) + +from .cooldowns import BucketType, CooldownMapping, MaxConcurrency +from .errors import CheckFailure, CommandError, CommandInvokeError, CommandOnCooldown + +if TYPE_CHECKING: + from typing_extensions import Concatenate, ParamSpec, Self + + from disnake.interactions import ApplicationCommandInteraction + + from ._types import AppCheck, Coro, Error, Hook + from .cog import Cog + from .interaction_bot_base import InteractionBotBase + + ApplicationCommandInteractionT = TypeVar( + "ApplicationCommandInteractionT", bound=ApplicationCommandInteraction, covariant=True + ) + + P = ParamSpec("P") + + CommandCallback = Callable[..., Coro[Any]] + InteractionCommandCallback = Union[ + Callable[Concatenate["CogT", ApplicationCommandInteractionT, P], Coro[Any]], + Callable[Concatenate[ApplicationCommandInteractionT, P], Coro[Any]], + ] + + +__all__ = ( + "InvokableApplicationCommand", + "default_member_permissions", + "install_types", + "contexts", +) + + +T = TypeVar("T") +AppCommandT = TypeVar("AppCommandT", bound="InvokableApplicationCommand") +CogT = TypeVar("CogT", bound="Cog") +HookT = TypeVar("HookT", bound="Hook") +ErrorT = TypeVar("ErrorT", bound="Error") + + +def _get_overridden_method(method): + return getattr(method.__func__, "__cog_special_method__", method) + + +def wrap_callback(coro): + @functools.wraps(coro) + async def wrapped(*args, **kwargs): + try: + ret = await coro(*args, **kwargs) + except CommandError: + raise + except asyncio.CancelledError: + return + except Exception as exc: + raise CommandInvokeError(exc) from exc + return ret + + return wrapped + + +class InvokableApplicationCommand(ABC): + """A base class that implements the protocol for a bot application command. + + These are not created manually, instead they are created via the + decorator or functional interface. + + The following classes implement this ABC: + + - :class:`~.InvokableSlashCommand` + - :class:`~.InvokableMessageCommand` + - :class:`~.InvokableUserCommand` + + Attributes + ---------- + name: :class:`str` + The name of the command. + qualified_name: :class:`str` + The full command name, including parent names in the case of slash subcommands or groups. + For example, the qualified name for ``/one two three`` would be ``one two three``. + body: :class:`.ApplicationCommand` + An object being registered in the API. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.ApplicationCommandInteraction`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationCommandInteraction` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.CommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_slash_command_error` + event. + guild_ids: Optional[Tuple[:class:`int`, ...]] + The list of IDs of the guilds where the command is synced. ``None`` if this command is global. + auto_sync: :class:`bool` + Whether to automatically register the command. + extras: Dict[:class:`str`, Any] + A dict of user provided extras to attach to the command. + + .. versionadded:: 2.5 + """ + + __original_kwargs__: Dict[str, Any] + body: ApplicationCommand + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + self = super().__new__(cls) + # todo: refactor to not require None and change this to be based on the presence of a kwarg + self.__original_kwargs__ = {k: v for k, v in kwargs.items() if v is not None} + return self + + def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwargs: Any) -> None: + self.__command_flag__ = None + self._callback: CommandCallback = func + self.name: str = name or func.__name__ + self.qualified_name: str = self.name + # Annotation parser needs this attribute because body doesn't exist at this moment. + # We will use this attribute later in order to set the allowed contexts. + self._guild_only: bool = kwargs.get("guild_only", False) + self.extras: Dict[str, Any] = kwargs.get("extras") or {} + + if not isinstance(self.name, str): + raise TypeError("Name of a command must be a string.") + + if "default_permission" in kwargs: + raise TypeError( + "`default_permission` is deprecated and will always be set to `True`. " + "See `default_member_permissions` and `contexts` instead." + ) + + # XXX: remove in next major/minor version + # the parameter was called `integration_types` in earlier stages of the user apps PR. + # since unknown kwargs unfortunately get silently ignored, at least try to warn users + # in this specific case + if "integration_types" in kwargs: + raise TypeError("`integration_types` has been renamed to `install_types`.") + + try: + checks = func.__commands_checks__ + checks.reverse() + except AttributeError: + checks = kwargs.get("checks", []) + + self.checks: List[AppCheck] = checks + + try: + cooldown = func.__commands_cooldown__ + except AttributeError: + cooldown = kwargs.get("cooldown") + + # TODO: Figure out how cooldowns even work with interactions + if cooldown is None: + buckets = CooldownMapping(cooldown, BucketType.default) + elif isinstance(cooldown, CooldownMapping): + buckets = cooldown + else: + raise TypeError("Cooldown must be a an instance of CooldownMapping or None.") + self._buckets: CooldownMapping = buckets + + try: + max_concurrency = func.__commands_max_concurrency__ + except AttributeError: + max_concurrency = kwargs.get("max_concurrency") + self._max_concurrency: Optional[MaxConcurrency] = max_concurrency + + self.cog: Optional[Cog] = None + self.guild_ids: Optional[Tuple[int, ...]] = None + self.auto_sync: bool = True + + self._before_invoke: Optional[Hook] = None + self._after_invoke: Optional[Hook] = None + + # this should copy all attributes that can be changed after instantiation via decorators + def _ensure_assignment_on_copy(self, other: AppCommandT) -> AppCommandT: + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + if self.checks != other.checks: + other.checks = self.checks.copy() + if self._buckets.valid and not other._buckets.valid: + other._buckets = self._buckets.copy() + if self._max_concurrency != other._max_concurrency: + # _max_concurrency won't be None at this point + other._max_concurrency = cast("MaxConcurrency", self._max_concurrency).copy() + + if ( + # see https://github.com/DisnakeDev/disnake/pull/678#discussion_r938113624: + # if these are not equal, then either `self` had a decorator, or `other` got a + # value from `*_command_attrs`; we only want to copy in the former case + self.body._default_member_permissions != other.body._default_member_permissions + and self.body._default_member_permissions is not None + ): + other.body._default_member_permissions = self.body._default_member_permissions + + if ( + self.body.install_types != other.body.install_types + and self.body.install_types is not None # see above + ): + other.body.install_types = ApplicationInstallTypes._from_value( + self.body.install_types.value + ) + + if ( + self.body.contexts != other.body.contexts + and self.body.contexts is not None # see above + ): + other.body.contexts = InteractionContextTypes._from_value(self.body.contexts.value) + + try: + other.on_error = self.on_error + except AttributeError: + pass + + return other + + def copy(self: AppCommandT) -> AppCommandT: + """Create a copy of this application command. + + Returns + ------- + :class:`InvokableApplicationCommand` + A new instance of this application command. + """ + copy = type(self)(self.callback, **self.__original_kwargs__) + return self._ensure_assignment_on_copy(copy) + + def _update_copy(self: AppCommandT, kwargs: Dict[str, Any]) -> AppCommandT: + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = type(self)(self.callback, **kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + def _apply_guild_only(self) -> None: + # If we have a `GuildCommandInteraction` annotation, set `contexts` and `install_types` accordingly. + # This matches the old pre-user-apps behavior. + if self._guild_only: + # n.b. this overwrites any user-specified parameter + # FIXME(3.0): this should raise if these were set elsewhere (except `*_command_attrs`) already + self.body.contexts = InteractionContextTypes(guild=True) + self.body.install_types = ApplicationInstallTypes(guild=True) + + def _apply_defaults(self, bot: InteractionBotBase) -> None: + self.body._default_install_types = bot._default_install_types + self.body._default_contexts = bot._default_contexts + + @property + def dm_permission(self) -> bool: + """:class:`bool`: Whether this command can be used in DMs.""" + return self.body.dm_permission + + @property + def default_member_permissions(self) -> Optional[Permissions]: + """Optional[:class:`.Permissions`]: The default required member permissions for this command. + A member must have *all* these permissions to be able to invoke the command in a guild. + + This is a default value, the set of users/roles that may invoke this command can be + overridden by moderators on a guild-specific basis, disregarding this setting. + + If ``None`` is returned, it means everyone can use the command by default. + If an empty :class:`.Permissions` object is returned (that is, all permissions set to ``False``), + this means no one can use the command. + + .. versionadded:: 2.5 + """ + return self.body.default_member_permissions + + @property + def install_types(self) -> Optional[ApplicationInstallTypes]: + """Optional[:class:`.ApplicationInstallTypes`]: The installation types + where the command is available. Only available for global commands. + + .. versionadded:: 2.10 + """ + return self.body.install_types + + @property + def contexts(self) -> Optional[InteractionContextTypes]: + """Optional[:class:`.InteractionContextTypes`]: The interaction contexts + where the command can be used. Only available for global commands. + + .. versionadded:: 2.10 + """ + return self.body.contexts + + @property + def callback(self) -> CommandCallback: + return self._callback + + def add_check(self, func: AppCheck) -> None: + """Adds a check to the application command. + + This is the non-decorator interface to :func:`.app_check`. + + Parameters + ---------- + func + The function that will be used as a check. + """ + self.checks.append(func) + + def remove_check(self, func: AppCheck) -> None: + """Removes a check from the application command. + + This function is idempotent and will not raise an exception + if the function is not in the command's checks. + + Parameters + ---------- + func + The function to remove from the checks. + """ + try: + self.checks.remove(func) + except ValueError: + pass + + async def __call__( + self, interaction: ApplicationCommandInteraction, *args: Any, **kwargs: Any + ) -> Any: + """|coro| + + Calls the internal callback that the application command holds. + + .. note:: + + This bypasses all mechanisms -- including checks, converters, + invoke hooks, cooldowns, etc. You must take care to pass + the proper arguments and types to this function. + + """ + if self.cog is not None: + return await self.callback(self.cog, interaction, *args, **kwargs) + else: + return await self.callback(interaction, *args, **kwargs) + + def _prepare_cooldowns(self, inter: ApplicationCommandInteraction) -> None: + if self._buckets.valid: + dt = inter.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + bucket = self._buckets.get_bucket(inter, current) # type: ignore + if bucket is not None: # pyright: ignore[reportUnnecessaryComparison] + retry_after = bucket.update_rate_limit(current) + if retry_after: + raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore + + async def prepare(self, inter: ApplicationCommandInteraction) -> None: + inter.application_command = self + + if not await self.can_run(inter): + raise CheckFailure(f"The check functions for command {self.qualified_name!r} failed.") + + if self._max_concurrency is not None: + await self._max_concurrency.acquire(inter) # type: ignore + + try: + self._prepare_cooldowns(inter) + await self.call_before_hooks(inter) + except Exception: + if self._max_concurrency is not None: + await self._max_concurrency.release(inter) # type: ignore + raise + + def is_on_cooldown(self, inter: ApplicationCommandInteraction) -> bool: + """Checks whether the application command is currently on cooldown. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction with the application command currently being invoked. + + Returns + ------- + :class:`bool` + A boolean indicating if the application command is on cooldown. + """ + if not self._buckets.valid: + return False + + bucket = self._buckets.get_bucket(inter) # type: ignore + dt = inter.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_tokens(current) == 0 + + def reset_cooldown(self, inter: ApplicationCommandInteraction) -> None: + """Resets the cooldown on this application command. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction with this application command + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(inter) # type: ignore + bucket.reset() + + def get_cooldown_retry_after(self, inter: ApplicationCommandInteraction) -> float: + """Retrieves the amount of seconds before this application command can be tried again. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction with this application command. + + Returns + ------- + :class:`float` + The amount of time left on this command's cooldown in seconds. + If this is ``0.0`` then the command isn't on cooldown. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(inter) # type: ignore + dt = inter.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_retry_after(current) + + return 0.0 + + # This method isn't really usable in this class, but it's usable in subclasses. + async def invoke(self, inter: ApplicationCommandInteraction, *args: Any, **kwargs: Any) -> None: + await self.prepare(inter) + + try: + await self(inter, *args, **kwargs) + except CommandError: + inter.command_failed = True + raise + except asyncio.CancelledError: + inter.command_failed = True + return + except Exception as exc: + inter.command_failed = True + raise CommandInvokeError(exc) from exc + finally: + if self._max_concurrency is not None: + await self._max_concurrency.release(inter) # type: ignore + + await self.call_after_hooks(inter) + + def error(self, coro: ErrorT) -> ErrorT: + """A decorator that registers a coroutine as a local error handler. + + A local error handler is an error event limited to a single application command. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the local error handler. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The error handler must be a coroutine.") + + self.on_error: Error = coro + return coro + + def has_error_handler(self) -> bool: + """Checks whether the application command has an error handler registered.""" + return hasattr(self, "on_error") + + async def _call_local_error_handler( + self, inter: ApplicationCommandInteraction, error: CommandError + ) -> Any: + if not self.has_error_handler(): + return + + injected = wrap_callback(self.on_error) + if self.cog is not None: + return await injected(self.cog, inter, error) + else: + return await injected(inter, error) + + async def _call_external_error_handlers( + self, inter: ApplicationCommandInteraction, error: CommandError + ) -> None: + """Overridden in subclasses""" + raise error + + async def dispatch_error( + self, inter: ApplicationCommandInteraction, error: CommandError + ) -> None: + if not await self._call_local_error_handler(inter, error): + await self._call_external_error_handlers(inter, error) + + async def call_before_hooks(self, inter: ApplicationCommandInteraction) -> None: + # now that we're done preparing we can call the pre-command hooks + # first, call the command local hook: + cog = self.cog + if self._before_invoke is not None: + # should be cog if @commands.before_invoke is used + instance = getattr(self._before_invoke, "__self__", cog) + # __self__ only exists for methods, not functions + # however, if @command.before_invoke is used, it will be a function + if instance: + await self._before_invoke(instance, inter) # type: ignore + else: + await self._before_invoke(inter) # type: ignore + + if inter.data.type is ApplicationCommandType.chat_input: + partial_attr_name = "slash_command" + elif inter.data.type is ApplicationCommandType.user: + partial_attr_name = "user_command" + elif inter.data.type is ApplicationCommandType.message: + partial_attr_name = "message_command" + else: + return + + # call the cog local hook if applicable: + if cog is not None: + meth = getattr(cog, f"cog_before_{partial_attr_name}_invoke", None) + hook = _get_overridden_method(meth) + if hook is not None: + await hook(inter) + + # call the bot global hook if necessary + hook = getattr(inter.bot, f"_before_{partial_attr_name}_invoke", None) + if hook is not None: + await hook(inter) + + async def call_after_hooks(self, inter: ApplicationCommandInteraction) -> None: + cog = self.cog + if self._after_invoke is not None: + instance = getattr(self._after_invoke, "__self__", cog) + if instance: + await self._after_invoke(instance, inter) # type: ignore + else: + await self._after_invoke(inter) # type: ignore + + if inter.data.type is ApplicationCommandType.chat_input: + partial_attr_name = "slash_command" + elif inter.data.type is ApplicationCommandType.user: + partial_attr_name = "user_command" + elif inter.data.type is ApplicationCommandType.message: + partial_attr_name = "message_command" + else: + return + + # call the cog local hook if applicable: + if cog is not None: + meth = getattr(cog, f"cog_after_{partial_attr_name}_invoke", None) + hook = _get_overridden_method(meth) + if hook is not None: + await hook(inter) + + # call the bot global hook if necessary + hook = getattr(inter.bot, f"_after_{partial_attr_name}_invoke", None) + if hook is not None: + await hook(inter) + + def before_invoke(self, coro: HookT) -> HookT: + """A decorator that registers a coroutine as a pre-invoke hook. + + A pre-invoke hook is called directly before the command is called. + + This pre-invoke hook takes a sole parameter, a :class:`.ApplicationCommandInteraction`. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The pre-invoke hook must be a coroutine.") + + self._before_invoke = coro + return coro + + def after_invoke(self, coro: HookT) -> HookT: + """A decorator that registers a coroutine as a post-invoke hook. + + A post-invoke hook is called directly after the command is called. + + This post-invoke hook takes a sole parameter, a :class:`.ApplicationCommandInteraction`. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The post-invoke hook must be a coroutine.") + + self._after_invoke = coro + return coro + + @property + def cog_name(self) -> Optional[str]: + """Optional[:class:`str`]: The name of the cog this application command belongs to, if any.""" + return type(self.cog).__cog_name__ if self.cog is not None else None + + async def can_run(self, inter: ApplicationCommandInteraction) -> bool: + """|coro| + + Checks if the command can be executed by checking all the predicates + inside the :attr:`~Command.checks` attribute. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction with the application command currently being invoked. + + Raises + ------ + :class:`CommandError` + Any application command error that was raised during a check call will be propagated + by this function. + + Returns + ------- + :class:`bool` + A boolean indicating if the application command can be invoked. + """ + original = inter.application_command + inter.application_command = self + + if inter.data.type is ApplicationCommandType.chat_input: + partial_attr_name = "slash_command" + elif inter.data.type is ApplicationCommandType.user: + partial_attr_name = "user_command" + elif inter.data.type is ApplicationCommandType.message: + partial_attr_name = "message_command" + else: + return True + + try: + if inter.bot and not await inter.bot.application_command_can_run(inter): + raise CheckFailure( + f"The global check functions for command {self.qualified_name} failed." + ) + + cog = self.cog + if cog is not None: + meth = getattr(cog, f"cog_{partial_attr_name}_check", None) + local_check = _get_overridden_method(meth) + if local_check is not None: + ret = await maybe_coroutine(local_check, inter) + if not ret: + return False + + predicates = self.checks + if not predicates: + # since we have no checks, then we just return True. + return True + + return await async_all(predicate(inter) for predicate in predicates) # type: ignore + finally: + inter.application_command = original + + +@overload +@_generated +def default_member_permissions( + value: int = 0, + *, + add_reactions: bool = ..., + administrator: bool = ..., + attach_files: bool = ..., + ban_members: bool = ..., + change_nickname: bool = ..., + connect: bool = ..., + create_events: bool = ..., + create_forum_threads: bool = ..., + create_guild_expressions: bool = ..., + create_instant_invite: bool = ..., + create_private_threads: bool = ..., + create_public_threads: bool = ..., + deafen_members: bool = ..., + embed_links: bool = ..., + external_emojis: bool = ..., + external_stickers: bool = ..., + kick_members: bool = ..., + manage_channels: bool = ..., + manage_emojis: bool = ..., + manage_emojis_and_stickers: bool = ..., + manage_events: bool = ..., + manage_guild: bool = ..., + manage_guild_expressions: bool = ..., + manage_messages: bool = ..., + manage_nicknames: bool = ..., + manage_permissions: bool = ..., + manage_roles: bool = ..., + manage_threads: bool = ..., + manage_webhooks: bool = ..., + mention_everyone: bool = ..., + moderate_members: bool = ..., + move_members: bool = ..., + mute_members: bool = ..., + pin_messages: bool = ..., + priority_speaker: bool = ..., + read_message_history: bool = ..., + read_messages: bool = ..., + request_to_speak: bool = ..., + send_messages: bool = ..., + send_messages_in_threads: bool = ..., + send_polls: bool = ..., + send_tts_messages: bool = ..., + send_voice_messages: bool = ..., + speak: bool = ..., + start_embedded_activities: bool = ..., + stream: bool = ..., + use_application_commands: bool = ..., + use_embedded_activities: bool = ..., + use_external_apps: bool = ..., + use_external_emojis: bool = ..., + use_external_sounds: bool = ..., + use_external_stickers: bool = ..., + use_slash_commands: bool = ..., + use_soundboard: bool = ..., + use_voice_activation: bool = ..., + view_audit_log: bool = ..., + view_channel: bool = ..., + view_creator_monetization_analytics: bool = ..., + view_guild_insights: bool = ..., +) -> Callable[[T], T]: ... + + +@overload +@_generated +def default_member_permissions( + value: int = 0, +) -> Callable[[T], T]: ... + + +@_overload_with_permissions +def default_member_permissions(value: int = 0, **permissions: bool) -> Callable[[T], T]: + """A decorator that sets default required member permissions for the application command. + Unlike :func:`~.has_permissions`, this decorator does not add any checks. + Instead, it prevents the command from being run by members without *all* required permissions, + if not overridden by moderators on a guild-specific basis. + + See also the ``default_member_permissions`` parameter for application command decorators. + + .. note:: + This does not work with slash subcommands/groups. + + .. versionadded:: 2.5 + + Example + ------- + + This would only allow members with :attr:`~.Permissions.manage_messages` *and* + :attr:`~.Permissions.view_audit_log` permissions to use the command by default, + however moderators can override this and allow/disallow specific users and + roles to use the command in their guilds regardless of this setting. + + .. code-block:: python3 + + @bot.slash_command() + @commands.default_member_permissions(manage_messages=True, view_audit_log=True) + async def purge(inter, num: int): + ... + + Parameters + ---------- + value: :class:`int` + A raw permission bitfield of an integer representing the required permissions. + May be used instead of specifying kwargs. + **permissions: bool + The required permissions for a command. A member must have *all* these + permissions to be able to invoke the command. + Setting a permission to ``False`` does not affect the result. + """ + if isinstance(value, bool): + raise TypeError("`value` cannot be a bool value") + perms_value = Permissions(value, **permissions).value + + def decorator(func: T) -> T: + from .slash_core import SubCommand, SubCommandGroup + + if isinstance(func, InvokableApplicationCommand): + if isinstance(func, (SubCommand, SubCommandGroup)): + raise TypeError( + "Cannot set `default_member_permissions` on subcommands or subcommand groups" + ) + if func.body._default_member_permissions is not None: + raise ValueError( + "Cannot set `default_member_permissions` in both parameter and decorator" + ) + func.body._default_member_permissions = perms_value + else: + func.__default_member_permissions__ = perms_value # type: ignore + return func + + return decorator + + +def install_types(*, guild: bool = False, user: bool = False) -> Callable[[T], T]: + """A decorator that sets the installation types where the + application command is available. + + See also the ``install_types`` parameter for application command decorators. + + .. note:: + This does not work with slash subcommands/groups. + + .. versionadded:: 2.10 + + Parameters + ---------- + **params: bool + The installation types; see :class:`.ApplicationInstallTypes`. + Setting a parameter to ``False`` does not affect the result. + """ + + def decorator(func: T) -> T: + from .slash_core import SubCommand, SubCommandGroup + + install_types = ApplicationInstallTypes(guild=guild, user=user) + if isinstance(func, InvokableApplicationCommand): + if isinstance(func, (SubCommand, SubCommandGroup)): + raise TypeError("Cannot set `install_types` on subcommands or subcommand groups") + # special case - don't overwrite if `_guild_only` was set, since that takes priority + if not func._guild_only: + if func.body.install_types is not None: + raise ValueError("Cannot set `install_types` in both parameter and decorator") + func.body.install_types = install_types + else: + func.__install_types__ = install_types # type: ignore + return func + + return decorator + + +def contexts( + *, guild: bool = False, bot_dm: bool = False, private_channel: bool = False +) -> Callable[[T], T]: + """A decorator that sets the interaction contexts where the application command can be used. + + See also the ``contexts`` parameter for application command decorators. + + .. note:: + This does not work with slash subcommands/groups. + + .. versionadded:: 2.10 + + Parameters + ---------- + **params: bool + The interaction contexts; see :class:`.InteractionContextTypes`. + Setting a parameter to ``False`` does not affect the result. + """ + + def decorator(func: T) -> T: + from .slash_core import SubCommand, SubCommandGroup + + contexts = InteractionContextTypes( + guild=guild, bot_dm=bot_dm, private_channel=private_channel + ) + if isinstance(func, InvokableApplicationCommand): + if isinstance(func, (SubCommand, SubCommandGroup)): + raise TypeError("Cannot set `contexts` on subcommands or subcommand groups") + # special case - don't overwrite if `_guild_only` was set, since that takes priority + if not func._guild_only: + if func.body._dm_permission is not None: + raise ValueError( + "Cannot use both `dm_permission` and `contexts` at the same time" + ) + if func.body.contexts is not None: + raise ValueError("Cannot set `contexts` in both parameter and decorator") + func.body.contexts = contexts + else: + func.__contexts__ = contexts # type: ignore + return func + + return decorator diff --git a/disnake/disnake/ext/commands/bot.py b/disnake/disnake/ext/commands/bot.py new file mode 100644 index 0000000000..0cd80cb628 --- /dev/null +++ b/disnake/disnake/ext/commands/bot.py @@ -0,0 +1,563 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Sequence, Set, Union + +import disnake + +from .bot_base import BotBase, when_mentioned, when_mentioned_or +from .interaction_bot_base import InteractionBotBase + +if TYPE_CHECKING: + import asyncio + + import aiohttp + from typing_extensions import Self + + from disnake.activity import BaseActivity + from disnake.client import GatewayParams + from disnake.enums import Status + from disnake.flags import ( + ApplicationInstallTypes, + Intents, + InteractionContextTypes, + MemberCacheFlags, + ) + from disnake.i18n import LocalizationProtocol + from disnake.mentions import AllowedMentions + from disnake.message import Message + + from ._types import MaybeCoro + from .bot_base import PrefixType + from .flags import CommandSyncFlags + from .help import HelpCommand + + +__all__ = ( + "when_mentioned", + "when_mentioned_or", + "BotBase", + "Bot", + "InteractionBot", + "AutoShardedBot", + "AutoShardedInteractionBot", +) + +MISSING: Any = disnake.utils.MISSING + + +class Bot(BotBase, InteractionBotBase, disnake.Client): + """Represents a discord bot. + + This class is a subclass of :class:`disnake.Client` and as a result + anything that you can do with a :class:`disnake.Client` you can do with + this bot. + + This class also subclasses :class:`.GroupMixin` to provide the functionality + to manage commands. + + Parameters + ---------- + test_guilds: List[:class:`int`] + The list of IDs of the guilds where you're going to test your application commands. + Defaults to ``None``, which means global registration of commands across + all guilds. + + .. versionadded:: 2.1 + + command_sync_flags: :class:`.CommandSyncFlags` + The command sync flags for the session. This is a way of + controlling when and how application commands will be synced with the Discord API. + If not given, defaults to :func:`CommandSyncFlags.default`. + + .. versionadded:: 2.7 + + sync_commands: :class:`bool` + Whether to enable automatic synchronization of application commands in your code. + Defaults to ``True``, which means that commands in API are automatically synced + with the commands in your code. + + .. versionadded:: 2.1 + + .. deprecated:: 2.7 + Replaced with ``command_sync_flags``. + + sync_commands_on_cog_unload: :class:`bool` + Whether to sync the application commands on cog unload / reload. Defaults to ``True``. + + .. versionadded:: 2.1 + + .. deprecated:: 2.7 + Replaced with ``command_sync_flags``. + + sync_commands_debug: :class:`bool` + Whether to always show sync debug logs (uses ``INFO`` log level if it's enabled, prints otherwise). + If disabled, uses the default ``DEBUG`` log level which isn't shown unless the log level is changed manually. + Useful for tracking the commands being registered in the API. + Defaults to ``False``. + + .. versionadded:: 2.1 + + .. versionchanged:: 2.4 + Changes the log level of corresponding messages from ``DEBUG`` to ``INFO`` or ``print``\\s them, + instead of controlling whether they are enabled at all. + + .. deprecated:: 2.7 + Replaced with ``command_sync_flags``. + + localization_provider: :class:`.LocalizationProtocol` + An implementation of :class:`.LocalizationProtocol` to use for localization of + application commands. + If not provided, the default :class:`.LocalizationStore` implementation is used. + + .. versionadded:: 2.5 + + strict_localization: :class:`bool` + Whether to raise an exception when localizations for a specific key couldn't be found. + This is mainly useful for testing/debugging, consider disabling this eventually + as missing localized names will automatically fall back to the default/base name without it. + Only applicable if the ``localization_provider`` parameter is not provided. + Defaults to ``False``. + + .. versionadded:: 2.5 + + default_install_types: Optional[:class:`.ApplicationInstallTypes`] + The default installation types where application commands will be available. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.install_types` decorator, + the ``install_types`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + + default_contexts: Optional[:class:`.InteractionContextTypes`] + The default contexts where application commands will be usable. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.contexts` decorator, + the ``contexts`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + + Attributes + ---------- + command_prefix + The command prefix is what the message content must contain initially + to have a command invoked. This prefix could either be a string to + indicate what the prefix should be, or a callable that takes in the bot + as its first parameter and :class:`disnake.Message` as its second + parameter and returns the prefix. This is to facilitate "dynamic" + command prefixes. This callable can be either a regular function or + a coroutine. + + An empty string as the prefix always matches, enabling prefix-less + command invocation. While this may be useful in DMs it should be avoided + in servers, as it's likely to cause performance issues and unintended + command invocations. + + The command prefix could also be an iterable of strings indicating that + multiple checks for the prefix should be used and the first one to + match will be the invocation prefix. You can get this prefix via + :attr:`.Context.prefix`. To avoid confusion empty iterables are not + allowed. + + If the prefix is ``None``, the bot won't listen to any prefixes, and prefix + commands will not be processed. If you don't need prefix commands, consider + using :class:`InteractionBot` or :class:`AutoShardedInteractionBot` instead, + which are drop-in replacements, just without prefix command support. + + This can be provided as a parameter at creation. + + .. note:: + + When passing multiple prefixes be careful to not pass a prefix + that matches a longer prefix occurring later in the sequence. For + example, if the command prefix is ``('!', '!?')`` the ``'!?'`` + prefix will never be matched to any message as the previous one + matches messages starting with ``!?``. This is especially important + when passing an empty string, it should always be last as no prefix + after it will be matched. + case_insensitive: :class:`bool` + Whether the commands should be case insensitive. Defaults to ``False``. This + attribute does not carry over to groups. You must set it to every group if + you require group commands to be case insensitive as well. + + This can be provided as a parameter at creation. + + description: :class:`str` + The content prefixed into the default help message. + + This can be provided as a parameter at creation. + + help_command: Optional[:class:`.HelpCommand`] + The help command implementation to use. This can be dynamically + set at runtime. To remove the help command pass ``None``. For more + information on implementing a help command, see :ref:`ext_commands_api_help_commands`. + + This can be provided as a parameter at creation. + + owner_id: Optional[:class:`int`] + The ID of the user that owns the bot. If this is not set and is then queried via + :meth:`.is_owner` then it is fetched automatically using + :meth:`~.Bot.application_info`. + + This can be provided as a parameter at creation. + + owner_ids: Optional[Collection[:class:`int`]] + The IDs of the users that own the bot. This is similar to :attr:`owner_id`. + If this is not set and the application is team based, then it is + fetched automatically using :meth:`~.Bot.application_info` (taking team roles into account). + For performance reasons it is recommended to use a :class:`set` + for the collection. You cannot set both ``owner_id`` and ``owner_ids``. + + This can be provided as a parameter at creation. + + .. versionadded:: 1.3 + + strip_after_prefix: :class:`bool` + Whether to strip whitespace characters after encountering the command + prefix. This allows for ``! hello`` and ``!hello`` to both work if + the ``command_prefix`` is set to ``!``. Defaults to ``False``. + + This can be provided as a parameter at creation. + + .. versionadded:: 1.7 + + reload: :class:`bool` + Whether to enable automatic extension reloading on file modification for debugging. + Whenever you save an extension with reloading enabled the file will be automatically + reloaded for you so you do not have to reload the extension manually. Defaults to ``False`` + + This can be provided as a parameter at creation. + + .. versionadded:: 2.1 + + i18n: :class:`.LocalizationProtocol` + An implementation of :class:`.LocalizationProtocol` used for localization of + application commands. + + .. versionadded:: 2.5 + """ + + if TYPE_CHECKING: + + def __init__( + self, + command_prefix: Optional[ + Union[PrefixType, Callable[[Self, Message], MaybeCoro[PrefixType]]] + ] = None, + help_command: Optional[HelpCommand] = ..., + description: Optional[str] = None, + *, + strip_after_prefix: bool = False, + owner_id: Optional[int] = None, + owner_ids: Optional[Set[int]] = None, + reload: bool = False, + case_insensitive: bool = False, + command_sync_flags: CommandSyncFlags = ..., + sync_commands: bool = ..., + sync_commands_debug: bool = ..., + sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, + asyncio_debug: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, + shard_id: Optional[int] = None, + shard_count: Optional[int] = None, + enable_debug_events: bool = False, + enable_gateway_error_handler: bool = True, + gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, + proxy: Optional[str] = None, + proxy_auth: Optional[aiohttp.BasicAuth] = None, + assume_unsync_clock: bool = True, + max_messages: Optional[int] = 1000, + application_id: Optional[int] = None, + heartbeat_timeout: float = 60.0, + guild_ready_timeout: float = 2.0, + allowed_mentions: Optional[AllowedMentions] = None, + activity: Optional[BaseActivity] = None, + status: Optional[Union[Status, str]] = None, + intents: Optional[Intents] = None, + chunk_guilds_at_startup: Optional[bool] = None, + member_cache_flags: Optional[MemberCacheFlags] = None, + localization_provider: Optional[LocalizationProtocol] = None, + strict_localization: bool = False, + ) -> None: ... + + +class AutoShardedBot(BotBase, InteractionBotBase, disnake.AutoShardedClient): + """Similar to :class:`.Bot`, except that it is inherited from + :class:`disnake.AutoShardedClient` instead. + """ + + if TYPE_CHECKING: + + def __init__( + self, + command_prefix: Optional[ + Union[PrefixType, Callable[[Self, Message], MaybeCoro[PrefixType]]] + ] = None, + help_command: Optional[HelpCommand] = ..., + description: Optional[str] = None, + *, + strip_after_prefix: bool = False, + owner_id: Optional[int] = None, + owner_ids: Optional[Set[int]] = None, + reload: bool = False, + case_insensitive: bool = False, + command_sync_flags: CommandSyncFlags = ..., + sync_commands: bool = ..., + sync_commands_debug: bool = ..., + sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, + asyncio_debug: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, + shard_ids: Optional[List[int]] = None, # instead of shard_id + shard_count: Optional[int] = None, + enable_debug_events: bool = False, + enable_gateway_error_handler: bool = True, + gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, + proxy: Optional[str] = None, + proxy_auth: Optional[aiohttp.BasicAuth] = None, + assume_unsync_clock: bool = True, + max_messages: Optional[int] = 1000, + application_id: Optional[int] = None, + heartbeat_timeout: float = 60.0, + guild_ready_timeout: float = 2.0, + allowed_mentions: Optional[AllowedMentions] = None, + activity: Optional[BaseActivity] = None, + status: Optional[Union[Status, str]] = None, + intents: Optional[Intents] = None, + chunk_guilds_at_startup: Optional[bool] = None, + member_cache_flags: Optional[MemberCacheFlags] = None, + localization_provider: Optional[LocalizationProtocol] = None, + strict_localization: bool = False, + ) -> None: ... + + +class InteractionBot(InteractionBotBase, disnake.Client): + """Represents a discord bot for application commands only. + + This class is a subclass of :class:`disnake.Client` and as a result + anything that you can do with a :class:`disnake.Client` you can do with + this bot. + + This class also subclasses InteractionBotBase to provide the functionality + to manage application commands. + + Parameters + ---------- + test_guilds: List[:class:`int`] + The list of IDs of the guilds where you're going to test your application commands. + Defaults to ``None``, which means global registration of commands across + all guilds. + + .. versionadded:: 2.1 + + command_sync_flags: :class:`.CommandSyncFlags` + The command sync flags for the session. This is a way of + controlling when and how application commands will be synced with the Discord API. + If not given, defaults to :func:`CommandSyncFlags.default`. + + .. versionadded:: 2.7 + + sync_commands: :class:`bool` + Whether to enable automatic synchronization of application commands in your code. + Defaults to ``True``, which means that commands in API are automatically synced + with the commands in your code. + + .. versionadded:: 2.1 + + .. deprecated:: 2.7 + Replaced with ``command_sync_flags``. + + sync_commands_on_cog_unload: :class:`bool` + Whether to sync the application commands on cog unload / reload. Defaults to ``True``. + + .. versionadded:: 2.1 + + .. deprecated:: 2.7 + Replaced with ``command_sync_flags``. + + sync_commands_debug: :class:`bool` + Whether to always show sync debug logs (uses ``INFO`` log level if it's enabled, prints otherwise). + If disabled, uses the default ``DEBUG`` log level which isn't shown unless the log level is changed manually. + Useful for tracking the commands being registered in the API. + Defaults to ``False``. + + .. versionadded:: 2.1 + + .. versionchanged:: 2.4 + Changes the log level of corresponding messages from ``DEBUG`` to ``INFO`` or ``print``\\s them, + instead of controlling whether they are enabled at all. + + .. deprecated:: 2.7 + Replaced with ``command_sync_flags``. + + localization_provider: :class:`.LocalizationProtocol` + An implementation of :class:`.LocalizationProtocol` to use for localization of + application commands. + If not provided, the default :class:`.LocalizationStore` implementation is used. + + .. versionadded:: 2.5 + + strict_localization: :class:`bool` + Whether to raise an exception when localizations for a specific key couldn't be found. + This is mainly useful for testing/debugging, consider disabling this eventually + as missing localized names will automatically fall back to the default/base name without it. + Only applicable if the ``localization_provider`` parameter is not provided. + Defaults to ``False``. + + .. versionadded:: 2.5 + + default_install_types: Optional[:class:`.ApplicationInstallTypes`] + The default installation types where application commands will be available. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.install_types` decorator, + the ``install_types`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + + default_contexts: Optional[:class:`.InteractionContextTypes`] + The default contexts where application commands will be usable. + This applies to all commands added either through the respective decorators + or directly using :meth:`.add_slash_command` (etc.). + + Any value set directly on the command, e.g. using the :func:`.contexts` decorator, + the ``contexts`` parameter, ``slash_command_attrs`` (etc.) at the cog-level, or from + the :class:`.GuildCommandInteraction` annotation, takes precedence over this default. + + .. versionadded:: 2.10 + + Attributes + ---------- + owner_id: Optional[:class:`int`] + The ID of the user that owns the bot. If this is not set and is then queried via + :meth:`.is_owner` then it is fetched automatically using + :meth:`~.Bot.application_info`. + + This can be provided as a parameter at creation. + + owner_ids: Optional[Collection[:class:`int`]] + The IDs of the users that own the bot. This is similar to :attr:`owner_id`. + If this is not set and the application is team based, then it is + fetched automatically using :meth:`~.Bot.application_info` (taking team roles into account). + For performance reasons it is recommended to use a :class:`set` + for the collection. You cannot set both ``owner_id`` and ``owner_ids``. + + This can be provided as a parameter at creation. + + reload: :class:`bool` + Whether to enable automatic extension reloading on file modification for debugging. + Whenever you save an extension with reloading enabled the file will be automatically + reloaded for you so you do not have to reload the extension manually. Defaults to ``False`` + + This can be provided as a parameter at creation. + + .. versionadded:: 2.1 + + i18n: :class:`.LocalizationProtocol` + An implementation of :class:`.LocalizationProtocol` used for localization of + application commands. + + .. versionadded:: 2.5 + """ + + if TYPE_CHECKING: + + def __init__( + self, + *, + owner_id: Optional[int] = None, + owner_ids: Optional[Set[int]] = None, + reload: bool = False, + command_sync_flags: CommandSyncFlags = ..., + sync_commands: bool = ..., + sync_commands_debug: bool = ..., + sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, + asyncio_debug: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, + shard_id: Optional[int] = None, + shard_count: Optional[int] = None, + enable_debug_events: bool = False, + enable_gateway_error_handler: bool = True, + gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, + proxy: Optional[str] = None, + proxy_auth: Optional[aiohttp.BasicAuth] = None, + assume_unsync_clock: bool = True, + max_messages: Optional[int] = 1000, + application_id: Optional[int] = None, + heartbeat_timeout: float = 60.0, + guild_ready_timeout: float = 2.0, + allowed_mentions: Optional[AllowedMentions] = None, + activity: Optional[BaseActivity] = None, + status: Optional[Union[Status, str]] = None, + intents: Optional[Intents] = None, + chunk_guilds_at_startup: Optional[bool] = None, + member_cache_flags: Optional[MemberCacheFlags] = None, + localization_provider: Optional[LocalizationProtocol] = None, + strict_localization: bool = False, + ) -> None: ... + + +class AutoShardedInteractionBot(InteractionBotBase, disnake.AutoShardedClient): + """Similar to :class:`.InteractionBot`, except that it is inherited from + :class:`disnake.AutoShardedClient` instead. + """ + + if TYPE_CHECKING: + + def __init__( + self, + *, + owner_id: Optional[int] = None, + owner_ids: Optional[Set[int]] = None, + reload: bool = False, + command_sync_flags: CommandSyncFlags = ..., + sync_commands: bool = ..., + sync_commands_debug: bool = ..., + sync_commands_on_cog_unload: bool = ..., + test_guilds: Optional[Sequence[int]] = None, + default_install_types: Optional[ApplicationInstallTypes] = None, + default_contexts: Optional[InteractionContextTypes] = None, + asyncio_debug: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, + shard_ids: Optional[List[int]] = None, # instead of shard_id + shard_count: Optional[int] = None, + enable_debug_events: bool = False, + enable_gateway_error_handler: bool = True, + gateway_params: Optional[GatewayParams] = None, + connector: Optional[aiohttp.BaseConnector] = None, + proxy: Optional[str] = None, + proxy_auth: Optional[aiohttp.BasicAuth] = None, + assume_unsync_clock: bool = True, + max_messages: Optional[int] = 1000, + application_id: Optional[int] = None, + heartbeat_timeout: float = 60.0, + guild_ready_timeout: float = 2.0, + allowed_mentions: Optional[AllowedMentions] = None, + activity: Optional[BaseActivity] = None, + status: Optional[Union[Status, str]] = None, + intents: Optional[Intents] = None, + chunk_guilds_at_startup: Optional[bool] = None, + member_cache_flags: Optional[MemberCacheFlags] = None, + localization_provider: Optional[LocalizationProtocol] = None, + strict_localization: bool = False, + ) -> None: ... diff --git a/disnake/disnake/ext/commands/bot_base.py b/disnake/disnake/ext/commands/bot_base.py new file mode 100644 index 0000000000..15b50102c1 --- /dev/null +++ b/disnake/disnake/ext/commands/bot_base.py @@ -0,0 +1,609 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import collections +import collections.abc +import inspect +import logging +import sys +import traceback +import warnings +from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Type, TypeVar, Union + +import disnake +from disnake.utils import iscoroutinefunction + +from . import errors +from .common_bot_base import CommonBotBase +from .context import Context +from .core import GroupMixin +from .custom_warnings import MessageContentPrefixWarning +from .help import DefaultHelpCommand, HelpCommand +from .view import StringView + +if TYPE_CHECKING: + from typing_extensions import Self + + from disnake.message import Message + + from ._types import Check, CoroFunc, MaybeCoro + +__all__ = ( + "when_mentioned", + "when_mentioned_or", + "BotBase", +) + +MISSING: Any = disnake.utils.MISSING + +T = TypeVar("T") +CFT = TypeVar("CFT", bound="CoroFunc") +CXT = TypeVar("CXT", bound="Context") + +PrefixType = Union[str, Iterable[str]] + +_log = logging.getLogger(__name__) + + +def when_mentioned(bot: BotBase, msg: Message) -> List[str]: + """A callable that implements a command prefix equivalent to being mentioned. + + These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. + """ + # bot.user will never be None when this is called + return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "] # type: ignore + + +def when_mentioned_or(*prefixes: str) -> Callable[[BotBase, Message], List[str]]: + """A callable that implements when mentioned or other prefixes provided. + + These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. + + Example + ------- + .. code-block:: python3 + + bot = commands.Bot(command_prefix=commands.when_mentioned_or('!')) + + + .. note:: + + This callable returns another callable, so if this is done inside a custom + callable, you must call the returned callable, for example: + + .. code-block:: python3 + + async def get_prefix(bot, message): + extras = await prefixes_for(message.guild) # returns a list + return commands.when_mentioned_or(*extras)(bot, message) + + + See Also + -------- + :func:`.when_mentioned` + """ + + def inner(bot: BotBase, msg: Message) -> List[str]: + r = list(prefixes) + r = when_mentioned(bot, msg) + r + return r + + return inner + + +def _is_submodule(parent: str, child: str) -> bool: + return parent == child or child.startswith(parent + ".") + + +class _DefaultRepr: + def __repr__(self) -> str: + return "" + + +_default: Any = _DefaultRepr() + + +class BotBase(CommonBotBase, GroupMixin): + def __init__( + self, + command_prefix: Optional[ + Union[PrefixType, Callable[[Self, Message], MaybeCoro[PrefixType]]] + ] = None, + help_command: Optional[HelpCommand] = _default, + description: Optional[str] = None, + *, + strip_after_prefix: bool = False, + **options: Any, + ) -> None: + super().__init__(**options) + + if not isinstance(self, disnake.Client): + raise RuntimeError("BotBase mixin must be used with disnake.Client") # noqa: TRY004 + + alternative = ( + "AutoShardedInteractionBot" + if isinstance(self, disnake.AutoShardedClient) + else "InteractionBot" + ) + if command_prefix is None: + disnake.utils.warn_deprecated( + "Using `command_prefix=None` is deprecated and will result in " + "an error in future versions. " + f"If you don't need any prefix functionality, consider using {alternative}.", + stacklevel=2, + ) + elif ( + # note: no need to check for empty iterables, + # as they won't be allowed by `get_prefix` + command_prefix is not when_mentioned and not self.intents.message_content + ): + warnings.warn( + "Message Content intent is not enabled and a prefix is configured. " + "This may cause limited functionality for prefix commands. " + "If you want prefix commands, pass an intents object with message_content set to True. " + f"If you don't need any prefix functionality, consider using {alternative}. " + "Alternatively, set prefix to disnake.ext.commands.when_mentioned to silence this warning.", + MessageContentPrefixWarning, + stacklevel=2, + ) + + self.command_prefix = command_prefix + + self._checks: List[Check] = [] + self._check_once: List[Check] = [] + + self._before_invoke: Optional[CoroFunc] = None + self._after_invoke: Optional[CoroFunc] = None + + self._help_command: Optional[HelpCommand] = None + self.description: str = inspect.cleandoc(description) if description else "" + self.strip_after_prefix: bool = strip_after_prefix + + if help_command is _default: + self.help_command = DefaultHelpCommand() + else: + self.help_command = help_command + + # internal helpers + + async def on_command_error(self, context: Context, exception: errors.CommandError) -> None: + """|coro| + + The default command error handler provided by the bot. + + This is for text commands only, and doesn't apply to application commands. + + By default this prints to :data:`sys.stderr` however it could be + overridden to have a different implementation. + + This only fires if you do not specify any listeners for command error. + """ + if self.extra_events.get("on_command_error", None): + return + + command = context.command + if command and command.has_error_handler(): + return + + cog = context.cog + if cog and cog.has_error_handler(): + return + + print(f"Ignoring exception in command {context.command}:", file=sys.stderr) + traceback.print_exception( + type(exception), exception, exception.__traceback__, file=sys.stderr + ) + + # global check registration + + def add_check( + self, + func: Check, + *, + call_once: bool = False, + ) -> None: + """Adds a global check to the bot. + + This is for text commands only, and doesn't apply to application commands. + + This is the non-decorator interface to :meth:`.check` and :meth:`.check_once`. + + Parameters + ---------- + func + The function that was used as a global check. + call_once: :class:`bool` + If the function should only be called once per + :meth:`.invoke` call. + """ + if call_once: + self._check_once.append(func) + else: + self._checks.append(func) + + def remove_check( + self, + func: Check, + *, + call_once: bool = False, + ) -> None: + """Removes a global check from the bot. + + This is for text commands only, and doesn't apply to application commands. + + This function is idempotent and will not raise an exception + if the function is not in the global checks. + + Parameters + ---------- + func + The function to remove from the global checks. + call_once: :class:`bool` + If the function was added with ``call_once=True`` in + the :meth:`.Bot.add_check` call or using :meth:`.check_once`. + """ + check_list = self._check_once if call_once else self._checks + try: + check_list.remove(func) + except ValueError: + pass + + def check(self, func: T) -> T: + """A decorator that adds a global check to the bot. + + This is for text commands only, and doesn't apply to application commands. + + A global check is similar to a :func:`.check` that is applied + on a per command basis except it is run before any command checks + have been verified and applies to every command the bot has. + + .. note:: + + This function can either be a regular function or a coroutine. + + Similar to a command :func:`.check`\\, this takes a single parameter + of type :class:`.Context` and can only raise exceptions inherited from + :exc:`.CommandError`. + + Example + ------- + .. code-block:: python3 + + @bot.check + def check_commands(ctx): + return ctx.command.qualified_name in allowed_commands + + """ + # T was used instead of Check to ensure the type matches on return + self.add_check(func) # type: ignore + return func + + def check_once(self, func: CFT) -> CFT: + """A decorator that adds a "call once" global check to the bot. + + This is for text commands only, and doesn't apply to application commands. + + Unlike regular global checks, this one is called only once + per :meth:`.invoke` call. + + Regular global checks are called whenever a command is called + or :meth:`.Command.can_run` is called. This type of check + bypasses that and ensures that it's called only once, even inside + the default help command. + + .. note:: + + When using this function the :class:`.Context` sent to a group subcommand + may only parse the parent command and not the subcommands due to it + being invoked once per :meth:`.Bot.invoke` call. + + .. note:: + + This function can either be a regular function or a coroutine. + + Similar to a command :func:`.check`\\, this takes a single parameter + of type :class:`.Context` and can only raise exceptions inherited from + :exc:`.CommandError`. + + Example + ------- + .. code-block:: python3 + + @bot.check_once + def whitelist(ctx): + return ctx.message.author.id in my_whitelist + + """ + self.add_check(func, call_once=True) + return func + + async def can_run(self, ctx: Context, *, call_once: bool = False) -> bool: + data = self._check_once if call_once else self._checks + + if len(data) == 0: + return True + + # type-checker doesn't distinguish between functions and methods + return await disnake.utils.async_all(f(ctx) for f in data) # type: ignore + + def before_invoke(self, coro: CFT) -> CFT: + """A decorator that registers a coroutine as a pre-invoke hook. + + This is for text commands only, and doesn't apply to application commands. + + A pre-invoke hook is called directly before the command is + called. This makes it a useful function to set up database + connections or any type of set up required. + + This pre-invoke hook takes a sole parameter, a :class:`.Context`. + + .. note:: + + The :meth:`~.Bot.before_invoke` and :meth:`~.Bot.after_invoke` hooks are + only called if all checks and argument parsing procedures pass + without error. If any check or argument parsing procedures fail + then the hooks are not called. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The pre-invoke hook must be a coroutine.") + + self._before_invoke = coro + return coro + + def after_invoke(self, coro: CFT) -> CFT: + """A decorator that registers a coroutine as a post-invoke hook. + + This is for text commands only, and doesn't apply to application commands. + + A post-invoke hook is called directly after the command is + called. This makes it a useful function to clean-up database + connections or any type of clean up required. + + This post-invoke hook takes a sole parameter, a :class:`.Context`. + + .. note:: + + Similar to :meth:`~.Bot.before_invoke`\\, this is not called unless + checks and argument parsing procedures succeed. This hook is, + however, **always** called regardless of the internal command + callback raising an error (i.e. :exc:`.CommandInvokeError`\\). + This makes it ideal for clean-up scenarios. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The post-invoke hook must be a coroutine.") + + self._after_invoke = coro + return coro + + # extensions + + def _remove_module_references(self, name: str) -> None: + super()._remove_module_references(name) + # remove all the commands from the module + for cmd in self.all_commands.copy().values(): + if cmd.module and _is_submodule(name, cmd.module): + if isinstance(cmd, GroupMixin): + cmd.recursively_remove_all_commands() + self.remove_command(cmd.name) + + # help command stuff + + @property + def help_command(self) -> Optional[HelpCommand]: + return self._help_command + + @help_command.setter + def help_command(self, value: Optional[HelpCommand]) -> None: + if value is not None and not isinstance(value, HelpCommand): + raise TypeError("help_command must be a subclass of HelpCommand or None") + + if self._help_command is not None: + self._help_command._remove_from_bot(self) + + self._help_command = value + + if value is not None: + value._add_to_bot(self) + + # command processing + + async def get_prefix(self, message: Message) -> Optional[Union[List[str], str]]: + """|coro| + + Retrieves the prefix the bot is listening to + with the message as a context. + + Parameters + ---------- + message: :class:`disnake.Message` + The message context to get the prefix of. + + Returns + ------- + Optional[Union[List[:class:`str`], :class:`str`]] + A list of prefixes or a single prefix that the bot is + listening for. None if the bot isn't listening for prefixes. + """ + ret = self.command_prefix + if callable(ret): + ret = await disnake.utils.maybe_coroutine(ret, self, message) + + if ret is None: + return None + + if not isinstance(ret, str): + try: + ret = list(ret) + except TypeError: + # It's possible that a generator raised this exception. Don't + # replace it with our own error if that's the case. + if isinstance(ret, collections.abc.Iterable): + raise + + raise TypeError( + "command_prefix must be plain string, iterable of strings, or callable " + f"returning either of these, not {ret.__class__.__name__}" + ) from None + + if not ret: + raise ValueError("Iterable command_prefix must contain at least one prefix") + + return ret + + async def get_context(self, message: Message, *, cls: Type[CXT] = Context) -> CXT: + """|coro| + + Returns the invocation context from the message. + + This is a more low-level counter-part for :meth:`.process_commands` + to allow users more fine grained control over the processing. + + The returned context is not guaranteed to be a valid invocation + context, :attr:`.Context.valid` must be checked to make sure it is. + If the context is not valid then it is not a valid candidate to be + invoked under :meth:`~.Bot.invoke`. + + Parameters + ---------- + message: :class:`disnake.Message` + The message to get the invocation context from. + cls + The factory class that will be used to create the context. + By default, this is :class:`.Context`. Should a custom + class be provided, it must be similar enough to :class:`.Context`\'s + interface. + + Returns + ------- + :class:`.Context` + The invocation context. The type of this can change via the + ``cls`` parameter. + """ + view = StringView(message.content) + ctx = cls(prefix=None, view=view, bot=self, message=message) + + if message.author.id == self.user.id: # type: ignore + return ctx + + prefix = await self.get_prefix(message) + invoked_prefix = prefix + + if prefix is None: + return ctx + elif isinstance(prefix, str): + if not view.skip_string(prefix): + return ctx + else: + try: + # if the context class' __init__ consumes something from the view this + # will be wrong. That seems unreasonable though. + if message.content.startswith(tuple(prefix)): + invoked_prefix = disnake.utils.find(view.skip_string, prefix) + else: + return ctx + + except TypeError: + if not isinstance(prefix, list): + raise TypeError( + "get_prefix must return either a string or a list of string, " + f"not {prefix.__class__.__name__}" + ) from None + + # It's possible a bad command_prefix got us here. + for value in prefix: + if not isinstance(value, str): + raise TypeError( + "Iterable command_prefix or list returned from get_prefix must " + f"contain only strings, not {value.__class__.__name__}" + ) from None + + # Getting here shouldn't happen + raise + + if self.strip_after_prefix: + view.skip_ws() + + invoker = view.get_word() + ctx.invoked_with = invoker + # type-checker fails to narrow invoked_prefix type. + ctx.prefix = invoked_prefix # type: ignore + ctx.command = self.all_commands.get(invoker) + return ctx + + async def invoke(self, ctx: Context) -> None: + """|coro| + + Invokes the command given under the invocation context and + handles all the internal event dispatch mechanisms. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to invoke. + """ + if ctx.command is not None: + self.dispatch("command", ctx) + try: + if await self.can_run(ctx, call_once=True): + await ctx.command.invoke(ctx) + else: + raise errors.CheckFailure("The global check once functions failed.") + except errors.CommandError as exc: + await ctx.command.dispatch_error(ctx, exc) + else: + self.dispatch("command_completion", ctx) + elif ctx.invoked_with: + exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found') + self.dispatch("command_error", ctx, exc) + + async def process_commands(self, message: Message) -> None: + """|coro| + + This function processes the commands that have been registered + to the bot and other groups. Without this coroutine, none of the + commands will be triggered. + + By default, this coroutine is called inside the :func:`.on_message` + event. If you choose to override the :func:`.on_message` event, then + you should invoke this coroutine as well. + + This is built using other low level tools, and is equivalent to a + call to :meth:`~.Bot.get_context` followed by a call to :meth:`~.Bot.invoke`. + + This also checks if the message's author is a bot and doesn't + call :meth:`~.Bot.get_context` or :meth:`~.Bot.invoke` if so. + + Parameters + ---------- + message: :class:`disnake.Message` + The message to process commands for. + """ + if message.author.bot: + return + + ctx = await self.get_context(message) + await self.invoke(ctx) + + async def on_message(self, message) -> None: + await self.process_commands(message) diff --git a/disnake/disnake/ext/commands/cog.py b/disnake/disnake/ext/commands/cog.py new file mode 100644 index 0000000000..1c796c1df3 --- /dev/null +++ b/disnake/disnake/ext/commands/cog.py @@ -0,0 +1,899 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import inspect +import logging +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + Union, +) + +import disnake +import disnake.utils +from disnake.enums import Event + +from ._types import _BaseCommand +from .base_core import InvokableApplicationCommand +from .ctx_menus_core import InvokableMessageCommand, InvokableUserCommand +from .slash_core import InvokableSlashCommand + +if TYPE_CHECKING: + from typing_extensions import Self + + from disnake.interactions import ApplicationCommandInteraction + + from ._types import FuncT, MaybeCoro + from .bot import AutoShardedBot, AutoShardedInteractionBot, Bot, InteractionBot + from .context import Context + from .core import Command + + AnyBot = Union[Bot, AutoShardedBot, InteractionBot, AutoShardedInteractionBot] + + +__all__ = ( + "CogMeta", + "Cog", +) + +MISSING: Any = disnake.utils.MISSING +_log = logging.getLogger(__name__) + + +def _cog_special_method(func: FuncT) -> FuncT: + func.__cog_special_method__ = None + return func + + +class CogMeta(type): + """A metaclass for defining a cog. + + Note that you should probably not use this directly. It is exposed + purely for documentation purposes along with making custom metaclasses to intermix + with other metaclasses such as the :class:`abc.ABCMeta` metaclass. + + For example, to create an abstract cog mixin class, the following would be done. + + .. code-block:: python3 + + import abc + + class CogABCMeta(commands.CogMeta, abc.ABCMeta): + pass + + class SomeMixin(metaclass=abc.ABCMeta): + pass + + class SomeCogMixin(SomeMixin, commands.Cog, metaclass=CogABCMeta): + pass + + .. note:: + + When passing an attribute of a metaclass that is documented below, note + that you must pass it as a keyword-only argument to the class creation + like the following example: + + .. code-block:: python3 + + class MyCog(commands.Cog, name='My Cog'): + pass + + Attributes + ---------- + name: :class:`str` + The cog name. By default, it is the name of the class with no modification. + description: :class:`str` + The cog description. By default, it is the cleaned docstring of the class. + + .. versionadded:: 1.6 + + command_attrs: Dict[:class:`str`, Any] + A list of attributes to apply to every command inside this cog. The dictionary + is passed into the :class:`Command` options at ``__init__``. + If you specify attributes inside the command attribute in the class, it will + override the one specified inside this attribute. For example: + + .. code-block:: python3 + + class MyCog(commands.Cog, command_attrs=dict(hidden=True)): + @commands.command() + async def foo(self, ctx): + pass # hidden -> True + + @commands.command(hidden=False) + async def bar(self, ctx): + pass # hidden -> False + + slash_command_attrs: Dict[:class:`str`, Any] + A list of attributes to apply to every slash command inside this cog. The dictionary + is passed into the options of every :class:`InvokableSlashCommand` at ``__init__``. + Usage of this kwarg is otherwise the same as with ``command_attrs``. + + .. note:: This does not apply to instances of :class:`SubCommand` or :class:`SubCommandGroup`. + + .. versionadded:: 2.5 + + user_command_attrs: Dict[:class:`str`, Any] + A list of attributes to apply to every user command inside this cog. The dictionary + is passed into the options of every :class:`InvokableUserCommand` at ``__init__``. + Usage of this kwarg is otherwise the same as with ``command_attrs``. + + .. versionadded:: 2.5 + + message_command_attrs: Dict[:class:`str`, Any] + A list of attributes to apply to every message command inside this cog. The dictionary + is passed into the options of every :class:`InvokableMessageCommand` at ``__init__``. + Usage of this kwarg is otherwise the same as with ``command_attrs``. + + .. versionadded:: 2.5 + """ + + __cog_name__: str + __cog_settings__: Dict[str, Any] + __cog_slash_settings__: Dict[str, Any] + __cog_user_settings__: Dict[str, Any] + __cog_message_settings__: Dict[str, Any] + __cog_commands__: List[Command] + __cog_app_commands__: List[InvokableApplicationCommand] + __cog_listeners__: List[Tuple[str, str]] + + def __new__(cls: Type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: + name, bases, attrs = args + attrs["__cog_name__"] = kwargs.pop("name", name) + attrs["__cog_settings__"] = kwargs.pop("command_attrs", {}) + attrs["__cog_slash_settings__"] = kwargs.pop("slash_command_attrs", {}) + attrs["__cog_user_settings__"] = kwargs.pop("user_command_attrs", {}) + attrs["__cog_message_settings__"] = kwargs.pop("message_command_attrs", {}) + + description = kwargs.pop("description", None) + if description is None: + description = inspect.cleandoc(attrs.get("__doc__", "")) + attrs["__cog_description__"] = description + + commands = {} + app_commands = {} + listeners = {} + no_bot_cog = ( + "Commands or listeners must not start with cog_ or bot_ (in method {0.__name__}.{1})" + ) + + new_cls = super().__new__(cls, name, bases, attrs, **kwargs) + for base in reversed(new_cls.__mro__): + for elem, value in base.__dict__.items(): + commands.pop(elem, None) + app_commands.pop(elem, None) + listeners.pop(elem, None) + + is_static_method = isinstance(value, staticmethod) + if is_static_method: + value = value.__func__ + if isinstance(value, _BaseCommand): + if is_static_method: + raise TypeError( + f"Command in method {base}.{elem!r} must not be staticmethod." + ) + if elem.startswith(("cog_", "bot_")): + raise TypeError(no_bot_cog.format(base, elem)) + commands[elem] = value + elif isinstance(value, InvokableApplicationCommand): + if is_static_method: + raise TypeError( + f"Application command in method {base}.{elem!r} must not be staticmethod." + ) + if elem.startswith(("cog_", "bot_")): + raise TypeError(no_bot_cog.format(base, elem)) + app_commands[elem] = value + elif disnake.utils.iscoroutinefunction(value): + if hasattr(value, "__cog_listener__"): + if elem.startswith(("cog_", "bot_")): + raise TypeError(no_bot_cog.format(base, elem)) + listeners[elem] = value + + new_cls.__cog_commands__ = list(commands.values()) # this will be copied in Cog.__new__ + new_cls.__cog_app_commands__ = list(app_commands.values()) + + listeners_as_list = [] + for listener in listeners.values(): + for listener_name in listener.__cog_listener_names__: + # I use __name__ instead of just storing the value so I can inject + # the self attribute when the time comes to add them to the bot + listeners_as_list.append((listener_name, listener.__name__)) + + new_cls.__cog_listeners__ = listeners_as_list + return new_cls + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args) + + @classmethod + def qualified_name(cls) -> str: + return cls.__cog_name__ + + +class Cog(metaclass=CogMeta): + """The base class that all cogs must inherit from. + + A cog is a collection of commands, listeners, and optional state to + help group commands together. More information on them can be found on + the :ref:`ext_commands_cogs` page. + + When inheriting from this class, the options shown in :class:`CogMeta` + are equally valid here. + """ + + __cog_name__: ClassVar[str] + __cog_settings__: ClassVar[Dict[str, Any]] + __cog_commands__: ClassVar[List[Command]] + __cog_app_commands__: ClassVar[List[InvokableApplicationCommand]] + __cog_listeners__: ClassVar[List[Tuple[str, str]]] + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + # For issue 426, we need to store a copy of the command objects + # since we modify them to inject `self` to them. + # To do this, we need to interfere with the Cog creation process. + self = super().__new__(cls) + cmd_attrs = cls.__cog_settings__ + slash_cmd_attrs = cls.__cog_slash_settings__ + user_cmd_attrs = cls.__cog_user_settings__ + message_cmd_attrs = cls.__cog_message_settings__ + + # Either update the command with the cog provided defaults or copy it. + cog_app_commands: List[InvokableApplicationCommand] = [] + for c in cls.__cog_app_commands__: + if isinstance(c, InvokableSlashCommand): + c = c._update_copy(slash_cmd_attrs) + elif isinstance(c, InvokableUserCommand): + c = c._update_copy(user_cmd_attrs) + elif isinstance(c, InvokableMessageCommand): + c = c._update_copy(message_cmd_attrs) + + cog_app_commands.append(c) + + self.__cog_app_commands__ = tuple(cog_app_commands) # type: ignore # overriding ClassVar + # Replace the old command objects with the new copies + for app_command in self.__cog_app_commands__: + setattr(self, app_command.callback.__name__, app_command) + + self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__) # type: ignore # overriding ClassVar + + lookup = {cmd.qualified_name: cmd for cmd in self.__cog_commands__} + for command in self.__cog_commands__: + setattr(self, command.callback.__name__, command) + parent = command.parent + if parent is not None: + # Get the latest parent reference + parent = lookup[parent.qualified_name] # type: ignore + + # Update our parent's reference to our self + parent.remove_command(command.name) # type: ignore + parent.add_command(command) # type: ignore + + return self + + def get_commands(self) -> List[Command]: + """Returns a list of commands the cog has. + + Returns + ------- + List[:class:`.Command`] + A :class:`list` of :class:`.Command`\\s that are + defined inside this cog. + + .. note:: + + This does not include subcommands. + """ + return [c for c in self.__cog_commands__ if c.parent is None] + + def get_application_commands(self) -> List[InvokableApplicationCommand]: + """Returns a list of application commands the cog has. + + Returns + ------- + List[:class:`.InvokableApplicationCommand`] + A :class:`list` of :class:`.InvokableApplicationCommand`\\s that are + defined inside this cog. + + .. note:: + + This does not include subcommands. + """ + return list(self.__cog_app_commands__) + + def get_slash_commands(self) -> List[InvokableSlashCommand]: + """Returns a list of slash commands the cog has. + + Returns + ------- + List[:class:`.InvokableSlashCommand`] + A :class:`list` of :class:`.InvokableSlashCommand`\\s that are + defined inside this cog. + + .. note:: + + This does not include subcommands. + """ + return [c for c in self.__cog_app_commands__ if isinstance(c, InvokableSlashCommand)] + + def get_user_commands(self) -> List[InvokableUserCommand]: + """Returns a list of user commands the cog has. + + Returns + ------- + List[:class:`.InvokableUserCommand`] + A :class:`list` of :class:`.InvokableUserCommand`\\s that are + defined inside this cog. + """ + return [c for c in self.__cog_app_commands__ if isinstance(c, InvokableUserCommand)] + + def get_message_commands(self) -> List[InvokableMessageCommand]: + """Returns a list of message commands the cog has. + + Returns + ------- + List[:class:`.InvokableMessageCommand`] + A :class:`list` of :class:`.InvokableMessageCommand`\\s that are + defined inside this cog. + """ + return [c for c in self.__cog_app_commands__ if isinstance(c, InvokableMessageCommand)] + + @property + def qualified_name(self) -> str: + """:class:`str`: Returns the cog's specified name, not the class name.""" + return self.__cog_name__ + + @property + def description(self) -> str: + """:class:`str`: Returns the cog's description, typically the cleaned docstring.""" + return self.__cog_description__ + + @description.setter + def description(self, description: str) -> None: + self.__cog_description__ = description + + def walk_commands(self) -> Generator[Command, None, None]: + """An iterator that recursively walks through this cog's commands and subcommands. + + Yields + ------ + Union[:class:`.Command`, :class:`.Group`] + A command or group from the cog. + """ + from .core import GroupMixin + + for command in self.__cog_commands__: + if command.parent is None: + yield command + if isinstance(command, GroupMixin): + yield from command.walk_commands() + + def get_listeners(self) -> List[Tuple[str, Callable[..., Any]]]: + """Returns a :class:`list` of (name, function) listener pairs the cog has. + + Returns + ------- + List[Tuple[:class:`str`, :ref:`coroutine `]] + The listeners defined in this cog. + """ + return [(name, getattr(self, method_name)) for name, method_name in self.__cog_listeners__] + + @classmethod + def _get_overridden_method(cls, method: FuncT) -> Optional[FuncT]: + """Return None if the method is not overridden. Otherwise returns the overridden method.""" + return getattr(method.__func__, "__cog_special_method__", method) + + @classmethod + def listener(cls, name: Union[str, Event] = MISSING) -> Callable[[FuncT], FuncT]: + """A decorator that marks a function as a listener. + + This is the cog equivalent of :meth:`.Bot.listen`. + + Parameters + ---------- + name: Union[:class:`str`, :class:`.Event`] + The name of the event being listened to. If not provided, it + defaults to the function's name. + + Raises + ------ + TypeError + The function is not a coroutine function or a string or an :class:`.Event` enum member was not passed as + the name. + """ + if name is not MISSING and not isinstance(name, (str, Event)): + raise TypeError( + f"Cog.listener expected str or Enum but received {name.__class__.__name__!r} instead." + ) + + def decorator(func: FuncT) -> FuncT: + actual = func + if isinstance(actual, staticmethod): + actual = actual.__func__ + if not disnake.utils.iscoroutinefunction(actual): + raise TypeError("Listener function must be a coroutine function.") + actual.__cog_listener__ = True + to_assign = ( + actual.__name__ + if name is MISSING + else (name if isinstance(name, str) else f"on_{name.value}") + ) + try: + actual.__cog_listener_names__.append(to_assign) + except AttributeError: + actual.__cog_listener_names__ = [to_assign] + # we have to return `func` instead of `actual` because + # we need the type to be `staticmethod` for the metaclass + # to pick it up but the metaclass unfurls the function and + # thus the assignments need to be on the actual function + return func + + return decorator + + def has_error_handler(self) -> bool: + """Whether the cog has an error handler. + + .. versionadded:: 1.7 + + :return type: :class:`bool` + """ + return not hasattr(self.cog_command_error.__func__, "__cog_special_method__") + + def has_slash_error_handler(self) -> bool: + """Whether the cog has a slash command error handler. + + :return type: :class:`bool` + """ + return not hasattr(self.cog_slash_command_error.__func__, "__cog_special_method__") + + def has_user_error_handler(self) -> bool: + """Whether the cog has a user command error handler. + + :return type: :class:`bool` + """ + return not hasattr(self.cog_user_command_error.__func__, "__cog_special_method__") + + def has_message_error_handler(self) -> bool: + """Whether the cog has a message command error handler. + + :return type: :class:`bool` + """ + return not hasattr(self.cog_message_command_error.__func__, "__cog_special_method__") + + @_cog_special_method + async def cog_load(self) -> None: + """A special method that is called as a task when the cog is added.""" + pass + + @_cog_special_method + def cog_unload(self) -> None: + """A special method that is called when the cog gets removed. + + This function **cannot** be a coroutine. It must be a regular + function. + + Subclasses must replace this if they want special unloading behaviour. + """ + pass + + @_cog_special_method + def bot_check_once(self, ctx: Context) -> MaybeCoro[bool]: + """A special method that registers as a :meth:`.Bot.check_once` + check. + + This is for text commands only, and doesn't apply to application commands. + + This function **can** be a coroutine and must take a sole parameter, + ``ctx``, to represent the :class:`.Context`. + """ + return True + + @_cog_special_method + def bot_check(self, ctx: Context) -> MaybeCoro[bool]: + """A special method that registers as a :meth:`.Bot.check` + check. + + This is for text commands only, and doesn't apply to application commands. + + This function **can** be a coroutine and must take a sole parameter, + ``ctx``, to represent the :class:`.Context`. + """ + return True + + @_cog_special_method + def bot_slash_command_check_once(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """A special method that registers as a :meth:`.Bot.slash_command_check_once` + check. + + This function **can** be a coroutine and must take a sole parameter, + ``inter``, to represent the :class:`.ApplicationCommandInteraction`. + """ + return True + + @_cog_special_method + def bot_slash_command_check(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """A special method that registers as a :meth:`.Bot.slash_command_check` + check. + + This function **can** be a coroutine and must take a sole parameter, + ``inter``, to represent the :class:`.ApplicationCommandInteraction`. + """ + return True + + @_cog_special_method + def bot_user_command_check_once(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """Similar to :meth:`.Bot.slash_command_check_once` but for user commands.""" + return True + + @_cog_special_method + def bot_user_command_check(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """Similar to :meth:`.Bot.slash_command_check` but for user commands.""" + return True + + @_cog_special_method + def bot_message_command_check_once( + self, inter: ApplicationCommandInteraction + ) -> MaybeCoro[bool]: + """Similar to :meth:`.Bot.slash_command_check_once` but for message commands.""" + return True + + @_cog_special_method + def bot_message_command_check(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """Similar to :meth:`.Bot.slash_command_check` but for message commands.""" + return True + + @_cog_special_method + def cog_check(self, ctx: Context) -> MaybeCoro[bool]: + """A special method that registers as a :func:`~.check` + for every text command and subcommand in this cog. + + This is for text commands only, and doesn't apply to application commands. + + This function **can** be a coroutine and must take a sole parameter, + ``ctx``, to represent the :class:`.Context`. + """ + return True + + @_cog_special_method + def cog_slash_command_check(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """A special method that registers as a :func:`~.check` + for every slash command and subcommand in this cog. + + This function **can** be a coroutine and must take a sole parameter, + ``inter``, to represent the :class:`.ApplicationCommandInteraction`. + """ + return True + + @_cog_special_method + def cog_user_command_check(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """Similar to :meth:`.Cog.cog_slash_command_check` but for user commands.""" + return True + + @_cog_special_method + def cog_message_command_check(self, inter: ApplicationCommandInteraction) -> MaybeCoro[bool]: + """Similar to :meth:`.Cog.cog_slash_command_check` but for message commands.""" + return True + + @_cog_special_method + async def cog_command_error(self, ctx: Context, error: Exception) -> None: + """A special method that is called whenever an error + is dispatched inside this cog. + + This is for text commands only, and doesn't apply to application commands. + + This is similar to :func:`.on_command_error` except only applying + to the commands inside this cog. + + This **must** be a coroutine. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context where the error happened. + error: :class:`CommandError` + The error that was raised. + """ + pass + + @_cog_special_method + async def cog_slash_command_error( + self, inter: ApplicationCommandInteraction, error: Exception + ) -> None: + """A special method that is called whenever an error + is dispatched inside this cog. + + This is similar to :func:`.on_slash_command_error` except only applying + to the slash commands inside this cog. + + This **must** be a coroutine. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction where the error happened. + error: :class:`CommandError` + The error that was raised. + """ + pass + + @_cog_special_method + async def cog_user_command_error( + self, inter: ApplicationCommandInteraction, error: Exception + ) -> None: + """Similar to :func:`cog_slash_command_error` but for user commands.""" + pass + + @_cog_special_method + async def cog_message_command_error( + self, inter: ApplicationCommandInteraction, error: Exception + ) -> None: + """Similar to :func:`cog_slash_command_error` but for message commands.""" + pass + + @_cog_special_method + async def cog_before_invoke(self, ctx: Context) -> None: + """A special method that acts as a cog local pre-invoke hook, + similar to :meth:`.Command.before_invoke`. + + This is for text commands only, and doesn't apply to application commands. + + This **must** be a coroutine. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context. + """ + pass + + @_cog_special_method + async def cog_after_invoke(self, ctx: Context) -> None: + """A special method that acts as a cog local post-invoke hook, + similar to :meth:`.Command.after_invoke`. + + This is for text commands only, and doesn't apply to application commands. + + This **must** be a coroutine. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context. + """ + pass + + @_cog_special_method + async def cog_before_slash_command_invoke(self, inter: ApplicationCommandInteraction) -> None: + """A special method that acts as a cog local pre-invoke hook. + + This is similar to :meth:`.Command.before_invoke` but for slash commands. + + This **must** be a coroutine. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction of the slash command. + """ + pass + + @_cog_special_method + async def cog_after_slash_command_invoke(self, inter: ApplicationCommandInteraction) -> None: + """A special method that acts as a cog local post-invoke hook. + + This is similar to :meth:`.Command.after_invoke` but for slash commands. + + This **must** be a coroutine. + + Parameters + ---------- + inter: :class:`.ApplicationCommandInteraction` + The interaction of the slash command. + """ + pass + + @_cog_special_method + async def cog_before_user_command_invoke(self, inter: ApplicationCommandInteraction) -> None: + """Similar to :meth:`cog_before_slash_command_invoke` but for user commands.""" + pass + + @_cog_special_method + async def cog_after_user_command_invoke(self, inter: ApplicationCommandInteraction) -> None: + """Similar to :meth:`cog_after_slash_command_invoke` but for user commands.""" + pass + + @_cog_special_method + async def cog_before_message_command_invoke(self, inter: ApplicationCommandInteraction) -> None: + """Similar to :meth:`cog_before_slash_command_invoke` but for message commands.""" + pass + + @_cog_special_method + async def cog_after_message_command_invoke(self, inter: ApplicationCommandInteraction) -> None: + """Similar to :meth:`cog_after_slash_command_invoke` but for message commands.""" + pass + + def _inject(self, bot: AnyBot) -> Self: + from .bot import AutoShardedInteractionBot, InteractionBot + + cls = self.__class__ + + if ( + isinstance(bot, (InteractionBot, AutoShardedInteractionBot)) + and len(self.__cog_commands__) > 0 + ): + raise TypeError("@commands.command is not supported for interaction bots.") + + # realistically, the only thing that can cause loading errors + # is essentially just the command loading, which raises if there are + # duplicates. When this condition is met, we want to undo all what + # we've added so far for some form of atomic loading. + for index, command in enumerate(self.__cog_commands__): + command.cog = self + if command.parent is None: + try: + bot.add_command(command) # type: ignore + except Exception: + # undo our additions + for to_undo in self.__cog_commands__[:index]: + if to_undo.parent is None: + bot.remove_command(to_undo.name) # type: ignore + raise + + for index, command in enumerate(self.__cog_app_commands__): + command.cog = self + try: + if isinstance(command, InvokableSlashCommand): + bot.add_slash_command(command) + elif isinstance(command, InvokableUserCommand): + bot.add_user_command(command) + elif isinstance(command, InvokableMessageCommand): + bot.add_message_command(command) + except Exception: + # undo our additions + for to_undo in self.__cog_app_commands__[:index]: + if isinstance(to_undo, InvokableSlashCommand): + bot.remove_slash_command(to_undo.name) + elif isinstance(to_undo, InvokableUserCommand): + bot.remove_user_command(to_undo.name) + elif isinstance(to_undo, InvokableMessageCommand): + bot.remove_message_command(to_undo.name) + raise + + if not hasattr(self.cog_load.__func__, "__cog_special_method__"): + bot.loop.create_task(disnake.utils.maybe_coroutine(self.cog_load)) + + # check if we're overriding the default + if cls.bot_check is not Cog.bot_check: + if isinstance(bot, (InteractionBot, AutoShardedInteractionBot)): + raise TypeError("Cog.bot_check is not supported for interaction bots.") + + bot.add_check(self.bot_check) + + if cls.bot_check_once is not Cog.bot_check_once: + if isinstance(bot, (InteractionBot, AutoShardedInteractionBot)): + raise TypeError("Cog.bot_check_once is not supported for interaction bots.") + + bot.add_check(self.bot_check_once, call_once=True) + + # Add application command checks + if cls.bot_slash_command_check is not Cog.bot_slash_command_check: + bot.add_app_command_check(self.bot_slash_command_check, slash_commands=True) + + if cls.bot_user_command_check is not Cog.bot_user_command_check: + bot.add_app_command_check(self.bot_user_command_check, user_commands=True) + + if cls.bot_message_command_check is not Cog.bot_message_command_check: + bot.add_app_command_check(self.bot_message_command_check, message_commands=True) + + # Add app command one-off checks + if cls.bot_slash_command_check_once is not Cog.bot_slash_command_check_once: + bot.add_app_command_check( + self.bot_slash_command_check_once, + call_once=True, + slash_commands=True, + ) + + if cls.bot_user_command_check_once is not Cog.bot_user_command_check_once: + bot.add_app_command_check( + self.bot_user_command_check_once, call_once=True, user_commands=True + ) + + if cls.bot_message_command_check_once is not Cog.bot_message_command_check_once: + bot.add_app_command_check( + self.bot_message_command_check_once, + call_once=True, + message_commands=True, + ) + + # while Bot.add_listener can raise if it's not a coroutine, + # this precondition is already met by the listener decorator + # already, thus this should never raise. + # Outside of, memory errors and the like... + for name, method_name in self.__cog_listeners__: + bot.add_listener(getattr(self, method_name), name) + + try: + if bot._command_sync_flags.sync_on_cog_actions: + bot._schedule_delayed_command_sync() + except NotImplementedError: + pass + + return self + + def _eject(self, bot: AnyBot) -> None: + cls = self.__class__ + + try: + for command in self.__cog_commands__: + if command.parent is None: + bot.remove_command(command.name) # type: ignore + + for app_command in self.__cog_app_commands__: + if isinstance(app_command, InvokableSlashCommand): + bot.remove_slash_command(app_command.name) + elif isinstance(app_command, InvokableUserCommand): + bot.remove_user_command(app_command.name) + elif isinstance(app_command, InvokableMessageCommand): + bot.remove_message_command(app_command.name) + + for name, method_name in self.__cog_listeners__: + bot.remove_listener(getattr(self, method_name), name) + + if cls.bot_check is not Cog.bot_check: + bot.remove_check(self.bot_check) # type: ignore + + if cls.bot_check_once is not Cog.bot_check_once: + bot.remove_check(self.bot_check_once, call_once=True) # type: ignore + + # Remove application command checks + if cls.bot_slash_command_check is not Cog.bot_slash_command_check: + bot.remove_app_command_check(self.bot_slash_command_check, slash_commands=True) + + if cls.bot_user_command_check is not Cog.bot_user_command_check: + bot.remove_app_command_check(self.bot_user_command_check, user_commands=True) + + if cls.bot_message_command_check is not Cog.bot_message_command_check: + bot.remove_app_command_check(self.bot_message_command_check, message_commands=True) + + # Remove app command one-off checks + if cls.bot_slash_command_check_once is not Cog.bot_slash_command_check_once: + bot.remove_app_command_check( + self.bot_slash_command_check_once, + call_once=True, + slash_commands=True, + ) + + if cls.bot_user_command_check_once is not Cog.bot_user_command_check_once: + bot.remove_app_command_check( + self.bot_user_command_check_once, + call_once=True, + user_commands=True, + ) + + if cls.bot_message_command_check_once is not Cog.bot_message_command_check_once: + bot.remove_app_command_check( + self.bot_message_command_check_once, + call_once=True, + message_commands=True, + ) + + finally: + try: + if bot._command_sync_flags.sync_on_cog_actions: + bot._schedule_delayed_command_sync() + except NotImplementedError: + pass + try: + self.cog_unload() + except Exception as e: + _log.error( + "An error occurred while unloading the %s cog.", self.qualified_name, exc_info=e + ) diff --git a/disnake/disnake/ext/commands/common_bot_base.py b/disnake/disnake/ext/commands/common_bot_base.py new file mode 100644 index 0000000000..8658aa369a --- /dev/null +++ b/disnake/disnake/ext/commands/common_bot_base.py @@ -0,0 +1,545 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +import collections.abc +import importlib.machinery +import importlib.util +import logging +import os +import sys +import time +import types +from typing import TYPE_CHECKING, Any, Dict, Generic, List, Mapping, Optional, Set, TypeVar, Union + +import disnake +import disnake.utils + +from . import errors +from .cog import Cog + +if TYPE_CHECKING: + from ._types import CoroFunc + from .bot import AutoShardedBot, AutoShardedInteractionBot, Bot, InteractionBot + from .help import HelpCommand + + AnyBot = Union[Bot, AutoShardedBot, InteractionBot, AutoShardedInteractionBot] + +__all__ = ("CommonBotBase",) +_log = logging.getLogger(__name__) + +CogT = TypeVar("CogT", bound="Cog") +CFT = TypeVar("CFT", bound="CoroFunc") + +MISSING: Any = disnake.utils.MISSING + + +def _is_submodule(parent: str, child: str) -> bool: + return parent == child or child.startswith(parent + ".") + + +class CommonBotBase(Generic[CogT]): + if TYPE_CHECKING: + extra_events: Dict[str, List[CoroFunc]] + + def __init__( + self, + *args: Any, + owner_id: Optional[int] = None, + owner_ids: Optional[Set[int]] = None, + reload: bool = False, + **kwargs: Any, + ) -> None: + self.__cogs: Dict[str, Cog] = {} + self.__extensions: Dict[str, types.ModuleType] = {} + self._is_closed: bool = False + + self.owner_id: Optional[int] = owner_id + self.owner_ids: Set[int] = owner_ids or set() + self.owner: Optional[disnake.User] = None + self.owners: Set[disnake.TeamMember] = set() + + if self.owner_id and self.owner_ids: + raise TypeError("Both owner_id and owner_ids are set.") + + if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection): + raise TypeError(f"owner_ids must be a collection not {self.owner_ids.__class__!r}") + + self.reload: bool = reload + + super().__init__(*args, **kwargs) + + # FIXME: make event name pos-only or remove entirely in v3.0 + def dispatch(self, event_name: str, *args: Any, **kwargs: Any) -> None: + # super() will resolve to Client + super().dispatch(event_name, *args, **kwargs) # type: ignore + + async def _fill_owners(self) -> None: + if self.owner_id or self.owner_ids: + return + + app: disnake.AppInfo = await self.application_info() # type: ignore + if app.team: + self.owners = owners = { + member + for member in app.team.members + # these roles can access the bot token, consider them bot owners + if member.role in (disnake.TeamMemberRole.admin, disnake.TeamMemberRole.developer) + } + self.owner_ids = {m.id for m in owners} + else: + self.owner = app.owner + self.owner_id = app.owner.id + + async def close(self) -> None: + self._is_closed = True + + for extension in tuple(self.__extensions): + try: + self.unload_extension(extension) + except Exception as error: + error.__suppress_context__ = True + _log.error("Failed to unload extension %r", extension, exc_info=error) + + for cog in tuple(self.__cogs): + try: + self.remove_cog(cog) + except Exception as error: + error.__suppress_context__ = True + _log.exception("Failed to remove cog %r", cog, exc_info=error) + + await super().close() # type: ignore + + @disnake.utils.copy_doc(disnake.Client.login) + async def login(self, token: str) -> None: + await super().login(token=token) # type: ignore + + loop: asyncio.AbstractEventLoop = self.loop # type: ignore + if self.reload: + loop.create_task(self._watchdog()) + + # prefetch + loop.create_task(self._fill_owners()) + + async def is_owner(self, user: Union[disnake.User, disnake.Member]) -> bool: + """|coro| + + Checks if a :class:`~disnake.User` or :class:`~disnake.Member` is the owner of + this bot. + + If :attr:`owner_id` and :attr:`owner_ids` are not set, they are fetched automatically + through the use of :meth:`~.Bot.application_info`. + + .. versionchanged:: 1.3 + The function also checks if the application is team-owned if + :attr:`owner_ids` is not set. + + .. versionchanged:: 2.10 + Also takes team roles into account; only team members with the :attr:`~disnake.TeamMemberRole.admin` + or :attr:`~disnake.TeamMemberRole.developer` roles are considered bot owners. + + Parameters + ---------- + user: :class:`.abc.User` + The user to check for. + + Returns + ------- + :class:`bool` + Whether the user is the owner. + """ + if not self.owner_id and not self.owner_ids: + await self._fill_owners() + + if self.owner_id: + return user.id == self.owner_id + else: + return user.id in self.owner_ids + + def add_cog(self, cog: Cog, *, override: bool = False) -> None: + """Adds a "cog" to the bot. + + A cog is a class that has its own event listeners and commands. + + This automatically re-syncs application commands, provided that + :attr:`command_sync_flags.sync_on_cog_actions <.CommandSyncFlags.sync_on_cog_actions>` + isn't disabled. + + .. versionchanged:: 2.0 + + :exc:`.ClientException` is raised when a cog with the same name + is already loaded. + + Parameters + ---------- + cog: :class:`.Cog` + The cog to register to the bot. + override: :class:`bool` + If a previously loaded cog with the same name should be ejected + instead of raising an error. + + .. versionadded:: 2.0 + + Raises + ------ + TypeError + The cog does not inherit from :class:`.Cog`. + CommandError + An error happened during loading. + ClientException + A cog with the same name is already loaded. + """ + if not isinstance(cog, Cog): + raise TypeError("cogs must derive from Cog") + + cog_name = cog.__cog_name__ + existing = self.__cogs.get(cog_name) + + if existing is not None: + if not override: + raise disnake.ClientException(f"Cog named {cog_name!r} already loaded") + self.remove_cog(cog_name) + + # NOTE: Should be covariant + cog = cog._inject(self) # type: ignore + self.__cogs[cog_name] = cog + + def get_cog(self, name: str) -> Optional[Cog]: + """Gets the cog instance requested. + + If the cog is not found, ``None`` is returned instead. + + Parameters + ---------- + name: :class:`str` + The name of the cog you are requesting. + This is equivalent to the name passed via keyword + argument in class creation or the class name if unspecified. + + Returns + ------- + Optional[:class:`Cog`] + The cog that was requested. If not found, returns ``None``. + """ + return self.__cogs.get(name) + + def remove_cog(self, name: str) -> Optional[Cog]: + """Removes a cog from the bot and returns it. + + All registered commands and event listeners that the + cog has registered will be removed as well. + + If no cog is found then this method has no effect. + + This automatically re-syncs application commands, provided that + :attr:`command_sync_flags.sync_on_cog_actions <.CommandSyncFlags.sync_on_cog_actions>` + isn't disabled. + + Parameters + ---------- + name: :class:`str` + The name of the cog to remove. + + Returns + ------- + Optional[:class:`.Cog`] + The cog that was removed. Returns ``None`` if not found. + """ + cog = self.__cogs.pop(name, None) + if cog is None: + return + + help_command: Optional[HelpCommand] = getattr(self, "_help_command", None) + if help_command and help_command.cog is cog: + help_command.cog = None + # NOTE: Should be covariant + cog._eject(self) # type: ignore + + return cog + + @property + def cogs(self) -> Mapping[str, Cog]: + """Mapping[:class:`str`, :class:`Cog`]: A read-only mapping of cog name to cog.""" + return types.MappingProxyType(self.__cogs) + + # extensions + + def _remove_module_references(self, name: str) -> None: + # find all references to the module + # remove the cogs registered from the module + for cogname, cog in self.__cogs.copy().items(): + if _is_submodule(name, cog.__module__): + self.remove_cog(cogname) + # remove all the listeners from the module + for event_list in self.extra_events.copy().values(): + remove = [ + index + for index, event in enumerate(event_list) + if event.__module__ and _is_submodule(name, event.__module__) + ] + + for index in reversed(remove): + del event_list[index] + + def _call_module_finalizers(self, lib: types.ModuleType, key: str) -> None: + try: + func = lib.teardown + except AttributeError: + pass + else: + try: + func(self) + except Exception as error: + error.__suppress_context__ = True + _log.error("Exception in extension finalizer %r", key, exc_info=error) + finally: + self.__extensions.pop(key, None) + sys.modules.pop(key, None) + name = lib.__name__ + for module in list(sys.modules.keys()): + if _is_submodule(name, module): + del sys.modules[module] + + def _load_from_module_spec(self, spec: importlib.machinery.ModuleSpec, key: str) -> None: + # precondition: key not in self.__extensions + lib = importlib.util.module_from_spec(spec) + sys.modules[key] = lib + try: + spec.loader.exec_module(lib) # type: ignore + except Exception as e: + del sys.modules[key] + raise errors.ExtensionFailed(key, e) from e + + try: + setup = lib.setup + except AttributeError: + del sys.modules[key] + raise errors.NoEntryPointError(key) from None + + try: + setup(self) + except Exception as e: + del sys.modules[key] + self._remove_module_references(lib.__name__) + self._call_module_finalizers(lib, key) + raise errors.ExtensionFailed(key, e) from e + else: + self.__extensions[key] = lib + + def _resolve_name(self, name: str, package: Optional[str]) -> str: + try: + return importlib.util.resolve_name(name, package) + except ImportError as e: + raise errors.ExtensionNotFound(name) from e + + def load_extension(self, name: str, *, package: Optional[str] = None) -> None: + """Loads an extension. + + An extension is a python module that contains commands, cogs, or + listeners. + + An extension must have a global function, ``setup`` defined as + the entry point on what to do when the extension is loaded. This entry + point must have a single argument, the ``bot``. + + Parameters + ---------- + name: :class:`str` + The extension name to load. It must be dot separated like + regular Python imports if accessing a sub-module. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when loading an extension using a relative path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + + Raises + ------ + ExtensionNotFound + The extension could not be imported. + This is also raised if the name of the extension could not + be resolved using the provided ``package`` parameter. + ExtensionAlreadyLoaded + The extension is already loaded. + NoEntryPointError + The extension does not have a setup function. + ExtensionFailed + The extension or its setup function had an execution error. + """ + name = self._resolve_name(name, package) + if name in self.__extensions: + raise errors.ExtensionAlreadyLoaded(name) + + spec = importlib.util.find_spec(name) + if spec is None: + raise errors.ExtensionNotFound(name) + + self._load_from_module_spec(spec, name) + + def unload_extension(self, name: str, *, package: Optional[str] = None) -> None: + """Unloads an extension. + + When the extension is unloaded, all commands, listeners, and cogs are + removed from the bot and the module is un-imported. + + The extension can provide an optional global function, ``teardown``, + to do miscellaneous clean-up if necessary. This function takes a single + parameter, the ``bot``, similar to ``setup`` from + :meth:`~.Bot.load_extension`. + + Parameters + ---------- + name: :class:`str` + The extension name to unload. It must be dot separated like + regular Python imports if accessing a sub-module. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when unloading an extension using a relative path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + + Raises + ------ + ExtensionNotFound + The name of the extension could not + be resolved using the provided ``package`` parameter. + ExtensionNotLoaded + The extension was not loaded. + """ + name = self._resolve_name(name, package) + lib = self.__extensions.get(name) + if lib is None: + raise errors.ExtensionNotLoaded(name) + + self._remove_module_references(lib.__name__) + self._call_module_finalizers(lib, name) + + def reload_extension(self, name: str, *, package: Optional[str] = None) -> None: + """Atomically reloads an extension. + + This replaces the extension with the same extension, only refreshed. This is + equivalent to a :meth:`unload_extension` followed by a :meth:`load_extension` + except done in an atomic way. That is, if an operation fails mid-reload then + the bot will roll-back to the prior working state. + + Parameters + ---------- + name: :class:`str` + The extension name to reload. It must be dot separated like + regular Python imports if accessing a sub-module. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when reloading an extension using a relative path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + + Raises + ------ + ExtensionNotLoaded + The extension was not loaded. + ExtensionNotFound + The extension could not be imported. + This is also raised if the name of the extension could not + be resolved using the provided ``package`` parameter. + NoEntryPointError + The extension does not have a setup function. + ExtensionFailed + The extension setup function had an execution error. + """ + name = self._resolve_name(name, package) + lib = self.__extensions.get(name) + if lib is None: + raise errors.ExtensionNotLoaded(name) + + # get the previous module states from sys modules + modules = { + name: module + for name, module in sys.modules.items() + if _is_submodule(lib.__name__, name) + } + + try: + # Unload and then load the module... + self._remove_module_references(lib.__name__) + self._call_module_finalizers(lib, name) + self.load_extension(name) + except Exception: + # if the load failed, the remnants should have been + # cleaned from the load_extension function call + # so let's load it from our old compiled library. + lib.setup(self) + self.__extensions[name] = lib + + # revert sys.modules back to normal and raise back to caller + sys.modules.update(modules) + raise + + def load_extensions(self, path: str) -> None: + """Loads all extensions in a directory. + + .. versionadded:: 2.4 + + Parameters + ---------- + path: :class:`str` + The path to search for extensions + """ + for extension in disnake.utils.search_directory(path): + self.load_extension(extension) + + @property + def extensions(self) -> Mapping[str, types.ModuleType]: + """Mapping[:class:`str`, :class:`py:types.ModuleType`]: A read-only mapping of extension name to extension.""" + return types.MappingProxyType(self.__extensions) + + async def _watchdog(self) -> None: + """|coro| + + Starts the bot watchdog which will watch currently loaded extensions + and reload them when they're modified. + """ + if isinstance(self, disnake.Client): + await self.wait_until_ready() + + reload_log = logging.getLogger(__name__) + + if isinstance(self, disnake.Client): + is_closed = self.is_closed + else: + is_closed = lambda: False + + reload_log.info("WATCHDOG: Watching extensions") + + last = time.time() + while not is_closed(): + t = time.time() + + extensions = set() + for name, module in self.extensions.items(): + file = module.__file__ + if file and os.stat(file).st_mtime > last: + extensions.add(name) + + if extensions: + try: + self.i18n.reload() # type: ignore + except Exception as e: + reload_log.exception(e) + + for name in extensions: + try: + self.reload_extension(name) + except errors.ExtensionError as e: + reload_log.exception(e) + else: + reload_log.info(f"WATCHDOG: Reloaded '{name}'") + + await asyncio.sleep(1) + last = t diff --git a/disnake/disnake/ext/commands/context.py b/disnake/disnake/ext/commands/context.py new file mode 100644 index 0000000000..9c23583d28 --- /dev/null +++ b/disnake/disnake/ext/commands/context.py @@ -0,0 +1,387 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import inspect +import re +from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union + +import disnake.abc +import disnake.utils +from disnake import ApplicationCommandInteraction +from disnake.message import Message + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + + from disnake.channel import DMChannel, GroupChannel + from disnake.guild import Guild, GuildMessageable + from disnake.member import Member + from disnake.state import ConnectionState + from disnake.user import ClientUser, User + from disnake.voice_client import VoiceProtocol + + from .bot import AutoShardedBot, Bot + from .cog import Cog + from .core import Command + from .view import StringView + +__all__ = ("Context", "GuildContext") + +MISSING: Any = disnake.utils.MISSING + + +T = TypeVar("T") +BotT = TypeVar("BotT", bound="Union[Bot, AutoShardedBot]") +CogT = TypeVar("CogT", bound="Cog") + +if TYPE_CHECKING: + P = ParamSpec("P") +else: + P = TypeVar("P") + + +class Context(disnake.abc.Messageable, Generic[BotT]): + """Represents the context in which a command is being invoked under. + + This class contains a lot of meta data to help you understand more about + the invocation context. This class is not created manually and is instead + passed around to commands as the first parameter. + + This class implements the :class:`.abc.Messageable` ABC. + + Attributes + ---------- + message: :class:`.Message` + The message that triggered the command being executed. + bot: :class:`.Bot` + The bot that contains the command being executed. + args: :class:`list` + The list of transformed arguments that were passed into the command. + If this is accessed during the :func:`.on_command_error` event + then this list could be incomplete. + kwargs: :class:`dict` + A dictionary of transformed arguments that were passed into the command. + Similar to :attr:`args`\\, if this is accessed in the + :func:`.on_command_error` event then this dict could be incomplete. + current_parameter: Optional[:class:`inspect.Parameter`] + The parameter that is currently being inspected and converted. + This is only of use for within converters. + + .. versionadded:: 2.0 + + prefix: Optional[:class:`str`] + The prefix that was used to invoke the command. + command: Optional[:class:`Command`] + The command that is being invoked currently. + invoked_with: Optional[:class:`str`] + The command name that triggered this invocation. Useful for finding out + which alias called the command. + invoked_parents: List[:class:`str`] + The command names of the parents that triggered this invocation. Useful for + finding out which aliases called the command. + + For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``. + + .. versionadded:: 1.7 + + invoked_subcommand: Optional[:class:`Command`] + The subcommand that was invoked. + If no valid subcommand was invoked then this is equal to ``None``. + subcommand_passed: Optional[:class:`str`] + The string that was attempted to call a subcommand. This does not have + to point to a valid registered subcommand and could just point to a + nonsense string. If nothing was passed to attempt a call to a + subcommand then this is set to ``None``. + command_failed: :class:`bool` + Whether the command failed to be parsed, checked, or invoked. + """ + + def __init__( + self, + *, + message: Message, + bot: BotT, + view: StringView, + args: List[Any] = MISSING, + kwargs: Dict[str, Any] = MISSING, + prefix: Optional[str] = None, + command: Optional[Command] = None, + invoked_with: Optional[str] = None, + invoked_parents: List[str] = MISSING, + invoked_subcommand: Optional[Command] = None, + subcommand_passed: Optional[str] = None, + command_failed: bool = False, + current_parameter: Optional[inspect.Parameter] = None, + ) -> None: + self.message: Message = message + self.bot: BotT = bot + self.args: List[Any] = args or [] + self.kwargs: Dict[str, Any] = kwargs or {} + self.prefix: Optional[str] = prefix + self.command: Optional[Command] = command + self.view: StringView = view + self.invoked_with: Optional[str] = invoked_with + self.invoked_parents: List[str] = invoked_parents or [] + self.invoked_subcommand: Optional[Command] = invoked_subcommand + self.subcommand_passed: Optional[str] = subcommand_passed + self.command_failed: bool = command_failed + self.current_parameter: Optional[inspect.Parameter] = current_parameter + self._state: ConnectionState = self.message._state + + async def invoke(self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs) -> T: + """|coro| + + Calls a command with the arguments given. + + This is useful if you want to just call the callback that a + :class:`.Command` holds internally. + + .. note:: + + This does not handle converters, checks, cooldowns, pre-invoke, + or after-invoke hooks in any matter. It calls the internal callback + directly as-if it was a regular function. + + You must take care in passing the proper arguments when + using this function. + + Parameters + ---------- + command: :class:`.Command` + The command that is going to be called. + *args + The arguments to use. + **kwargs + The keyword arguments to use. + + Raises + ------ + TypeError + The command argument to invoke is missing. + """ + return await command(self, *args, **kwargs) + + async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> None: + """|coro| + + Calls the command again. + + This is similar to :meth:`.invoke` except that it bypasses + checks, cooldowns, and error handlers. + + .. note:: + + If you want to bypass :exc:`.UserInputError` derived exceptions, + it is recommended to use the regular :meth:`.invoke` + as it will work more naturally. After all, this will end up + using the old arguments the user has used and will thus just + fail again. + + Parameters + ---------- + call_hooks: :class:`bool` + Whether to call the before and after invoke hooks. + restart: :class:`bool` + Whether to start the call chain from the very beginning + or where we left off (i.e. the command that caused the error). + The default is to start where we left off. + + Raises + ------ + ValueError + The context to reinvoke is not valid. + """ + cmd = self.command + view = self.view + if cmd is None: + raise ValueError("This context is not valid.") + + # some state to revert to when we're done + index, previous = view.index, view.previous + invoked_with = self.invoked_with + invoked_subcommand = self.invoked_subcommand + invoked_parents = self.invoked_parents + subcommand_passed = self.subcommand_passed + + if restart: + to_call = cmd.root_parent or cmd + view.index = len(self.prefix or "") + view.previous = 0 + self.invoked_parents = [] + self.invoked_with = view.get_word() # advance to get the root command + else: + to_call = cmd + + try: + await to_call.reinvoke(self, call_hooks=call_hooks) + finally: + self.command = cmd + view.index = index + view.previous = previous + self.invoked_with = invoked_with + self.invoked_subcommand = invoked_subcommand + self.invoked_parents = invoked_parents + self.subcommand_passed = subcommand_passed + + @property + def valid(self) -> bool: + """:class:`bool`: Whether the invocation context is valid to be invoked with.""" + return self.prefix is not None and self.command is not None + + async def _get_channel(self) -> disnake.abc.Messageable: + return self.channel + + @property + def clean_prefix(self) -> str: + """:class:`str`: The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``. + + .. versionadded:: 2.0 + """ + if self.prefix is None: + return "" + + user = self.me + # this breaks if the prefix mention is not the bot itself but I + # consider this to be an *incredibly* strange use case. I'd rather go + # for this common use case rather than waste performance for the + # odd one. + pattern = re.compile(rf"<@!?{user.id}>") + return pattern.sub("@" + user.display_name.replace("\\", r"\\"), self.prefix) + + @property + def cog(self) -> Optional[Cog]: + """Optional[:class:`.Cog`]: Returns the cog associated with this context's command. Returns ``None`` if it does not exist.""" + if self.command is None: + return None + return self.command.cog + + @disnake.utils.cached_property + def guild(self) -> Optional[Guild]: + """Optional[:class:`.Guild`]: Returns the guild associated with this context's command. Returns ``None`` if not available.""" + return self.message.guild + + @disnake.utils.cached_property + def channel(self) -> Union[GuildMessageable, DMChannel, GroupChannel]: + """Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command. + Shorthand for :attr:`.Message.channel`. + """ + return self.message.channel + + @disnake.utils.cached_property + def author(self) -> Union[User, Member]: + """Union[:class:`~disnake.User`, :class:`.Member`]: + Returns the author associated with this context's command. Shorthand for :attr:`.Message.author` + """ + return self.message.author + + @disnake.utils.cached_property + def me(self) -> Union[Member, ClientUser]: + """Union[:class:`.Member`, :class:`.ClientUser`]: + Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message contexts. + """ + # bot.user will never be None at this point. + return self.guild.me if self.guild is not None else self.bot.user + + @property + def voice_client(self) -> Optional[VoiceProtocol]: + r"""Optional[:class:`.VoiceProtocol`]: A shortcut to :attr:`.Guild.voice_client`\, if applicable.""" + g = self.guild + return g.voice_client if g else None + + async def send_help(self, *args: Any) -> Any: + """|coro| + + Shows the help command for the specified entity if given. + The entity can be a command or a cog. + + If no entity is given, then it'll show help for the + entire bot. + + If the entity is a string, then it looks up whether it's a + :class:`Cog` or a :class:`Command`. + + .. note:: + + Due to the way this function works, instead of returning + something similar to :meth:`~.commands.HelpCommand.command_not_found` + this returns :class:`None` on bad input or no help command. + + Parameters + ---------- + entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]] + The entity to show help for. + + Returns + ------- + Any + The result of the help command, if any. + """ + from .core import Command, Group, wrap_callback + from .errors import CommandError + + bot = self.bot + cmd = bot.help_command + + if cmd is None: + return None + + cmd = cmd.copy() + cmd.context = self + if len(args) == 0: + await cmd.prepare_help_command(self, None) + mapping = cmd.get_bot_mapping() + injected = wrap_callback(cmd.send_bot_help) + try: + return await injected(mapping) + except CommandError as e: + await cmd.on_help_command_error(self, e) + return None + + entity = args[0] + if isinstance(entity, str): + entity = bot.get_cog(entity) or bot.get_command(entity) + + if entity is None: + return None + + if not hasattr(entity, "qualified_name"): + # if we're here then it's not a cog, group, or command. + return None + + await cmd.prepare_help_command(self, entity.qualified_name) + + try: + if hasattr(entity, "__cog_commands__"): + injected = wrap_callback(cmd.send_cog_help) + return await injected(entity) + elif isinstance(entity, Group): + injected = wrap_callback(cmd.send_group_help) + return await injected(entity) + elif isinstance(entity, Command): + injected = wrap_callback(cmd.send_command_help) + return await injected(entity) + else: + return None + except CommandError as e: + await cmd.on_help_command_error(self, e) + + @disnake.utils.copy_doc(Message.reply) + async def reply(self, content: Optional[str] = None, **kwargs: Any) -> Message: + return await self.message.reply(content, **kwargs) + + +class GuildContext(Context): + """A Context subclass meant for annotation + + No runtime behavior is changed but annotations are modified + to seem like the context may never be invoked in a DM. + """ + + guild: Guild + channel: GuildMessageable + author: Member + me: Member + + +AnyContext = Union[Context, ApplicationCommandInteraction] diff --git a/disnake/disnake/ext/commands/converter.py b/disnake/disnake/ext/commands/converter.py new file mode 100644 index 0000000000..e5ab4b2b3d --- /dev/null +++ b/disnake/disnake/ext/commands/converter.py @@ -0,0 +1,1390 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import functools +import inspect +import re +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generic, + Iterable, + List, + Literal, + Optional, + Protocol, + Tuple, + Type, + TypeVar, + Union, + runtime_checkable, +) + +import disnake + +from .context import AnyContext, Context +from .errors import ( + BadArgument, + BadBoolArgument, + BadColourArgument, + BadInviteArgument, + BadLiteralArgument, + BadUnionArgument, + ChannelNotFound, + ChannelNotReadable, + CommandError, + ConversionError, + EmojiNotFound, + GuildNotFound, + GuildScheduledEventNotFound, + GuildSoundboardSoundNotFound, + GuildStickerNotFound, + MemberNotFound, + MessageNotFound, + NoPrivateMessage, + ObjectNotFound, + PartialEmojiConversionFailure, + RoleNotFound, + ThreadNotFound, + UserNotFound, +) + +if TYPE_CHECKING: + from disnake.abc import MessageableChannel + + +# TODO: USE ACTUAL FUNCTIONS INSTEAD OF USELESS CLASSES + +__all__ = ( + "Converter", + "IDConverter", + "ObjectConverter", + "MemberConverter", + "UserConverter", + "PartialMessageConverter", + "MessageConverter", + "GuildChannelConverter", + "TextChannelConverter", + "VoiceChannelConverter", + "StageChannelConverter", + "CategoryChannelConverter", + "ForumChannelConverter", + "MediaChannelConverter", + "ThreadConverter", + "ColourConverter", + "ColorConverter", + "RoleConverter", + "GameConverter", + "InviteConverter", + "GuildConverter", + "EmojiConverter", + "PartialEmojiConverter", + "GuildStickerConverter", + "GuildSoundboardSoundConverter", + "PermissionsConverter", + "GuildScheduledEventConverter", + "clean_content", + "Greedy", + "run_converters", +) + + +_utils_get = disnake.utils.get +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +CT = TypeVar("CT", bound=disnake.abc.GuildChannel) +TT = TypeVar("TT", bound=disnake.Thread) + + +def _get_from_guilds( + client: disnake.Client, func: Callable[[disnake.Guild], Optional[T]] +) -> Optional[T]: + for guild in client.guilds: + if result := func(guild): + return result + return None + + +@runtime_checkable +class Converter(Protocol[T_co]): + """The base class of custom converters that require the :class:`.Context` + or :class:`.ApplicationCommandInteraction` to be passed to be useful. + + This allows you to implement converters that function similar to the + special cased ``disnake`` classes. + + Classes that derive from this should override the :meth:`~.Converter.convert` + method to do its conversion logic. This method must be a :ref:`coroutine `. + """ + + async def convert(self, ctx: AnyContext, argument: str) -> T_co: + """|coro| + + The method to override to do conversion logic. + + If an error is found while converting, it is recommended to + raise a :exc:`.CommandError` derived exception as it will + properly propagate to the error handlers. + + Parameters + ---------- + ctx: Union[:class:`.Context`, :class:`.ApplicationCommandInteraction`] + The invocation context that the argument is being used in. + argument: :class:`str` + The argument that is being converted. + + Raises + ------ + CommandError + A generic exception occurred when converting the argument. + BadArgument + The converter failed to convert the argument. + """ + raise NotImplementedError("Derived classes need to implement this.") + + +_ID_REGEX = re.compile(r"([0-9]{17,19})$") + + +class IDConverter(Converter[T_co]): + @staticmethod + def _get_id_match(argument: str) -> Optional[re.Match[str]]: + return _ID_REGEX.match(argument) + + +class ObjectConverter(IDConverter[disnake.Object]): + """Converts to a :class:`~disnake.Object`. + + The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`). + + .. versionadded:: 2.0 + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by member, role, or channel mention. + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Object: + match = self._get_id_match(argument) or re.match( + r"<(?:@(?:!|&)?|#)([0-9]{17,19})>$", argument + ) + + if match is None: + raise ObjectNotFound(argument) + + result = int(match.group(1)) + + return disnake.Object(id=result) + + +class MemberConverter(IDConverter[disnake.Member]): + """Converts to a :class:`~disnake.Member`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID + 2. Lookup by mention + 3. Lookup by username#discrim + 4. Lookup by username#0 + 5. Lookup by nickname + 6. Lookup by global name + 7. Lookup by username + + The name resolution order matches the one used by :meth:`.Guild.get_member_named`. + + .. versionchanged:: 1.5 + Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 1.5.1 + This converter now lazily fetches members from the gateway and HTTP APIs, + optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled. + + .. versionchanged:: 2.9 + Name resolution order changed from ``username > nick`` to + ``nick > global_name > username`` to account for the username migration. + """ + + async def query_member_named( + self, guild: disnake.Guild, argument: str + ) -> Optional[disnake.Member]: + cache = guild._state.member_cache_flags.joined + + username, _, discriminator = argument.rpartition("#") + if username and ( + discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal()) + ): + # legacy behavior + members = await guild.query_members(username, limit=100, cache=cache) + return _utils_get(members, name=username, discriminator=discriminator) + else: + members = await guild.query_members(argument, limit=100, cache=cache) + return disnake.utils.find( + lambda m: m.nick == argument or m.global_name == argument or m.name == argument, + members, + ) + + async def query_member_by_id( + self, bot: disnake.Client, guild: disnake.Guild, user_id: int + ) -> Optional[disnake.Member]: + ws = bot._get_websocket(shard_id=guild.shard_id) + cache = guild._state.member_cache_flags.joined + if ws.is_ratelimited(): + # If we're being rate limited on the WS, then fall back to using the HTTP API + # So we don't have to wait ~60 seconds for the query to finish + try: + member = await guild.fetch_member(user_id) + except disnake.HTTPException: + return None + + if cache: + guild._add_member(member) + return member + + # If we're not being rate limited then we can use the websocket to actually query + members = await guild.query_members(limit=1, user_ids=[user_id], cache=cache) + if not members: + return None + return members[0] + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Member: + bot: disnake.Client = ctx.bot + match = self._get_id_match(argument) or re.match(r"<@!?([0-9]{17,19})>$", argument) + guild = ctx.guild + result: Optional[disnake.Member] = None + user_id: Optional[int] = None + + if match is None: + # not a mention... + if guild: + result = guild.get_member_named(argument) + else: + result = _get_from_guilds(bot, lambda g: g.get_member_named(argument)) + else: + user_id = int(match.group(1)) + if guild: + mentions: Iterable[disnake.Member] + if isinstance(ctx, Context): + mentions = ( + user for user in ctx.message.mentions if isinstance(user, disnake.Member) + ) + else: + mentions = [] + result = guild.get_member(user_id) or _utils_get(mentions, id=user_id) + else: + result = _get_from_guilds(bot, lambda g: g.get_member(user_id)) + + if result is None: + if guild is None: + raise MemberNotFound(argument) + + if user_id is not None: + result = await self.query_member_by_id(bot, guild, user_id) + else: + result = await self.query_member_named(guild, argument) + + if not result: + raise MemberNotFound(argument) + + return result + + +class UserConverter(IDConverter[disnake.User]): + """Converts to a :class:`~disnake.User`. + + All lookups are via the global user cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID + 2. Lookup by mention + 3. Lookup by username#discrim + 4. Lookup by username#0 + 5. Lookup by global name + 6. Lookup by username + + .. versionchanged:: 1.5 + Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 1.6 + This converter now lazily fetches users from the HTTP APIs if an ID is passed + and it's not available in cache. + + .. versionchanged:: 2.9 + Now takes :attr:`~disnake.User.global_name` into account. + No longer automatically removes ``"@"`` prefix from arguments. + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.User: + match = self._get_id_match(argument) or re.match(r"<@!?([0-9]{17,19})>$", argument) + state = ctx._state + bot: disnake.Client = ctx.bot + result: Optional[Union[disnake.User, disnake.Member]] = None + + if match is not None: + user_id = int(match.group(1)) + + mentions: Iterable[Union[disnake.User, disnake.Member]] + if isinstance(ctx, Context): + mentions = ctx.message.mentions + else: + mentions = [] + result = bot.get_user(user_id) or _utils_get(mentions, id=user_id) + + if result is None: + try: + result = await bot.fetch_user(user_id) + except disnake.HTTPException: + raise UserNotFound(argument) from None + + if isinstance(result, disnake.Member): + return result._user + return result # type: ignore + + username, _, discriminator = argument.rpartition("#") + # n.b. there's no builtin method that only matches arabic digits, `isdecimal` is the closest one. + # it really doesn't matter much, worst case is unnecessary computations + if username and ( + discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal()) + ): + # legacy behavior + result = _utils_get(state._users.values(), name=username, discriminator=discriminator) + if result is not None: + return result + + result = disnake.utils.find( + lambda u: u.global_name == argument or u.name == argument, + state._users.values(), + ) + + if result is None: + raise UserNotFound(argument) + + return result + + +class PartialMessageConverter(Converter[disnake.PartialMessage]): + """Converts to a :class:`~disnake.PartialMessage`. + + .. versionadded:: 1.7 + + The creation strategy is as follows (in order): + + 1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID") + 2. By message ID (The message is assumed to be in the context channel.) + 3. By message URL + """ + + @staticmethod + def _get_id_matches(ctx: AnyContext, argument: str) -> Tuple[Optional[int], int, int]: + id_regex = re.compile(r"(?:(?P[0-9]{17,19})-)?(?P[0-9]{17,19})$") + link_regex = re.compile( + r"https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/" + r"(?P[0-9]{17,19}|@me)" + r"/(?P[0-9]{17,19})/(?P[0-9]{17,19})/?$" + ) + match = id_regex.match(argument) or link_regex.match(argument) + if not match: + raise MessageNotFound(argument) + data = match.groupdict() + channel_id = disnake.utils._get_as_snowflake(data, "channel_id") or ctx.channel.id + message_id = int(data["message_id"]) + guild_id_str: Optional[str] = data.get("guild_id") + if guild_id_str is None: + guild_id = ctx.guild and ctx.guild.id + elif guild_id_str == "@me": + guild_id = None + else: + guild_id = int(guild_id_str) + return guild_id, message_id, channel_id + + @staticmethod + def _resolve_channel( + ctx: AnyContext, guild_id: Optional[int], channel_id: int + ) -> Optional[MessageableChannel]: + bot: disnake.Client = ctx.bot + if guild_id is None: + return bot.get_channel(channel_id) if channel_id else ctx.channel # type: ignore + + guild = bot.get_guild(guild_id) + if guild is not None: + return guild._resolve_channel(channel_id) # type: ignore + return None + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.PartialMessage: + guild_id, message_id, channel_id = self._get_id_matches(ctx, argument) + channel = self._resolve_channel(ctx, guild_id, channel_id) + if not channel: + raise ChannelNotFound(str(channel_id)) + return disnake.PartialMessage(channel=channel, id=message_id) + + +class MessageConverter(IDConverter[disnake.Message]): + """Converts to a :class:`~disnake.Message`. + + .. versionadded:: 1.1 + + The lookup strategy is as follows (in order): + + 1. Lookup by "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID") + 2. Lookup by message ID (the message **must** be in the context channel) + 3. Lookup by message URL + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Message: + guild_id, message_id, channel_id = PartialMessageConverter._get_id_matches(ctx, argument) + bot: disnake.Client = ctx.bot + message = bot._connection._get_message(message_id) + if message: + return message + channel = PartialMessageConverter._resolve_channel(ctx, guild_id, channel_id) + if not channel: + raise ChannelNotFound(str(channel_id)) + try: + return await channel.fetch_message(message_id) + except disnake.NotFound: + raise MessageNotFound(argument) from None + except disnake.Forbidden: + raise ChannelNotReadable(channel) from None # type: ignore + + +class GuildChannelConverter(IDConverter[disnake.abc.GuildChannel]): + """Converts to a :class:`.abc.GuildChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name. + + .. versionadded:: 2.0 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.abc.GuildChannel: + return self._resolve_channel(ctx, argument, "channels", disnake.abc.GuildChannel) + + @staticmethod + def _resolve_channel(ctx: AnyContext, argument: str, attribute: str, type: Type[CT]) -> CT: + bot: disnake.Client = ctx.bot + + match = IDConverter._get_id_match(argument) or re.match(r"<#([0-9]{17,19})>$", argument) + result: Optional[disnake.abc.GuildChannel] = None + guild = ctx.guild + + if match is None: + # not a mention + if guild: + iterable: Iterable[CT] = getattr(guild, attribute) + result = _utils_get(iterable, name=argument) + else: + result = disnake.utils.find( + lambda c: isinstance(c, type) and c.name == argument, bot.get_all_channels() + ) + else: + channel_id = int(match.group(1)) + if guild: + result = guild.get_channel(channel_id) + else: + result = _get_from_guilds(bot, lambda g: g.get_channel(channel_id)) + + if not isinstance(result, type): + raise ChannelNotFound(argument) + + return result + + @staticmethod + def _resolve_thread(ctx: AnyContext, argument: str, attribute: str, type: Type[TT]) -> TT: + match = IDConverter._get_id_match(argument) or re.match(r"<#([0-9]{17,19})>$", argument) + result: Optional[disnake.Thread] = None + guild = ctx.guild + + if match is None: + # not a mention + if guild: + iterable: Iterable[TT] = getattr(guild, attribute) + result = _utils_get(iterable, name=argument) + else: + thread_id = int(match.group(1)) + if guild: + result = guild.get_thread(thread_id) + + if not isinstance(result, type): + raise ThreadNotFound(argument) + + return result + + +class TextChannelConverter(IDConverter[disnake.TextChannel]): + """Converts to a :class:`~disnake.TextChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.TextChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "text_channels", disnake.TextChannel + ) + + +class VoiceChannelConverter(IDConverter[disnake.VoiceChannel]): + """Converts to a :class:`~disnake.VoiceChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.VoiceChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "voice_channels", disnake.VoiceChannel + ) + + +class StageChannelConverter(IDConverter[disnake.StageChannel]): + """Converts to a :class:`~disnake.StageChannel`. + + .. versionadded:: 1.7 + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.StageChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "stage_channels", disnake.StageChannel + ) + + +class CategoryChannelConverter(IDConverter[disnake.CategoryChannel]): + """Converts to a :class:`~disnake.CategoryChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.CategoryChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "categories", disnake.CategoryChannel + ) + + +class ForumChannelConverter(IDConverter[disnake.ForumChannel]): + """Converts to a :class:`~disnake.ForumChannel`. + + .. versionadded:: 2.5 + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.ForumChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "forum_channels", disnake.ForumChannel + ) + + +class MediaChannelConverter(IDConverter[disnake.MediaChannel]): + """Converts to a :class:`~disnake.MediaChannel`. + + .. versionadded:: 2.10 + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.MediaChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "media_channels", disnake.MediaChannel + ) + + +class ThreadConverter(IDConverter[disnake.Thread]): + """Converts to a :class:`~disnake.Thread`. + + All lookups are via the local guild. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name. + + .. versionadded:: 2.0 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Thread: + return GuildChannelConverter._resolve_thread(ctx, argument, "threads", disnake.Thread) + + +class ColourConverter(Converter[disnake.Colour]): + """Converts to a :class:`~disnake.Colour`. + + .. versionchanged:: 1.5 + Add an alias named ColorConverter + + The following formats are accepted: + + - ``0x`` + - ``#`` + - ``0x#`` + - ``rgb(, , )`` + - Any of the ``classmethod`` in :class:`~disnake.Colour` + + - The ``_`` in the name can be optionally replaced with spaces. + + Like CSS, ```` can be either 0-255 or 0-100% and ```` can be + either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff). + + .. versionchanged:: 1.5 + Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 1.7 + Added support for ``rgb`` function and 3-digit hex shortcuts + """ + + RGB_REGEX = re.compile( + r"rgb\s*\((?P[0-9]{1,3}%?)\s*,\s*(?P[0-9]{1,3}%?)\s*,\s*(?P[0-9]{1,3}%?)\s*\)" + ) + + def parse_hex_number(self, argument: str) -> disnake.Color: + arg = "".join(i * 2 for i in argument) if len(argument) == 3 else argument + try: + value = int(arg, base=16) + if not (0 <= value <= 0xFFFFFF): + raise BadColourArgument(argument) + except ValueError: + raise BadColourArgument(argument) from None + else: + return disnake.Color(value=value) + + def parse_rgb_number(self, argument: str, number: str) -> int: + if number[-1] == "%": + value = int(number[:-1]) + if not (0 <= value <= 100): + raise BadColourArgument(argument) + return round(255 * (value / 100)) + + value = int(number) + if not (0 <= value <= 255): + raise BadColourArgument(argument) + return value + + def parse_rgb(self, argument: str, *, regex: re.Pattern[str] = RGB_REGEX) -> disnake.Color: + match = regex.match(argument) + if match is None: + raise BadColourArgument(argument) + + red = self.parse_rgb_number(argument, match.group("r")) + green = self.parse_rgb_number(argument, match.group("g")) + blue = self.parse_rgb_number(argument, match.group("b")) + return disnake.Color.from_rgb(red, green, blue) + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Color: + if argument[0] == "#": + return self.parse_hex_number(argument[1:]) + + if argument[0:2] == "0x": + rest = argument[2:] + # Legacy backwards compatible syntax + if rest.startswith("#"): + return self.parse_hex_number(rest[1:]) + return self.parse_hex_number(rest) + + arg = argument.lower() + if arg[0:3] == "rgb": + return self.parse_rgb(arg) + + arg = arg.replace(" ", "_") + method = getattr(disnake.Colour, arg, None) + if arg.startswith("from_") or method is None or not inspect.ismethod(method): + raise BadColourArgument(arg) + return method() + + +ColorConverter = ColourConverter + + +class RoleConverter(IDConverter[disnake.Role]): + """Converts to a :class:`~disnake.Role`. + + All lookups are via the local guild. If in a DM context, the converter raises + :exc:`.NoPrivateMessage` exception. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Role: + guild = ctx.guild + if not guild: + raise NoPrivateMessage + + match = self._get_id_match(argument) or re.match(r"<@&([0-9]{17,19})>$", argument) + if match: + result = guild.get_role(int(match.group(1))) + else: + result = _utils_get(guild._roles.values(), name=argument) + + if result is None: + raise RoleNotFound(argument) + return result + + +class GameConverter(Converter[disnake.Game]): + """Converts to :class:`~disnake.Game`.""" + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Game: + return disnake.Game(name=argument) + + +class InviteConverter(Converter[disnake.Invite]): + """Converts to a :class:`~disnake.Invite`. + + This is done via an HTTP request using :meth:`.Bot.fetch_invite`. + + .. versionchanged:: 1.5 + Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Invite: + try: + return await ctx.bot.fetch_invite(argument) + except Exception as exc: + raise BadInviteArgument(argument) from exc + + +class GuildConverter(IDConverter[disnake.Guild]): + """Converts to a :class:`~disnake.Guild`. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names). + + .. versionadded:: 1.7 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Guild: + match = self._get_id_match(argument) + bot: disnake.Client = ctx.bot + result: Optional[disnake.Guild] = None + + if match is not None: + guild_id = int(match.group(1)) + result = bot.get_guild(guild_id) + + if result is None: + result = _utils_get(bot.guilds, name=argument) + + if result is None: + raise GuildNotFound(argument) + return result + + +class EmojiConverter(IDConverter[disnake.Emoji]): + """Converts to a :class:`~disnake.Emoji`. + + All lookups are done for the local guild first, if available. If that lookup + fails, then it checks the client's global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by extracting ID from the emoji. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Emoji: + match = self._get_id_match(argument) or re.match( + r"$", argument + ) + result: Optional[disnake.Emoji] = None + bot = ctx.bot + guild = ctx.guild + + if match is None: + # Try to get the emoji by name. Try local guild first. + if guild: + result = _utils_get(guild.emojis, name=argument) + + if result is None: + result = _utils_get(bot.emojis, name=argument) + else: + # Try to look up emoji by id. + result = bot.get_emoji(int(match.group(1))) + + if result is None: + raise EmojiNotFound(argument) + + return result + + +class PartialEmojiConverter(Converter[disnake.PartialEmoji]): + """Converts to a :class:`~disnake.PartialEmoji`. + + This is done by extracting the animated flag, name and ID from the emoji. + + .. versionchanged:: 1.5 + Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.PartialEmoji: + match = re.match(r"<(a?):([a-zA-Z0-9\_]{1,32}):([0-9]{17,19})>$", argument) + + if match: + emoji_animated = bool(match.group(1)) + emoji_name: str = match.group(2) + emoji_id = int(match.group(3)) + + return disnake.PartialEmoji.with_state( + ctx.bot._connection, animated=emoji_animated, name=emoji_name, id=emoji_id + ) + + raise PartialEmojiConversionFailure(argument) + + +class GuildStickerConverter(IDConverter[disnake.GuildSticker]): + """Converts to a :class:`~disnake.GuildSticker`. + + All lookups are done for the local guild first, if available. If that lookup + fails, then it checks the client's global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID + 2. Lookup by name + + .. versionadded:: 2.0 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.GuildSticker: + match = self._get_id_match(argument) + result = None + bot: disnake.Client = ctx.bot + guild = ctx.guild + + if match is None: + # Try to get the sticker by name. Try local guild first. + if guild: + result = _utils_get(guild.stickers, name=argument) + + if result is None: + result = _utils_get(bot.stickers, name=argument) + else: + # Try to look up sticker by id. + result = bot.get_sticker(int(match.group(1))) + + if result is None: + raise GuildStickerNotFound(argument) + + return result + + +class GuildSoundboardSoundConverter(IDConverter[disnake.GuildSoundboardSound]): + """Converts to a :class:`~disnake.GuildSoundboardSound`. + + All lookups are done for the local guild first, if available. If that lookup + fails, then it checks the client's global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID + 2. Lookup by name + + .. versionadded:: 2.10 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.GuildSoundboardSound: + match = self._get_id_match(argument) + result = None + bot: disnake.Client = ctx.bot + guild = ctx.guild + + if match is None: + # Try to get the sound by name. Try local guild first. + if guild: + result = _utils_get(guild.soundboard_sounds, name=argument) + + if result is None: + result = _utils_get(bot.soundboard_sounds, name=argument) + else: + # Try to look up sound by id. + result = bot.get_soundboard_sound(int(match.group(1))) + + if result is None: + raise GuildSoundboardSoundNotFound(argument) + + return result + + +class PermissionsConverter(Converter[disnake.Permissions]): + """Converts to a :class:`~disnake.Permissions`. + + Accepts an integer or a string of space-separated permission names (or just a single one) as input. + + .. versionadded:: 2.3 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.Permissions: + # try the permission bit value + try: + value = int(argument) + except ValueError: + pass + else: + return disnake.Permissions(value) + + argument = argument.replace("server", "guild") + + # try multiple attributes, then a single one + perms: List[disnake.Permissions] = [] + for name in argument.split(): + attr = getattr(disnake.Permissions, name, None) + if attr is None: + break + + if callable(attr): + perms.append(attr()) + else: + perms.append(disnake.Permissions(**{name: True})) + else: + return functools.reduce(lambda a, b: disnake.Permissions(a.value | b.value), perms) + + name = argument.replace(" ", "_") + + attr = getattr(disnake.Permissions, name, None) + if attr is None: + raise BadArgument(f"Invalid Permissions: {name!r}") + + if callable(attr): + return attr() + else: + return disnake.Permissions(**{name: True}) + + +class GuildScheduledEventConverter(IDConverter[disnake.GuildScheduledEvent]): + """Converts to a :class:`~disnake.GuildScheduledEvent`. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID (in current guild) + 2. Lookup as event URL + 3. Lookup by name (in current guild; there is no disambiguation for scheduled events with multiple matching names) + + .. versionadded:: 2.5 + """ + + async def convert(self, ctx: AnyContext, argument: str) -> disnake.GuildScheduledEvent: + event_regex = re.compile( + r"https?://(?:(?:ptb|canary|www)\.)?discord(?:app)?\.com/events/" + r"([0-9]{17,19})/([0-9]{17,19})/?$" + ) + bot: disnake.Client = ctx.bot + result: Optional[disnake.GuildScheduledEvent] = None + guild = ctx.guild + + # 1. + if guild and (match := self._get_id_match(argument)): + result = guild.get_scheduled_event(int(match.group(1))) + + # 2. + if not result and (match := event_regex.match(argument)): + event_guild = bot.get_guild(int(match.group(1))) + if event_guild: + result = event_guild.get_scheduled_event(int(match.group(2))) + + # 3. + if not result and guild: + result = _utils_get(guild.scheduled_events, name=argument) + + if not result: + raise GuildScheduledEventNotFound(argument) + return result + + +class clean_content(Converter[str]): + """Converts the argument to mention scrubbed version of + said content. + + This behaves similarly to :attr:`~disnake.Message.clean_content`. + + Attributes + ---------- + fix_channel_mentions: :class:`bool` + Whether to clean channel mentions. + use_nicknames: :class:`bool` + Whether to use :attr:`nicknames <.Member.nick>` and + :attr:`global names <.Member.global_name>` when transforming mentions. + escape_markdown: :class:`bool` + Whether to also escape special markdown characters. + remove_markdown: :class:`bool` + Whether to also remove special markdown characters. This option is not supported with ``escape_markdown`` + + .. versionadded:: 1.7 + """ + + def __init__( + self, + *, + fix_channel_mentions: bool = False, + use_nicknames: bool = True, + escape_markdown: bool = False, + remove_markdown: bool = False, + ) -> None: + self.fix_channel_mentions = fix_channel_mentions + self.use_nicknames = use_nicknames + self.escape_markdown = escape_markdown + self.remove_markdown = remove_markdown + + async def convert(self, ctx: AnyContext, argument: str) -> str: + msg = ctx.message if isinstance(ctx, Context) else None + bot: disnake.Client = ctx.bot + + def resolve_user(id: int) -> str: + m = ( + (msg and _utils_get(msg.mentions, id=id)) + or (ctx.guild and ctx.guild.get_member(id)) + or bot.get_user(id) + ) + return f"@{m.display_name if self.use_nicknames else m.name}" if m else "@deleted-user" + + def resolve_role(id: int) -> str: + if ctx.guild is None: + return "@deleted-role" + r = (msg and _utils_get(msg.role_mentions, id=id)) or ctx.guild.get_role(id) + return f"@{r.name}" if r else "@deleted-role" + + def resolve_channel(id: int) -> str: + if ctx.guild and self.fix_channel_mentions: + c = ctx.guild.get_channel(id) + return f"#{c.name}" if c else "#deleted-channel" + + return f"<#{id}>" + + transforms: Dict[str, Callable[[int], str]] = { + "@": resolve_user, + "@!": resolve_user, + "#": resolve_channel, + "@&": resolve_role, + } + + def repl(match: re.Match) -> str: + type = match[1] + id = int(match[2]) + return transforms[type](id) + + result = re.sub(r"<(@[!&]?|#)([0-9]{17,19})>", repl, argument) + if self.escape_markdown: + result = disnake.utils.escape_markdown(result) + elif self.remove_markdown: + result = disnake.utils.remove_markdown(result) + + # Completely ensure no mentions escape: + return disnake.utils.escape_mentions(result) + + +class Greedy(List[T]): + """A special converter that greedily consumes arguments until it can't. + As a consequence of this behaviour, most input errors are silently discarded, + since it is used as an indicator of when to stop parsing. + + When a parser error is met the greedy converter stops converting, undoes the + internal string parsing routine, and continues parsing regularly. + + For example, in the following code: + + .. code-block:: python3 + + @commands.command() + async def test(ctx, numbers: Greedy[int], reason: str): + await ctx.send("numbers: {0}, reason: {1}".format(numbers, reason)) + + An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with + ``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``. + + For more information, check :ref:`ext_commands_special_converters`. + """ + + __slots__ = ("converter",) + + def __init__(self, *, converter: T) -> None: + self.converter = converter + + def __repr__(self) -> str: + converter = getattr(self.converter, "__name__", repr(self.converter)) + return f"Greedy[{converter}]" + + def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]: + if not isinstance(params, tuple): + params = (params,) + if len(params) != 1: + raise TypeError("Greedy[...] only takes a single argument") + converter = params[0] + + origin = getattr(converter, "__origin__", None) + args = getattr(converter, "__args__", ()) + + if not (callable(converter) or isinstance(converter, Converter) or origin is not None): + raise TypeError("Greedy[...] expects a type or a Converter instance.") + + if converter in (str, type(None)) or origin is Greedy: + raise TypeError(f"Greedy[{converter.__name__}] is invalid.") + + if origin is Union and type(None) in args: + raise TypeError(f"Greedy[{converter!r}] is invalid.") + + return cls(converter=converter) + + +def _convert_to_bool(argument: str) -> bool: + lowered = argument.lower() + if lowered in ("yes", "y", "true", "t", "1", "enable", "on"): + return True + elif lowered in ("no", "n", "false", "f", "0", "disable", "off"): + return False + else: + raise BadBoolArgument(lowered) + + +def get_converter(param: inspect.Parameter) -> Any: + converter = param.annotation + if converter is param.empty: + if param.default is not param.empty: + converter = str if param.default is None else type(param.default) + else: + converter = str + return converter + + +_GenericAlias = type(List[Any]) + + +def is_generic_type(tp: Any, *, _GenericAlias: Type = _GenericAlias) -> bool: + return (isinstance(tp, type) and issubclass(tp, Generic)) or isinstance(tp, _GenericAlias) + + +CONVERTER_MAPPING: Dict[Type[Any], Type[Converter]] = { + disnake.Object: ObjectConverter, + disnake.Member: MemberConverter, + disnake.User: UserConverter, + disnake.Message: MessageConverter, + disnake.PartialMessage: PartialMessageConverter, + disnake.TextChannel: TextChannelConverter, + disnake.Invite: InviteConverter, + disnake.Guild: GuildConverter, + disnake.Role: RoleConverter, + disnake.Game: GameConverter, + disnake.Colour: ColourConverter, + disnake.VoiceChannel: VoiceChannelConverter, + disnake.StageChannel: StageChannelConverter, + disnake.Emoji: EmojiConverter, + disnake.PartialEmoji: PartialEmojiConverter, + disnake.CategoryChannel: CategoryChannelConverter, + disnake.ForumChannel: ForumChannelConverter, + disnake.MediaChannel: MediaChannelConverter, + disnake.Thread: ThreadConverter, + disnake.abc.GuildChannel: GuildChannelConverter, + disnake.GuildSticker: GuildStickerConverter, + disnake.GuildSoundboardSound: GuildSoundboardSoundConverter, + disnake.Permissions: PermissionsConverter, + disnake.GuildScheduledEvent: GuildScheduledEventConverter, +} + + +async def _actual_conversion( + ctx: Context, + converter: Union[Type[T], Type[Converter[T]], Converter[T], Callable[[str], T]], + argument: str, + param: inspect.Parameter, +) -> T: + if converter is bool: + return _convert_to_bool(argument) # type: ignore + + if isinstance(converter, type): + module = converter.__module__ + if module.startswith("disnake.") and not module.endswith("converter"): + converter = CONVERTER_MAPPING.get(converter, converter) + + try: + if isinstance(converter, type) and issubclass(converter, Converter): + if inspect.ismethod(converter.convert): + return await converter.convert(ctx, argument) + else: + return await converter().convert(ctx, argument) + elif isinstance(converter, Converter): + return await converter.convert(ctx, argument) # type: ignore + except CommandError: + raise + except Exception as exc: + raise ConversionError(converter, exc) from exc + + try: + return converter(argument) # type: ignore + except CommandError: + raise + except Exception as exc: + try: + name = converter.__name__ + except AttributeError: + name = converter.__class__.__name__ + + raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc + + +async def run_converters( + ctx: Context, + converter: Any, + argument: str, + param: inspect.Parameter, +) -> Any: + """|coro| + + Runs converters for a given converter, argument, and parameter. + + This function does the same work that the library does under the hood. + + .. versionadded:: 2.0 + + Parameters + ---------- + ctx: :class:`Context` + The invocation context to run the converters under. + converter: Any + The converter to run, this corresponds to the annotation in the function. + argument: :class:`str` + The argument to convert to. + param: :class:`inspect.Parameter` + The parameter being converted. This is mainly for error reporting. + + Raises + ------ + CommandError + The converter failed to convert. + + Returns + ------- + Any + The resulting conversion. + """ + origin = getattr(converter, "__origin__", None) + + if origin is Union: + errors: List[CommandError] = [] + _NoneType = type(None) + union_args = converter.__args__ + for conv in union_args: + # if we got to this part in the code, then the previous conversions have failed + # so we should just undo the view, return the default, and allow parsing to continue + # with the other parameters + if conv is _NoneType and param.kind != param.VAR_POSITIONAL: + ctx.view.undo() + return None if param.default is param.empty else param.default + + try: + value = await run_converters(ctx, conv, argument, param) + except CommandError as exc: + errors.append(exc) + else: + return value + + # if we're here, then we failed all the converters + raise BadUnionArgument(param, union_args, errors) + + if origin is Literal: + errors: List[CommandError] = [] + conversions = {} + literal_args = converter.__args__ + for literal in literal_args: + literal_type = type(literal) + try: + value = conversions[literal_type] + except KeyError: + try: + value = await _actual_conversion(ctx, literal_type, argument, param) + except CommandError as exc: + errors.append(exc) + conversions[literal_type] = object() + continue + else: + conversions[literal_type] = value + + if value == literal: + return value + + # if we're here, then we failed to match all the literals + raise BadLiteralArgument(param, literal_args, errors) + + # This must be the last if-clause in the chain of origin checking + # Nearly every type is a generic type within the typing library + # So care must be taken to make sure a more specialised origin handle + # isn't overwritten by the widest if clause + if origin is not None and is_generic_type(converter): + converter = origin + + return await _actual_conversion(ctx, converter, argument, param) diff --git a/disnake/disnake/ext/commands/cooldowns.py b/disnake/disnake/ext/commands/cooldowns.py new file mode 100644 index 0000000000..971af2bd0f --- /dev/null +++ b/disnake/disnake/ext/commands/cooldowns.py @@ -0,0 +1,391 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +import time +from collections import deque +from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, Optional + +from disnake.enums import Enum +from disnake.member import Member + +from .errors import MaxConcurrencyReached + +if TYPE_CHECKING: + from typing_extensions import Self + + from ...message import Message + +__all__ = ( + "BucketType", + "Cooldown", + "CooldownMapping", + "DynamicCooldownMapping", + "MaxConcurrency", +) + + +class BucketType(Enum): + """Specifies a type of bucket for, e.g. a cooldown.""" + + default = 0 + """The default bucket operates on a global basis.""" + user = 1 + """The user bucket operates on a per-user basis.""" + guild = 2 + """The guild bucket operates on a per-guild basis.""" + channel = 3 + """The channel bucket operates on a per-channel basis.""" + member = 4 + """The member bucket operates on a per-member basis.""" + category = 5 + """The category bucket operates on a per-category basis.""" + role = 6 + """The role bucket operates on a per-role basis. + + .. versionadded:: 1.3 + """ + + def get_key(self, msg: Message) -> Any: + if self is BucketType.user: + return msg.author.id + elif self is BucketType.guild: + return (msg.guild or msg.author).id + elif self is BucketType.channel: + return msg.channel.id + elif self is BucketType.member: + return ((msg.guild and msg.guild.id), msg.author.id) + elif self is BucketType.category: + return (msg.channel.category or msg.channel).id # type: ignore + elif self is BucketType.role: + # if author is not a Member we are in a private-channel context; returning its id + # yields the same result as for a guild with only the @everyone role + return ( + msg.author.top_role if msg.guild and isinstance(msg.author, Member) else msg.channel + ).id + + def __call__(self, msg: Message) -> Any: + return self.get_key(msg) + + +class Cooldown: + """Represents a cooldown for a command. + + Attributes + ---------- + rate: :class:`int` + The total number of tokens available per :attr:`per` seconds. + per: :class:`float` + The length of the cooldown period in seconds. + """ + + __slots__ = ("rate", "per", "_window", "_tokens", "_last") + + def __init__(self, rate: float, per: float) -> None: + self.rate: int = int(rate) + self.per: float = float(per) + self._window: float = 0.0 + self._tokens: int = self.rate + self._last: float = 0.0 + + def get_tokens(self, current: Optional[float] = None) -> int: + """Returns the number of available tokens before rate limiting is applied. + + Parameters + ---------- + current: Optional[:class:`float`] + The time in seconds since Unix epoch to calculate tokens at. + If not supplied then :func:`time.time()` is used. + + Returns + ------- + :class:`int` + The number of tokens available before the cooldown is to be applied. + """ + if not current: + current = time.time() + + tokens = self._tokens + + if current > self._window + self.per: + tokens = self.rate + return tokens + + def get_retry_after(self, current: Optional[float] = None) -> float: + """Returns the time in seconds until the cooldown will be reset. + + Parameters + ---------- + current: Optional[:class:`float`] + The current time in seconds since Unix epoch. + If not supplied, then :func:`time.time()` is used. + + Returns + ------- + :class:`float` + The number of seconds to wait before this cooldown will be reset. + """ + current = current or time.time() + tokens = self.get_tokens(current) + + if tokens == 0: + return self.per - (current - self._window) + + return 0.0 + + def update_rate_limit(self, current: Optional[float] = None) -> Optional[float]: + """Updates the cooldown rate limit. + + Parameters + ---------- + current: Optional[:class:`float`] + The time in seconds since Unix epoch to update the rate limit at. + If not supplied, then :func:`time.time()` is used. + + Returns + ------- + Optional[:class:`float`] + The retry-after time in seconds if rate limited. + """ + current = current or time.time() + self._last = current + + self._tokens = self.get_tokens(current) + + # first token used means that we start a new rate limit window + if self._tokens == self.rate: + self._window = current + + # check if we are rate limited + if self._tokens == 0: + return self.per - (current - self._window) + + # we're not so decrement our tokens + self._tokens -= 1 + + def reset(self) -> None: + """Reset the cooldown to its initial state.""" + self._tokens = self.rate + self._last = 0.0 + + def copy(self) -> Cooldown: + """Creates a copy of this cooldown. + + Returns + ------- + :class:`Cooldown` + A new instance of this cooldown. + """ + return Cooldown(self.rate, self.per) + + def __repr__(self) -> str: + return f"" + + +class CooldownMapping: + def __init__( + self, + original: Optional[Cooldown], + type: Callable[[Message], Any], + ) -> None: + if not callable(type): + raise TypeError("Cooldown type must be a BucketType or callable") + + self._cache: Dict[Any, Cooldown] = {} + self._cooldown: Optional[Cooldown] = original + self._type: Callable[[Message], Any] = type + + def copy(self) -> CooldownMapping: + ret = CooldownMapping(self._cooldown, self._type) + ret._cache = self._cache.copy() + return ret + + @property + def valid(self) -> bool: + return self._cooldown is not None + + @property + def type(self) -> Callable[[Message], Any]: + return self._type + + @classmethod + def from_cooldown(cls, rate: float, per: float, type) -> Self: + return cls(Cooldown(rate, per), type) + + def _bucket_key(self, msg: Message) -> Any: + return self._type(msg) + + def _verify_cache_integrity(self, current: Optional[float] = None) -> None: + # we want to delete all cache objects that haven't been used + # in a cooldown window. e.g. if we have a command that has a + # cooldown of 60s and it has not been used in 60s then that key should be deleted + current = current or time.time() + dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per] + for k in dead_keys: + del self._cache[k] + + def _is_default(self) -> bool: + # This method can be overridden in subclasses + return self._type is BucketType.default + + def create_bucket(self, message: Message) -> Cooldown: + return self._cooldown.copy() # type: ignore + + def get_bucket(self, message: Message, current: Optional[float] = None) -> Cooldown: + if self._is_default(): + return self._cooldown # type: ignore + + self._verify_cache_integrity(current) + key = self._bucket_key(message) + if key not in self._cache: + bucket = self.create_bucket(message) + if bucket is not None: # pyright: ignore[reportUnnecessaryComparison] + self._cache[key] = bucket + else: + bucket = self._cache[key] + + return bucket + + def update_rate_limit( + self, message: Message, current: Optional[float] = None + ) -> Optional[float]: + bucket = self.get_bucket(message, current) + return bucket.update_rate_limit(current) + + +class DynamicCooldownMapping(CooldownMapping): + def __init__( + self, factory: Callable[[Message], Cooldown], type: Callable[[Message], Any] + ) -> None: + super().__init__(None, type) + self._factory: Callable[[Message], Cooldown] = factory + + def copy(self) -> DynamicCooldownMapping: + ret = DynamicCooldownMapping(self._factory, self._type) + ret._cache = self._cache.copy() + return ret + + @property + def valid(self) -> bool: + return True + + def _is_default(self) -> bool: + # In dynamic mappings even default bucket types may have custom behavior + return False + + def create_bucket(self, message: Message) -> Cooldown: + return self._factory(message) + + +class _Semaphore: + """A custom version of a semaphore. + + If you're wondering why asyncio.Semaphore isn't being used, + it's because it doesn't expose the internal value. This internal + value is necessary because I need to support both `wait=True` and + `wait=False`. + + An asyncio.Queue could have been used to do this as well -- but it is + not as inefficient since internally that uses two queues and is a bit + overkill for what is basically a counter. + """ + + __slots__ = ("value", "loop", "_waiters") + + def __init__(self, number: int) -> None: + self.value: int = number + self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() + self._waiters: Deque[asyncio.Future] = deque() + + def __repr__(self) -> str: + return f"<_Semaphore value={self.value} waiters={len(self._waiters)}>" + + def locked(self) -> bool: + return self.value == 0 + + def is_active(self) -> bool: + return len(self._waiters) > 0 + + def wake_up(self) -> None: + while self._waiters: + future = self._waiters.popleft() + if not future.done(): + future.set_result(None) + return + + async def acquire(self, *, wait: bool = False) -> bool: + if not wait and self.value <= 0: + # signal that we're not acquiring + return False + + while self.value <= 0: + future = self.loop.create_future() + self._waiters.append(future) + try: + await future + except Exception: + future.cancel() + if self.value > 0 and not future.cancelled(): + self.wake_up() + raise + + self.value -= 1 + return True + + def release(self) -> None: + self.value += 1 + self.wake_up() + + +class MaxConcurrency: + __slots__ = ("number", "per", "wait", "_mapping") + + def __init__(self, number: int, *, per: BucketType, wait: bool) -> None: + self._mapping: Dict[Any, _Semaphore] = {} + self.per: BucketType = per + self.number: int = number + self.wait: bool = wait + + if number <= 0: + raise ValueError("max_concurrency 'number' cannot be less than 1") + + if not isinstance(per, BucketType): + raise TypeError(f"max_concurrency 'per' must be of type BucketType not {type(per)!r}") + + def copy(self) -> Self: + return self.__class__(self.number, per=self.per, wait=self.wait) + + def __repr__(self) -> str: + return f"" + + def get_key(self, message: Message) -> Any: + return self.per.get_key(message) + + async def acquire(self, message: Message) -> None: + key = self.get_key(message) + + try: + sem = self._mapping[key] + except KeyError: + self._mapping[key] = sem = _Semaphore(self.number) + + acquired = await sem.acquire(wait=self.wait) + if not acquired: + raise MaxConcurrencyReached(self.number, self.per) + + async def release(self, message: Message) -> None: + # Technically there's no reason for this function to be async + # But it might be more useful in the future + key = self.get_key(message) + + try: + sem = self._mapping[key] + except KeyError: + # ...? peculiar + return + else: + sem.release() + + if sem.value >= self.number and not sem.is_active(): + del self._mapping[key] diff --git a/disnake/disnake/ext/commands/core.py b/disnake/disnake/ext/commands/core.py new file mode 100644 index 0000000000..b0c9705d5b --- /dev/null +++ b/disnake/disnake/ext/commands/core.py @@ -0,0 +1,2681 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import asyncio +import datetime +import functools +import inspect +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + Generic, + List, + Literal, + Optional, + Protocol, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, + overload, +) + +import disnake +from disnake.utils import ( + _generated, + _overload_with_permissions, + get_signature_parameters, + iscoroutinefunction, + unwrap_function, +) + +from ._types import _BaseCommand +from .cog import Cog +from .context import AnyContext, Context +from .converter import Greedy, get_converter, run_converters +from .cooldowns import BucketType, Cooldown, CooldownMapping, DynamicCooldownMapping, MaxConcurrency +from .errors import ( + ArgumentParsingError, + BotMissingAnyRole, + BotMissingPermissions, + BotMissingRole, + CheckAnyFailure, + CheckFailure, + CommandError, + CommandInvokeError, + CommandOnCooldown, + CommandRegistrationError, + DisabledCommand, + MissingAnyRole, + MissingPermissions, + MissingRequiredArgument, + MissingRole, + NoPrivateMessage, + NotOwner, + NSFWChannelRequired, + PrivateMessageOnly, + TooManyArguments, +) + +if TYPE_CHECKING: + from typing_extensions import Concatenate, ParamSpec, Self, TypeGuard + + from disnake.message import Message + + from ._types import AppCheck, Check, Coro, CoroFunc, Error, Hook + + +__all__ = ( + "Command", + "Group", + "GroupMixin", + "command", + "group", + "has_role", + "has_permissions", + "has_any_role", + "check", + "check_any", + "app_check", + "app_check_any", + "before_invoke", + "after_invoke", + "bot_has_role", + "bot_has_permissions", + "bot_has_any_role", + "cooldown", + "dynamic_cooldown", + "max_concurrency", + "dm_only", + "guild_only", + "is_owner", + "is_nsfw", + "has_guild_permissions", + "bot_has_guild_permissions", +) + +MISSING: Any = disnake.utils.MISSING + +T = TypeVar("T") +CogT = TypeVar("CogT", bound="Optional[Cog]") +CommandT = TypeVar("CommandT", bound="Command") +ContextT = TypeVar("ContextT", bound="Context") +GroupT = TypeVar("GroupT", bound="Group") +HookT = TypeVar("HookT", bound="Hook") +ErrorT = TypeVar("ErrorT", bound="Error") + + +if TYPE_CHECKING: + P = ParamSpec("P") + + CommandCallback = Union[ + Callable[Concatenate[CogT, ContextT, P], Coro[T]], + Callable[Concatenate[ContextT, P], Coro[T]], + ] +else: + P = TypeVar("P") + + +def wrap_callback(coro): + @functools.wraps(coro) + async def wrapped(*args, **kwargs): + try: + ret = await coro(*args, **kwargs) + except CommandError: + raise + except asyncio.CancelledError: + return + except Exception as exc: + raise CommandInvokeError(exc) from exc + return ret + + return wrapped + + +def hooked_wrapped_callback(command, ctx, coro): + @functools.wraps(coro) + async def wrapped(*args, **kwargs): + try: + ret = await coro(*args, **kwargs) + except CommandError: + ctx.command_failed = True + raise + except asyncio.CancelledError: + ctx.command_failed = True + return + except Exception as exc: + ctx.command_failed = True + raise CommandInvokeError(exc) from exc + finally: + if command._max_concurrency is not None: + await command._max_concurrency.release(ctx) + + await command.call_after_hooks(ctx) + return ret + + return wrapped + + +class _CaseInsensitiveDict(dict): + def __contains__(self, k) -> bool: + return super().__contains__(k.casefold()) + + def __delitem__(self, k) -> None: + return super().__delitem__(k.casefold()) + + def __getitem__(self, k): + return super().__getitem__(k.casefold()) + + def get(self, k, default=None): + return super().get(k.casefold(), default) + + def pop(self, k, default=None): + return super().pop(k.casefold(), default) + + def __setitem__(self, k, v) -> None: + super().__setitem__(k.casefold(), v) + + +# TODO: ideally, `ContextT` should be bound on the class here as well +class Command(_BaseCommand, Generic[CogT, P, T]): + """A class that implements the protocol for a bot text command. + + These are not created manually, instead they are created via the + decorator or functional interface. + + Attributes + ---------- + name: :class:`str` + The name of the command. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + help: Optional[:class:`str`] + The long help text for the command. + brief: Optional[:class:`str`] + The short help text for the command. + usage: Optional[:class:`str`] + A replacement for arguments in the default help text. + aliases: Union[List[:class:`str`], Tuple[:class:`str`]] + The list of aliases the command can be invoked under. + enabled: :class:`bool` + Whether the command is currently enabled. + If the command is invoked while it is disabled, then + :exc:`.DisabledCommand` is raised to the :func:`.on_command_error` + event. Defaults to ``True``. + parent: Optional[:class:`Group`] + The parent group that this command belongs to. ``None`` if there isn't one. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.Context`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.Context` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.CommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_command_error` + event. + description: :class:`str` + The message prefixed into the default help command. + hidden: :class:`bool` + If ``True``, the default help command does not show this in the help output. + rest_is_raw: :class:`bool` + If ``False`` and a keyword-only argument is provided then the keyword + only argument is stripped and handled as if it was a regular argument + that handles :exc:`.MissingRequiredArgument` and default values in a + regular matter rather than passing the rest completely raw. If ``True`` + then the keyword-only argument will pass in the rest of the arguments + in a completely raw matter. Defaults to ``False``. + invoked_subcommand: Optional[:class:`Command`] + The subcommand that was invoked, if any. + require_var_positional: :class:`bool` + If ``True`` and a variadic positional argument is specified, requires + the user to specify at least one argument. Defaults to ``False``. + + .. versionadded:: 1.5 + + ignore_extra: :class:`bool` + If ``True``, ignores extraneous strings passed to a command if all its + requirements are met (e.g. ``?foo a b c`` when only expecting ``a`` + and ``b``). Otherwise :func:`.on_command_error` and local error handlers + are called with :exc:`.TooManyArguments`. Defaults to ``True``. + cooldown_after_parsing: :class:`bool` + If ``True``, cooldown processing is done after argument parsing, + which calls converters. If ``False`` then cooldown processing is done + first and then the converters are called second. Defaults to ``False``. + extras: :class:`dict` + A dict of user provided extras to attach to the Command. + + .. note:: + This object may be copied by the library. + + .. versionadded:: 2.0 + """ + + __original_kwargs__: Dict[str, Any] + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + # if you're wondering why this is done, it's because we need to ensure + # we have a complete original copy of **kwargs even for classes that + # mess with it by popping before delegating to the subclass __init__. + # In order to do this, we need to control the instance creation and + # inject the original kwargs through __new__ rather than doing it + # inside __init__. + self = super().__new__(cls) + + # we do a shallow copy because it's probably the most common use case. + # this could potentially break if someone modifies a list or something + # while it's in movement, but for now this is the cheapest and + # fastest way to do what we want. + self.__original_kwargs__ = kwargs.copy() + return self + + def __init__( + self, + func: CommandCallback[CogT, ContextT, P, T], + **kwargs: Any, + ) -> None: + if not iscoroutinefunction(func): + raise TypeError("Callback must be a coroutine.") + + name = kwargs.get("name") or func.__name__ + if not isinstance(name, str): + raise TypeError("Name of a command must be a string.") + self.name: str = name + + self.callback = func + self.enabled: bool = kwargs.get("enabled", True) + + help_doc = kwargs.get("help") + if help_doc is not None: + help_doc = inspect.cleandoc(help_doc) + else: + help_doc = inspect.getdoc(func) + if isinstance(help_doc, bytes): + help_doc = help_doc.decode("utf-8") + + self.help: Optional[str] = help_doc + + self.brief: Optional[str] = kwargs.get("brief") + self.usage: Optional[str] = kwargs.get("usage") + self.rest_is_raw: bool = kwargs.get("rest_is_raw", False) + self.aliases: Union[List[str], Tuple[str]] = kwargs.get("aliases", []) + self.extras: Dict[str, Any] = kwargs.get("extras", {}) + + if not isinstance(self.aliases, (list, tuple)): + raise TypeError("Aliases of a command must be a list or a tuple of strings.") + + self.description: str = inspect.cleandoc(kwargs.get("description", "")) + self.hidden: bool = kwargs.get("hidden", False) + + try: + checks = func.__commands_checks__ + checks.reverse() + except AttributeError: + checks = kwargs.get("checks", []) + + self.checks: List[Check] = checks + + try: + cooldown = func.__commands_cooldown__ + except AttributeError: + cooldown = kwargs.get("cooldown") + + if cooldown is None: + buckets = CooldownMapping(cooldown, BucketType.default) + elif isinstance(cooldown, CooldownMapping): + buckets = cooldown + else: + raise TypeError("Cooldown must be a an instance of CooldownMapping or None.") + self._buckets: CooldownMapping = buckets + + try: + max_concurrency = func.__commands_max_concurrency__ + except AttributeError: + max_concurrency = kwargs.get("max_concurrency") + + self._max_concurrency: Optional[MaxConcurrency] = max_concurrency + + self.require_var_positional: bool = kwargs.get("require_var_positional", False) + self.ignore_extra: bool = kwargs.get("ignore_extra", True) + self.cooldown_after_parsing: bool = kwargs.get("cooldown_after_parsing", False) + self.cog: CogT = None # type: ignore + + # bandaid for the fact that sometimes parent can be the bot instance + parent = kwargs.get("parent") + self.parent: Optional[GroupMixin] = parent if isinstance(parent, _BaseCommand) else None # type: ignore + + self._before_invoke: Optional[Hook] = None + try: + before_invoke = func.__before_invoke__ + except AttributeError: + pass + else: + self.before_invoke(before_invoke) + + self._after_invoke: Optional[Hook] = None + try: + after_invoke = func.__after_invoke__ + except AttributeError: + pass + else: + self.after_invoke(after_invoke) + + self.__command_flag__ = None + + @property + def callback(self) -> CommandCallback[CogT, ContextT, P, T]: + return self._callback + + @callback.setter + def callback(self, function: CommandCallback[CogT, Any, P, T]) -> None: + self._callback = function + unwrap = unwrap_function(function) + self.module = unwrap.__module__ + + try: + globalns = unwrap.__globals__ + except AttributeError: + globalns = {} + + params = get_signature_parameters(function, globalns, skip_standard_params=True) + for param in params.values(): + if param.annotation is Greedy: + raise TypeError("Unparameterized Greedy[...] is disallowed in signature.") + self.params = params + + def add_check(self, func: Check) -> None: + """Adds a check to the command. + + This is the non-decorator interface to :func:`.check`. + + .. versionadded:: 1.3 + + Parameters + ---------- + func + The function that will be used as a check. + """ + self.checks.append(func) + + def remove_check(self, func: Check) -> None: + """Removes a check from the command. + + This function is idempotent and will not raise an exception + if the function is not in the command's checks. + + .. versionadded:: 1.3 + + Parameters + ---------- + func + The function to remove from the checks. + """ + try: + self.checks.remove(func) + except ValueError: + pass + + def update(self, **kwargs: Any) -> None: + """Updates :class:`Command` instance with updated attribute. + + This works similarly to the :func:`.command` decorator in terms + of parameters in that they are passed to the :class:`Command` or + subclass constructors, sans the name and callback. + """ + self.__init__(self.callback, **dict(self.__original_kwargs__, **kwargs)) + + async def __call__(self, context: Context, *args: P.args, **kwargs: P.kwargs) -> T: + """|coro| + + Calls the internal callback that the command holds. + + .. note:: + + This bypasses all mechanisms -- including checks, converters, + invoke hooks, cooldowns, etc. You must take care to pass + the proper arguments and types to this function. + + .. versionadded:: 1.3 + """ + if self.cog is not None: + return await self.callback(self.cog, context, *args, **kwargs) # type: ignore + else: + return await self.callback(context, *args, **kwargs) # type: ignore + + def _ensure_assignment_on_copy(self, other: CommandT) -> CommandT: + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + if self.checks != other.checks: + other.checks = self.checks.copy() + if self._buckets.valid and not other._buckets.valid: + other._buckets = self._buckets.copy() + if self._max_concurrency != other._max_concurrency: + # _max_concurrency won't be None at this point + other._max_concurrency = self._max_concurrency.copy() # type: ignore + + try: + other.on_error = self.on_error + except AttributeError: + pass + return other + + def copy(self: CommandT) -> CommandT: + """Creates a copy of this command. + + Returns + ------- + :class:`Command` + A new instance of this command. + """ + ret = self.__class__(self.callback, **self.__original_kwargs__) + return self._ensure_assignment_on_copy(ret) + + def _update_copy(self: CommandT, kwargs: Dict[str, Any]) -> CommandT: + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = self.__class__(self.callback, **kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + async def dispatch_error(self, ctx: Context, error: Exception) -> None: + stop_propagation = False + ctx.command_failed = True + cog = self.cog + try: + coro = self.on_error + except AttributeError: + pass + else: + injected = wrap_callback(coro) + if cog is not None: + stop_propagation = await injected(cog, ctx, error) + else: + stop_propagation = await injected(ctx, error) + if stop_propagation: + return + + try: + if cog is not None: + local = Cog._get_overridden_method(cog.cog_command_error) + if local is not None: + wrapped = wrap_callback(local) + stop_propagation = await wrapped(ctx, error) + # User has an option to cancel the global error handler by returning True + finally: + if not stop_propagation: + ctx.bot.dispatch("command_error", ctx, error) + + async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: + required = param.default is param.empty + converter = get_converter(param) + consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw + view = ctx.view + view.skip_ws() + + # The greedy converter is simple -- it keeps going until it fails in which case, + # it undos the view ready for the next parameter to use instead + if isinstance(converter, Greedy): + if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): + return await self._transform_greedy_pos(ctx, param, required, converter.converter) + elif param.kind == param.VAR_POSITIONAL: + return await self._transform_greedy_var_pos(ctx, param, converter.converter) + else: + # if we're here, then it's a KEYWORD_ONLY param type + # since this is mostly useless, we'll helpfully transform Greedy[X] + # into just X and do the parsing that way. + converter = converter.converter + + if view.eof: + if param.kind == param.VAR_POSITIONAL: + raise RuntimeError # break the loop + if required: + if self._is_typing_optional(param.annotation): + return None + if hasattr(converter, "__commands_is_flag__") and converter._can_be_constructible(): + return await converter._construct_default(ctx) + raise MissingRequiredArgument(param) + return param.default + + previous = view.index + if consume_rest_is_special: + argument = view.read_rest().strip() + else: + try: + argument = view.get_quoted_word() + except ArgumentParsingError: + if ( + self._is_typing_optional(param.annotation) + and not param.kind == param.VAR_POSITIONAL + ): + view.index = previous + return None + else: + raise + view.previous = previous + + # type-checker fails to narrow argument + return await run_converters(ctx, converter, argument, param) # type: ignore + + async def _transform_greedy_pos( + self, ctx: Context, param: inspect.Parameter, required: bool, converter: Any + ) -> Any: + view = ctx.view + result = [] + while not view.eof: + # for use with a manual undo + previous = view.index + + view.skip_ws() + try: + argument = view.get_quoted_word() + value = await run_converters(ctx, converter, argument, param) # type: ignore + except (CommandError, ArgumentParsingError): + view.index = previous + break + else: + result.append(value) + + if not result and not required: + return param.default + return result + + async def _transform_greedy_var_pos( + self, ctx: Context, param: inspect.Parameter, converter: Any + ) -> Any: + view = ctx.view + previous = view.index + try: + argument = view.get_quoted_word() + value = await run_converters(ctx, converter, argument, param) # type: ignore + except (CommandError, ArgumentParsingError): + view.index = previous + raise RuntimeError from None # break loop + else: + return value + + @property + def clean_params(self) -> Dict[str, inspect.Parameter]: + """Dict[:class:`str`, :class:`inspect.Parameter`]: + Retrieves the parameter dictionary without the context or self parameters. + + Useful for inspecting signature. + """ + return self.params.copy() + + @property + def full_parent_name(self) -> str: + """:class:`str`: Retrieves the fully qualified parent command name. + + This the base command name required to execute it. For example, + in ``?one two three`` the parent name would be ``one two``. + """ + entries = [] + command: Command[Any, ..., Any] = self + # command.parent is type-hinted as GroupMixin some attributes are resolved via MRO + while command.parent is not None: + command = command.parent # type: ignore + entries.append(command.name) + + return " ".join(reversed(entries)) + + @property + def parents(self) -> List[Group]: + """List[:class:`Group`]: Retrieves the parents of this command. + + If the command has no parents then it returns an empty :class:`list`. + + For example in commands ``?a b c test``, the parents are ``[c, b, a]``. + + .. versionadded:: 1.1 + """ + entries = [] + command: Command[Any, ..., Any] = self + while command.parent is not None: + command = command.parent # type: ignore + entries.append(command) + + return entries + + @property + def root_parent(self) -> Optional[Group]: + """Optional[:class:`Group`]: Retrieves the root parent of this command. + + If the command has no parents then it returns ``None``. + + For example in commands ``?a b c test``, the root parent is ``a``. + """ + if not self.parent: + return None + return self.parents[-1] + + @property + def qualified_name(self) -> str: + """:class:`str`: Retrieves the fully qualified command name. + + This is the full parent name with the command name as well. + For example, in ``?one two three`` the qualified name would be + ``one two three``. + """ + parent = self.full_parent_name + if parent: + return f"{parent} {self.name}" + else: + return self.name + + def __str__(self) -> str: + return self.qualified_name + + async def _parse_arguments(self, ctx: Context) -> None: + ctx.args = [ctx] if self.cog is None else [self.cog, ctx] + ctx.kwargs = {} + args = ctx.args + kwargs = ctx.kwargs + + view = ctx.view + for name, param in self.params.items(): + ctx.current_parameter = param + if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): + transformed = await self.transform(ctx, param) + args.append(transformed) + elif param.kind == param.KEYWORD_ONLY: + # kwarg only param denotes "consume rest" semantics + if self.rest_is_raw: + converter = get_converter(param) + argument = view.read_rest() + kwargs[name] = await run_converters(ctx, converter, argument, param) + else: + kwargs[name] = await self.transform(ctx, param) + break + elif param.kind == param.VAR_POSITIONAL: + if view.eof and self.require_var_positional: + raise MissingRequiredArgument(param) + while not view.eof: + try: + transformed = await self.transform(ctx, param) + args.append(transformed) + except RuntimeError: + break + + if not self.ignore_extra and not view.eof: + raise TooManyArguments(f"Too many arguments passed to {self.qualified_name}") + + async def call_before_hooks(self, ctx: Context) -> None: + # now that we're done preparing we can call the pre-command hooks + # first, call the command local hook: + cog = self.cog + if self._before_invoke is not None: + # should be cog if @commands.before_invoke is used + instance = getattr(self._before_invoke, "__self__", cog) + # __self__ only exists for methods, not functions + # however, if @command.before_invoke is used, it will be a function + if instance: + await self._before_invoke(instance, ctx) # type: ignore + else: + await self._before_invoke(ctx) # type: ignore + + # call the cog local hook if applicable: + if cog is not None: + hook = Cog._get_overridden_method(cog.cog_before_invoke) + if hook is not None: + await hook(ctx) + + # call the bot global hook if necessary + hook = ctx.bot._before_invoke + if hook is not None: + await hook(ctx) + + async def call_after_hooks(self, ctx: Context) -> None: + cog = self.cog + if self._after_invoke is not None: + instance = getattr(self._after_invoke, "__self__", cog) + if instance: + await self._after_invoke(instance, ctx) # type: ignore + else: + await self._after_invoke(ctx) # type: ignore + + # call the cog local hook if applicable: + if cog is not None: + hook = Cog._get_overridden_method(cog.cog_after_invoke) + if hook is not None: + await hook(ctx) + + hook = ctx.bot._after_invoke + if hook is not None: + await hook(ctx) + + def _prepare_cooldowns(self, ctx: Context) -> None: + if self._buckets.valid: + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + bucket = self._buckets.get_bucket(ctx.message, current) + if bucket is not None: # pyright: ignore[reportUnnecessaryComparison] + retry_after = bucket.update_rate_limit(current) + if retry_after: + raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore + + async def prepare(self, ctx: Context) -> None: + ctx.command = self + + if not await self.can_run(ctx): + raise CheckFailure(f"The check functions for command {self.qualified_name} failed.") + + if self._max_concurrency is not None: + # For this application, context can be duck-typed as a Message + await self._max_concurrency.acquire(ctx) # type: ignore + + try: + if self.cooldown_after_parsing: + await self._parse_arguments(ctx) + self._prepare_cooldowns(ctx) + else: + self._prepare_cooldowns(ctx) + await self._parse_arguments(ctx) + + await self.call_before_hooks(ctx) + except Exception: + if self._max_concurrency is not None: + await self._max_concurrency.release(ctx) # type: ignore + raise + + def is_on_cooldown(self, ctx: Context) -> bool: + """Checks whether the command is currently on cooldown. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to use when checking the commands cooldown status. + + Returns + ------- + :class:`bool` + A boolean indicating if the command is on cooldown. + """ + if not self._buckets.valid: + return False + + bucket = self._buckets.get_bucket(ctx.message) + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_tokens(current) == 0 + + def reset_cooldown(self, ctx: Context) -> None: + """Resets the cooldown on this command. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to reset the cooldown under. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx.message) + bucket.reset() + + def get_cooldown_retry_after(self, ctx: Context) -> float: + """Retrieves the amount of seconds before this command can be tried again. + + .. versionadded:: 1.4 + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to retrieve the cooldown from. + + Returns + ------- + :class:`float` + The amount of time left on this command's cooldown in seconds. + If this is ``0.0`` then the command isn't on cooldown. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx.message) + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_retry_after(current) + + return 0.0 + + async def invoke(self, ctx: Context) -> None: + await self.prepare(ctx) + + # terminate the invoked_subcommand chain. + # since we're in a regular prefix command (and not a group) then + # the invoked subcommand is None. + ctx.invoked_subcommand = None + ctx.subcommand_passed = None + injected = hooked_wrapped_callback(self, ctx, self.callback) + await injected(*ctx.args, **ctx.kwargs) + + async def reinvoke(self, ctx: Context, *, call_hooks: bool = False) -> None: + ctx.command = self + await self._parse_arguments(ctx) + + if call_hooks: + await self.call_before_hooks(ctx) + + ctx.invoked_subcommand = None + try: + await self.callback(*ctx.args, **ctx.kwargs) # type: ignore + except Exception: + ctx.command_failed = True + raise + finally: + if call_hooks: + await self.call_after_hooks(ctx) + + def error(self, coro: ErrorT) -> ErrorT: + """A decorator that registers a coroutine as a local error handler. + + A local error handler is an :func:`.on_command_error` event limited to + a single command. However, the :func:`.on_command_error` is still + invoked afterwards as the catch-all. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the local error handler. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The error handler must be a coroutine.") + + self.on_error: Error = coro + return coro + + def has_error_handler(self) -> bool: + """Whether the command has an error handler registered. + + .. versionadded:: 1.7 + + :return type: :class:`bool` + """ + return hasattr(self, "on_error") + + def before_invoke(self, coro: HookT) -> HookT: + """A decorator that registers a coroutine as a pre-invoke hook. + + A pre-invoke hook is called directly before the command is + called. This makes it a useful function to set up database + connections or any type of set up required. + + This pre-invoke hook takes a sole parameter, a :class:`.Context`. + + See :meth:`.Bot.before_invoke` for more info. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The pre-invoke hook must be a coroutine.") + + self._before_invoke = coro + return coro + + def after_invoke(self, coro: HookT) -> HookT: + """A decorator that registers a coroutine as a post-invoke hook. + + A post-invoke hook is called directly after the command is + called. This makes it a useful function to clean-up database + connections or any type of clean up required. + + This post-invoke hook takes a sole parameter, a :class:`.Context`. + + See :meth:`.Bot.after_invoke` for more info. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not iscoroutinefunction(coro): + raise TypeError("The post-invoke hook must be a coroutine.") + + self._after_invoke = coro + return coro + + @property + def cog_name(self) -> Optional[str]: + """Optional[:class:`str`]: The name of the cog this command belongs to, if any.""" + return type(self.cog).__cog_name__ if self.cog is not None else None + + @property + def short_doc(self) -> str: + """:class:`str`: Gets the "short" documentation of a command. + + By default, this is the :attr:`.brief` attribute. + If that lookup leads to an empty string then the first line of the + :attr:`.help` attribute is used instead. + """ + if self.brief is not None: + return self.brief + if self.help is not None: + return self.help.split("\n", 1)[0] + return "" + + def _is_typing_optional(self, annotation: Union[T, Optional[T]]) -> TypeGuard[Optional[T]]: + return ( + getattr(annotation, "__origin__", None) is Union and type(None) in annotation.__args__ # type: ignore + ) + + @property + def signature(self) -> str: + """:class:`str`: Returns a POSIX-like signature useful for help command output.""" + if self.usage is not None: + return self.usage + + params = self.clean_params + if not params: + return "" + + result: List[str] = [] + for name, param in params.items(): + greedy = isinstance(param.annotation, Greedy) + optional = False # postpone evaluation of if it's an optional argument + + # for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the + # parameter signature is a literal list of it's values + annotation = param.annotation.converter if greedy else param.annotation + origin = getattr(annotation, "__origin__", None) + if not greedy and origin is Union: + none_cls = type(None) + union_args = annotation.__args__ + optional = union_args[-1] is none_cls + if len(union_args) == 2 and optional: + annotation = union_args[0] + origin = getattr(annotation, "__origin__", None) + + if origin is Literal: + name = "|".join( + f'"{v}"' if isinstance(v, str) else str(v) for v in annotation.__args__ + ) + if param.default is not param.empty: + # We don't want None or '' to trigger the [name=value] case and instead it should + # do [name] since [name=None] or [name=] are not exactly useful for the user. + should_print = ( + param.default if isinstance(param.default, str) else param.default is not None + ) + if should_print: + result.append( + f"[{name}={param.default}]" + if not greedy + else f"[{name}={param.default}]..." + ) + continue + else: + result.append(f"[{name}]") + + elif param.kind == param.VAR_POSITIONAL: + if self.require_var_positional: + result.append(f"<{name}...>") + else: + result.append(f"[{name}...]") + elif greedy: + result.append(f"[{name}]...") + elif optional: + result.append(f"[{name}]") + else: + result.append(f"<{name}>") + + return " ".join(result) + + async def can_run(self, ctx: Context) -> bool: + """|coro| + + Checks if the command can be executed by checking all the predicates + inside the :attr:`~Command.checks` attribute. This also checks whether the + command is disabled. + + .. versionchanged:: 1.3 + Checks whether the command is disabled. + + Parameters + ---------- + ctx: :class:`.Context` + The ctx of the command currently being invoked. + + Raises + ------ + CommandError + Any command error that was raised during a check call will be propagated + by this function. + + Returns + ------- + :class:`bool` + Whether the command can be invoked. + """ + if not self.enabled: + raise DisabledCommand(f"{self.name} command is disabled") + + original = ctx.command + ctx.command = self + + try: + if not await ctx.bot.can_run(ctx): + raise CheckFailure( + f"The global check functions for command {self.qualified_name} failed." + ) + + cog = self.cog + if cog is not None: + local_check = Cog._get_overridden_method(cog.cog_check) + if local_check is not None: + ret = await disnake.utils.maybe_coroutine(local_check, ctx) + if not ret: + return False + + predicates = self.checks + if not predicates: + # since we have no checks, then we just return True. + return True + + return await disnake.utils.async_all(predicate(ctx) for predicate in predicates) # type: ignore + finally: + ctx.command = original + + +class GroupMixin(Generic[CogT]): + """A mixin that implements common functionality for classes that behave + similar to :class:`.Group` and are allowed to register commands. + + Attributes + ---------- + all_commands: :class:`dict` + A mapping of command name to :class:`.Command` + objects. + case_insensitive: :class:`bool` + Whether the commands should be case insensitive. Defaults to ``False``. + """ + + def __init__(self, *args: Any, case_insensitive: bool = False, **kwargs: Any) -> None: + self.all_commands: Dict[str, Command[CogT, Any, Any]] = ( + _CaseInsensitiveDict() if case_insensitive else {} + ) + self.case_insensitive: bool = case_insensitive + super().__init__(*args, **kwargs) + + @property + def commands(self) -> Set[Command[CogT, Any, Any]]: + """Set[:class:`.Command`]: A unique set of commands without aliases that are registered.""" + return set(self.all_commands.values()) + + def recursively_remove_all_commands(self) -> None: + for command in self.all_commands.copy().values(): + if isinstance(command, GroupMixin): + command.recursively_remove_all_commands() + self.remove_command(command.name) + + def add_command(self, command: Command[CogT, Any, Any]) -> None: + """Adds a :class:`.Command` into the internal list of commands. + + This is usually not called, instead the :meth:`~.GroupMixin.command` or + :meth:`~.GroupMixin.group` shortcut decorators are used instead. + + .. versionchanged:: 1.4 + Raise :exc:`.CommandRegistrationError` instead of generic :exc:`.ClientException` + + Parameters + ---------- + command: :class:`Command` + The command to add. + + Raises + ------ + CommandRegistrationError + If the command or its alias is already registered by different command. + TypeError + If the command passed is not a subclass of :class:`.Command`. + """ + if not isinstance(command, Command): + raise TypeError("The command passed must be a subclass of Command") + + if isinstance(self, Command): + command.parent = self + + if command.name in self.all_commands: + raise CommandRegistrationError(command.name) + + self.all_commands[command.name] = command + for alias in command.aliases: + if alias in self.all_commands: + self.remove_command(command.name) + raise CommandRegistrationError(alias, alias_conflict=True) + self.all_commands[alias] = command + + def remove_command(self, name: str) -> Optional[Command[CogT, Any, Any]]: + """Remove a :class:`.Command` from the internal list + of commands. + + This could also be used as a way to remove aliases. + + Parameters + ---------- + name: :class:`str` + The name of the command to remove. + + Returns + ------- + Optional[:class:`.Command`] + The command that was removed. If the name is not valid then + ``None`` is returned instead. + """ + command = self.all_commands.pop(name, None) + + # does not exist + if command is None: + return None + + if name in command.aliases: + # we're removing an alias so we don't want to remove the rest + return command + + # we're not removing the alias so let's delete the rest of them. + for alias in command.aliases: + cmd = self.all_commands.pop(alias, None) + # in the case of a CommandRegistrationError, an alias might conflict + # with an already existing command. If this is the case, we want to + # make sure the pre-existing command is not removed. + if cmd is not None and cmd != command: + self.all_commands[alias] = cmd + return command + + def walk_commands(self) -> Generator[Command[CogT, Any, Any], None, None]: + """An iterator that recursively walks through all commands and subcommands. + + .. versionchanged:: 1.4 + Duplicates due to aliases are no longer returned + + Yields + ------ + Union[:class:`.Command`, :class:`.Group`] + A command or group from the internal list of commands. + """ + for command in self.commands: + yield command + if isinstance(command, GroupMixin): + yield from command.walk_commands() + + def get_command(self, name: str) -> Optional[Command[CogT, Any, Any]]: + """Get a :class:`.Command` from the internal list + of commands. + + This could also be used as a way to get aliases. + + The name could be fully qualified (e.g. ``'foo bar'``) will get + the subcommand ``bar`` of the group command ``foo``. If a + subcommand is not found then ``None`` is returned just as usual. + + Parameters + ---------- + name: :class:`str` + The name of the command to get. + + Returns + ------- + Optional[:class:`Command`] + The command that was requested. If not found, returns ``None``. + """ + # fast path, no space in name. + if " " not in name: + return self.all_commands.get(name) + + names = name.split() + if not names: + return None + obj = self.all_commands.get(names[0]) + if not isinstance(obj, GroupMixin): + return obj + + for name in names[1:]: + try: + obj = obj.all_commands[name] # type: ignore + except (AttributeError, KeyError): + return None + + return obj + + # see `commands.command` for details regarding these overloads + + @overload + def command( + self, + name: str, + cls: Type[CommandT], + *args: Any, + **kwargs: Any, + ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], CommandT]: ... + + @overload + def command( + self, + name: str = ..., + *args: Any, + cls: Type[CommandT], + **kwargs: Any, + ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], CommandT]: ... + + @overload + def command( + self, + name: str = ..., + *args: Any, + **kwargs: Any, + ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], Command[CogT, P, T]]: ... + + def command( + self, + name: str = MISSING, + cls: Type[Command[Any, Any, Any]] = Command, + *args: Any, + **kwargs: Any, + ) -> Any: + """A shortcut decorator that invokes :func:`.command` and adds it to + the internal command list via :meth:`~.GroupMixin.add_command`. + + Returns + ------- + Callable[..., :class:`Command`] + A decorator that converts the provided method into a Command, adds it to the bot, then returns it. + """ + + def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> Command[Any, Any, Any]: + kwargs.setdefault("parent", self) + result = command(name=name, cls=cls, *args, **kwargs)(func) + self.add_command(result) + return result + + return decorator + + @overload + def group( + self, + name: str, + cls: Type[GroupT], + *args: Any, + **kwargs: Any, + ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], GroupT]: ... + + @overload + def group( + self, + name: str = ..., + *args: Any, + cls: Type[GroupT], + **kwargs: Any, + ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], GroupT]: ... + + @overload + def group( + self, + name: str = ..., + *args: Any, + **kwargs: Any, + ) -> Callable[[CommandCallback[CogT, ContextT, P, T]], Group[CogT, P, T]]: ... + + def group( + self, + name: str = MISSING, + cls: Type[Group[Any, Any, Any]] = MISSING, + *args: Any, + **kwargs: Any, + ) -> Any: + """A shortcut decorator that invokes :func:`.group` and adds it to + the internal command list via :meth:`~.GroupMixin.add_command`. + + Returns + ------- + Callable[..., :class:`Group`] + A decorator that converts the provided method into a Group, adds it to the bot, then returns it. + """ + + def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> Group[Any, Any, Any]: + kwargs.setdefault("parent", self) + result = group(name=name, cls=cls, *args, **kwargs)(func) + self.add_command(result) + return result + + return decorator + + +class Group(GroupMixin[CogT], Command[CogT, P, T]): + """A class that implements a grouping protocol for commands to be + executed as subcommands. + + This class is a subclass of :class:`.Command` and thus all options + valid in :class:`.Command` are valid in here as well. + + Attributes + ---------- + invoke_without_command: :class:`bool` + Indicates if the group callback should begin parsing and + invocation only if no subcommand was found. Useful for + making it an error handling function to tell the user that + no subcommand was found or to have different functionality + in case no subcommand was found. If this is ``False``, then + the group callback will always be invoked first. This means + that the checks and the parsing dictated by its parameters + will be executed. Defaults to ``False``. + case_insensitive: :class:`bool` + Indicates if the group's commands should be case insensitive. + Defaults to ``False``. + """ + + def __init__(self, *args: Any, **attrs: Any) -> None: + self.invoke_without_command: bool = attrs.pop("invoke_without_command", False) + super().__init__(*args, **attrs) + + def copy(self: GroupT) -> GroupT: + """Creates a copy of this :class:`Group`. + + Returns + ------- + :class:`Group` + A new instance of this group. + """ + ret = super().copy() + for cmd in self.commands: + ret.add_command(cmd.copy()) + return ret + + async def invoke(self, ctx: Context) -> None: + ctx.invoked_subcommand = None + ctx.subcommand_passed = None + early_invoke = not self.invoke_without_command + if early_invoke: + await self.prepare(ctx) + + view = ctx.view + previous = view.index + view.skip_ws() + trigger = view.get_word() + + if trigger: + ctx.subcommand_passed = trigger + ctx.invoked_subcommand = self.all_commands.get(trigger, None) + + if early_invoke: + injected = hooked_wrapped_callback(self, ctx, self.callback) + await injected(*ctx.args, **ctx.kwargs) + + ctx.invoked_parents.append(ctx.invoked_with) # type: ignore + + if trigger and ctx.invoked_subcommand: + ctx.invoked_with = trigger + await ctx.invoked_subcommand.invoke(ctx) + elif not early_invoke: + # undo the trigger parsing + view.index = previous + view.previous = previous + await super().invoke(ctx) + + async def reinvoke(self, ctx: Context, *, call_hooks: bool = False) -> None: + ctx.invoked_subcommand = None + early_invoke = not self.invoke_without_command + if early_invoke: + ctx.command = self + await self._parse_arguments(ctx) + + if call_hooks: + await self.call_before_hooks(ctx) + + view = ctx.view + previous = view.index + view.skip_ws() + trigger = view.get_word() + + if trigger: + ctx.subcommand_passed = trigger + ctx.invoked_subcommand = self.all_commands.get(trigger, None) + + if early_invoke: + try: + await self.callback(*ctx.args, **ctx.kwargs) # type: ignore + except Exception: + ctx.command_failed = True + raise + finally: + if call_hooks: + await self.call_after_hooks(ctx) + + ctx.invoked_parents.append(ctx.invoked_with) # type: ignore + + if trigger and ctx.invoked_subcommand: + ctx.invoked_with = trigger + await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks) + elif not early_invoke: + # undo the trigger parsing + view.index = previous + view.previous = previous + await super().reinvoke(ctx, call_hooks=call_hooks) + + +# Decorators + +if TYPE_CHECKING: + + class CommandDecorator(Protocol): + @overload + def __call__( + self, func: Callable[Concatenate[ContextT, P], Coro[T]] + ) -> Command[None, P, T]: ... + + @overload + def __call__( + self, func: Callable[Concatenate[CogT, ContextT, P], Coro[T]] + ) -> Command[CogT, P, T]: ... + + class GroupDecorator(Protocol): + @overload + def __call__( + self, func: Callable[Concatenate[ContextT, P], Coro[T]] + ) -> Group[None, P, T]: ... + + @overload + def __call__( + self, func: Callable[Concatenate[CogT, ContextT, P], Coro[T]] + ) -> Group[CogT, P, T]: ... + + +# Small explanation regarding these overloads: +# The overloads with the `cls` parameter need to be first, +# as the other overload would otherwise match first even if `cls` is given. +# To prevent the overloads with `cls` from matching everything, the parameter +# cannot have a default value, which in turn means it has to be split into two +# overloads, one with a positional `cls` parameter and one with a kwarg parameter, +# as `name` should still be optional. + + +@overload +def command( + name: str, + cls: Type[CommandT], + **attrs: Any, +) -> Callable[[CommandCallback[CogT, ContextT, P, T]], CommandT]: ... + + +@overload +def command( + name: str = ..., + *, + cls: Type[CommandT], + **attrs: Any, +) -> Callable[[CommandCallback[CogT, ContextT, P, T]], CommandT]: ... + + +@overload +def command( + name: str = ..., + **attrs: Any, +) -> CommandDecorator: ... + + +def command( + name: str = MISSING, + cls: Type[Command[Any, Any, Any]] = MISSING, + **attrs: Any, +) -> Any: + """A decorator that transforms a function into a :class:`.Command` + or if called with :func:`.group`, :class:`.Group`. + + By default the ``help`` attribute is received automatically from the + docstring of the function and is cleaned up with the use of + ``inspect.cleandoc``. If the docstring is ``bytes``, then it is decoded + into :class:`str` using utf-8 encoding. + + All checks added using the :func:`.check` & co. decorators are added into + the function. There is no way to supply your own checks through this + decorator. + + Parameters + ---------- + name: :class:`str` + The name to create the command with. By default this uses the + function name unchanged. + cls + The class to construct with. By default this is :class:`.Command`. + You usually do not change this. + attrs + Keyword arguments to pass into the construction of the class denoted + by ``cls``. + + Raises + ------ + TypeError + If the function is not a coroutine or is already a command. + """ + if cls is MISSING: + cls = Command + + def decorator(func: CommandCallback[CogT, ContextT, P, T]) -> Command[Any, Any, Any]: + if hasattr(func, "__command_flag__"): + raise TypeError("Callback is already a command.") + return cls(func, name=name, **attrs) + + return decorator + + +@overload +def group( + name: str, + cls: Type[GroupT], + **attrs: Any, +) -> Callable[[CommandCallback[CogT, ContextT, P, T]], GroupT]: ... + + +@overload +def group( + name: str = ..., + *, + cls: Type[GroupT], + **attrs: Any, +) -> Callable[[CommandCallback[CogT, ContextT, P, T]], GroupT]: ... + + +@overload +def group( + name: str = ..., + **attrs: Any, +) -> GroupDecorator: ... + + +def group( + name: str = MISSING, + cls: Type[Group[Any, Any, Any]] = MISSING, + **attrs: Any, +) -> Any: + """A decorator that transforms a function into a :class:`.Group`. + + This is similar to the :func:`.command` decorator but the ``cls`` + parameter is set to :class:`Group` by default. + + .. versionchanged:: 1.1 + The ``cls`` parameter can now be passed. + """ + if cls is MISSING: + cls = Group + return command(name=name, cls=cls, **attrs) + + +def check(predicate: Check) -> Callable[[T], T]: + """A decorator that adds a check to the :class:`.Command` or its + subclasses. These checks could be accessed via :attr:`.Command.checks`. + + These checks should be predicates that take in a single parameter taking + a :class:`.Context`. If the check returns a ``False``-like value then + during invocation a :exc:`.CheckFailure` exception is raised and sent to + the :func:`.on_command_error` event. + + If an exception should be thrown in the predicate then it should be a + subclass of :exc:`.CommandError`. Any exception not subclassed from it + will be propagated while those subclassed will be sent to + :func:`.on_command_error`. + + A special attribute named ``predicate`` is bound to the value + returned by this decorator to retrieve the predicate passed to the + decorator. This allows the following introspection and chaining to be done: + + .. code-block:: python3 + + def owner_or_permissions(**perms): + original = commands.has_permissions(**perms).predicate + async def extended_check(ctx): + if ctx.guild is None: + return False + return ctx.guild.owner_id == ctx.author.id or await original(ctx) + return commands.check(extended_check) + + .. note:: + + The function returned by ``predicate`` is **always** a coroutine, + even if the original function was not a coroutine. + + .. note:: + See :func:`.app_check` for this function's application command counterpart. + + .. versionchanged:: 1.3 + The ``predicate`` attribute was added. + + Examples + -------- + Creating a basic check to see if the command invoker is you. + + .. code-block:: python3 + + def check_if_it_is_me(ctx): + return ctx.message.author.id == 85309593344815104 + + @bot.command() + @commands.check(check_if_it_is_me) + async def only_for_me(ctx): + await ctx.send('I know you!') + + Transforming common checks into its own decorator: + + .. code-block:: python3 + + def is_me(): + def predicate(ctx): + return ctx.message.author.id == 85309593344815104 + return commands.check(predicate) + + @bot.command() + @is_me() + async def only_me(ctx): + await ctx.send('Only you!') + + Parameters + ---------- + predicate: Callable[[:class:`Context`], :class:`bool`] + The predicate to check if the command should be invoked. + """ + + def decorator(func: Union[Command, CoroFunc]) -> Union[Command, CoroFunc]: + if hasattr(func, "__command_flag__"): + func.checks.append(predicate) + else: + if not hasattr(func, "__commands_checks__"): + func.__commands_checks__ = [] # type: ignore + + func.__commands_checks__.append(predicate) # type: ignore + + return func + + if iscoroutinefunction(predicate): + decorator.predicate = predicate + else: + + @functools.wraps(predicate) # type: ignore + async def wrapper(ctx): + return predicate(ctx) # type: ignore + + decorator.predicate = wrapper + + return decorator # type: ignore + + +def check_any(*checks: Check) -> Callable[[T], T]: + """A :func:`check` that is added that checks if any of the checks passed + will pass, i.e. using logical OR. + + If all checks fail then :exc:`.CheckAnyFailure` is raised to signal the failure. + It inherits from :exc:`.CheckFailure`. + + .. note:: + + The ``predicate`` attribute for this function **is** a coroutine. + + .. note:: + See :func:`.app_check_any` for this function's application command counterpart. + + .. versionadded:: 1.3 + + Parameters + ---------- + *checks: Callable[[:class:`Context`], :class:`bool`] + An argument list of checks that have been decorated with + the :func:`check` decorator. + + Raises + ------ + TypeError + A check passed has not been decorated with the :func:`check` + decorator. + + Examples + -------- + Creating a basic check to see if it's the bot owner or + the server owner: + + .. code-block:: python3 + + def is_guild_owner(): + def predicate(ctx): + return ctx.guild is not None and ctx.guild.owner_id == ctx.author.id + return commands.check(predicate) + + @bot.command() + @commands.check_any(commands.is_owner(), is_guild_owner()) + async def only_for_owners(ctx): + await ctx.send('Hello mister owner!') + """ + unwrapped = [] + for wrapped in checks: + try: + pred = wrapped.predicate + except AttributeError: + raise TypeError(f"{wrapped!r} must be wrapped by commands.check decorator") from None + else: + unwrapped.append(pred) + + async def predicate(ctx: AnyContext) -> bool: + errors = [] + for func in unwrapped: + try: + value = await func(ctx) + except CheckFailure as e: + errors.append(e) + else: + if value: + return True + # if we're here, all checks failed + raise CheckAnyFailure(unwrapped, errors) + + return check(predicate) + + +def app_check(predicate: AppCheck) -> Callable[[T], T]: + """Same as :func:`.check`, but for app commands. + + .. versionadded:: 2.10 + + Parameters + ---------- + predicate: Callable[[:class:`disnake.ApplicationCommandInteraction`], :class:`bool`] + The predicate to check if the command should be invoked. + """ + return check(predicate) # type: ignore # impl is the same, typings are different + + +def app_check_any(*checks: AppCheck) -> Callable[[T], T]: + """Same as :func:`.check_any`, but for app commands. + + .. note:: + See :func:`.check_any` for this function's prefix command counterpart. + + .. versionadded:: 2.10 + + Parameters + ---------- + *checks: Callable[[:class:`disnake.ApplicationCommandInteraction`], :class:`bool`] + An argument list of checks that have been decorated with + the :func:`app_check` decorator. + + Raises + ------ + TypeError + A check passed has not been decorated with the :func:`app_check` + decorator. + """ + try: + return check_any(*checks) # type: ignore # impl is the same, typings are different + except TypeError as e: + msg = str(e).replace("commands.check", "commands.app_check") # fix err message + raise TypeError(msg) from None + + +def has_role(item: Union[int, str]) -> Callable[[T], T]: + """A :func:`.check` that is added that checks if the member invoking the + command has the role specified via the name or ID specified. + + If a string is specified, you must give the exact name of the role, including + caps and spelling. + + If an integer is specified, you must give the exact snowflake ID of the role. + + If the message is invoked in a private message context then the check will + return ``False``. + + This check raises one of two special exceptions, :exc:`.MissingRole` if the user + is missing a role, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.MissingRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure` + + Parameters + ---------- + item: Union[:class:`int`, :class:`str`] + The name or ID of the role to check. + """ + + def predicate(ctx: AnyContext) -> bool: + if ctx.guild is None: + raise NoPrivateMessage + + # ctx.guild is None doesn't narrow ctx.author to Member + if isinstance(item, int): + role = disnake.utils.get(ctx.author.roles, id=item) # type: ignore + else: + role = disnake.utils.get(ctx.author.roles, name=item) # type: ignore + if role is None: + raise MissingRole(item) + return True + + return check(predicate) + + +def has_any_role(*items: Union[int, str]) -> Callable[[T], T]: + """A :func:`.check` that is added that checks if the member invoking the + command has **any** of the roles specified. This means that if they have + one out of the three roles specified, then this check will return `True`. + + Similar to :func:`.has_role`\\, the names or IDs passed in must be exact. + + This check raises one of two special exceptions, :exc:`.MissingAnyRole` if the user + is missing all roles, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.MissingAnyRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure` + + Parameters + ---------- + items: List[Union[:class:`str`, :class:`int`]] + An argument list of names or IDs to check that the member has roles wise. + + Example + -------- + + .. code-block:: python3 + + @bot.command() + @commands.has_any_role('Library Devs', 'Moderators', 492212595072434186) + async def cool(ctx): + await ctx.send('You are cool indeed') + """ + + def predicate(ctx: AnyContext) -> bool: + if ctx.guild is None: + raise NoPrivateMessage + + # ctx.guild is None doesn't narrow ctx.author to Member + getter = functools.partial(disnake.utils.get, ctx.author.roles) # type: ignore + if any( + getter(id=item) is not None if isinstance(item, int) else getter(name=item) is not None + for item in items + ): + return True + # NOTE: variance problems + raise MissingAnyRole(list(items)) # type: ignore + + return check(predicate) + + +def bot_has_role(item: int) -> Callable[[T], T]: + """Similar to :func:`.has_role` except checks if the bot itself has the + role. + + This check raises one of two special exceptions, :exc:`.BotMissingRole` if the bot + is missing the role, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.BotMissingRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure` + """ + + def predicate(ctx: AnyContext) -> bool: + if ctx.guild is None: + raise NoPrivateMessage + + me = cast("disnake.Member", ctx.me) + if isinstance(item, int): + role = disnake.utils.get(me.roles, id=item) + else: + role = disnake.utils.get(me.roles, name=item) + if role is None: + raise BotMissingRole(item) + return True + + return check(predicate) + + +def bot_has_any_role(*items: int) -> Callable[[T], T]: + """Similar to :func:`.has_any_role` except checks if the bot itself has + any of the roles listed. + + This check raises one of two special exceptions, :exc:`.BotMissingAnyRole` if the bot + is missing all roles, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.BotMissingAnyRole` or :exc:`.NoPrivateMessage` + instead of generic checkfailure + """ + + def predicate(ctx: AnyContext) -> bool: + if ctx.guild is None: + raise NoPrivateMessage + + me = cast("disnake.Member", ctx.me) + getter = functools.partial(disnake.utils.get, me.roles) + if any( + getter(id=item) is not None if isinstance(item, int) else getter(name=item) is not None + for item in items + ): + return True + raise BotMissingAnyRole(list(items)) + + return check(predicate) + + +@overload +@_generated +def has_permissions( + *, + add_reactions: bool = ..., + administrator: bool = ..., + attach_files: bool = ..., + ban_members: bool = ..., + change_nickname: bool = ..., + connect: bool = ..., + create_events: bool = ..., + create_forum_threads: bool = ..., + create_guild_expressions: bool = ..., + create_instant_invite: bool = ..., + create_private_threads: bool = ..., + create_public_threads: bool = ..., + deafen_members: bool = ..., + embed_links: bool = ..., + external_emojis: bool = ..., + external_stickers: bool = ..., + kick_members: bool = ..., + manage_channels: bool = ..., + manage_emojis: bool = ..., + manage_emojis_and_stickers: bool = ..., + manage_events: bool = ..., + manage_guild: bool = ..., + manage_guild_expressions: bool = ..., + manage_messages: bool = ..., + manage_nicknames: bool = ..., + manage_permissions: bool = ..., + manage_roles: bool = ..., + manage_threads: bool = ..., + manage_webhooks: bool = ..., + mention_everyone: bool = ..., + moderate_members: bool = ..., + move_members: bool = ..., + mute_members: bool = ..., + pin_messages: bool = ..., + priority_speaker: bool = ..., + read_message_history: bool = ..., + read_messages: bool = ..., + request_to_speak: bool = ..., + send_messages: bool = ..., + send_messages_in_threads: bool = ..., + send_polls: bool = ..., + send_tts_messages: bool = ..., + send_voice_messages: bool = ..., + speak: bool = ..., + start_embedded_activities: bool = ..., + stream: bool = ..., + use_application_commands: bool = ..., + use_embedded_activities: bool = ..., + use_external_apps: bool = ..., + use_external_emojis: bool = ..., + use_external_sounds: bool = ..., + use_external_stickers: bool = ..., + use_slash_commands: bool = ..., + use_soundboard: bool = ..., + use_voice_activation: bool = ..., + view_audit_log: bool = ..., + view_channel: bool = ..., + view_creator_monetization_analytics: bool = ..., + view_guild_insights: bool = ..., +) -> Callable[[T], T]: ... + + +@overload +@_generated +def has_permissions() -> Callable[[T], T]: ... + + +@_overload_with_permissions +def has_permissions(**perms: bool) -> Callable[[T], T]: + """A :func:`.check` that is added that checks if the member has all of + the permissions necessary. + + Note that this check operates on the current channel permissions, not the + guild wide permissions. + + The permissions passed in must be exactly like the properties shown under + :class:`.disnake.Permissions`. + + This check raises a special exception, :exc:`.MissingPermissions` + that is inherited from :exc:`.CheckFailure`. + + .. versionchanged:: 2.6 + Considers if the author is timed out. + + Parameters + ---------- + perms + An argument list of permissions to check for. + + Example + --------- + + .. code-block:: python3 + + @bot.command() + @commands.has_permissions(manage_messages=True) + async def test(ctx): + await ctx.send('You can manage messages.') + + """ + invalid = set(perms) - set(disnake.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: AnyContext) -> bool: + if isinstance(ctx, disnake.Interaction): + permissions = ctx.permissions + else: + ch = ctx.channel + permissions = ch.permissions_for(ctx.author, ignore_timeout=False) # type: ignore + + missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] + + if not missing: + return True + + raise MissingPermissions(missing) + + return check(predicate) + + +@overload +@_generated +def bot_has_permissions( + *, + add_reactions: bool = ..., + administrator: bool = ..., + attach_files: bool = ..., + ban_members: bool = ..., + change_nickname: bool = ..., + connect: bool = ..., + create_events: bool = ..., + create_forum_threads: bool = ..., + create_guild_expressions: bool = ..., + create_instant_invite: bool = ..., + create_private_threads: bool = ..., + create_public_threads: bool = ..., + deafen_members: bool = ..., + embed_links: bool = ..., + external_emojis: bool = ..., + external_stickers: bool = ..., + kick_members: bool = ..., + manage_channels: bool = ..., + manage_emojis: bool = ..., + manage_emojis_and_stickers: bool = ..., + manage_events: bool = ..., + manage_guild: bool = ..., + manage_guild_expressions: bool = ..., + manage_messages: bool = ..., + manage_nicknames: bool = ..., + manage_permissions: bool = ..., + manage_roles: bool = ..., + manage_threads: bool = ..., + manage_webhooks: bool = ..., + mention_everyone: bool = ..., + moderate_members: bool = ..., + move_members: bool = ..., + mute_members: bool = ..., + pin_messages: bool = ..., + priority_speaker: bool = ..., + read_message_history: bool = ..., + read_messages: bool = ..., + request_to_speak: bool = ..., + send_messages: bool = ..., + send_messages_in_threads: bool = ..., + send_polls: bool = ..., + send_tts_messages: bool = ..., + send_voice_messages: bool = ..., + speak: bool = ..., + start_embedded_activities: bool = ..., + stream: bool = ..., + use_application_commands: bool = ..., + use_embedded_activities: bool = ..., + use_external_apps: bool = ..., + use_external_emojis: bool = ..., + use_external_sounds: bool = ..., + use_external_stickers: bool = ..., + use_slash_commands: bool = ..., + use_soundboard: bool = ..., + use_voice_activation: bool = ..., + view_audit_log: bool = ..., + view_channel: bool = ..., + view_creator_monetization_analytics: bool = ..., + view_guild_insights: bool = ..., +) -> Callable[[T], T]: ... + + +@overload +@_generated +def bot_has_permissions() -> Callable[[T], T]: ... + + +@_overload_with_permissions +def bot_has_permissions(**perms: bool) -> Callable[[T], T]: + """Similar to :func:`.has_permissions` except checks if the bot itself has + the permissions listed. + + This check raises a special exception, :exc:`.BotMissingPermissions` + that is inherited from :exc:`.CheckFailure`. + + .. versionchanged:: 2.6 + Considers if the author is timed out. + """ + invalid = set(perms) - set(disnake.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: AnyContext) -> bool: + if isinstance(ctx, disnake.Interaction): + permissions = ctx.app_permissions + else: + ch = ctx.channel + permissions = ch.permissions_for(ctx.me, ignore_timeout=False) # type: ignore + + missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] + + if not missing: + return True + + raise BotMissingPermissions(missing) + + return check(predicate) + + +@overload +@_generated +def has_guild_permissions( + *, + add_reactions: bool = ..., + administrator: bool = ..., + attach_files: bool = ..., + ban_members: bool = ..., + change_nickname: bool = ..., + connect: bool = ..., + create_events: bool = ..., + create_forum_threads: bool = ..., + create_guild_expressions: bool = ..., + create_instant_invite: bool = ..., + create_private_threads: bool = ..., + create_public_threads: bool = ..., + deafen_members: bool = ..., + embed_links: bool = ..., + external_emojis: bool = ..., + external_stickers: bool = ..., + kick_members: bool = ..., + manage_channels: bool = ..., + manage_emojis: bool = ..., + manage_emojis_and_stickers: bool = ..., + manage_events: bool = ..., + manage_guild: bool = ..., + manage_guild_expressions: bool = ..., + manage_messages: bool = ..., + manage_nicknames: bool = ..., + manage_permissions: bool = ..., + manage_roles: bool = ..., + manage_threads: bool = ..., + manage_webhooks: bool = ..., + mention_everyone: bool = ..., + moderate_members: bool = ..., + move_members: bool = ..., + mute_members: bool = ..., + pin_messages: bool = ..., + priority_speaker: bool = ..., + read_message_history: bool = ..., + read_messages: bool = ..., + request_to_speak: bool = ..., + send_messages: bool = ..., + send_messages_in_threads: bool = ..., + send_polls: bool = ..., + send_tts_messages: bool = ..., + send_voice_messages: bool = ..., + speak: bool = ..., + start_embedded_activities: bool = ..., + stream: bool = ..., + use_application_commands: bool = ..., + use_embedded_activities: bool = ..., + use_external_apps: bool = ..., + use_external_emojis: bool = ..., + use_external_sounds: bool = ..., + use_external_stickers: bool = ..., + use_slash_commands: bool = ..., + use_soundboard: bool = ..., + use_voice_activation: bool = ..., + view_audit_log: bool = ..., + view_channel: bool = ..., + view_creator_monetization_analytics: bool = ..., + view_guild_insights: bool = ..., +) -> Callable[[T], T]: ... + + +@overload +@_generated +def has_guild_permissions() -> Callable[[T], T]: ... + + +@_overload_with_permissions +def has_guild_permissions(**perms: bool) -> Callable[[T], T]: + """Similar to :func:`.has_permissions`, but operates on guild wide + permissions instead of the current channel permissions. + + If this check is called in a DM context, it will raise an + exception, :exc:`.NoPrivateMessage`. + + .. versionadded:: 1.3 + """ + invalid = set(perms) - set(disnake.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: AnyContext) -> bool: + if not ctx.guild: + raise NoPrivateMessage + + permissions = ctx.author.guild_permissions # type: ignore + missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] + + if not missing: + return True + + raise MissingPermissions(missing) + + return check(predicate) + + +@overload +@_generated +def bot_has_guild_permissions( + *, + add_reactions: bool = ..., + administrator: bool = ..., + attach_files: bool = ..., + ban_members: bool = ..., + change_nickname: bool = ..., + connect: bool = ..., + create_events: bool = ..., + create_forum_threads: bool = ..., + create_guild_expressions: bool = ..., + create_instant_invite: bool = ..., + create_private_threads: bool = ..., + create_public_threads: bool = ..., + deafen_members: bool = ..., + embed_links: bool = ..., + external_emojis: bool = ..., + external_stickers: bool = ..., + kick_members: bool = ..., + manage_channels: bool = ..., + manage_emojis: bool = ..., + manage_emojis_and_stickers: bool = ..., + manage_events: bool = ..., + manage_guild: bool = ..., + manage_guild_expressions: bool = ..., + manage_messages: bool = ..., + manage_nicknames: bool = ..., + manage_permissions: bool = ..., + manage_roles: bool = ..., + manage_threads: bool = ..., + manage_webhooks: bool = ..., + mention_everyone: bool = ..., + moderate_members: bool = ..., + move_members: bool = ..., + mute_members: bool = ..., + pin_messages: bool = ..., + priority_speaker: bool = ..., + read_message_history: bool = ..., + read_messages: bool = ..., + request_to_speak: bool = ..., + send_messages: bool = ..., + send_messages_in_threads: bool = ..., + send_polls: bool = ..., + send_tts_messages: bool = ..., + send_voice_messages: bool = ..., + speak: bool = ..., + start_embedded_activities: bool = ..., + stream: bool = ..., + use_application_commands: bool = ..., + use_embedded_activities: bool = ..., + use_external_apps: bool = ..., + use_external_emojis: bool = ..., + use_external_sounds: bool = ..., + use_external_stickers: bool = ..., + use_slash_commands: bool = ..., + use_soundboard: bool = ..., + use_voice_activation: bool = ..., + view_audit_log: bool = ..., + view_channel: bool = ..., + view_creator_monetization_analytics: bool = ..., + view_guild_insights: bool = ..., +) -> Callable[[T], T]: ... + + +@overload +@_generated +def bot_has_guild_permissions() -> Callable[[T], T]: ... + + +@_overload_with_permissions +def bot_has_guild_permissions(**perms: bool) -> Callable[[T], T]: + """Similar to :func:`.has_guild_permissions`, but checks the bot + members guild permissions. + + .. versionadded:: 1.3 + """ + invalid = set(perms) - set(disnake.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: AnyContext) -> bool: + if not ctx.guild: + raise NoPrivateMessage + + permissions = ctx.me.guild_permissions # type: ignore + missing = [perm for perm, value in perms.items() if getattr(permissions, perm) != value] + + if not missing: + return True + + raise BotMissingPermissions(missing) + + return check(predicate) + + +def dm_only() -> Callable[[T], T]: + """A :func:`.check` that indicates this command must only be used in a + DM context. Only private messages are allowed when + using the command. + + This check raises a special exception, :exc:`.PrivateMessageOnly` + that is inherited from :exc:`.CheckFailure`. + + .. note:: + For application commands, consider setting the allowed :ref:`contexts ` instead. + + .. versionadded:: 1.1 + """ + + def predicate(ctx: AnyContext) -> bool: + if (ctx.guild if isinstance(ctx, Context) else ctx.guild_id) is not None: + raise PrivateMessageOnly + return True + + return check(predicate) + + +def guild_only() -> Callable[[T], T]: + """A :func:`.check` that indicates this command must only be used in a + guild context only. Basically, no private messages are allowed when + using the command. + + This check raises a special exception, :exc:`.NoPrivateMessage` + that is inherited from :exc:`.CheckFailure`. + + .. note:: + For application commands, consider setting the allowed :ref:`contexts ` instead. + """ + + def predicate(ctx: AnyContext) -> bool: + if (ctx.guild if isinstance(ctx, Context) else ctx.guild_id) is None: + raise NoPrivateMessage + return True + + return check(predicate) + + +def is_owner() -> Callable[[T], T]: + """A :func:`.check` that checks if the person invoking this command is the + owner of the bot. + + This is powered by :meth:`.Bot.is_owner`. + + This check raises a special exception, :exc:`.NotOwner` that is derived + from :exc:`.CheckFailure`. + """ + + async def predicate(ctx: AnyContext) -> bool: + if not await ctx.bot.is_owner(ctx.author): + raise NotOwner("You do not own this bot.") + return True + + return check(predicate) + + +def is_nsfw() -> Callable[[T], T]: + """A :func:`.check` that checks if the channel is a NSFW channel. + + This check raises a special exception, :exc:`.NSFWChannelRequired` + that is derived from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.NSFWChannelRequired` instead of generic :exc:`.CheckFailure`. + DM channels will also now pass this check. + """ + + def pred(ctx: AnyContext) -> bool: + ch = ctx.channel + if ctx.guild is None or ( + isinstance( + ch, + ( + disnake.TextChannel, + disnake.VoiceChannel, + disnake.Thread, + disnake.StageChannel, + ), + ) + and ch.is_nsfw() + ): + return True + raise NSFWChannelRequired(ch) # type: ignore + + return check(pred) + + +def cooldown( + rate: int, per: float, type: Union[BucketType, Callable[[Message], Any]] = BucketType.default +) -> Callable[[T], T]: + """A decorator that adds a cooldown to a :class:`.Command` + + A cooldown allows a command to only be used a specific amount + of times in a specific time frame. These cooldowns can be based + either on a per-guild, per-channel, per-user, per-role or global basis. + Denoted by the third argument of ``type`` which must be of enum + type :class:`.BucketType`. + + If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in + :func:`.on_command_error` and the local error handler. + + A command can only have a single cooldown. + + Parameters + ---------- + rate: :class:`int` + The number of times a command can be used before triggering a cooldown. + per: :class:`float` + The amount of seconds to wait for a cooldown when it's been triggered. + type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]] + The type of cooldown to have. If callable, should return a key for the mapping. + + .. versionchanged:: 1.7 + Callables are now supported for custom bucket types. + """ + + def decorator(func: Union[Command, CoroFunc]) -> Union[Command, CoroFunc]: + if hasattr(func, "__command_flag__"): + func._buckets = CooldownMapping(Cooldown(rate, per), type) + else: + func.__commands_cooldown__ = CooldownMapping(Cooldown(rate, per), type) # type: ignore + return func + + return decorator # type: ignore + + +def dynamic_cooldown( + cooldown: Union[BucketType, Callable[[Message], Any]], type: BucketType = BucketType.default +) -> Callable[[T], T]: + """A decorator that adds a dynamic cooldown to a :class:`.Command` + + This differs from :func:`.cooldown` in that it takes a function that + accepts a single parameter of type :class:`.disnake.Message` and must + return a :class:`.Cooldown` or ``None``. If ``None`` is returned then + that cooldown is effectively bypassed. + + A cooldown allows a command to only be used a specific amount + of times in a specific time frame. These cooldowns can be based + either on a per-guild, per-channel, per-user, per-role or global basis. + Denoted by the third argument of ``type`` which must be of enum + type :class:`.BucketType`. + + If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in + :func:`.on_command_error` and the local error handler. + + A command can only have a single cooldown. + + .. versionadded:: 2.0 + + Parameters + ---------- + cooldown: Callable[[:class:`.disnake.Message`], Optional[:class:`.Cooldown`]] + A function that takes a message and returns a cooldown that will + apply to this invocation or ``None`` if the cooldown should be bypassed. + type: :class:`.BucketType` + The type of cooldown to have. + """ + if not callable(cooldown): + raise TypeError("A callable must be provided") + + def decorator(func: Union[Command, CoroFunc]) -> Union[Command, CoroFunc]: + if hasattr(func, "__command_flag__"): + func._buckets = DynamicCooldownMapping(cooldown, type) + else: + func.__commands_cooldown__ = DynamicCooldownMapping(cooldown, type) # type: ignore + return func + + return decorator # type: ignore + + +def max_concurrency( + number: int, per: BucketType = BucketType.default, *, wait: bool = False +) -> Callable[[T], T]: + """A decorator that adds a maximum concurrency to a :class:`.Command` or its subclasses. + + This enables you to only allow a certain number of command invocations at the same time, + for example if a command takes too long or if only one user can use it at a time. This + differs from a cooldown in that there is no set waiting period or token bucket -- only + a set number of people can run the command. + + .. versionadded:: 1.3 + + Parameters + ---------- + number: :class:`int` + The maximum number of invocations of this command that can be running at the same time. + per: :class:`.BucketType` + The bucket that this concurrency is based on, e.g. ``BucketType.guild`` would allow + it to be used up to ``number`` times per guild. + wait: :class:`bool` + Whether the command should wait for the queue to be over. If this is set to ``False`` + then instead of waiting until the command can run again, the command raises + :exc:`.MaxConcurrencyReached` to its error handler. If this is set to ``True`` + then the command waits until it can be executed. + """ + + def decorator(func: Union[Command, CoroFunc]) -> Union[Command, CoroFunc]: + value = MaxConcurrency(number, per=per, wait=wait) + if hasattr(func, "__command_flag__"): + func._max_concurrency = value + else: + func.__commands_max_concurrency__ = value # type: ignore + return func + + return decorator # type: ignore + + +def before_invoke(coro) -> Callable[[T], T]: + """A decorator that registers a coroutine as a pre-invoke hook. + + This allows you to refer to one before invoke hook for several commands that + do not have to be within the same cog. + + .. versionadded:: 1.4 + + Example + ------- + .. code-block:: python3 + + async def record_usage(ctx): + print(ctx.author, 'used', ctx.command, 'at', ctx.message.created_at) + + @bot.command() + @commands.before_invoke(record_usage) + async def who(ctx): # Output: used who at