Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement user installations #1952

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions changes/1952.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for user install commands
8 changes: 8 additions & 0 deletions hikari/api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6684,6 +6684,8 @@ async def create_slash_command(
] = undefined.UNDEFINED,
dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
integration_types: typing.Sequence[commands.CommandIntegrationType] = undefined.UNDEFINED,
contexts: typing.Sequence[commands.CommandInteractionContextType]
) -> commands.SlashCommand:
r"""Create an application slash command.

Expand Down Expand Up @@ -6758,6 +6760,8 @@ async def create_context_menu_command(
] = undefined.UNDEFINED,
dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
integration_types: typing.Sequence[commands.CommandIntegrationType] = undefined.UNDEFINED,
contexts: typing.Sequence[commands.CommandInteractionContextType]
) -> commands.ContextMenuCommand:
r"""Create an application context menu command.

Expand Down Expand Up @@ -6820,6 +6824,10 @@ async def set_application_commands(
) -> typing.Sequence[commands.PartialCommand]:
"""Set the commands for an application.

!!! note
When creating user commands, make sure to not pass the `guild` argument.
There is no feedback from Discord when this happens and commands will not be created properly

!!! warning
Any existing commands not included in the provided commands array
will be deleted.
Expand Down
34 changes: 34 additions & 0 deletions hikari/api/special_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,40 @@ def set_name_localizations(
Object of this command builder.
"""

@abc.abstractmethod
def set_integration_types(
self, integration_types: typing.Sequence[commands.CommandIntegrationType], /
) -> Self:
"""Set the command integration types

Parameters
----------
integration_types
Integration types that show where command would be shown up

Returns
-------
CommandBuilder
Object of this command builder for chained calls.
"""

@abc.abstractmethod
def set_contexts(
self, contexts: typing.Sequence[commands.CommandInteractionContextType], /
) -> Self:
"""Set the command contexts

Parameters
----------
contexts
Where command can be used

Returns
-------
CommandBuilder
Object of this command builder for chained calls.
"""

@abc.abstractmethod
def build(self, entity_factory: entity_factory_.EntityFactory, /) -> typing.MutableMapping[str, typing.Any]:
"""Build a JSON object from this builder.
Expand Down
29 changes: 29 additions & 0 deletions hikari/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"CommandType",
"GuildCommandPermissions",
"OptionType",
"CommandIntegrationType",
"CommandInteractionContextType"
)

import typing
Expand Down Expand Up @@ -110,6 +112,27 @@ class OptionType(int, enums.Enum):
"""Denotes a command option where the value will be an attachment."""


@typing.final
class CommandIntegrationType(int, enums.Enum):
GUILD_INSTALL = 0
"""A guild install command integration type"""

USER_INSTALL = 1
"""A user install command integration type"""


@typing.final
class CommandInteractionContextType(int, enums.Enum):
GUILD = 0
"""Interaction can be used within server"""

BOT_DM = 1
"""Interaction can be used within DM's"""

PRIVATE_CHANNEL = 2
"""Interaction can be used within group DM's and DM's"""


@attrs_extensions.with_copy
@attrs.define(hash=False, kw_only=True, weakref_slot=False)
class CommandChoice:
Expand Down Expand Up @@ -265,6 +288,12 @@ class PartialCommand(snowflakes.Unique):
)
"""A mapping of name localizations for this command."""

integration_types: typing.Sequence[CommandIntegrationType] = attrs.field(eq=False, hash=False, repr=True)
"""A sequence of command integration types"""

contexts: typing.Sequence[CommandInteractionContextType] = attrs.field(eq=False, hash=False, repr=True)
"""A sequence of command contexts"""
MagM1go marked this conversation as resolved.
Show resolved Hide resolved

async def fetch_self(self) -> PartialCommand:
"""Fetch an up-to-date version of this command object.

Expand Down
32 changes: 32 additions & 0 deletions hikari/impl/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2279,6 +2279,21 @@ def deserialize_slash_command(
else:
default_member_permissions = permission_models.Permissions(default_member_permissions or 0)

integration_types: typing.Sequence[commands.CommandIntegrationType]
if raw_integration_types := payload.get("integration_types"):
integration_types = [commands.CommandIntegrationType(integration_type) for integration_type in raw_integration_types]
else:
integration_types = [commands.CommandIntegrationType.GUILD_INSTALL]

contexts: typing.Sequence[commands.CommandInteractionContextType]
if raw_contexts := payload.get("contexts"):
contexts = [commands.CommandInteractionContextType(context) for context in raw_contexts]
else:
contexts = [
commands.CommandInteractionContextType.GUILD,
commands.CommandInteractionContextType.BOT_DM
]

return commands.SlashCommand(
app=self._app,
id=snowflakes.Snowflake(payload["id"]),
Expand All @@ -2294,6 +2309,8 @@ def deserialize_slash_command(
version=snowflakes.Snowflake(payload["version"]),
name_localizations=name_localizations,
description_localizations=description_localizations,
integration_types=integration_types,
contexts=contexts
)

def deserialize_context_menu_command(
Expand All @@ -2320,6 +2337,19 @@ def deserialize_context_menu_command(
else:
default_member_permissions = permission_models.Permissions(default_member_permissions or 0)

integration_types: typing.Sequence[commands.CommandIntegrationType]
if raw_integration_types := payload.get("integration_types"):
integration_types = [commands.CommandIntegrationType(integration_type) for integration_type in
raw_integration_types]
else:
integration_types = [commands.CommandIntegrationType.GUILD_INSTALL]

contexts: typing.Sequence[commands.CommandInteractionContextType]
if raw_contexts := payload.get("contexts"):
contexts = [commands.CommandInteractionContextType(context) for context in raw_contexts]
else:
contexts = [commands.CommandInteractionContextType.GUILD]

return commands.ContextMenuCommand(
app=self._app,
id=snowflakes.Snowflake(payload["id"]),
Expand All @@ -2332,6 +2362,8 @@ def deserialize_context_menu_command(
guild_id=guild_id,
version=snowflakes.Snowflake(payload["version"]),
name_localizations=name_localizations,
integration_types=integration_types,
contexts=contexts
)

def deserialize_command(
Expand Down
13 changes: 13 additions & 0 deletions hikari/impl/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3760,6 +3760,8 @@ async def _create_application_command(
] = undefined.UNDEFINED,
dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
integration_types: typing.Sequence[commands.CommandIntegrationType] = undefined.UNDEFINED,
contexts: typing.Sequence[commands.CommandInteractionContextType] = undefined.UNDEFINED
) -> data_binding.JSONObject:
if guild is undefined.UNDEFINED:
route = routes.POST_APPLICATION_COMMAND.compile(application=application)
Expand All @@ -3780,8 +3782,11 @@ async def _create_application_command(
# but we consider it to be the same as None for developer sanity reasons
body.put("default_member_permissions", None if default_member_permissions == 0 else default_member_permissions)
body.put("dm_permission", dm_enabled)
body.put("integration_types", integration_types)
body.put("contexts", contexts)

response = await self._request(route, json=body)

assert isinstance(response, dict)
return response

Expand All @@ -3804,6 +3809,8 @@ async def create_slash_command(
] = undefined.UNDEFINED,
dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
integration_types: typing.Sequence[commands.CommandIntegrationType] = undefined.UNDEFINED,
contexts: typing.Sequence[commands.CommandInteractionContextType]
) -> commands.SlashCommand:
response = await self._create_application_command(
application=application,
Expand All @@ -3817,6 +3824,8 @@ async def create_slash_command(
default_member_permissions=default_member_permissions,
dm_enabled=dm_enabled,
nsfw=nsfw,
integration_types=integration_types,
contexts=contexts
)
return self._entity_factory.deserialize_slash_command(
response, guild_id=snowflakes.Snowflake(guild) if guild is not undefined.UNDEFINED else None
Expand All @@ -3837,6 +3846,8 @@ async def create_context_menu_command(
] = undefined.UNDEFINED,
dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
integration_types: typing.Sequence[commands.CommandIntegrationType] = undefined.UNDEFINED,
contexts: typing.Sequence[commands.CommandInteractionContextType]
) -> commands.ContextMenuCommand:
response = await self._create_application_command(
application=application,
Expand All @@ -3847,6 +3858,8 @@ async def create_context_menu_command(
default_member_permissions=default_member_permissions,
dm_enabled=dm_enabled,
nsfw=nsfw,
integration_types=integration_types,
contexts=contexts
)
return self._entity_factory.deserialize_context_menu_command(
response, guild_id=snowflakes.Snowflake(guild) if guild is not undefined.UNDEFINED else None
Expand Down
22 changes: 22 additions & 0 deletions hikari/impl/special_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,14 @@ class CommandBuilder(special_endpoints.CommandBuilder):
alias="name_localizations", factory=dict, kw_only=True
)

_integration_types: typing.Sequence[commands.CommandIntegrationType] = attrs.field(
alias="integration_types", default=undefined.UNDEFINED, kw_only=True
)

_contexts: typing.Sequence[commands.CommandInteractionContextType] = attrs.field(
alias="contexts", default=undefined.UNDEFINED, kw_only=True
)

@property
def id(self) -> undefined.UndefinedOr[snowflakes.Snowflake]:
return self._id
Expand Down Expand Up @@ -1336,6 +1344,18 @@ def set_name_localizations(
self._name_localizations = name_localizations
return self

def set_integration_types(
self, integration_types: typing.Sequence[commands.CommandIntegrationType], /
) -> Self:
self._integration_types = integration_types
return self

def set_contexts(
self, contexts: typing.Sequence[commands.CommandInteractionContextType], /
) -> Self:
self._contexts = contexts
return self

def build(self, _: entity_factory_.EntityFactory, /) -> typing.MutableMapping[str, typing.Any]:
data = data_binding.JSONObjectBuilder()
data["name"] = self._name
Expand All @@ -1344,6 +1364,8 @@ def build(self, _: entity_factory_.EntityFactory, /) -> typing.MutableMapping[st
data.put("name_localizations", self._name_localizations)
data.put("dm_permission", self._is_dm_enabled)
data.put("nsfw", self._is_nsfw)
data.put_array("integration_types", self._integration_types)
data.put_array("contexts", self._contexts)

# Discord considers 0 the same thing as ADMINISTRATORS, but we make it nicer to work with
# by using it correctly.
Expand Down
2 changes: 2 additions & 0 deletions tests/hikari/impl/test_entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4056,6 +4056,8 @@ def test_deserialize_slash_command(self, entity_factory_impl, mock_app, slash_co
assert command.is_dm_enabled is False
assert command.is_nsfw is True
assert command.version == 123321123
assert command.integration_types == [commands.CommandIntegrationType.GUILD_INSTALL]
assert command.contexts == [commands.CommandInteractionContextType.GUILD, commands.CommandInteractionContextType.BOT_DM]

# CommandOption
assert len(command.options) == 1
Expand Down
2 changes: 2 additions & 0 deletions tests/hikari/impl/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5517,6 +5517,8 @@ async def test__create_application_command_with_optionals(self, rest_client: res
default_member_permissions=permissions.Permissions.ADMINISTRATOR,
dm_enabled=False,
nsfw=True,
integration_types=[commands.CommandIntegrationType.GUILD_INSTALL],
contexts=[commands.CommandInteractionContextType.GUILD, commands.CommandInteractionContextType.BOT_DM]
)

assert result is rest_client._request.return_value
Expand Down
10 changes: 10 additions & 0 deletions tests/hikari/impl/test_special_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,16 @@ def test_name_localizations_property(self, stub_command):

assert builder.name_localizations == {"aaa": "bbb", "ccc": "DDd"}

def test_set_integration_types(self, stub_command):
builder = stub_command("oksksksk").set_integration_types([commands.CommandIntegrationType.GUILD_INSTALL])

assert builder.integration_types == [commands.CommandIntegrationType.GUILD_INSTALL]

def test_set_contexts(self, stub_contexts):
builder = stub_command("oksksksk").set_contexts([commands.CommandInteractionContextType.BOT_DM])

assert builder.integration_types == [commands.CommandInteractionContextType.BOT_DM]


class TestSlashCommandBuilder:
def test_description_property(self):
Expand Down
9 changes: 9 additions & 0 deletions tests/hikari/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ def mock_command(self, mock_app):
guild_id=snowflakes.Snowflake(31231235),
version=snowflakes.Snowflake(43123123),
name_localizations={},
integration_types=[
commands.CommandIntegrationType.GUILD_INSTALL,
commands.CommandIntegrationType.USER_INSTALL
],
contexts=[
commands.CommandInteractionContextType.GUILD,
commands.CommandInteractionContextType.BOT_DM,
commands.CommandInteractionContextType.PRIVATE_CHANNEL
]
)

@pytest.mark.asyncio
Expand Down