Skip to content

Commit

Permalink
User-Installable Integrations (#1)
Browse files Browse the repository at this point in the history
* add enums, do command stuff, add context to interaction

* style(pre-commit): auto fixes from pre-commit.com hooks

* add authorizing_integration_owners

* style(pre-commit): auto fixes from pre-commit.com hooks

* add application_metadata

* style(pre-commit): auto fixes from pre-commit.com hooks

* don't trust copilot

* style(pre-commit): auto fixes from pre-commit.com hooks

* update __all__

* circular import

* style(pre-commit): auto fixes from pre-commit.com hooks

* fix numbers

* h

* style(pre-commit): auto fixes from pre-commit.com hooks

* update guild_only deco to use contexts

* type

* style(pre-commit): auto fixes from pre-commit.com hooks

* deprecation warnings

* style(pre-commit): auto fixes from pre-commit.com hooks

* docs

* example

* style(pre-commit): auto fixes from pre-commit.com hooks

* edit docs

* update changelog

* style(pre-commit): auto fixes from pre-commit.com hooks

* update changelog

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
plun1331 and pre-commit-ci[bot] authored Mar 24, 2024
1 parent c1ec0b1 commit 6cdb3e3
Show file tree
Hide file tree
Showing 12 changed files with 485 additions and 26 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2396](https://github.com/Pycord-Development/pycord/pull/2396))
- Added `user` argument to `Paginator.edit`.
([#2390](https://github.com/Pycord-Development/pycord/pull/2390))
- Added support for user-installable applications.
([#2409](https://github.com/Pycord-Development/pycord/pull/2409))

### Fixed

Expand All @@ -32,13 +34,20 @@ These changes are available on the `master` branch, but have not yet been releas
([#2400](https://github.com/Pycord-Development/pycord/pull/2400))
- Fixed `ScheduledEvent.subscribers` behavior with `limit=None`.
([#2407](https://github.com/Pycord-Development/pycord/pull/2407))
- Fixed an issue with `Interaction` that would cause bots to crash if a guild was not
cached. ([#2409](https://github.com/Pycord-Development/pycord/pull/2409))

### Changed

- Changed the type of `Guild.bitrate_limit` to `int`.
([#2387](https://github.com/Pycord-Development/pycord/pull/2387))
- HTTP requests that fail with a 503 status are now re-tried.
([#2395](https://github.com/Pycord-Development/pycord/pull/2395))
- `ApplicationCommand.guild_only` is now deprecated in favor of
`ApplicationCommand.contexts`.
([#2409](https://github.com/Pycord-Development/pycord/pull/2409))
- `Message.interaction` is now deprecated in favor of `Message.interaction_metadata`.
([#2409](https://github.com/Pycord-Development/pycord/pull/2409))

## [2.5.0] - 2024-03-02

Expand Down
150 changes: 138 additions & 12 deletions discord/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,19 @@

from ..channel import _threaded_guild_channel_factory
from ..enums import Enum as DiscordEnum
from ..enums import MessageType, SlashCommandOptionType, try_enum
from ..enums import (
IntegrationType,
InteractionContextType,
MessageType,
SlashCommandOptionType,
try_enum,
)
from ..errors import (
ApplicationCommandError,
ApplicationCommandInvokeError,
CheckFailure,
ClientException,
InvalidArgument,
ValidationError,
)
from ..member import Member
Expand All @@ -61,7 +68,7 @@
from ..role import Role
from ..threads import Thread
from ..user import User
from ..utils import MISSING, async_all, find, maybe_coroutine, utcnow
from ..utils import MISSING, async_all, find, maybe_coroutine, utcnow, warn_deprecated
from .context import ApplicationContext, AutocompleteContext
from .options import Option, OptionChoice

Expand Down Expand Up @@ -226,11 +233,37 @@ def __init__(self, func: Callable, **kwargs) -> None:
"__default_member_permissions__",
kwargs.get("default_member_permissions", None),
)
self.guild_only: bool | None = getattr(
func, "__guild_only__", kwargs.get("guild_only", None)
)
self.nsfw: bool | None = getattr(func, "__nsfw__", kwargs.get("nsfw", None))

integration_types = getattr(
func, "__integration_types__", kwargs.get("integration_types", None)
)
contexts = getattr(func, "__contexts__", kwargs.get("contexts", None))
guild_only = getattr(func, "__guild_only__", kwargs.get("guild_only", MISSING))
if guild_only is not MISSING:
warn_deprecated("guild_only", "contexts", "2.6")
if contexts and guild_only:
raise InvalidArgument(
"cannot pass both 'contexts' and 'guild_only' to ApplicationCommand"
)
if self.guild_ids and (
(contexts is not None) or guild_only or integration_types
):
raise InvalidArgument(
"the 'contexts' and 'integration_types' parameters are not available for guild commands"
)

self.contexts: set[InteractionContextType] = contexts or {
InteractionContextType.guild,
InteractionContextType.bot_dm,
InteractionContextType.private_channel,
}
if guild_only:
self.guild_only: bool | None = guild_only
self.integration_types: set[IntegrationType] = integration_types or {
IntegrationType.guild_install
}

def __repr__(self) -> str:
return f"<discord.commands.{self.__class__.__name__} name={self.name}>"

Expand Down Expand Up @@ -274,6 +307,33 @@ def callback(
unwrap = unwrap_function(function)
self.module = unwrap.__module__

@property
def guild_only(self) -> bool:
warn_deprecated(
"guild_only",
"contexts",
"2.6",
reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview",
)
return InteractionContextType.guild in self.contexts and len(self.contexts) == 1

@guild_only.setter
def guild_only(self, value: bool) -> None:
warn_deprecated(
"guild_only",
"contexts",
"2.6",
reference="https://discord.com/developers/docs/change-log#userinstallable-apps-preview",
)
if value:
self.contexts = {InteractionContextType.guild}
else:
self.contexts = {
InteractionContextType.guild,
InteractionContextType.bot_dm,
InteractionContextType.private_channel,
}

def _prepare_cooldowns(self, ctx: ApplicationContext):
if self._buckets.valid:
current = datetime.datetime.now().timestamp()
Expand Down Expand Up @@ -631,6 +691,9 @@ class SlashCommand(ApplicationCommand):
Returns a string that allows you to mention the slash command.
guild_only: :class:`bool`
Whether the command should only be usable inside a guild.
.. deprecated:: 2.6
Use the :attr:`contexts` parameter instead.
nsfw: :class:`bool`
Whether the command should be restricted to 18+ channels and users.
Apps intending to be listed in the App Directory cannot have NSFW commands.
Expand All @@ -654,6 +717,10 @@ class SlashCommand(ApplicationCommand):
description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this command. The values of this should be ``"locale": "description"``.
See `here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales.
integration_types: Set[:class:`IntegrationType`]
The installation contexts where this command is available. Cannot be set if this is a guild command.
contexts: Set[:class:`InteractionContextType`]
The interaction contexts where this command is available. Cannot be set if this is a guild command.
"""

type = 1
Expand Down Expand Up @@ -881,9 +948,6 @@ def to_dict(self) -> dict:
if self.is_subcommand:
as_dict["type"] = SlashCommandOptionType.sub_command.value

if self.guild_only is not None:
as_dict["dm_permission"] = not self.guild_only

if self.nsfw is not None:
as_dict["nsfw"] = self.nsfw

Expand All @@ -892,6 +956,10 @@ def to_dict(self) -> dict:
self.default_member_permissions.value
)

if not self.guild_ids:
as_dict["integration_types"] = [it.value for it in self.integration_types]
as_dict["contexts"] = [ctx.value for ctx in self.contexts]

return as_dict

async def _invoke(self, ctx: ApplicationContext) -> None:
Expand Down Expand Up @@ -1100,6 +1168,9 @@ class SlashCommandGroup(ApplicationCommand):
isn't one.
guild_only: :class:`bool`
Whether the command should only be usable inside a guild.
.. deprecated:: 2.6
Use the :attr:`contexts` parameter instead.
nsfw: :class:`bool`
Whether the command should be restricted to 18+ channels and users.
Apps intending to be listed in the App Directory cannot have NSFW commands.
Expand All @@ -1118,6 +1189,10 @@ class SlashCommandGroup(ApplicationCommand):
description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this command. The values of this should be ``"locale": "description"``.
See `here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales.
integration_types: Set[:class:`IntegrationType`]
The installation contexts where this command is available. Cannot be set if this is a guild command.
contexts: Set[:class:`InteractionContextType`]
The interaction contexts where this command is available. Cannot be set if this is a guild command.
"""

__initial_commands__: list[SlashCommand | SlashCommandGroup]
Expand Down Expand Up @@ -1177,9 +1252,35 @@ def __init__(
self.default_member_permissions: Permissions | None = kwargs.get(
"default_member_permissions", None
)
self.guild_only: bool | None = kwargs.get("guild_only", None)
self.nsfw: bool | None = kwargs.get("nsfw", None)

integration_types = kwargs.get("integration_types", None)
contexts = kwargs.get("contexts", None)
guild_only = kwargs.get("guild_only", MISSING)
if guild_only is not MISSING:
warn_deprecated("guild_only", "contexts", "2.6")
if contexts and guild_only:
raise InvalidArgument(
"cannot pass both 'contexts' and 'guild_only' to ApplicationCommand"
)
if self.guild_ids and (
(contexts is not None) or guild_only or integration_types
):
raise InvalidArgument(
"the 'contexts' and 'integration_types' parameters are not available for guild commands"
)

self.contexts: set[InteractionContextType] = contexts or {
InteractionContextType.guild,
InteractionContextType.bot_dm,
InteractionContextType.private_channel,
}
if guild_only:
self.guild_only: bool | None = guild_only
self.integration_types: set[IntegrationType] = integration_types or {
IntegrationType.guild_install
}

self.name_localizations: dict[str, str] = kwargs.get(
"name_localizations", MISSING
)
Expand Down Expand Up @@ -1218,6 +1319,23 @@ def __init__(
def module(self) -> str | None:
return self.__module__

@property
def guild_only(self) -> bool:
warn_deprecated("guild_only", "contexts", "2.6")
return InteractionContextType.guild in self.contexts and len(self.contexts) == 1

@guild_only.setter
def guild_only(self, value: bool) -> None:
warn_deprecated("guild_only", "contexts", "2.6")
if value:
self.contexts = {InteractionContextType.guild}
else:
self.contexts = {
InteractionContextType.guild,
InteractionContextType.bot_dm,
InteractionContextType.private_channel,
}

def to_dict(self) -> dict:
as_dict = {
"name": self.name,
Expand All @@ -1232,9 +1350,6 @@ def to_dict(self) -> dict:
if self.parent is not None:
as_dict["type"] = self.input_type.value

if self.guild_only is not None:
as_dict["dm_permission"] = not self.guild_only

if self.nsfw is not None:
as_dict["nsfw"] = self.nsfw

Expand All @@ -1243,6 +1358,10 @@ def to_dict(self) -> dict:
self.default_member_permissions.value
)

if not self.guild_ids:
as_dict["integration_types"] = [it.value for it in self.integration_types]
as_dict["contexts"] = [ctx.value for ctx in self.contexts]

return as_dict

def add_command(self, command: SlashCommand) -> None:
Expand Down Expand Up @@ -1476,6 +1595,9 @@ class ContextMenuCommand(ApplicationCommand):
The ids of the guilds where this command will be registered.
guild_only: :class:`bool`
Whether the command should only be usable inside a guild.
.. deprecated:: 2.6
Use the ``contexts`` parameter instead.
nsfw: :class:`bool`
Whether the command should be restricted to 18+ channels and users.
Apps intending to be listed in the App Directory cannot have NSFW commands.
Expand All @@ -1496,6 +1618,10 @@ class ContextMenuCommand(ApplicationCommand):
name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales.
integration_types: Set[:class:`IntegrationType`]
The installation contexts where this command is available. Cannot be set if this is a guild command.
contexts: Set[:class:`InteractionContextType`]
The interaction contexts where this command is available. Cannot be set if this is a guild command.
"""

def __new__(cls, *args, **kwargs) -> ContextMenuCommand:
Expand Down
5 changes: 3 additions & 2 deletions discord/commands/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from typing import Callable

from ..enums import InteractionContextType
from ..permissions import Permissions
from .core import ApplicationCommand

Expand Down Expand Up @@ -98,9 +99,9 @@ async def test(ctx):

def inner(command: Callable):
if isinstance(command, ApplicationCommand):
command.guild_only = True
command.contexts = {InteractionContextType.guild}
else:
command.__guild_only__ = True
command.__contexts__ = {InteractionContextType.guild}

return command

Expand Down
17 changes: 17 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
"SKUType",
"EntitlementType",
"EntitlementOwnerType",
"IntegrationType",
"InteractionContextType",
)


Expand Down Expand Up @@ -1020,6 +1022,21 @@ class EntitlementOwnerType(Enum):
user = 2


class IntegrationType(Enum):
"""The application's integration type"""

guild_install = 0
user_install = 1


class InteractionContextType(Enum):
"""The interaction's context type"""

guild = 0
bot_dm = 1
private_channel = 2


T = TypeVar("T")


Expand Down
Loading

0 comments on commit 6cdb3e3

Please sign in to comment.