Component Manager#

A simple example on the use of component managers with disnake-ext-components.

First and foremost, we create a bot as per usual. Since we don’t need any prefix command capabilities, we opt for an InteractionBot.

examples/manager.py - creating a Bot object#
import os
import typing

import disnake
from disnake.ext import commands, components

bot = commands.InteractionBot()

Next, we create a component manager.

A call to get_manager() without arguments returns the root manager. We register the root manager to the bot, which will ensure all components we register to any other manager will automatically be handled. This is because a manager handles its own components along with any of its children’s.

examples/manager.py - create a root component manager#
manager = components.get_manager()
manager.add_to_bot(bot)

We can create a child manager as follows

examples/manager.py - create a child manager#
foo_manager = components.get_manager("foo")

We can go deeper in the parent/child hierarchy by separating them with dots

examples/manager.py - create a deeply nested manager#
deeply_nested_manager = components.get_manager("foo.bar.baz")

Note

Any missing bits will automatically be filled in– the above line has automatically created a manager named “foo.bar”, too.

Now let us quickly register a button each to our foo_manager and our deeply_nested_manager. To this end, we will use the button example

examples/manager.py - creating a button component#
@foo_manager.register
class FooButton(components.RichButton):
    label: typing.Optional[str] = "0"

    count: int

    async def callback(self, interaction: components.MessageInteraction) -> None:
        self.count += 1
        self.label = str(self.count)

        await interaction.response.edit_message(components=self)


@deeply_nested_manager.register
class FooBarBazButton(components.RichButton):
    label: typing.Optional[str] = "0"

    count: int

    async def callback(self, interaction: components.MessageInteraction) -> None:
        self.count += 1
        self.label = str(self.count)

        await interaction.response.edit_message(components=self)

Customizing your component manager#

For most use cases, the default implementation of the component manager should suffice. Two methods of interest to customise your managers without having to subclass them are ComponentManager.as_callback_wrapper() and ComponentManager.as_error_handler().

ComponentManager.as_callback_wrapper() wraps the callbacks of all components registered to that manager along with those of its children. Therefore, if we were to add a callback wrapper to the root manager, we would ensure it applies to all components. For example, say we want to log all component interactions

examples/manager.py - customizing your component manager#
@manager.as_callback_wrapper
async def wrapper(
    manager: components.ComponentManager,
    component: components.api.RichComponent,
    interaction: disnake.Interaction,
):
    print(
        f"User {interaction.user.name!r} interacted with component"
        f" {type(component).__name__!r}..."
    )

    yield

    print(
        f"User {interaction.user.name!r}s interaction with component"
        f" {type(component).__name__!r} was successful!"
    )

Tip

For actual production code, please use logging instead of print.

Any code placed after the yield statement runs after the component callback is invoked. This can be used for cleanup of resources

Note

Any changes made to the component instance by other wrappers and/or the callback itself are reflected here.

This feature can also be used as a check. By raising an exception before the component callback is invoked, you can prevent it from being invoked entirely. The exception is then also passed to exception handlers.

For example, let’s allow only the original slash command author to interact with any components on this manager.

examples/manager.py - creating a check to prevent the component callback call#
 1class InvalidUserError(Exception):
 2    def __init__(self, message: str, user: typing.Union[disnake.User, disnake.Member]):
 3        super().__init__(message)
 4        self.message = message
 5        self.user = user
 6
 7
 8@deeply_nested_manager.as_callback_wrapper
 9async def check_wrapper(
10    manager: components.api.ComponentManager,
11    component: components.api.RichComponent,
12    interaction: disnake.Interaction,
13):
14    if (
15        isinstance(interaction, disnake.MessageInteraction)
16        and interaction.message.interaction
17        and interaction.user != interaction.message.interaction.user
18    ):
19        message = "You are not allowed to use this component."
20        raise InvalidUserError(message, interaction.user)
21
22    yield

Now in our custom check we adds some logic to filter out who should be able to use our component:

This check only applies to message interactions (line 15)… The message must have been sent as interaction response (line 16)… The component user is NOT the same as the original interaction user (line 17)…

If all the conditions are satisfied we raise our custom error for convenience (lines 19-20).

Similarly, we can create an exception handler for our components. An exception handler function should return True if the error was handled, and False or None otherwise. The default implementation hands the exception down to the next handler until it either is handled or reaches the root manager. If the root manager is reached (and does not have a custom exception handler), the exception is logged.

To demonstrate the difference, we will make a custom error handler only for the deeply_nested_manager.

examples/manager.py - components custom error handler#
@deeply_nested_manager.as_exception_handler
async def error_handler(
    manager: components.ComponentManager,
    component: components.api.RichComponent,
    interaction: disnake.Interaction,
    exception: Exception,
):
    if isinstance(exception, InvalidUserError):
        message = f"{exception.user.mention}, {exception.message}"
        await interaction.response.send_message(message, ephemeral=True)
        return True

    return False

Note

You do not need to explicitly return False. Returning None is sufficient. Explicitly returning False is simply preferred for clarity.

Finally, we send the components to test the managers.

examples/manager.py - sending the components#
@bot.slash_command()  # pyright: ignore  # still some unknowns in disnake
async def test_button(inter: disnake.CommandInteraction) -> None:
    wrapped = components.wrap_interaction(inter)
    component = FooButton(count=0)

    await wrapped.response.send_message(components=component)


@bot.slash_command()  # pyright: ignore  # still some unknowns in disnake
async def test_nested_button(inter: disnake.CommandInteraction) -> None:
    wrapped = components.wrap_interaction(inter)
    component = FooBarBazButton(count=0)

    await wrapped.response.send_message(components=component)

Lastly, we run the bot.

examples/manager.py - running the bot#
bot.run(os.getenv("EXAMPLE_TOKEN"))

Source Code#

examples/manager.py#
  1import os
  2import typing
  3
  4import disnake
  5from disnake.ext import commands, components
  6
  7bot = commands.InteractionBot()
  8
  9manager = components.get_manager()
 10manager.add_to_bot(bot)
 11
 12foo_manager = components.get_manager("foo")
 13deeply_nested_manager = components.get_manager("foo.bar.baz")
 14
 15
 16@foo_manager.register
 17class FooButton(components.RichButton):
 18    label: typing.Optional[str] = "0"
 19
 20    count: int
 21
 22    async def callback(self, interaction: components.MessageInteraction) -> None:
 23        self.count += 1
 24        self.label = str(self.count)
 25
 26        await interaction.response.edit_message(components=self)
 27
 28
 29@deeply_nested_manager.register
 30class FooBarBazButton(components.RichButton):
 31    label: typing.Optional[str] = "0"
 32
 33    count: int
 34
 35    async def callback(self, interaction: components.MessageInteraction) -> None:
 36        self.count += 1
 37        self.label = str(self.count)
 38
 39        await interaction.response.edit_message(components=self)
 40
 41
 42@manager.as_callback_wrapper
 43async def wrapper(
 44    manager: components.ComponentManager,
 45    component: components.api.RichComponent,
 46    interaction: disnake.Interaction,
 47):
 48    print(
 49        f"User {interaction.user.name!r} interacted with component"
 50        f" {type(component).__name__!r}..."
 51    )
 52
 53    yield
 54
 55    print(
 56        f"User {interaction.user.name!r}s interaction with component"
 57        f" {type(component).__name__!r} was successful!"
 58    )
 59
 60
 61class InvalidUserError(Exception):
 62    def __init__(self, message: str, user: typing.Union[disnake.User, disnake.Member]):
 63        super().__init__(message)
 64        self.message = message
 65        self.user = user
 66
 67
 68@deeply_nested_manager.as_callback_wrapper
 69async def check_wrapper(
 70    manager: components.api.ComponentManager,
 71    component: components.api.RichComponent,
 72    interaction: disnake.Interaction,
 73):
 74    if (
 75        isinstance(interaction, disnake.MessageInteraction)
 76        and interaction.message.interaction
 77        and interaction.user != interaction.message.interaction.user
 78    ):
 79        message = "You are not allowed to use this component."
 80        raise InvalidUserError(message, interaction.user)
 81
 82    yield
 83
 84
 85@deeply_nested_manager.as_exception_handler
 86async def error_handler(
 87    manager: components.ComponentManager,
 88    component: components.api.RichComponent,
 89    interaction: disnake.Interaction,
 90    exception: Exception,
 91):
 92    if isinstance(exception, InvalidUserError):
 93        message = f"{exception.user.mention}, {exception.message}"
 94        await interaction.response.send_message(message, ephemeral=True)
 95        return True
 96
 97    return False
 98
 99
100@bot.slash_command()  # pyright: ignore  # still some unknowns in disnake
101async def test_button(inter: disnake.CommandInteraction) -> None:
102    wrapped = components.wrap_interaction(inter)
103    component = FooButton(count=0)
104
105    await wrapped.response.send_message(components=component)
106
107
108@bot.slash_command()  # pyright: ignore  # still some unknowns in disnake
109async def test_nested_button(inter: disnake.CommandInteraction) -> None:
110    wrapped = components.wrap_interaction(inter)
111    component = FooBarBazButton(count=0)
112
113    await wrapped.response.send_message(components=component)
114
115
116bot.run(os.getenv("EXAMPLE_TOKEN"))