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
1 change: 1 addition & 0 deletions changelog/1570.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add basic support for "entry point" application commands (:class:`EntryPointCommand`), and launching an activity in response to an interaction (:meth:`InteractionResponse.launch_activity`).
157 changes: 155 additions & 2 deletions disnake/app_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from . import utils
from .enums import (
ApplicationCommandHandlerType,
ApplicationCommandPermissionType,
ApplicationCommandType,
ChannelType,
Expand Down Expand Up @@ -48,7 +49,8 @@
| Sequence[Localized[str]]
)

APIApplicationCommand: TypeAlias = "APIUserCommand | APIMessageCommand | APISlashCommand"
APIGuildApplicationCommand: TypeAlias = "APIUserCommand | APIMessageCommand | APISlashCommand"
APIApplicationCommand: TypeAlias = APIGuildApplicationCommand | "APIEntryPointCommand"


__all__ = (
Expand All @@ -60,22 +62,39 @@
"APIUserCommand",
"MessageCommand",
"APIMessageCommand",
"EntryPointCommand",
"APIEntryPointCommand",
"OptionChoice",
"Option",
"ApplicationCommandPermissions",
"GuildApplicationCommandPermissions",
)


def application_command_factory(data: ApplicationCommandPayload) -> APIApplicationCommand:
# `guild_only` is purely a type checking hint, we assume the API only returns the
# types it's supposed (and documented) to return
@overload
def application_command_factory(
data: ApplicationCommandPayload, guild_only: Literal[False] = False
) -> APIApplicationCommand: ...
@overload
def application_command_factory(
data: ApplicationCommandPayload, guild_only: Literal[True]
) -> APIGuildApplicationCommand: ...
def application_command_factory(
data: ApplicationCommandPayload, guild_only: bool = False
) -> 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)
if cmd_type is ApplicationCommandType.primary_entry_point:
return APIEntryPointCommand.from_dict(data)

utils.assert_never(cmd_type)
msg = f"Application command of type {cmd_type} is not valid"
raise TypeError(msg)

Expand Down Expand Up @@ -514,6 +533,7 @@ class ApplicationCommand(ABC): # noqa: B024 # this will get refactored eventua
- :class:`~.SlashCommand`
- :class:`~.MessageCommand`
- :class:`~.UserCommand`
- :class:`~.EntryPointCommand`

Attributes
----------
Expand Down Expand Up @@ -1370,6 +1390,139 @@ def from_dict(cls, data: ApplicationCommandPayload) -> Self:
return self


class EntryPointCommand(ApplicationCommand):
"""A primary entry point command, used for launching an app's associated activity.

More details can be found in the :ddocs:`API documentation <interactions/application-commands#entry-point-commands>`.

.. versionadded:: |vnext|

Attributes
----------
name: :class:`str`
The command's name.
name_localizations: :class:`.LocalizationValue`
Localizations for ``name``.
nsfw: :class:`bool`
Whether this command is :ddocs:`age-restricted <interactions/application-commands#age-restricted-commands>`.
Defaults to ``False``.
install_types: :class:`ApplicationInstallTypes` | :data:`None`
The installation types where the command is available.
Defaults to :attr:`ApplicationInstallTypes.guild` only.
contexts: :class:`InteractionContextTypes` | :data:`None`
The interaction contexts where the command can be used.
handler: :class:`ApplicationCommandHandlerType`
Whether the interaction triggered by invoking this command should be handled
by the app or Discord itself.
Defaults to :attr:`ApplicationCommandHandlerType.discord`.
"""

__repr_attributes__: ClassVar[tuple[str, ...]] = (
*tuple(n for n in ApplicationCommand.__repr_attributes__ if n != "type"),
"handler",
)

def __init__(
self,
name: LocalizedRequired,
default_member_permissions: Permissions | int | None = None,
nsfw: bool | None = None,
install_types: ApplicationInstallTypes | None = None,
contexts: InteractionContextTypes | None = None,
handler: ApplicationCommandHandlerType | None = None,
) -> None:
super().__init__(
type=ApplicationCommandType.primary_entry_point,
name=name,
default_member_permissions=default_member_permissions,
nsfw=nsfw,
install_types=install_types,
contexts=contexts,
)

self.handler: ApplicationCommandHandlerType = (
handler or ApplicationCommandHandlerType.discord
)

def __eq__(self, other: object) -> bool:
return (
super().__eq__(other) and self.handler == other.handler # pyright: ignore[reportAttributeAccessIssue]
)

def to_dict(self) -> EditApplicationCommandPayload:
res = super().to_dict()
res["handler"] = self.handler.value
return res


class APIEntryPointCommand(EntryPointCommand, _APIApplicationCommandMixin):
"""A primary entry point command returned by the API.

.. versionadded:: |vnext|

Attributes
----------
name: :class:`str`
The command's name.
name_localizations: :class:`.LocalizationValue`
Localizations for ``name``.
nsfw: :class:`bool`
Whether this command is :ddocs:`age-restricted <interactions/application-commands#age-restricted-commands>`.
install_types: :class:`ApplicationInstallTypes` | :data:`None`
The installation types where the command is available.
Defaults to :attr:`ApplicationInstallTypes.guild` only.
contexts: :class:`InteractionContextTypes` | :data:`None`
The interaction contexts where the command can be used.
handler: :class:`ApplicationCommandHandlerType`
Whether the interaction triggered by invoking this command should be handled
by the app or Discord itself.
id: :class:`int`
The command's ID.
application_id: :class:`int`
The application ID this command belongs to.
guild_id: :data:`None`
The ID of the guild this command is enabled in.
Always :data:`None` for this type of command.
version: :class:`int`
Autoincrementing version identifier updated during substantial record changes.
"""

__repr_attributes__: ClassVar[tuple[str, ...]] = (
*EntryPointCommand.__repr_attributes__,
*_APIApplicationCommandMixin.__repr_attributes__,
)

@classmethod
def from_dict(cls, data: ApplicationCommandPayload) -> Self:
cmd_type = data.get("type", 0)
if cmd_type != ApplicationCommandType.primary_entry_point.value:
msg = f"Invalid payload type for EntryPointCommand: {cmd_type}"
raise ValueError(msg)

self = cls(
name=Localized(data["name"], data=data.get("name_localizations")),
default_member_permissions=_get_as_snowflake(data, "default_member_permissions"),
nsfw=data.get("nsfw"),
install_types=(
ApplicationInstallTypes._from_values(install_types)
if (install_types := data.get("integration_types")) is not None
else None
),
contexts=(
InteractionContextTypes._from_values(contexts)
if (contexts := data.get("contexts")) is not None
else None
),
handler=(
try_enum(ApplicationCommandHandlerType, handler)
if (handler := data.get("handler")) is not None
else None
),
)
self._update_common(data)
return self


class ApplicationCommandPermissions:
"""Represents application command permissions for a role, user, or channel.

Expand Down
56 changes: 38 additions & 18 deletions disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from . import abc, utils
from .activity import ActivityTypes, BaseActivity, create_activity
from .app_commands import (
APIEntryPointCommand,
APIMessageCommand,
APISlashCommand,
APIUserCommand,
Expand Down Expand Up @@ -76,7 +77,14 @@
from typing_extensions import NotRequired

from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import APIApplicationCommand, MessageCommand, SlashCommand, UserCommand
from .app_commands import (
APIApplicationCommand,
APIGuildApplicationCommand,
EntryPointCommand,
MessageCommand,
SlashCommand,
UserCommand,
)
from .asset import AssetBytes
from .channel import DMChannel
from .member import Member
Expand Down Expand Up @@ -609,7 +617,7 @@ def application_flags(self) -> ApplicationFlags:

@property
def global_application_commands(self) -> list[APIApplicationCommand]:
r""":class:`list`\[:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand`]: The client's global application commands."""
r""":class:`list`\[:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand`]: The client's global application commands."""
return list(self._connection._global_application_commands.values())

@property
Expand Down Expand Up @@ -1545,7 +1553,7 @@ def get_all_members(self) -> Generator[Member]:
for guild in self.guilds:
yield from guild.members

def get_guild_application_commands(self, guild_id: int) -> list[APIApplicationCommand]:
def get_guild_application_commands(self, guild_id: int) -> list[APIGuildApplicationCommand]:
r"""Returns a list of all application commands in the guild with the given ID.

Parameters
Expand Down Expand Up @@ -1619,12 +1627,12 @@ def get_global_command(self, id: int) -> APIApplicationCommand | None:

Returns
-------
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :data:`None`
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand` | :data:`None`
The application command.
"""
return self._connection._get_global_application_command(id)

def get_guild_command(self, guild_id: int, id: int) -> APIApplicationCommand | None:
def get_guild_command(self, guild_id: int, id: int) -> APIGuildApplicationCommand | None:
"""Returns a guild application command with the given guild ID and application command ID.

Parameters
Expand Down Expand Up @@ -1655,14 +1663,14 @@ def get_global_command_named(

Returns
-------
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :data:`None`
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand` | :data:`None`
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: ApplicationCommandType | None = None
) -> APIApplicationCommand | None:
) -> APIGuildApplicationCommand | None:
"""Returns a guild application command matching the given name.

Parameters
Expand Down Expand Up @@ -2708,7 +2716,7 @@ async def fetch_global_commands(

Returns
-------
:class:`list`\[:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand`]
:class:`list`\[:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand`]
A list of application commands.
"""
return await self._connection.fetch_global_commands(with_localizations=with_localizations)
Expand All @@ -2727,7 +2735,7 @@ async def fetch_global_command(self, command_id: int) -> APIApplicationCommand:

Returns
-------
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand`
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand`
The requested application command.
"""
return await self._connection.fetch_global_command(command_id)
Expand All @@ -2743,6 +2751,11 @@ async def create_global_command(
self, application_command: MessageCommand
) -> APIMessageCommand: ...

@overload
async def create_global_command(
self, application_command: EntryPointCommand
) -> APIEntryPointCommand: ...

@overload
async def create_global_command(
self, application_command: ApplicationCommand
Expand All @@ -2764,7 +2777,7 @@ async def create_global_command(

Returns
-------
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand`
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand`
The application command that was created.
"""
application_command.localize(self.i18n)
Expand All @@ -2785,6 +2798,11 @@ 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: EntryPointCommand
) -> APIEntryPointCommand: ...

@overload
async def edit_global_command(
self, command_id: int, new_command: ApplicationCommand
Expand All @@ -2808,7 +2826,7 @@ async def edit_global_command(

Returns
-------
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand`
:class:`.APIUserCommand` | :class:`.APIMessageCommand` | :class:`.APISlashCommand` | :class:`.APIEntryPointCommand`
The edited application command.
"""
new_command.localize(self.i18n)
Expand Down Expand Up @@ -2858,7 +2876,7 @@ async def fetch_guild_commands(
guild_id: int,
*,
with_localizations: bool = True,
) -> list[APIApplicationCommand]:
) -> list[APIGuildApplicationCommand]:
r"""|coro|

Retrieves a list of guild application commands.
Expand All @@ -2883,7 +2901,9 @@ async def fetch_guild_commands(
guild_id, with_localizations=with_localizations
)

async def fetch_guild_command(self, guild_id: int, command_id: int) -> APIApplicationCommand:
async def fetch_guild_command(
self, guild_id: int, command_id: int
) -> APIGuildApplicationCommand:
"""|coro|

Retrieves a guild application command.
Expand Down Expand Up @@ -2922,11 +2942,11 @@ async def create_guild_command(
@overload
async def create_guild_command(
self, guild_id: int, application_command: ApplicationCommand
) -> APIApplicationCommand: ...
) -> APIGuildApplicationCommand: ...

async def create_guild_command(
self, guild_id: int, application_command: ApplicationCommand
) -> APIApplicationCommand:
) -> APIGuildApplicationCommand:
"""|coro|

Creates a guild application command.
Expand Down Expand Up @@ -2966,11 +2986,11 @@ async def edit_guild_command(
@overload
async def edit_guild_command(
self, guild_id: int, command_id: int, new_command: ApplicationCommand
) -> APIApplicationCommand: ...
) -> APIGuildApplicationCommand: ...

async def edit_guild_command(
self, guild_id: int, command_id: int, new_command: ApplicationCommand
) -> APIApplicationCommand:
) -> APIGuildApplicationCommand:
"""|coro|

Edits a guild application command.
Expand Down Expand Up @@ -3012,7 +3032,7 @@ async def delete_guild_command(self, guild_id: int, command_id: int) -> None:

async def bulk_overwrite_guild_commands(
self, guild_id: int, application_commands: list[ApplicationCommand]
) -> list[APIApplicationCommand]:
) -> list[APIGuildApplicationCommand]:
r"""|coro|

Overwrites several guild application commands in one API request.
Expand Down
Loading
Loading