Skip to content

Commit

Permalink
Merge branch 'master' into sku_migration
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftinv authored Dec 29, 2024
2 parents 7f5ea52 + 42ef060 commit 1c457bf
Show file tree
Hide file tree
Showing 30 changed files with 409 additions and 183 deletions.
1 change: 1 addition & 0 deletions changelog/1173.feature.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Add support for user-installed commands. See :ref:`app_command_contexts` for fur
- |commands| Add ``install_types`` and ``contexts`` parameters to application command decorators.
- |commands| Add :func:`~ext.commands.install_types` and :func:`~ext.commands.contexts` decorators.
- |commands| Using the :class:`GuildCommandInteraction` annotation now sets :attr:`~ApplicationCommand.install_types` and :attr:`~ApplicationCommand.contexts`, instead of :attr:`~ApplicationCommand.dm_permission`.
- |commands| Add ``default_install_types`` and ``default_contexts`` parameters to :class:`~ext.commands.Bot`.
1 change: 1 addition & 0 deletions changelog/1190.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``cls`` parameter of UI component decorators (such as :func:`ui.button`) now accepts any matching callable, in addition to item subclasses.
14 changes: 14 additions & 0 deletions changelog/1261.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Add support for user-installed commands. See :ref:`app_command_contexts` for further details.
- Add :attr:`ApplicationCommand.install_types` and :attr:`ApplicationCommand.contexts` fields,
with respective :class:`ApplicationInstallTypes` and :class:`InteractionContextTypes` flag types.
- :class:`Interaction` changes:
- Add :attr:`Interaction.context` field, reflecting the context in which the interaction occurred.
- Add :attr:`Interaction.authorizing_integration_owners` field and :class:`AuthorizingIntegrationOwners` class, containing details about the application installation.
- :attr:`Interaction.app_permissions` is now always provided by Discord.
- Add :attr:`Message.interaction_metadata` and :class:`InteractionMetadata` type, containing metadata for the interaction associated with a message.
- Add ``integration_type`` parameter to :func:`utils.oauth_url`.
- Add :attr:`AppInfo.guild_install_type_config` and :attr:`AppInfo.user_install_type_config` fields.
- |commands| Add ``install_types`` and ``contexts`` parameters to application command decorators.
- |commands| Add :func:`~ext.commands.install_types` and :func:`~ext.commands.contexts` decorators.
- |commands| Using the :class:`GuildCommandInteraction` annotation now sets :attr:`~ApplicationCommand.install_types` and :attr:`~ApplicationCommand.contexts`, instead of :attr:`~ApplicationCommand.dm_permission`.
- |commands| Add ``default_install_types`` and ``default_contexts`` parameters to :class:`~ext.commands.Bot`.
1 change: 1 addition & 0 deletions changelog/914.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :class:`ui.Modal` timeout issues with long-running callbacks, and multiple modals with the same user and ``custom_id``.
37 changes: 34 additions & 3 deletions disnake/app_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,13 @@ def __init__(
self.install_types: Optional[ApplicationInstallTypes] = install_types
self.contexts: Optional[InteractionContextTypes] = contexts

# TODO(3.0): refactor
# These are for ext.commands defaults. It's quite ugly to do it this way,
# but since __eq__ and to_dict functionality is encapsulated here and can't be moved trivially,
# it'll do until the presumably soon-ish refactor of the entire commands framework.
self._default_install_types: Optional[ApplicationInstallTypes] = None
self._default_contexts: Optional[InteractionContextTypes] = None

self._always_synced: bool = False

# reset `default_permission` if set before
Expand Down Expand Up @@ -614,6 +621,9 @@ def __str__(self) -> str:
return self.name

def __eq__(self, other) -> bool:
if not isinstance(other, ApplicationCommand):
return False

if not (
self.type == other.type
and self.name == other.name
Expand All @@ -634,8 +644,10 @@ def __eq__(self, other) -> bool:
# `contexts` takes priority over `dm_permission`;
# ignore `dm_permission` if `contexts` is set,
# since the API returns both even when only `contexts` was provided
if self.contexts is not None or other.contexts is not None:
if self.contexts != other.contexts:
self_contexts = self._contexts_with_default
other_contexts = other._contexts_with_default
if self_contexts is not None or other_contexts is not None:
if self_contexts != other_contexts:
return False
else:
# this is a bit awkward; `None` is equivalent to `True` in this case
Expand All @@ -648,6 +660,9 @@ def __eq__(self, other) -> bool:
def _install_types_with_default(self) -> Optional[ApplicationInstallTypes]:
# if this is an api-provided command object, keep things as-is
if self.install_types is None and not isinstance(self, _APIApplicationCommandMixin):
if self._default_install_types is not None:
return self._default_install_types

# The purpose of this default is to avoid re-syncing after the updating to the new version,
# at least as long as the user hasn't enabled user installs in the dev portal
# (i.e. if they haven't, the api defaults to this value as well).
Expand All @@ -658,6 +673,20 @@ def _install_types_with_default(self) -> Optional[ApplicationInstallTypes]:

return self.install_types

@property
def _contexts_with_default(self) -> Optional[InteractionContextTypes]:
# (basically the same logic as `_install_types_with_default`, but without a fallback)
if (
self.contexts is None
and not isinstance(self, _APIApplicationCommandMixin)
and self._default_contexts is not None
# only use default if legacy `dm_permission` wasn't set
and self._dm_permission is None
):
return self._default_contexts

return self.contexts

def to_dict(self) -> EditApplicationCommandPayload:
data: EditApplicationCommandPayload = {
"type": try_enum_to_int(self.type),
Expand All @@ -678,7 +707,9 @@ def to_dict(self) -> EditApplicationCommandPayload:
)
data["integration_types"] = install_types

contexts = self.contexts.values if self.contexts is not None else None
contexts = (
self._contexts_with_default.values if self._contexts_with_default is not None else None
)
data["contexts"] = contexts

# don't set `dm_permission` if `contexts` is set
Expand Down
5 changes: 5 additions & 0 deletions disnake/ext/commands/base_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from ._types import AppCheck, Coro, Error, Hook
from .cog import Cog
from .interaction_bot_base import InteractionBotBase

ApplicationCommandInteractionT = TypeVar(
"ApplicationCommandInteractionT", bound=ApplicationCommandInteraction, covariant=True
Expand Down Expand Up @@ -268,6 +269,10 @@ def _apply_guild_only(self) -> None:
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."""
Expand Down
67 changes: 62 additions & 5 deletions disnake/ext/commands/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
from disnake.activity import BaseActivity
from disnake.client import GatewayParams
from disnake.enums import Status
from disnake.flags import Intents, MemberCacheFlags
from disnake.flags import (
ApplicationInstallTypes,
Intents,
InteractionContextTypes,
MemberCacheFlags,
)
from disnake.i18n import LocalizationProtocol
from disnake.mentions import AllowedMentions
from disnake.message import Message
Expand Down Expand Up @@ -117,6 +122,28 @@ class Bot(BotBase, InteractionBotBase, disnake.Client):
.. 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
Expand Down Expand Up @@ -233,10 +260,12 @@ def __init__(
reload: bool = False,
case_insensitive: bool = False,
command_sync_flags: CommandSyncFlags = ...,
test_guilds: Optional[Sequence[int]] = None,
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,
Expand Down Expand Up @@ -285,10 +314,12 @@ def __init__(
reload: bool = False,
case_insensitive: bool = False,
command_sync_flags: CommandSyncFlags = ...,
test_guilds: Optional[Sequence[int]] = None,
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
Expand Down Expand Up @@ -391,6 +422,28 @@ class InteractionBot(InteractionBotBase, disnake.Client):
.. 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`]
Expand Down Expand Up @@ -434,10 +487,12 @@ def __init__(
owner_ids: Optional[Set[int]] = None,
reload: bool = False,
command_sync_flags: CommandSyncFlags = ...,
test_guilds: Optional[Sequence[int]] = None,
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,
Expand Down Expand Up @@ -479,10 +534,12 @@ def __init__(
owner_ids: Optional[Set[int]] = None,
reload: bool = False,
command_sync_flags: CommandSyncFlags = ...,
test_guilds: Optional[Sequence[int]] = None,
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
Expand Down
4 changes: 3 additions & 1 deletion disnake/ext/commands/cooldowns.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def get_key(self, msg: Message) -> Any:
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 isinstance(msg.author, Member) else msg.channel).id
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)
Expand Down
10 changes: 8 additions & 2 deletions disnake/ext/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2426,11 +2426,14 @@ def dm_only() -> Callable[[T], T]:
This check raises a special exception, :exc:`.PrivateMessageOnly`
that is inherited from :exc:`.CheckFailure`.
.. note::
For application commands, consider setting the allowed :ref:`contexts <app_command_contexts>` instead.
.. versionadded:: 1.1
"""

def predicate(ctx: AnyContext) -> bool:
if ctx.guild is not None:
if (ctx.guild if isinstance(ctx, Context) else ctx.guild_id) is not None:
raise PrivateMessageOnly
return True

Expand All @@ -2444,10 +2447,13 @@ def guild_only() -> Callable[[T], T]:
This check raises a special exception, :exc:`.NoPrivateMessage`
that is inherited from :exc:`.CheckFailure`.
.. note::
For application commands, consider setting the allowed :ref:`contexts <app_command_contexts>` instead.
"""

def predicate(ctx: AnyContext) -> bool:
if ctx.guild is None:
if (ctx.guild if isinstance(ctx, Context) else ctx.guild_id) is None:
raise NoPrivateMessage
return True

Expand Down
8 changes: 8 additions & 0 deletions disnake/ext/commands/interaction_bot_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ def __init__(
sync_commands_debug: bool = MISSING,
sync_commands_on_cog_unload: bool = MISSING,
test_guilds: Optional[Sequence[int]] = None,
default_install_types: Optional[ApplicationInstallTypes] = None,
default_contexts: Optional[InteractionContextTypes] = None,
**options: Any,
) -> None:
if test_guilds and not all(isinstance(guild_id, int) for guild_id in test_guilds):
Expand Down Expand Up @@ -200,6 +202,9 @@ def __init__(
self._command_sync_flags = command_sync_flags
self._sync_queued: asyncio.Lock = asyncio.Lock()

self._default_install_types = default_install_types
self._default_contexts = default_contexts

self._slash_command_checks = []
self._slash_command_check_once = []
self._user_command_checks = []
Expand Down Expand Up @@ -286,6 +291,7 @@ def add_slash_command(self, slash_command: InvokableSlashCommand) -> None:
if slash_command.name in self.all_slash_commands:
raise CommandRegistrationError(slash_command.name)

slash_command._apply_defaults(self)
slash_command.body.localize(self.i18n)
self.all_slash_commands[slash_command.name] = slash_command

Expand Down Expand Up @@ -316,6 +322,7 @@ def add_user_command(self, user_command: InvokableUserCommand) -> None:
if user_command.name in self.all_user_commands:
raise CommandRegistrationError(user_command.name)

user_command._apply_defaults(self)
user_command.body.localize(self.i18n)
self.all_user_commands[user_command.name] = user_command

Expand Down Expand Up @@ -348,6 +355,7 @@ def add_message_command(self, message_command: InvokableMessageCommand) -> None:
if message_command.name in self.all_message_commands:
raise CommandRegistrationError(message_command.name)

message_command._apply_defaults(self)
message_command.body.localize(self.i18n)
self.all_message_commands[message_command.name] = message_command

Expand Down
7 changes: 7 additions & 0 deletions disnake/interactions/application_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ class ApplicationCommandInteraction(Interaction[ClientT]):
author: Union[:class:`User`, :class:`Member`]
The user or member that sent the interaction.
.. note::
In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable,
such as with user-installed applications in guilds, some attributes of :class:`Member`\\s
that depend on the guild/role cache will not work due to an API limitation.
This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`,
and :attr:`~Member.guild_permissions`.
locale: :class:`Locale`
The selected language of the interaction's author.
Expand Down
7 changes: 7 additions & 0 deletions disnake/interactions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ class Interaction(Generic[ClientT]):
author: Union[:class:`User`, :class:`Member`]
The user or member that sent the interaction.
.. note::
In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable,
such as with user-installed applications in guilds, some attributes of :class:`Member`\\s
that depend on the guild/role cache will not work due to an API limitation.
This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`,
and :attr:`~Member.guild_permissions`.
locale: :class:`Locale`
The selected language of the interaction's author.
Expand Down
7 changes: 7 additions & 0 deletions disnake/interactions/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ class MessageInteraction(Interaction[ClientT]):
author: Union[:class:`User`, :class:`Member`]
The user or member that sent the interaction.
.. note::
In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable,
such as with user-installed applications in guilds, some attributes of :class:`Member`\\s
that depend on the guild/role cache will not work due to an API limitation.
This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`,
and :attr:`~Member.guild_permissions`.
locale: :class:`Locale`
The selected language of the interaction's author.
Expand Down
7 changes: 7 additions & 0 deletions disnake/interactions/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ class ModalInteraction(Interaction[ClientT]):
author: Union[:class:`User`, :class:`Member`]
The user or member that sent the interaction.
.. note::
In scenarios where an interaction occurs in a guild but :attr:`.guild` is unavailable,
such as with user-installed applications in guilds, some attributes of :class:`Member`\\s
that depend on the guild/role cache will not work due to an API limitation.
This includes :attr:`~Member.roles`, :attr:`~Member.top_role`, :attr:`~Member.role_icon`,
and :attr:`~Member.guild_permissions`.
locale: :class:`Locale`
The selected language of the interaction's author.
Expand Down
Loading

0 comments on commit 1c457bf

Please sign in to comment.