Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ These changes are available on the `master` branch, but have not yet been releas

### Added

- Add support for Entry Point Commands.
([#3276](https://github.com/Pycord-Development/pycord/pull/3276))
- Add support for launching activities as an interaction response.
([#3276](https://github.com/Pycord-Development/pycord/pull/3276))

### Changed

### Fixed
Expand Down
57 changes: 55 additions & 2 deletions discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,20 @@
ApplicationCommand,
ApplicationContext,
AutocompleteContext,
EntryPointCommand,
MessageCommand,
SlashCommand,
SlashCommandGroup,
UserCommand,
command,
)
from .enums import IntegrationType, InteractionContextType, InteractionType, TeamRole
from .enums import (
EntryPointHandler,
IntegrationType,
InteractionContextType,
InteractionType,
TeamRole,
)
from .errors import CheckFailure, DiscordException
from .interactions import Interaction
from .shard import AutoShardedClient
Expand All @@ -74,7 +81,7 @@
from .member import Member
from .permissions import Permissions

C = TypeVar("C", bound=MessageCommand | SlashCommand | UserCommand)
C = TypeVar("C", bound=MessageCommand | SlashCommand | UserCommand | EntryPointCommand)
CoroFunc = Callable[..., Coroutine[Any, Any, Any]]
CFT = TypeVar("CFT", bound=CoroFunc)

Expand Down Expand Up @@ -1056,6 +1063,52 @@ def message_command(
**kwargs,
)

def entry_point_command(
self,
*,
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
cog: Cog | None = MISSING,
contexts: set[InteractionContextType] | None = MISSING,
cooldown: Cooldown | None = MISSING,
default_member_permissions: Permissions | None = MISSING,
description: str | None = MISSING,
description_localizations: dict[str, str] | None = MISSING,
integration_types: set[IntegrationType] | None = MISSING,
name: str | None = MISSING,
name_localizations: dict[str, str] | None = MISSING,
nsfw: bool | None = MISSING,
handler: EntryPointHandler = MISSING,
**kwargs: Never,
) -> Callable[..., EntryPointCommand]:
"""A shortcut decorator for adding an entry point command to the bot.
This is equivalent to using :meth:`application_command`, providing
the :class:`EntryPointCommand` class.

.. versionadded:: 2.9.0

Returns
-------
Callable[..., :class:`EntryPointCommand`]
A decorator that converts the provided function into a :class:`.EntryPointCommand`,
adds it to the bot, and returns it.
"""
return self.application_command(
cls=EntryPointCommand,
checks=checks,
cog=cog,
contexts=contexts,
cooldown=cooldown,
default_member_permissions=default_member_permissions,
description=description,
description_localizations=description_localizations,
integration_types=integration_types,
name=name,
name_localizations=name_localizations,
nsfw=nsfw,
handler=handler,
**kwargs,
)

def application_command(
self,
*,
Expand Down
227 changes: 227 additions & 0 deletions discord/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
)

from ..channel import PartialMessageable, _threaded_guild_channel_factory
from ..enums import (
EntryPointHandler,
)
from ..enums import Enum as DiscordEnum
from ..enums import (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combine imports

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isort seems to disagree. Might be worth looking into enabling this in py-cords config https://isort.readthedocs.io/en/latest/configuration/options.html#combine-as-imports

IntegrationType,
Expand Down Expand Up @@ -82,11 +85,13 @@
"application_command",
"user_command",
"message_command",
"entry_point_command",
"command",
"SlashCommandGroup",
"ContextMenuCommand",
"UserCommand",
"MessageCommand",
"EntryPointCommand",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -1975,6 +1980,185 @@ def _update_copy(self, kwargs: dict[str, Any]):
return self.copy()


class EntryPointCommand(ApplicationCommand):
r"""A class that implements the protocol for an entry point command.

These are not created manually, instead they are created via the
decorator or functional interface.

.. versionadded:: 2.9.0

Attributes
-----------
name: :class:`str`
The name of the command.
callback: :ref:`coroutine <coroutine>`
The coroutine that is executed when the command is run.
This is only called if the handler is not :attr:`EntryPointHandler.discord_launch_activity`.
description: Optional[:class:`str`]
The description for the command.
mention: :class:`str`
Returns a string that allows you to mention the slash command.
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.
default_member_permissions: :class:`~discord.Permissions`
The default permissions a member needs to be able to run the command.
cog: Optional[:class:`Cog`]
The cog that this command belongs to. ``None`` if there isn't one.
checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]]
A list of predicates that verifies if the command could be executed
with the given :class:`.ApplicationContext` as the sole parameter. If an exception
is necessary to be thrown to signal failure, then one inherited from
:exc:`.ApplicationCommandError` should be used. Note that if the checks fail then
:exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error`
event.
cooldown: Optional[:class:`~discord.ext.commands.Cooldown`]
The cooldown applied when the command is invoked. ``None`` if the command
doesn't have a cooldown.
name_localizations: Dict[:class:`str`, :class:`str`]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
description_localizations: Dict[:class:`str`, :class:`str`]
The description localizations for this command. The values of this should be ``"locale": "description"``.
See `here <https://docs.discord.com/developers/reference#locales>`_ for a list of valid locales.
integration_types: Set[:class:`IntegrationType`]
The type of installation this command should be available to. For instance, if set to
:attr:`IntegrationType.user_install`, the command will only be available to users with
the application installed on their account. Unapplicable for guild commands.
contexts: Set[:class:`InteractionContextType`]
The location where this command can be used. Cannot be set if this is a guild command.
handler: :class:`EntryPointHandler`
The action to take when the user executes this command.
"""

type = 4

def __new__(cls, *args, **kwargs) -> EntryPointCommand:
self = super().__new__(cls)

self.__original_kwargs__ = kwargs.copy()
return self

def __init__(self, func: Callable, *args, **kwargs) -> None:
super().__init__(func, **kwargs)
if not asyncio.iscoroutinefunction(func):
raise TypeError("Callback must be a coroutine.")
self.callback = func

self.name_localizations: dict[str, str] = kwargs.get(
"name_localizations", MISSING
)
_validate_names(self)

description = kwargs.get("description") or (
inspect.cleandoc(func.__doc__).splitlines()[0]
if func.__doc__ is not None
else "No description provided"
)

self.description: str = description
self.description_localizations: dict[str, str] = kwargs.get(
"description_localizations", MISSING
)
_validate_descriptions(self)

self.handler: EntryPointHandler = kwargs.get("handler", MISSING)

self.attached_to_group: bool = False

try:
checks = func.__commands_checks__
checks.reverse()
except AttributeError:
checks = kwargs.get("checks", [])

self.checks = checks

self._before_invoke = None
self._after_invoke = None

def _validate_parameters(self):
params = self._get_signature_parameters()
if list(params.items())[0][0] == "self":
temp = list(params.items())
temp.pop(0)
params = dict(temp)
params = iter(params)

# next we have the 'ctx' as the next parameter
try:
next(params)
except StopIteration:
raise ClientException(
f'Callback for {self.name} command is missing "ctx" parameter.'
)

# next there should be no more parameters
try:
next(params)
raise ClientException(
f"Callback for {self.name} command has too many parameters."
)
except StopIteration:
pass

@property
def cog(self):
return getattr(self, "_cog", None)

@cog.setter
def cog(self, value):
old_cog = self.cog
self._cog = value

if (
old_cog is None
and value is not None
or value is None
and old_cog is not None
):
self._validate_parameters()

@property
def mention(self) -> str:
return f"</{self.qualified_name}:{self.qualified_id}>"

def to_dict(self) -> dict:
as_dict = {
"name": self.name,
"description": self.description,
"type": self.type,
}
if self.name_localizations is not MISSING:
as_dict["name_localizations"] = self.name_localizations
if self.description_localizations is not MISSING:
as_dict["description_localizations"] = self.description_localizations

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

if self.default_member_permissions is not None:
as_dict["default_member_permissions"] = (
self.default_member_permissions.value
)

if self.handler is not MISSING:
as_dict["handler"] = self.handler.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:
if self.cog is not None:
await self.callback(self.cog, ctx)
else:
await self.callback(ctx)


def slash_command(
*,
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
Expand Down Expand Up @@ -2106,6 +2290,49 @@ def message_command(
)


def entry_point_command(
*,
checks: list[Callable[[ApplicationContext], bool]] | None = MISSING,
cog: Cog | None = MISSING,
contexts: set[InteractionContextType] | None = MISSING,
cooldown: Cooldown | None = MISSING,
default_member_permissions: Permissions | None = MISSING,
description: str | None = MISSING,
description_localizations: dict[str, str] | None = MISSING,
integration_types: set[IntegrationType] | None = MISSING,
name: str | None = MISSING,
name_localizations: dict[str, str] | None = MISSING,
nsfw: bool | None = MISSING,
handler: EntryPointHandler = MISSING,
**kwargs: Never,
) -> Callable[..., EntryPointCommand]:
"""Decorator for entry point commands that invokes :func:`application_command`.

.. versionadded:: 2.9.0

Returns
-------
Callable[..., :class:`.EntryPointCommand`]
A decorator that converts the provided method into a :class:`.EntryPointCommand`.
"""
return application_command(
cls=EntryPointCommand,
checks=checks,
cog=cog,
contexts=contexts,
cooldown=cooldown,
default_member_permissions=default_member_permissions,
description=description,
description_localizations=description_localizations,
integration_types=integration_types,
name=name,
name_localizations=name_localizations,
nsfw=nsfw,
handler=handler,
**kwargs,
)


def application_command(
*,
cls: type[C] = SlashCommand,
Expand Down
9 changes: 9 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"SelectDefaultValueType",
"ApplicationEventWebhookStatus",
"InviteTargetUsersJobStatusCode",
"EntryPointHandler",
)


Expand Down Expand Up @@ -714,6 +715,7 @@ class InteractionResponseType(Enum):
auto_complete_result = 8 # for autocomplete interactions
modal = 9 # for modal dialogs
premium_required = 10
launch_activity = 12


class VideoQualityMode(Enum):
Expand Down Expand Up @@ -1212,6 +1214,13 @@ class InviteTargetUsersJobStatusCode(Enum):
failed = 3


class EntryPointHandler(Enum):
"""Represents the handler for entry point commands (type 4)."""

app_handler = 1
discord_launch_activity = 2


T = TypeVar("T")


Expand Down
Loading
Loading