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
.
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.
manager = components.get_manager()
manager.add_to_bot(bot)
We can create a child manager as follows
foo_manager = components.get_manager("foo")
We can go deeper in the parent/child hierarchy by separating them with dots
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
@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
@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.
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
.
@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.
@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.
bot.run(os.getenv("EXAMPLE_TOKEN"))
Source Code#
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"))