diff --git a/changes/1952.feature.md b/changes/1952.feature.md new file mode 100644 index 0000000000..f850e380e4 --- /dev/null +++ b/changes/1952.feature.md @@ -0,0 +1 @@ +Add support for user install commands diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 23cc255d34..007582b13a 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -6684,6 +6684,12 @@ async def create_slash_command( ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> commands.SlashCommand: r"""Create an application slash command. @@ -6719,6 +6725,10 @@ async def create_slash_command( This can only be applied to non-guild commands. nsfw Whether this command should be age-restricted. + integration_types + The integration types for this command. + contexts + The contexts for this command. Returns ------- @@ -6758,6 +6768,12 @@ async def create_context_menu_command( ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> commands.ContextMenuCommand: r"""Create an application context menu command. @@ -6788,6 +6804,10 @@ async def create_context_menu_command( This can only be applied to non-guild commands. nsfw Whether this command should be age-restricted. + integration_types + The integration types for this command. + contexts + The contexts for this command. Returns ------- @@ -6820,6 +6840,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. @@ -6872,6 +6896,12 @@ async def edit_application_command( undefined.UndefinedType, int, permissions_.Permissions ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> commands.PartialCommand: """Edit a registered application command. @@ -6903,6 +6933,10 @@ async def edit_application_command( Whether this command is enabled in DMs with the bot. This can only be applied to non-guild commands. + integration_types + The integration types for this command. + contexts + The contexts for this command. Returns ------- diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 1ae3b71e9d..2112267e47 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -59,6 +59,7 @@ from typing_extensions import Self + from hikari import applications from hikari import channels from hikari import colors from hikari import commands @@ -1124,6 +1125,38 @@ def set_name_localizations( Object of this command builder. """ + @abc.abstractmethod + def set_integration_types( + self, integration_types: typing.Sequence[applications.ApplicationIntegrationType], / + ) -> 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[applications.ApplicationInstallationContextType], /) -> 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. diff --git a/hikari/applications.py b/hikari/applications.py index 41ea209ce2..a2f40afaff 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -45,6 +45,8 @@ "ApplicationRoleConnectionMetadataRecordType", "ApplicationRoleConnectionMetadataRecord", "get_token_id", + "ApplicationIntegrationType", + "ApplicationInstallationContextType", ) import base64 @@ -71,6 +73,31 @@ from hikari import webhooks +@typing.final +class ApplicationIntegrationType(int, enums.Enum): + """The known integration types.""" + + GUILD_INSTALL = 0 + """A guild install command integration type.""" + + USER_INSTALL = 1 + """A user install command integration type.""" + + +@typing.final +class ApplicationInstallationContextType(int, enums.Enum): + """The known installation context types.""" + + 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.""" + + @typing.final class ApplicationFlags(enums.Flag): """The known application flag bits.""" @@ -632,6 +659,11 @@ class Application(guilds.PartialApplication): install_parameters: typing.Optional[ApplicationInstallParameters] = attrs.field(eq=False, hash=False, repr=False) """Settings for the application's default in-app authorization link, if enabled.""" + integration_types_config: typing.Mapping[ApplicationIntegrationType, ApplicationInstallParameters] = attrs.field( + eq=False, hash=False, repr=False + ) + """Default scopes and permissions for each supported installation context.""" + approximate_guild_count: int = attrs.field(eq=False, hash=False, repr=False) """The approximate number of guilds this application is part of.""" diff --git a/hikari/commands.py b/hikari/commands.py index a35fd23222..4dea5cdf29 100644 --- a/hikari/commands.py +++ b/hikari/commands.py @@ -48,6 +48,7 @@ from hikari.internal import enums if typing.TYPE_CHECKING: + from hikari import applications from hikari import channels from hikari import guilds from hikari import locales @@ -265,6 +266,16 @@ class PartialCommand(snowflakes.Unique): ) """A mapping of name localizations for this command.""" + integration_types: typing.Sequence[applications.ApplicationIntegrationType] = attrs.field( + eq=False, hash=False, repr=True + ) + """A sequence of command integration types.""" + + contexts: typing.Sequence[applications.ApplicationInstallationContextType] = attrs.field( + eq=False, hash=False, repr=True + ) + """A sequence of command contexts.""" + async def fetch_self(self) -> PartialCommand: """Fetch an up-to-date version of this command object. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 665a121839..25479d148e 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -603,6 +603,15 @@ def deserialize_own_application_role_connection( metadata=payload.get("metadata") or {}, ) + def _deserialize_install_parameters( + self, payload: data_binding.JSONObject + ) -> application_models.ApplicationInstallParameters: + + return application_models.ApplicationInstallParameters( + scopes=[application_models.OAuth2Scope(scope) for scope in payload["scopes"]], + permissions=permission_models.Permissions(payload["permissions"]), + ) + def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application: team: typing.Optional[application_models.Team] = None if (team_payload := payload.get("team")) is not None: @@ -627,10 +636,18 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati install_parameters: typing.Optional[application_models.ApplicationInstallParameters] = None if (install_payload := payload.get("install_params")) is not None: - install_parameters = application_models.ApplicationInstallParameters( - scopes=[application_models.OAuth2Scope(scope) for scope in install_payload["scopes"]], - permissions=permission_models.Permissions(install_payload["permissions"]), - ) + install_parameters = self._deserialize_install_parameters(install_payload) + + integration_types_config: typing.Mapping[ + application_models.ApplicationIntegrationType, application_models.ApplicationInstallParameters + ] = {} + if (integration_types_config_payload := payload.get("integration_types_config")) is not None: + integration_types_config = { + application_models.ApplicationIntegrationType(k): self._deserialize_install_parameters( + v["oauth2_install_params"] + ) + for k, v in integration_types_config_payload.items() + } return application_models.Application( app=self._app, @@ -652,6 +669,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati custom_install_url=payload.get("custom_install_url"), tags=payload.get("tags") or [], install_parameters=install_parameters, + integration_types_config=integration_types_config, approximate_guild_count=payload["approximate_guild_count"], ) @@ -2279,6 +2297,24 @@ def deserialize_slash_command( else: default_member_permissions = permission_models.Permissions(default_member_permissions or 0) + integration_types: typing.Sequence[application_models.ApplicationIntegrationType] + if raw_integration_types := payload.get("integration_types"): + integration_types = [ + application_models.ApplicationIntegrationType(integration_type) + for integration_type in raw_integration_types + ] + else: + integration_types = [application_models.ApplicationIntegrationType.GUILD_INSTALL] + + contexts: typing.Sequence[application_models.ApplicationInstallationContextType] + if raw_contexts := payload.get("contexts"): + contexts = [application_models.ApplicationInstallationContextType(context) for context in raw_contexts] + else: + contexts = [ + application_models.ApplicationInstallationContextType.GUILD, + application_models.ApplicationInstallationContextType.BOT_DM, + ] + return commands.SlashCommand( app=self._app, id=snowflakes.Snowflake(payload["id"]), @@ -2294,6 +2330,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( @@ -2320,6 +2358,21 @@ def deserialize_context_menu_command( else: default_member_permissions = permission_models.Permissions(default_member_permissions or 0) + integration_types: typing.Sequence[application_models.ApplicationIntegrationType] + if raw_integration_types := payload.get("integration_types"): + integration_types = [ + application_models.ApplicationIntegrationType(integration_type) + for integration_type in raw_integration_types + ] + else: + integration_types = [application_models.ApplicationIntegrationType.GUILD_INSTALL] + + contexts: typing.Sequence[application_models.ApplicationInstallationContextType] + if raw_contexts := payload.get("contexts"): + contexts = [application_models.ApplicationInstallationContextType(context) for context in raw_contexts] + else: + contexts = [application_models.ApplicationInstallationContextType.GUILD] + return commands.ContextMenuCommand( app=self._app, id=snowflakes.Snowflake(payload["id"]), @@ -2332,6 +2385,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( @@ -2371,6 +2426,16 @@ def serialize_command_permission(self, permission: commands.CommandPermission) - return {"id": str(permission.id), "type": permission.type, "permission": permission.has_access} def deserialize_partial_interaction(self, payload: data_binding.JSONObject) -> base_interactions.PartialInteraction: + authorizing_integration_owners: typing.Mapping[ + application_models.ApplicationIntegrationType, snowflakes.Snowflake + ] = {} + if (authorizing_integration_owners_payload := payload.get("authorizing_integration_owners")) is not None: + authorizing_integration_owners = { + application_models.ApplicationIntegrationType(k): snowflakes.Snowflake(v) + for k, v in authorizing_integration_owners_payload.items() + } + + context = payload.get("context") return base_interactions.PartialInteraction( app=self._app, id=snowflakes.Snowflake(payload["id"]), @@ -2378,6 +2443,8 @@ def deserialize_partial_interaction(self, payload: data_binding.JSONObject) -> b token=payload["token"], version=payload["version"], application_id=snowflakes.Snowflake(payload["application_id"]), + authorizing_integration_owners=authorizing_integration_owners, + context=application_models.ApplicationInstallationContextType(context) if context is not None else None, ) def _deserialize_interaction_command_option( @@ -2553,8 +2620,7 @@ def deserialize_command_interaction( if raw_target_id := data_payload.get("target_id"): target_id = snowflakes.Snowflake(raw_target_id) - app_perms = payload.get("app_permissions") - + app_permissions = payload.get("app_permissions") entitlements = [self.deserialize_entitlement(entitlement) for entitlement in payload.get("entitlements", ())] return command_interactions.CommandInteraction( @@ -2576,9 +2642,11 @@ def deserialize_command_interaction( options=options, resolved=resolved, target_id=target_id, - app_permissions=permission_models.Permissions(app_perms) if app_perms else None, + app_permissions=permission_models.Permissions(app_permissions) if app_permissions else None, registered_guild_id=snowflakes.Snowflake(data_payload["guild_id"]) if "guild_id" in data_payload else None, entitlements=entitlements, + authorizing_integration_owners=payload["authorizing_integration_owners"], + context=payload.get("context"), ) def deserialize_autocomplete_interaction( @@ -2622,6 +2690,8 @@ def deserialize_autocomplete_interaction( guild_locale=locales.Locale(payload["guild_locale"]) if "guild_locale" in payload else None, registered_guild_id=snowflakes.Snowflake(data_payload["guild_id"]) if "guild_id" in data_payload else None, entitlements=[self.deserialize_entitlement(entitlement) for entitlement in payload.get("entitlements", ())], + authorizing_integration_owners=payload["authorizing_integration_owners"], + context=payload.get("context"), ) def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> modal_interactions.ModalInteraction: @@ -2647,6 +2717,17 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod message = self.deserialize_message(message_payload) app_perms = payload.get("app_permissions") + + authorizing_integration_owners: typing.Mapping[ + application_models.ApplicationIntegrationType, snowflakes.Snowflake + ] = {} + if (authorizing_integration_owners_payload := payload.get("authorizing_integration_owners")) is not None: + authorizing_integration_owners = { + application_models.ApplicationIntegrationType(k): snowflakes.Snowflake(v) + for k, v in authorizing_integration_owners_payload.items() + } + + context = payload.get("context") return modal_interactions.ModalInteraction( app=self._app, application_id=snowflakes.Snowflake(payload["application_id"]), @@ -2665,6 +2746,8 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod components=self._deserialize_components(data_payload["components"], self._modal_component_type_mapping), message=message, entitlements=[self.deserialize_entitlement(entitlement) for entitlement in payload.get("entitlements", ())], + authorizing_integration_owners=authorizing_integration_owners, + context=application_models.ApplicationInstallationContextType(context) if context is not None else None, ) def deserialize_interaction(self, payload: data_binding.JSONObject) -> base_interactions.PartialInteraction: @@ -2737,7 +2820,17 @@ def deserialize_component_interaction( if resolved_payload := data_payload.get("resolved"): resolved = self._deserialize_resolved_option_data(resolved_payload, guild_id=guild_id) + authorizing_integration_owners: typing.Mapping[ + application_models.ApplicationIntegrationType, snowflakes.Snowflake + ] = {} + if (authorizing_integration_owners_payload := payload.get("authorizing_integration_owners")) is not None: + authorizing_integration_owners = { + application_models.ApplicationIntegrationType(k): snowflakes.Snowflake(v) + for k, v in authorizing_integration_owners_payload.items() + } + app_perms = payload.get("app_permissions") + context = payload.get("context") return component_interactions.ComponentInteraction( app=self._app, application_id=snowflakes.Snowflake(payload["application_id"]), @@ -2758,6 +2851,8 @@ def deserialize_component_interaction( guild_locale=locales.Locale(payload["guild_locale"]) if "guild_locale" in payload else None, app_permissions=permission_models.Permissions(app_perms) if app_perms else None, entitlements=[self.deserialize_entitlement(entitlement) for entitlement in payload.get("entitlements", ())], + authorizing_integration_owners=authorizing_integration_owners, + context=application_models.ApplicationInstallationContextType(context) if context is not None else None, ) ################## diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 9d3d4b5f1d..afb687c6c4 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -3760,6 +3760,12 @@ async def _create_application_command( ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> data_binding.JSONObject: if guild is undefined.UNDEFINED: route = routes.POST_APPLICATION_COMMAND.compile(application=application) @@ -3780,8 +3786,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 @@ -3804,6 +3813,12 @@ async def create_slash_command( ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> commands.SlashCommand: response = await self._create_application_command( application=application, @@ -3817,6 +3832,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 @@ -3837,6 +3854,12 @@ async def create_context_menu_command( ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> commands.ContextMenuCommand: response = await self._create_application_command( application=application, @@ -3847,6 +3870,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 @@ -3883,6 +3908,12 @@ async def edit_application_command( ] = undefined.UNDEFINED, dm_enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED, nsfw: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + integration_types: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationIntegrationType] + ] = undefined.UNDEFINED, + contexts: undefined.UndefinedOr[ + typing.Sequence[applications.ApplicationInstallationContextType] + ] = undefined.UNDEFINED, ) -> commands.PartialCommand: if guild is undefined.UNDEFINED: route = routes.PATCH_APPLICATION_COMMAND.compile(application=application, command=command) @@ -3901,6 +3932,8 @@ async def edit_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) diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index afdcf664ea..247e995371 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -68,6 +68,7 @@ from hikari.interactions import base_interactions from hikari.internal import attrs_extensions from hikari.internal import data_binding +from hikari.internal import deprecation from hikari.internal import mentions from hikari.internal import routes from hikari.internal import time @@ -1284,6 +1285,14 @@ class CommandBuilder(special_endpoints.CommandBuilder): alias="name_localizations", factory=dict, kw_only=True ) + _integration_types: typing.Sequence[applications.ApplicationIntegrationType] = attrs.field( + alias="integration_types", default=undefined.UNDEFINED, kw_only=True + ) + + _contexts: typing.Sequence[applications.ApplicationInstallationContextType] = attrs.field( + alias="contexts", default=undefined.UNDEFINED, kw_only=True + ) + @property def id(self) -> undefined.UndefinedOr[snowflakes.Snowflake]: return self._id @@ -1294,6 +1303,9 @@ def default_member_permissions(self) -> typing.Union[undefined.UndefinedType, pe @property def is_dm_enabled(self) -> undefined.UndefinedOr[bool]: + deprecation.warn_deprecated( + "is_dm_enabled", additional_info="use contexts instead", removal_version="2.0.0.dev129" + ) return self._is_dm_enabled @property @@ -1304,6 +1316,14 @@ def is_nsfw(self) -> undefined.UndefinedOr[bool]: def name(self) -> str: return self._name + @property + def integration_types(self) -> undefined.UndefinedOr[typing.Sequence[applications.ApplicationIntegrationType]]: + return self._integration_types + + @property + def contexts(self) -> undefined.UndefinedOr[typing.Sequence[applications.ApplicationInstallationContextType]]: + return self._contexts + def set_name(self, name: str, /) -> Self: self._name = name return self @@ -1336,6 +1356,16 @@ def set_name_localizations( self._name_localizations = name_localizations return self + def set_integration_types( + self, integration_types: typing.Sequence[applications.ApplicationIntegrationType], / + ) -> Self: + self._integration_types = integration_types + return self + + def set_contexts(self, contexts: typing.Sequence[applications.ApplicationInstallationContextType], /) -> 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 @@ -1344,6 +1374,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. @@ -1424,6 +1456,8 @@ async def create( default_member_permissions=self._default_member_permissions, dm_enabled=self._is_dm_enabled, nsfw=self._is_nsfw, + integration_types=self._integration_types, + contexts=self._contexts, ) @@ -1457,6 +1491,8 @@ async def create( default_member_permissions=self._default_member_permissions, dm_enabled=self._is_dm_enabled, nsfw=self.is_nsfw, + integration_types=self._integration_types, + contexts=self._contexts, ) diff --git a/hikari/interactions/base_interactions.py b/hikari/interactions/base_interactions.py index d9ad25e758..584f66eb9b 100644 --- a/hikari/interactions/base_interactions.py +++ b/hikari/interactions/base_interactions.py @@ -51,6 +51,7 @@ from hikari.internal import enums if typing.TYPE_CHECKING: + from hikari import applications from hikari import embeds as embeds_ from hikari import files from hikari import messages @@ -59,7 +60,6 @@ from hikari import users from hikari.api import special_endpoints - _CommandResponseTypesT = typing.TypeVar("_CommandResponseTypesT", bound=int) @@ -219,6 +219,14 @@ class PartialInteraction(snowflakes.Unique, webhooks.ExecutableWebhook): version: int = attrs.field(eq=False, repr=True) """Version of the interaction system this interaction is under.""" + authorizing_integration_owners: typing.Mapping[applications.ApplicationIntegrationType, snowflakes.Snowflake] = ( + attrs.field(eq=False, repr=False) + ) + """Mapping installation contexts authorized for interaction to related user or guild IDs.""" + + context: typing.Optional[applications.ApplicationInstallationContextType] = attrs.field(eq=False, repr=False) + """A context on where interaction was triggered.""" + @property def webhook_id(self) -> snowflakes.Snowflake: # <>. diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 6adcede333..04d83becca 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -4056,6 +4056,11 @@ 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 == [application_models.ApplicationIntegrationType.GUILD_INSTALL] + assert command.contexts == [ + application_models.ApplicationInstallationContextType.GUILD, + application_models.ApplicationInstallationContextType.BOT_DM, + ] # CommandOption assert len(command.options) == 1 @@ -4208,6 +4213,8 @@ def partial_interaction_payload(self): "type": 1, "version": 1, "application_id": "1", + "authorizing_integration_owners": {0: 12345}, + "context": 0, } def test_deserialize_partial_interaction(self, mock_app, entity_factory_impl, partial_interaction_payload): @@ -4219,6 +4226,10 @@ def test_deserialize_partial_interaction(self, mock_app, entity_factory_impl, pa assert interaction.type == 1 assert interaction.version == 1 assert interaction.application_id == 1 + assert interaction.authorizing_integration_owners == { + application_models.ApplicationIntegrationType.GUILD_INSTALL: 12345 + } + assert interaction.context == application_models.ApplicationInstallationContextType.GUILD assert type(interaction) is base_interactions.PartialInteraction @pytest.fixture @@ -4413,6 +4424,8 @@ def command_interaction_payload(self, interaction_member_payload, interaction_re "subscription_id": "1019653835926409216", } ], + "authorizing_integration_owners": {0: 12345}, + "context": 0, } def test_deserialize_command_interaction( @@ -4449,6 +4462,10 @@ def test_deserialize_command_interaction( assert len(interaction.entitlements) == 1 assert interaction.entitlements[0].id == 696969696969696 assert interaction.registered_guild_id == 12345678 + assert interaction.authorizing_integration_owners == { + application_models.ApplicationIntegrationType.GUILD_INSTALL: 12345 + } + assert interaction.context == 0 # CommandInteractionOption assert len(interaction.options) == 1 @@ -4512,6 +4529,8 @@ def context_menu_command_interaction_payload(self, interaction_member_payload, u "subscription_id": "1019653835926409216", } ], + "authorizing_integration_owners": {0: 12345}, + "context": 0, } def test_deserialize_command_interaction_with_context_menu_field( @@ -4588,6 +4607,8 @@ def autocomplete_interaction_payload(self, member_payload, user_payload, interac "subscription_id": "1019653835926409216", } ], + "authorizing_integration_owners": {0: 12345}, + "context": 0, } def test_deserialize_autocomplete_interaction( @@ -4615,6 +4636,10 @@ def test_deserialize_autocomplete_interaction( assert interaction.locale is locales.Locale.ES_ES assert interaction.guild_locale is locales.Locale.EN_US assert interaction.registered_guild_id == 12345678 + assert interaction.authorizing_integration_owners == { + application_models.ApplicationIntegrationType.GUILD_INSTALL: 12345 + } + assert interaction.context == 0 # AutocompleteInteractionOption assert len(interaction.options) == 1 @@ -4856,6 +4881,8 @@ def component_interaction_payload( "subscription_id": "1019653835926409216", } ], + "authorizing_integration_owners": {0: 12345}, + "context": 0, } def test_deserialize_component_interaction( @@ -4890,14 +4917,24 @@ def test_deserialize_component_interaction( assert interaction.guild_locale == "en-US" assert interaction.guild_locale is locales.Locale.EN_US assert interaction.app_permissions == 5431234 + assert interaction.authorizing_integration_owners == { + application_models.ApplicationIntegrationType.GUILD_INSTALL: 12345 + } + assert interaction.context == 0 + # ResolvedData assert interaction.resolved == entity_factory_impl._deserialize_resolved_option_data( interaction_resolved_data_payload, guild_id=290926798626357999 ) + assert isinstance(interaction, component_interactions.ComponentInteraction) assert len(interaction.entitlements) == 1 assert interaction.entitlements[0].id == 696969696969696 + assert interaction.authorizing_integration_owners == { + application_models.ApplicationIntegrationType.GUILD_INSTALL: 12345 + } + assert interaction.context == 0 def test_deserialize_component_interaction_with_undefined_fields( self, entity_factory_impl, user_payload, message_payload diff --git a/tests/hikari/impl/test_interaction_server.py b/tests/hikari/impl/test_interaction_server.py index 97fdc32e28..6c4cdcfe63 100644 --- a/tests/hikari/impl/test_interaction_server.py +++ b/tests/hikari/impl/test_interaction_server.py @@ -665,7 +665,14 @@ async def test_on_interaction( mock_file_1 = mock.Mock() mock_file_2 = mock.Mock() mock_entity_factory.deserialize_interaction.return_value = base_interactions.PartialInteraction( - app=None, id=123, application_id=541324, type=2, token="ok", version=1 + app=None, + id=123, + application_id=541324, + type=2, + token="ok", + version=1, + authorizing_integration_owners={0: 1}, + context=0, ) mock_builder = mock.Mock(build=mock.Mock(return_value=({"ok": "No boomer"}, [mock_file_1, mock_file_2]))) mock_listener = mock.AsyncMock(return_value=mock_builder) @@ -706,7 +713,14 @@ async def mock_generator_listener(event): mock_file_1 = mock.Mock() mock_file_2 = mock.Mock() mock_entity_factory.deserialize_interaction.return_value = base_interactions.PartialInteraction( - app=None, id=123, application_id=541324, type=2, token="ok", version=1 + app=None, + id=123, + application_id=541324, + type=2, + token="ok", + version=1, + authorizing_integration_owners={0: 1}, + context=0, ) mock_builder = mock.Mock(build=mock.Mock(return_value=({"ok": "No boomer"}, [mock_file_1, mock_file_2]))) g_called = False @@ -871,7 +885,14 @@ async def test_on_interaction_on_dispatch_error( mock_interaction_server._public_key = mock.Mock() mock_exception = TypeError("OK") mock_entity_factory.deserialize_interaction.return_value = base_interactions.PartialInteraction( - app=None, id=123, application_id=541324, type=2, token="ok", version=1 + app=None, + id=123, + application_id=541324, + type=2, + token="ok", + version=1, + authorizing_integration_owners={0: 1}, + context=0, ) mock_interaction_server.set_listener( base_interactions.PartialInteraction, mock.Mock(side_effect=mock_exception) @@ -900,7 +921,14 @@ async def test_on_interaction_when_response_builder_error( mock_interaction_server._public_key = mock.Mock() mock_exception = TypeError("OK") mock_entity_factory.deserialize_interaction.return_value = base_interactions.PartialInteraction( - app=None, id=123, application_id=541324, type=2, token="ok", version=1 + app=None, + id=123, + application_id=541324, + type=2, + token="ok", + version=1, + authorizing_integration_owners={0: 1}, + context=0, ) mock_builder = mock.Mock(build=mock.Mock(side_effect=mock_exception)) mock_interaction_server.set_listener( @@ -931,7 +959,14 @@ async def test_on_interaction_when_json_encode_fails( mock_exception = TypeError("OK") mock_interaction_server._dumps = mock.Mock(side_effect=mock_exception) mock_entity_factory.deserialize_interaction.return_value = base_interactions.PartialInteraction( - app=None, id=123, application_id=541324, type=2, token="ok", version=1 + app=None, + id=123, + application_id=541324, + type=2, + token="ok", + version=1, + authorizing_integration_owners={0: 1}, + context=0, ) mock_builder = mock.Mock(build=mock.Mock(return_value=({"ok": "No"}, []))) mock_interaction_server.set_listener( diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index a781f1f6ca..4856c833f8 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -5517,6 +5517,11 @@ 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=[applications.ApplicationIntegrationType.GUILD_INSTALL], + contexts=[ + applications.ApplicationInstallationContextType.GUILD, + applications.ApplicationInstallationContextType.BOT_DM, + ], ) assert result is rest_client._request.return_value @@ -5531,6 +5536,8 @@ async def test__create_application_command_with_optionals(self, rest_client: res "default_member_permissions": 8, "dm_permission": False, "nsfw": True, + "integration_types": [0], + "contexts": [0, 1], }, ) @@ -5584,6 +5591,11 @@ async def test_create_slash_command(self, rest_client: rest.RESTClientImpl): default_member_permissions=permissions.Permissions.ADMINISTRATOR, dm_enabled=False, nsfw=True, + integration_types=[applications.ApplicationIntegrationType.GUILD_INSTALL], + contexts=[ + applications.ApplicationInstallationContextType.GUILD, + applications.ApplicationInstallationContextType.BOT_DM, + ], ) assert result is rest_client._entity_factory.deserialize_slash_command.return_value @@ -5602,6 +5614,11 @@ async def test_create_slash_command(self, rest_client: rest.RESTClientImpl): default_member_permissions=permissions.Permissions.ADMINISTRATOR, dm_enabled=False, nsfw=True, + integration_types=[applications.ApplicationIntegrationType.GUILD_INSTALL], + contexts=[ + applications.ApplicationInstallationContextType.GUILD, + applications.ApplicationInstallationContextType.BOT_DM, + ], ) async def test_create_context_menu_command(self, rest_client: rest.RESTClientImpl): @@ -5618,6 +5635,11 @@ async def test_create_context_menu_command(self, rest_client: rest.RESTClientImp dm_enabled=False, nsfw=True, name_localizations={locales.Locale.TR: "hhh"}, + integration_types=[applications.ApplicationIntegrationType.GUILD_INSTALL], + contexts=[ + applications.ApplicationInstallationContextType.GUILD, + applications.ApplicationInstallationContextType.BOT_DM, + ], ) assert result is rest_client._entity_factory.deserialize_context_menu_command.return_value @@ -5633,6 +5655,11 @@ async def test_create_context_menu_command(self, rest_client: rest.RESTClientImp dm_enabled=False, nsfw=True, name_localizations={"tr": "hhh"}, + integration_types=[applications.ApplicationIntegrationType.GUILD_INSTALL], + contexts=[ + applications.ApplicationInstallationContextType.GUILD, + applications.ApplicationInstallationContextType.BOT_DM, + ], ) async def test_set_application_commands_with_guild(self, rest_client): @@ -5698,6 +5725,7 @@ async def test_edit_application_command_with_optionals(self, rest_client): options=[mock_option], default_member_permissions=permissions.Permissions.BAN_MEMBERS, dm_enabled=True, + contexts=[applications.ApplicationInstallationContextType.GUILD], ) assert result is rest_client._entity_factory.deserialize_command.return_value @@ -5712,6 +5740,7 @@ async def test_edit_application_command_with_optionals(self, rest_client): "options": [rest_client._entity_factory.serialize_command_option.return_value], "default_member_permissions": 4, "dm_permission": True, + "contexts": [0], }, ) rest_client._entity_factory.serialize_command_option.assert_called_once_with(mock_option) diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py index 6274f2d5f3..5589d72d7e 100644 --- a/tests/hikari/impl/test_special_endpoints.py +++ b/tests/hikari/impl/test_special_endpoints.py @@ -25,6 +25,7 @@ import mock import pytest +from hikari import applications from hikari import channels from hikari import commands from hikari import components @@ -1037,6 +1038,18 @@ 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( + [applications.ApplicationIntegrationType.GUILD_INSTALL] + ) + + assert builder.integration_types == [applications.ApplicationIntegrationType.GUILD_INSTALL] + + def test_set_contexts(self, stub_command): + builder = stub_command("oksksksk").set_contexts([applications.ApplicationInstallationContextType.BOT_DM]) + + assert builder.contexts == [applications.ApplicationInstallationContextType.BOT_DM] + class TestSlashCommandBuilder: def test_description_property(self): @@ -1112,6 +1125,13 @@ async def test_create(self): .set_default_member_permissions(permissions.Permissions.BAN_MEMBERS) .set_is_dm_enabled(True) .set_is_nsfw(True) + .set_integration_types( + [ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ] + ) + .set_contexts([applications.ApplicationInstallationContextType.GUILD]) ) mock_rest = mock.AsyncMock() @@ -1129,6 +1149,11 @@ async def test_create(self): default_member_permissions=permissions.Permissions.BAN_MEMBERS, dm_enabled=True, nsfw=True, + integration_types=[ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ], + contexts=[applications.ApplicationInstallationContextType.GUILD], ) @pytest.mark.asyncio @@ -1138,6 +1163,13 @@ async def test_create_with_guild(self): .set_default_member_permissions(permissions.Permissions.BAN_MEMBERS) .set_is_dm_enabled(True) .set_is_nsfw(True) + .set_integration_types( + [ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ] + ) + .set_contexts([applications.ApplicationInstallationContextType.GUILD]) ) mock_rest = mock.AsyncMock() @@ -1158,6 +1190,11 @@ async def test_create_with_guild(self): default_member_permissions=permissions.Permissions.BAN_MEMBERS, dm_enabled=True, nsfw=True, + integration_types=[ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ], + contexts=[applications.ApplicationInstallationContextType.GUILD], ) @@ -1199,6 +1236,13 @@ async def test_create(self): .set_name_localizations({"meow": "nyan"}) .set_is_dm_enabled(True) .set_is_nsfw(True) + .set_integration_types( + [ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ] + ) + .set_contexts([applications.ApplicationInstallationContextType.GUILD]) ) mock_rest = mock.AsyncMock() @@ -1214,6 +1258,8 @@ async def test_create(self): name_localizations={"meow": "nyan"}, dm_enabled=True, nsfw=True, + integration_types=[0, 1], + contexts=[0], ) @pytest.mark.asyncio @@ -1224,6 +1270,13 @@ async def test_create_with_guild(self): .set_name_localizations({"en-ghibli": "meow"}) .set_is_dm_enabled(True) .set_is_nsfw(True) + .set_integration_types( + [ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ] + ) + .set_contexts([applications.ApplicationInstallationContextType.GUILD]) ) mock_rest = mock.AsyncMock() @@ -1239,6 +1292,11 @@ async def test_create_with_guild(self): name_localizations={"en-ghibli": "meow"}, dm_enabled=True, nsfw=True, + integration_types=[ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ], + contexts=[applications.ApplicationInstallationContextType.GUILD], ) diff --git a/tests/hikari/interactions/test_base_interactions.py b/tests/hikari/interactions/test_base_interactions.py index ae2213550f..7d801f1caf 100644 --- a/tests/hikari/interactions/test_base_interactions.py +++ b/tests/hikari/interactions/test_base_interactions.py @@ -23,6 +23,7 @@ import mock import pytest +from hikari import applications from hikari import traits from hikari import undefined from hikari.interactions import base_interactions @@ -43,6 +44,8 @@ def mock_partial_interaction(self, mock_app): type=base_interactions.InteractionType.APPLICATION_COMMAND, token="399393939doodsodso", version=3122312, + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) def test_webhook_id_property(self, mock_partial_interaction): @@ -59,6 +62,8 @@ def mock_message_response_mixin(self, mock_app): type=base_interactions.InteractionType.APPLICATION_COMMAND, token="399393939doodsodso", version=3122312, + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) @pytest.mark.asyncio @@ -208,6 +213,8 @@ def mock_modal_response_mixin(self, mock_app): type=base_interactions.InteractionType.APPLICATION_COMMAND, token="399393939doodsodso", version=3122312, + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) @pytest.mark.asyncio diff --git a/tests/hikari/interactions/test_command_interactions.py b/tests/hikari/interactions/test_command_interactions.py index 874ca33491..67f536dcb2 100644 --- a/tests/hikari/interactions/test_command_interactions.py +++ b/tests/hikari/interactions/test_command_interactions.py @@ -22,6 +22,7 @@ import mock import pytest +from hikari import applications from hikari import channels from hikari import monetization from hikari import snowflakes @@ -73,6 +74,8 @@ def mock_command_interaction(self, mock_app): subscription_id=None, ) ], + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) def test_build_response(self, mock_command_interaction, mock_app): @@ -151,6 +154,8 @@ def mock_autocomplete_interaction(self, mock_app): subscription_id=None, ) ], + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) @pytest.fixture diff --git a/tests/hikari/interactions/test_component_interactions.py b/tests/hikari/interactions/test_component_interactions.py index 345734c795..a0f121ea95 100644 --- a/tests/hikari/interactions/test_component_interactions.py +++ b/tests/hikari/interactions/test_component_interactions.py @@ -22,6 +22,7 @@ import mock import pytest +from hikari import applications from hikari import channels from hikari import monetization from hikari import snowflakes @@ -71,6 +72,8 @@ def mock_component_interaction(self, mock_app): subscription_id=None, ) ], + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) def test_build_response(self, mock_component_interaction, mock_app): diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py index 506df033df..3175839fc7 100644 --- a/tests/hikari/interactions/test_modal_interactions.py +++ b/tests/hikari/interactions/test_modal_interactions.py @@ -22,6 +22,7 @@ import mock import pytest +from hikari import applications from hikari import channels from hikari import components from hikari import monetization @@ -77,6 +78,8 @@ def mock_modal_interaction(self, mock_app): subscription_id=None, ) ], + authorizing_integration_owners={applications.ApplicationIntegrationType.GUILD_INSTALL: 12345}, + context=applications.ApplicationInstallationContextType.GUILD, ) def test_build_response(self, mock_modal_interaction, mock_app): diff --git a/tests/hikari/test_commands.py b/tests/hikari/test_commands.py index e22f95ee05..ee1be72c68 100644 --- a/tests/hikari/test_commands.py +++ b/tests/hikari/test_commands.py @@ -22,6 +22,7 @@ import mock import pytest +from hikari import applications from hikari import commands from hikari import snowflakes from hikari import traits @@ -49,6 +50,15 @@ def mock_command(self, mock_app): guild_id=snowflakes.Snowflake(31231235), version=snowflakes.Snowflake(43123123), name_localizations={}, + integration_types=[ + applications.ApplicationIntegrationType.GUILD_INSTALL, + applications.ApplicationIntegrationType.USER_INSTALL, + ], + contexts=[ + applications.ApplicationInstallationContextType.GUILD, + applications.ApplicationInstallationContextType.BOT_DM, + applications.ApplicationInstallationContextType.PRIVATE_CHANNEL, + ], ) @pytest.mark.asyncio