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

feat: add support for user-installable apps #1647

Open
wants to merge 25 commits into
base: unstable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c9956bb
[feat] Initial UserApps support
zevaryx Mar 18, 2024
5664e5a
fix: use factory instead of default
AstreaTSS Mar 19, 2024
b57bf6a
fix: properly process and sync contexts and integration_types
AstreaTSS Mar 19, 2024
02da624
fix: correct typehints of new fields
AstreaTSS Mar 19, 2024
04489db
feat: add integration_types and contexts to decorators
AstreaTSS Mar 19, 2024
57cb4dd
fix: might as well
AstreaTSS Mar 19, 2024
895beb7
feat: add context and authorizing_integration_owners fields to context
AstreaTSS Mar 19, 2024
eab4f53
feat: add interaction_metadata to Message
AstreaTSS Mar 19, 2024
0510563
fix: presumably would be an issue here
AstreaTSS Mar 19, 2024
efa9514
fix: export MessageInteractionMetadata
AstreaTSS Mar 19, 2024
ff9a418
feat: lazly add integration_types_config
AstreaTSS Mar 19, 2024
c7bf125
feat: hybrid ctx parity
AstreaTSS Mar 19, 2024
41955f1
docs: add comments for hybrid ctx channel types
AstreaTSS Mar 19, 2024
5049292
fix: add new enums to __all__
AstreaTSS Mar 19, 2024
ebdcb71
ci: make pre-commit not complain
AstreaTSS Mar 19, 2024
2041c90
fix: properly handle integration_types and contexts for subcommands
AstreaTSS Mar 20, 2024
db5739b
feat: add contexts and integration_types decorators
AstreaTSS Mar 30, 2024
1a0b45c
docs: add basic docs for contexts/integration_types
AstreaTSS Mar 30, 2024
9dd846e
fix: do not compare contexts when guild cmd
AstreaTSS Mar 30, 2024
6a218c9
refactor: use less hacky logic for dm_permission
AstreaTSS Mar 30, 2024
0f264e0
fix: make hybrid cmd use prefixed channel when possible
AstreaTSS Mar 30, 2024
228f613
feat: add integration_types/contexts parity for hybrid cmds
AstreaTSS Mar 30, 2024
d6cfc18
refactor: just use already-determined context
AstreaTSS Mar 30, 2024
923a53d
fix: parse and use new user field for MessageInteractionMetadata
AstreaTSS Apr 23, 2024
40ddf62
fix: add custom context logic if context is empty
AstreaTSS May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 61 additions & 10 deletions docs/src/Guides/03 Creating Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,18 +423,69 @@ There are two ways to define permissions.

Multiple permissions are defined with the bitwise OR operator `|`.

### Blocking Commands in DMs
## Usable Contexts

You can also block commands in DMs. To do that, just set `dm_permission` to false.
You can control where slash commands (and other application commands) can be used using - in guilds, in DMs, and/or other private channels. By default, commands can be used in all contexts.

```py
@slash_command(
name="my_guild_only_command",
dm_permission=False,
)
async def my_command_function(ctx: SlashContext):
...
```
As with permissions, there are two ways to define the context.

=== ":one: Decorators"

```python
from interactions import contexts

@slash_command(name="my_guild_only_command")
@contexts(guild=True, bot_dm=False, private_channel=False)
async def my_command_function(ctx: SlashContext):
...
```

=== ":two: Function Definition"

```python
from interactions import ContextType

@slash_command(
name="my_command",
contexts=[ContextType.GUILD],
)
async def my_command_function(ctx: SlashContext):
...
```

## Integration Types

Applications can be installed/integrated in different ways:
- The one you are familiar with is the *guild* integration, where the application is installed in a specific guild, and so the entire guild can use the application.
- You can also install the application to a *user*, where the application can then be used by the user anywhere they desire.

By default, commands can only be used in guild integrations. Like many other properties, this can be changed.

There are two ways to define this:

=== ":one: Decorators"

```python
from interactions import integration_types

@slash_command(name="my_command")
@integration_types(guild=True, user=True)
async def my_command_function(ctx: SlashContext):
...
```

=== ":two: Function Definition"

```python
from interactions import IntegrationType

@slash_command(
name="my_command",
integration_types=[IntegrationType.GUILD_INSTALL, IntegrationType.USER_INSTALL],
)
async def my_command_function(ctx: SlashContext):
...
```

## Checks

Expand Down
10 changes: 10 additions & 0 deletions interactions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,11 @@
ComponentContext,
ComponentType,
ConsumeRest,
contexts,
context_menu,
ContextMenu,
ContextMenuContext,
ContextType,
Converter,
cooldown,
Cooldown,
Expand Down Expand Up @@ -178,6 +180,8 @@
IDConverter,
InputText,
IntegrationExpireBehaviour,
IntegrationType,
integration_types,
Intents,
InteractionCommand,
InteractionContext,
Expand Down Expand Up @@ -213,6 +217,7 @@
MessageConverter,
MessageFlags,
MessageInteraction,
MessageInteractionMetadata,
MessageReference,
MessageType,
MFALevel,
Expand Down Expand Up @@ -425,10 +430,12 @@
"ComponentType",
"ConsumeRest",
"const",
"contexts",
"context_menu",
"CONTEXT_MENU_NAME_LENGTH",
"ContextMenu",
"ContextMenuContext",
"ContextType",
"Converter",
"cooldown",
"Cooldown",
Expand Down Expand Up @@ -517,6 +524,8 @@
"IDConverter",
"InputText",
"IntegrationExpireBehaviour",
"IntegrationType",
"integration_types",
"Intents",
"InteractionCommand",
"InteractionContext",
Expand Down Expand Up @@ -556,6 +565,7 @@
"MessageConverter",
"MessageFlags",
"MessageInteraction",
"MessageInteractionMetadata",
"MessageReference",
"MessageType",
"MFALevel",
Expand Down
35 changes: 34 additions & 1 deletion interactions/ext/hybrid_commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
to_snowflake,
Attachment,
process_message_payload,
TYPE_MESSAGEABLE_CHANNEL,
)
from interactions.models.discord.enums import ContextType
from interactions.client.mixins.send import SendMixin
from interactions.client.errors import HTTPException
from interactions.ext import prefixed_commands as prefixed
Expand Down Expand Up @@ -60,6 +62,9 @@ class HybridContext(BaseContext, SendMixin):
ephemeral: bool
"""Whether the context response is ephemeral."""

context: Optional[ContextType]
"""Context where the command was triggered from"""

_command_name: str
"""The command name."""
_message: Message | None
Expand All @@ -81,6 +86,7 @@ def __init__(self, client: Client):
self.deferred = False
self.responded = False
self.ephemeral = False
self.context = None
self._command_name = ""
self.args = []
self.kwargs = {}
Expand All @@ -106,6 +112,7 @@ def from_slash_context(cls, ctx: SlashContext) -> Self:
self.deferred = ctx.deferred
self.responded = ctx.responded
self.ephemeral = ctx.ephemeral
self.context = ctx.context
self._command_name = ctx._command_name
self.args = ctx.args
self.kwargs = ctx.kwargs
Expand All @@ -121,9 +128,27 @@ def from_prefixed_context(cls, ctx: prefixed.PrefixedContext) -> Self:
elif ctx.channel.type in {10, 11, 12}: # it's a thread
app_permissions = ctx.channel.parent_channel.permissions_for(ctx.guild.me) # type: ignore
else:
app_permissions = Permissions(0)
# likely a dm, give a sane default
app_permissions = (
Permissions.VIEW_CHANNEL
| Permissions.SEND_MESSAGES
| Permissions.READ_MESSAGE_HISTORY
| Permissions.EMBED_LINKS
| Permissions.ATTACH_FILES
| Permissions.MENTION_EVERYONE
| Permissions.USE_EXTERNAL_EMOJIS
)

self = cls(ctx.client)

if ctx.channel.type == 1: # dm
# note that prefixed cmds for dms cannot be used outside of bot dms
self.context = ContextType.BOT_DM
elif ctx.channel.type == 3: # group dm - technically not possible but just in case
self.context = ContextType.PRIVATE_CHANNEL
else:
self.context = ContextType.GUILD

self.guild_id = ctx.guild_id
self.channel_id = ctx.channel_id
self.author_id = ctx.author_id
Expand Down Expand Up @@ -165,6 +190,14 @@ def deferred_ephemeral(self) -> bool:
"""Whether the interaction has been deferred ephemerally."""
return self.deferred and self.ephemeral

@property
def channel(self) -> "TYPE_MESSAGEABLE_CHANNEL":
"""The channel this context was invoked in."""
if self._prefixed_ctx:
return self._prefixed_ctx.channel

return self._slash_ctx.channel

@property
def message(self) -> Message | None:
"""The message that invoked this context."""
Expand Down
42 changes: 35 additions & 7 deletions interactions/ext/hybrid_commands/hybrid_slash.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@
SlashCommandOption,
Snowflake_Type,
Permissions,
ContextType,
IntegrationType,
)
from interactions.client.const import AsyncCallable, GLOBAL_SCOPE
from interactions.client.utils.serializer import no_export_meta
from interactions.client.utils.misc_utils import maybe_coroutine, get_object_name
from interactions.client.errors import BadArgument
from interactions.ext.prefixed_commands import PrefixedCommand, PrefixedContext
from interactions.models.internal.converters import _LiteralConverter, CONSUME_REST_MARKER
from interactions.models.internal.checks import guild_only

if TYPE_CHECKING:
from .context import HybridContext
Expand All @@ -45,6 +46,22 @@ def _values_wrapper(a_dict: dict | None) -> list:
return list(a_dict.values()) if a_dict else []


def generate_contexts_check(contexts: list[ContextType | int]) -> Callable[["HybridContext"], Awaitable[bool]]:
set_contexts = frozenset(contexts)

async def _contexts_check(ctx: "HybridContext") -> bool:
if ctx.context:
return ctx.context in set_contexts

if ctx.guild_id:
return ContextType.GUILD in set_contexts
if ctx.channel.type == 1 and ctx.channel.recipient.id == ctx.bot.user.id:
return ContextType.BOT_DM in set_contexts
return ContextType.PRIVATE_CHANNEL in set_contexts

return _contexts_check # type: ignore


def generate_permission_check(permissions: "Permissions") -> Callable[["HybridContext"], Awaitable[bool]]:
async def _permission_check(ctx: "HybridContext") -> bool:
return ctx.author.has_permission(*permissions) if ctx.guild_id else True # type: ignore
Expand Down Expand Up @@ -333,10 +350,9 @@ def slash_to_prefixed(cmd: HybridSlashCommand) -> _HybridToPrefixedCommand: # n
# can't be done in init due to how _binding works
prefixed_cmd._binding = cmd._binding

if not cmd.dm_permission:
prefixed_cmd.add_check(guild_only())

if cmd.scopes != [GLOBAL_SCOPE]:
if cmd.scopes == [GLOBAL_SCOPE]:
prefixed_cmd.add_check(generate_contexts_check(cmd.contexts))
else:
prefixed_cmd.add_check(generate_scope_check(cmd.scopes))

if cmd.default_member_permissions:
Expand Down Expand Up @@ -451,6 +467,8 @@ def hybrid_slash_command(
scopes: Absent[list["Snowflake_Type"]] = MISSING,
options: Optional[list[Union[SlashCommandOption, dict]]] = None,
default_member_permissions: Optional["Permissions"] = None,
integration_types: Optional[List[Union[IntegrationType, int]]] = None,
contexts: Optional[List[Union[ContextType, int]]] = None,
dm_permission: bool = True,
sub_cmd_name: str | LocalisedName = None,
group_name: str | LocalisedName = None,
Expand Down Expand Up @@ -480,7 +498,9 @@ def hybrid_slash_command(
scopes: The scope this command exists within
options: The parameters for the command, max 25
default_member_permissions: What permissions members need to have by default to use this command.
dm_permission: Should this command be available in DMs.
integration_types: Installation context(s) where the slash command is available, only for globally-scoped commands.
contexts: Interaction context(s) where the command can be used, only for globally-scoped commands.
dm_permission: Should this command be available in DMs (deprecated).
sub_cmd_name: 1-32 character name of the subcommand
sub_cmd_description: 1-100 character description of the subcommand
group_name: 1-32 character name of the group
Expand Down Expand Up @@ -521,6 +541,8 @@ def wrapper(func: AsyncCallable) -> HybridSlashCommand:
description=_description,
scopes=scopes or [GLOBAL_SCOPE],
default_member_permissions=perm,
integration_types=integration_types or [IntegrationType.GUILD_INSTALL],
contexts=contexts or [ContextType.GUILD, ContextType.BOT_DM, ContextType.PRIVATE_CHANNEL],
dm_permission=dm_permission,
callback=func,
options=options,
Expand All @@ -544,6 +566,8 @@ def hybrid_slash_subcommand(
base_description: Optional[str | LocalisedDesc] = None,
base_desc: Optional[str | LocalisedDesc] = None,
base_default_member_permissions: Optional["Permissions"] = None,
base_integration_types: Optional[List[Union[IntegrationType, int]]] = None,
base_contexts: Optional[List[Union[ContextType, int]]] = None,
base_dm_permission: bool = True,
subcommand_group_description: Optional[str | LocalisedDesc] = None,
sub_group_desc: Optional[str | LocalisedDesc] = None,
Expand All @@ -564,7 +588,9 @@ def hybrid_slash_subcommand(
base_description: The description of the base command
base_desc: An alias of `base_description`
base_default_member_permissions: What permissions members need to have by default to use this command.
base_dm_permission: Should this command be available in DMs.
base_integration_types: Installation context(s) where the slash command is available, only for globally-scoped commands.
base_contexts: Interaction context(s) where the command can be used, only for globally-scoped commands.
base_dm_permission: Should this command be available in DMs (deprecated).
subcommand_group_description: Description of the subcommand group
sub_group_desc: An alias for `subcommand_group_description`
scopes: The scopes of which this command is available, defaults to GLOBAL_SCOPE
Expand Down Expand Up @@ -597,6 +623,8 @@ def wrapper(func: AsyncCallable) -> HybridSlashCommand:
sub_cmd_name=_name,
sub_cmd_description=_description,
default_member_permissions=base_default_member_permissions,
integration_types=base_integration_types or [IntegrationType.GUILD_INSTALL],
contexts=base_contexts or [ContextType.GUILD, ContextType.BOT_DM, ContextType.PRIVATE_CHANNEL],
dm_permission=base_dm_permission,
scopes=scopes or [GLOBAL_SCOPE],
callback=func,
Expand Down
10 changes: 10 additions & 0 deletions interactions/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
Colour,
CommandType,
ComponentType,
ContextType,
CustomEmoji,
DefaultNotificationLevel,
DefaultReaction,
Expand Down Expand Up @@ -84,6 +85,7 @@
GuildWidgetSettings,
InputText,
IntegrationExpireBehaviour,
IntegrationType,
Intents,
InteractionPermissionTypes,
InteractionType,
Expand All @@ -103,6 +105,7 @@
MessageActivityType,
MessageFlags,
MessageInteraction,
MessageInteractionMetadata,
MessageReference,
MessageType,
MFALevel,
Expand Down Expand Up @@ -216,6 +219,7 @@
component_callback,
ComponentCommand,
ComponentContext,
contexts,
context_menu,
user_context_menu,
message_context_menu,
Expand Down Expand Up @@ -255,6 +259,7 @@
has_id,
has_role,
IDConverter,
integration_types,
InteractionCommand,
InteractionContext,
IntervalTrigger,
Expand Down Expand Up @@ -373,9 +378,11 @@
"ComponentContext",
"ComponentType",
"ConsumeRest",
"contexts",
"context_menu",
"ContextMenu",
"ContextMenuContext",
"ContextType",
"Converter",
"cooldown",
"Cooldown",
Expand Down Expand Up @@ -452,6 +459,8 @@
"IDConverter",
"InputText",
"IntegrationExpireBehaviour",
"IntegrationType",
"integration_types",
"Intents",
"InteractionCommand",
"InteractionContext",
Expand Down Expand Up @@ -487,6 +496,7 @@
"MessageConverter",
"MessageFlags",
"MessageInteraction",
"MessageInteractionMetadata",
"MessageReference",
"MessageType",
"MFALevel",
Expand Down