We have previously shown how Hikari allows you to build simple GUI's with buttons1, but it can feel a little clunky to work with. That's because Hikari only covers the Discord API expects you to use handler libraries to fill the gaps. Hikari has an extremely active community, with a number of handlers. The two most popular Command Handlers are Tanjun2 and Lightbulb3, Tanjun obviously being our favorite. For sound and music there is LavaLink4 and SongBird5. Cache handlers like Sake6. Lastly and most recently there are Component Handlers like Yuyo7 and Miru8.
This article will focus on hikari-yuyo and what all this library can do with components.
Note:
While Yuyo
hikari-yuyo
is written by the same author as Tanjun, they do not depend on each other. Both libraries (tanjun and yuyo) are dependent onhikari
only.
What does Yuyo Do?
Yuyo provides a number of utilities meant to synergize with Hikari; the part of which we used most is yuyo.ComponentClient
9. The ComponentClient
offers a centralized and uniform way to build components (buttons, dropdowns, etc), bind callbacks to components, and manage those components life-cycles. The way this all happens is through a concept called Executors
, which is a class/object that ensures a callback runs after a specific action. Yuyo provides a couple different types of Executors
that we will review. Once you create an Executor
you can either register it with the ComponentClient
to execute long term or use the Executor
to wait for a component interaction. Let's take a look at how these different executors work.
WaitForExecutor
The first executor yuyo provides is yuyo.WaitFor
10, which is an alias for yuyo.WaitForExecutor
. This executor provides the most basic functionality in this article. yuyo.WaitFor
accepts up to three parameters. The first parameter is authors
and accepts a tuple
of member ids. Providing this argument will ensure that WaitFor
only interacts with the provided member(s). The second parameter, default_ephemeral
controls whether responses to the future WaitFor
interactions will be ephemeral. The last parameter timeout
accepts a datetime.timedelta
11 and will modify how long WaitFor
will stay active before exiting.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
- Lines 1 - 15 import the common libraries we need and setup a basic Tanjun command.
- Lines 16 - 24 uses standard vanilla hikari to build 2 buttons for us.
- Lines 26 - 30 send and store a new message in
message
. We provideensure_result=True
so we are guaranteed a message id, as well ascomponent=row
so our buttons are added. - Line 31 creates the
yuyo.WaitFor()
and targetsctx.author.id
, or the member that executes the/wait-for
command. Any other members will receive and ephemeral message explaining the member is not authorized. - Line 32 registers the
WaitForExecutor
to theyuyo.ComponentClient
. Without this, the executor can't watch for events. - Lines 34 - 36 attempt to run our
WaitFor
, with the methodWaitFor.wait_for()
. This will cause the function to "pause" and wait for ahikari.InteractionCreateEvent
meeting the passed in requirements. - Line 37 - 38 catches an
asnycio.TimeoutError()
exception. This will occur if a member does not cause thatInteractionCreateEvent
withing the providedWaitFor
timeout. - Line 39 - 40 only occur if no exceptions happen, and respond with a message "The custom id was X" depending on what button you click.
If you open discord and load this command into a Tanjun bot (or your favorite command handler! yuyo doesn't care!), you should have a new /wait-for
command. Send the command and you should get the result in the gif above.
An important thing to note is yuyo.WaitFor
will collect a single hikari.InteractionCreateEvent
12. It will work for a single button click, menu select, or "interaction". After that single interaction, further interactions will raise an error. The command same command can be sent again and when a new button is produced, it will work for a single click again before causing raising an error.
MultiComponentExecutor
Another helpful class yuyo provides is yuyo.MultiComponentExecutor
13. This Executor allows you to register multiple button/callback sets to a single message. If you read our previous [Interactive Embed tutorial14, this would have been a perfect use case for that. We won't review that complex of an example today, but instead lets look at attaching 2 unique buttons to the Executor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
- Lines 9 - 14 define our callbacks for the two buttons. Both callbacks are very simple, just using yuyo's
ComponentContext
to respond to the channel. - Line 19 defines another simple Tanjun slash command.
- Lines 23 - 33 builds our actual
MultiComponentExecutor
, lets take a closer look:- Line 24 instantiates a new
MultiComponentExecutor
- Line 25 calls
MultiComponentExecutor.add_action_row()
15 which returns ayuyo.ChildActionRowExecutor
16. This object has multiple helper methods that allow us to quickly and easily build hikari components. - Lines 26 - 28 will build and add a
hikari.Button
17 with styling and an emoji. NoteChildActionRowExecutor.add_button()
's18 second parameter; it accepts a callback which we passcallback_prim
to satisfy. - Lines 29 - 31 build a second button with a different emote and using
calback_sec
. - Line 32 takes our
ChildActionRowExecutor
and registers it toMultiComponentExecutor
. If you wanted, you could repeat this process again starting at line 25 to add more rows and buttons.
- Line 24 instantiates a new
- Line 34 sends a message "Test!" to the calling channel and sources its
components=
keyword argument fromexecutor.builders
19. This tells our executor to build and provide all of the components for the message. - Line 35 registers our built
MultiComponentExecutor
and message to theComponentClient
.
Open Discord and run /multi
and you should see a simple "Test!" message with a blurple and grey button. If you click either button the relevant callback will be called and the bot will send "Click <Primary/Secondary>". While Dependency Injection won't work here, you will get access to most hikari utilities at ctx.interaction
20, which resolves to hikari.ComponentInteraction21. hikari.ComponentInteraction.app
also contains access to hikari.Cache
under .cache
and hikari.api.RESTClient
under .rest
.
yuyo.MultiComponentExecutor
also supports multiple interactions! That means you can click buttons, select options in a menu, or make any interaction. This executor will handle them all until the timeout
expires.
Pagination
First we will look at yuyo.ComponentPaginator
22, a helpful utility class to create a paginator for Text and Embeds. Discord has strict limits on how much text you can output in a single message. Messages can only contain 2,000 characters23 and Discord Embeds have limits for the title, description, field names, field values, and the footer24. If you need to go over this limit, you would normally have to send multiple messages or Embeds. ComponentPaginator
allows you to bind all this content together as a generator and navigate the generator with a few buttons:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
This example does use Tanjun, but anything, even just basic Hikari can work with this library in the exact same way.
- Lines 1-12 are basic Tanjun setup. We create a component and add a new SlashCommand accessible at
/paginator
. - Line 14 uses Tanjuns Dependency Injection25 to make sure we have the fully instantiated
ComponentClient
. - Line 16 defines an
entries
tuple with 3 separate strings. This is not required, but it allows us to setup a very simple iterator for this example. - Line 21 defines a Python Generator. This generator produces a tuple of two objects every time it is called. The tuple will look roughly like
("<message content>", <hikari.Embed Object>,)
, with the assumedMessage.content
in the first slot and an Embed in the second. In this iterator you can fully customize how you want the content to return. - Line 31 defines the actual paginator executor. We instantiate a
ComponentPaginator
class and pass in some arguments. These parameters will control how your paginator behaves and who it reacts to.- First we pass in an
iterator
object for the paginator to pull content from. - Next a keyword
authors=(ctx.author,)
. This will limit who the paginator will work for. Only users/members listed in this tuple will be able to click buttons to run the callbacks.
- First we pass in an
- Line 32 attempts to get the first tuple out of your paginator using the
get_next_entry()
method. - Line 33 then helpfully divides the tuple into it's separate parts; a content for the message and an embed.
- Line 34 sends the message that will be used for the pagination. We provide the parameters previously generated, like content and embed, as well as passing
component=paginator
. This ensure that your message will have the appropriate buttons and UI for the user. - Line 35 finally registers the executor to the
ComponentClient
so that it can run.
Notice only 1 embed is displayed at a time. Upon clicking the arrow buttons, the Embed and text will be edited in place automatically. If you click the X, the message is deleted. This is our ComponentPaginator
at work! Yuyo also provides a few other nice helper utilities to break your content into chunks too.
Long Term interactions
Sometimes you will want to setup buttons or menu's that will be persistent, or that will stay long term. One common is for a "Ticket" button to permanently stay pinned for any member to use. In cases like that it would be easier to just register a custom_id
to a callback directly. yuyo.ComponentClient
supports this by default!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Most of this should look pretty standard by now, so lets highlight the important parts:
- Lines 8 - 9 defines a short callback,
persistent_callback
that accepts a singleyuyo.ComponentContext
. In this callback you can do anything really, but we're just keeping it simple and sending a "Clicked!" message. - Lines 12 - 17 creates an event listener for
hikari.StartedEvent
26. Now when your application starts up theyuyo.ComponentClient
will automatically setpersistent_callback
to be run when ahikari.InteractionCreateEvent
with acustom_id
ofclick-me-id
is used with. - Lines 19 - 36 define a standard Tanjun command that sends a message saying "Persistent!" with a single button component reading "Click Me!".
If you were to load this into an application and run /presistent-button
the expected message and button will appear. Clicking the button multiple times will always trigger the "Clicked!" response. If you stop the application/bot and then restart it, the button will continue to respond to clicks!
This method of yuyo.ComponentClient.set_constant_id()
27 will also over rule any executors registered with the same custom_id
. If you try to create a yuyo.WaitFor
, MultiComponentExecutor
and add components using click-me-id
(the custom_id used above), only the callback registered through ComponentClient.set_constant_id
will run.
Bonus: yuyo.Backoff
and yuyo.ErrorManager
Yuyo doesn't just provide utilities for components and pagination! There are also 2 classes included to help manage rate limiting and error issues. Because rate limiting is a bit more of an abstract issue, so this won't be a runnable example like the others.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
- Lines 1 - 12 define a simple Tanjun command
/back-off
as usual. - Lines 13 - 14 define 2 placeholder values for us. You can replace these with actual channel/message id's that you know can produce these exceptions.
- Lines 15 - 16 instantiate a new
yuyo.Backoff()
28 object and begins to iterate over it. - Lines 18 - 20 will attempt to fetch a message from the Discord API and the delete that message.
- Line 21 handles if a
hikari.errors.RateLimitedError
29 is raised. In this case, Discord is Rate Limiting your application for sending to many requests too quickly. - Line 22 runs if
RateLimitedError
is raised. In that caseyoyu.Backoff.set_next_backoff(exec.retry_after)
30 is called. This tellsBackoff
to useexec.retry_after
for the amount of time to wait before attempting the failed function again. - Lines 23 - 24 handles if a
hikari.errors.InternalServerError
is raised. In this case Discord had an error on their side. For this issue we simplypass
. Nowyuyo.Backoff
will calculate how long to wait before a retry, instead of depending on the provided exception for a retry time. - Lines 25 - 26 handle
hikari.errors.NotFoundError
andbreak
if the exception is raised.break
will causeyuyo.Backoff
to end its retry attempts and break the loop. - Lines 27 - 28 end the
yuyo.Backoff
if/when everything succeeds! If the message is fetched and deleted,break
is used to stop the loop and move on.
It's also possible to use yuyo.Backoff
without the async for
too. Here is the same code as the previous example, but without using yuyo.Backoff
as an iterator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
In this example we use a while
loop to instead check for message
. The primary difference is in how exceptions are handled. RateLimitedError
is still handled the same, with yuyo.Backoff().backoff()
31 and taking the retry time from the exception. InternalServerError
is also handled the same way with just a .backoff()
. NotFoundError
now just pass
's and else
is unneeded.
ErrorManager
Writing all of those exception handlers out each time is a lot of code copy/pasting. Generally it's better to reuse this logic when possible and that is exactly the use case of yuyo.ErrorManager
32. ErrorManager
allows us to link callbacks to Exceptions in the most simple terms. Yuyo calls this callback-exception link a rule
. You can register multiple rules to the yuyo.ErrorManager
then use it as an context manager to automatically handle if any registered exception is raised.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Building off of our previous example:
- Lines 6 - 10 instantiate a new
yuyo.ErrorManager
. We also demonstrate one of the ways to register rules toErrorManager
, by passing them in at__init__
. You can pass in a tuple where each entry represents one rule. In our case we register bothNotFoundError
andForbiddenError
to callretry.finish
. - Line 11 registers a second rule using
yuyo.ErrorManger().with_rule
33 with the exceptionRateLimitedError
, which will then callrety.set_next_backoff()
with the exceptions retry time. - Line 12 registers a third and final rule with
InternalServerError
. If this exception is raised, we just returnFalse
, which will causeBackoff
to manage the next retry for us. - Line 15 starts the
async for
loop foryuyo.Backoff
- Line 16 invokes the
ErrorManager
context manager. NowErrorManager
will handle all exceptions raised by this code block. - Lines 17 - 19 again attempts to fetch and delete a message from Discord.
Footnotes & Links
-
Building an Interative Embed WYSIWYG Editor in Discord, Patchwork Collective, https://patchwork.systems/programming/hikari-discord-bot/embed-plugin-dependency-injection-components.html ↩
-
Tanjun Github, FasterSpeeding, https://github.com/FasterSpeeding/Tanjun ↩
-
Lightbulb Github, Thommohttps://github.com/tandemdude/hikari-lightbulb ↩
-
Lavalink-rs Github, Vicky5124, https://github.com/vicky5124/lavalink-rs ↩
-
Songbird-Py Github, Magpie-Dev, https://github.com/magpie-dev/Songbird-Py ↩
-
Sake Github, FasterSpeeding, https://github.com/FasterSpeeding/Sake ↩
-
Yuyo Github, FasterSpeeding, https://pypi.org/project/hikari-yuyo/ ↩
-
Miru Github, HyperGH, https://github.com/HyperGH/hikari-miru ↩
-
Yuyo Docs yuyo.components, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html ↩
-
Yuyo Docs yuyo.components.WaitFor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#WaitFor ↩
-
Python3 Docs datetime.timedelta, PSF, https://docs.python.org/3/library/datetime.html#datetime.timedelta ↩
-
Hikari Docs hikari.InteractionCreateEvent, Davfsa, https://www.hikari-py.dev/hikari/events/interaction_events.html#hikari.events.interaction_events.InteractionCreateEvent ↩
-
Yuyo Docs yuyo.components.MultiComponentExecutor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#MultiComponentExecutor ↩
-
Embeds and Dependency Injection, Patchwork Collective, https://patchwork.systems/programming/hikari-discord-bot/embed-plugin-dependency-injection-components.html ↩
-
Yuyo Docs yuyo.components.MultiComponentExecutor().add_action_row(), FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#MultiComponentExecutor.add_action_row ↩
-
Yuyo Docs yuyo.components.ChildActionRowExecutor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ChildActionRowExecutor ↩
-
Hikari Docs hikari.messages.ButtonComponent, Davfsahttps://www.hikari-py.dev/hikari/messages.html#hikari.messages.ButtonComponent ↩
-
Yuyo Docs yuyo.components.ActionRowExecutor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ActionRowExecutor.add_button ↩
-
Yyuo Docs yuyo.components.MultiComponentExecutor.builders, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#MultiComponentExecutor.builders ↩
-
Yuyo Docs yuyo.components.ComponentContext.interaction, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ComponentContext.interaction ↩
-
Hikari Docs hikari.interactions.component_interactions.ComponentInteraction, Davfsa, https://www.hikari-py.dev/hikari/interactions/component_interactions.html#hikari.interactions.component_interactions.ComponentInteraction ↩
-
Yuyo Docs yuyo.components.ComponentPaginator, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ComponentPaginator ↩
-
Discord Developer Portal Rate Limits, Discord, https://discord.com/developers/docs/resources/channel#create-message ↩
-
Discord Developer Portal Embed Limtis, Discord, https://discord.com/developers/docs/resources/channel#embed-limits ↩
-
Deeper Look at Dependency Injection in Tanjun, Patchwork Collective, https://patchwork.systems/programming/hikari-discord-bot/deeper-look-at-dependency-injection-tanjun.html ↩
-
Hikari Docs hikari.StartedEvent, Davfsa, https://www.hikari-py.dev/hikari/events/lifetime_events.html#hikari.events.lifetime_events.StartedEvent ↩
-
Yuyo Docs yuyo.ComponentClient.set_constant_id, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ComponentClient.set_constant_id ↩
-
Yuyo Docs yuyo.Backoff, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#Backoff ↩
-
Hikari Docs hikari.errors.RateLimitedError, FasterSpeeding, https://www.hikari-py.dev/hikari/errors.html#hikari.errors.RateLimitedError ↩
-
Yuyo Docs yuyo.Backoff.set_next_backoff, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#Backoff.set_next_backoff ↩
-
Yuyo Docs yuyo.Backoff.backoff, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#Backoff.backoff ↩
-
Yuyo Docs yuyo.ErrorManger, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#ErrorManager ↩
-
Yuyo Docs yuyo.ErrorManger.with_rule, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#ErrorManager.with_rule ↩