Skip to content

feat: support regex component callbacks #1332

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

Merged
merged 3 commits into from
Apr 11, 2023
Merged
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
14 changes: 14 additions & 0 deletions docs/src/Guides/05 Components.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,17 @@ When responding to a component you need to satisfy discord either by responding
)
)
```

=== ":four: Persistent Callbacks, with regex"
Ah, I see you are a masochist. You want to use regex to match your custom_ids. Well who am I to stop you?

```python
@component_callback(re.compile(r"\w*"))
async def test_callback(ctx: interactions.ComponentContext):
await ctx.send(f"Clicked {ctx.custom_id}")
```

Just like normal `@component_callback`, you can specify a regex pattern to match your custom_ids, instead of explicitly passing strings.
This is useful if you have a lot of components with similar custom_ids, and you want to handle them all in the same callback.

Please do bare in mind that using regex patterns can be a bit slower than using strings, especially if you have a lot of components.
26 changes: 20 additions & 6 deletions interactions/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def __init__(
] = {}
"""A dictionary of registered application commands in a tree"""
self._component_callbacks: Dict[str, Callable[..., Coroutine]] = {}
self._regex_component_callbacks: Dict[re.Pattern, Callable[..., Coroutine]] = {}
self._modal_callbacks: Dict[str, Callable[..., Coroutine]] = {}
self._global_autocompletes: Dict[str, GlobalAutoComplete] = {}
self.processors: Dict[str, Callable[..., Coroutine]] = {}
Expand Down Expand Up @@ -1270,10 +1271,15 @@ def add_component_callback(self, command: ComponentCommand) -> None:

"""
for listener in command.listeners:
# I know this isn't an ideal solution, but it means we can lookup callbacks with O(1)
if listener in self._component_callbacks.keys():
raise ValueError(f"Duplicate Component! Multiple component callbacks for `{listener}`")
self._component_callbacks[listener] = command
if isinstance(listener, re.Pattern):
if listener in self._regex_component_callbacks.keys():
raise ValueError(f"Duplicate Component! Multiple component callbacks for `{listener}`")
self._regex_component_callbacks[listener] = command
else:
# I know this isn't an ideal solution, but it means we can lookup callbacks with O(1)
if listener in self._component_callbacks.keys():
raise ValueError(f"Duplicate Component! Multiple component callbacks for `{listener}`")
self._component_callbacks[listener] = command
continue

def add_modal_callback(self, command: ModalCommand) -> None:
Expand Down Expand Up @@ -1410,7 +1416,7 @@ async def wrap(*args, **kwargs) -> Absent[List[Dict]]:
if cmd_name not in found and warn_missing:
self.logger.error(
f'Detected yet to sync slash command "/{cmd_name}" for scope '
f"{'global' if scope == GLOBAL_SCOPE else scope}"
f'{"global" if scope == GLOBAL_SCOPE else scope}'
)
continue
found.add(cmd_name)
Expand Down Expand Up @@ -1668,7 +1674,15 @@ async def _dispatch_interaction(self, event: RawGatewayEvent) -> None:
component_type = interaction_data["data"]["component_type"]

self.dispatch(events.Component(ctx=ctx))
if callback := self._component_callbacks.get(ctx.custom_id):
component_callback = self._component_callbacks.get(ctx.custom_id)
if not component_callback:
# evaluate regex component callbacks
for regex, callback in self._regex_component_callbacks.items():
if regex.match(ctx.custom_id):
component_callback = callback
break

if component_callback:
await self.__dispatch_interaction(
ctx=ctx,
callback=callback(ctx),
Expand Down
9 changes: 7 additions & 2 deletions interactions/models/internal/application_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ class ComponentCommand(InteractionCommand):
name: str = attrs.field(
repr=False,
)
listeners: list[str] = attrs.field(repr=False, factory=list)
listeners: list[str | re.Pattern] = attrs.field(repr=False, factory=list)


@attrs.define(eq=False, order=False, hash=False, kw_only=True)
Expand Down Expand Up @@ -1122,13 +1122,16 @@ def message_context_menu(
)


def component_callback(*custom_id: str) -> Callable[[AsyncCallable], ComponentCommand]:
def component_callback(*custom_id: str | re.Pattern) -> Callable[[AsyncCallable], ComponentCommand]:
"""
Register a coroutine as a component callback.

Component callbacks work the same way as commands, just using components as a way of invoking, instead of messages.
Your callback will be given a single argument, `ComponentContext`

Note:
This can optionally take a regex pattern, which will be used to match against the custom ID of the component

Args:
*custom_id: The custom ID of the component to wait for

Expand All @@ -1141,6 +1144,8 @@ def wrapper(func: AsyncCallable) -> ComponentCommand:
return ComponentCommand(name=f"ComponentCallback::{custom_id}", callback=func, listeners=custom_id)

custom_id = _unpack_helper(custom_id)
if not all(isinstance(i, re.Pattern) for i in custom_id) or all(isinstance(i, str) for i in custom_id):
raise ValueError("All custom IDs be either a string or a regex pattern, not a mix of both.")
return wrapper


Expand Down