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.
hikari-yuyois written by the same author as Tanjun, they do not depend on each other. Both libraries (tanjun and yuyo) are dependent on
What does Yuyo Do?
Yuyo provides a number of utilities meant to synergize with Hikari; the part of which we used most is
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.
The first executor yuyo provides is
yuyo.WaitFor10, 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.timedelta11 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 provide
ensure_result=Trueso we are guaranteed a message id, as well as
component=rowso our buttons are added.
- Line 31 creates the
ctx.author.id, or the member that executes the
/wait-forcommand. Any other members will receive and ephemeral message explaining the member is not authorized.
- Line 32 registers the
yuyo.ComponentClient. Without this, the executor can't watch for events.
- Lines 34 - 36 attempt to run our
WaitFor, with the method
WaitFor.wait_for(). This will cause the function to "pause" and wait for a
hikari.InteractionCreateEventmeeting the passed in requirements.
- Line 37 - 38 catches an
asnycio.TimeoutError()exception. This will occur if a member does not cause that
InteractionCreateEventwithing the provided
- 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.InteractionCreateEvent12. 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.
Another helpful class yuyo provides is
yuyo.MultiComponentExecutor13. 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
ComponentContextto 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
- Line 25 calls
MultiComponentExecutor.add_action_row()15 which returns a
yuyo.ChildActionRowExecutor16. 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.Button17 with styling and an emoji. Note
ChildActionRowExecutor.add_button()'s18 second parameter; it accepts a callback which we pass
- Lines 29 - 31 build a second button with a different emote and using
- Line 32 takes our
ChildActionRowExecutorand registers it to
MultiComponentExecutor. 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 from
executor.builders19. This tells our executor to build and provide all of the components for the message.
- Line 35 registers our built
MultiComponentExecutorand message to the
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.interaction20, which resolves to hikari.ComponentInteraction21.
hikari.ComponentInteraction.app also contains access to
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
First we will look at
yuyo.ComponentPaginator22, 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
- Line 14 uses Tanjuns Dependency Injection25 to make sure we have the fully instantiated
- Line 16 defines an
entriestuple 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 assumed
Message.contentin 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
ComponentPaginatorclass and pass in some arguments. These parameters will control how your paginator behaves and who it reacts to.
- First we pass in an
iteratorobject 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
- 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
ComponentClientso 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_callbackthat accepts a single
yuyo.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.StartedEvent26. Now when your application starts up the
yuyo.ComponentClientwill automatically set
persistent_callbackto be run when a
click-me-idis 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
MultiComponentExecutor and add components using
click-me-id (the custom_id used above), only the callback registered through
ComponentClient.set_constant_id will run.
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
- 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.RateLimitedError29 is raised. In this case, Discord is Rate Limiting your application for sending to many requests too quickly.
- Line 22 runs if
RateLimitedErroris raised. In that case
yoyu.Backoff.set_next_backoff(exec.retry_after)30 is called. This tells
exec.retry_afterfor the amount of time to wait before attempting the failed function again.
- Lines 23 - 24 handles if a
hikari.errors.InternalServerErroris raised. In this case Discord had an error on their side. For this issue we simply
yuyo.Backoffwill calculate how long to wait before a retry, instead of depending on the provided exception for a retry time.
- Lines 25 - 26 handle
breakif the exception is raised.
yuyo.Backoffto end its retry attempts and break the loop.
- Lines 27 - 28 end the
yuyo.Backoffif/when everything succeeds! If the message is fetched and deleted,
breakis 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
NotFoundError now just
else is unneeded.
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
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 to
ErrorManager, by passing them in at
__init__. You can pass in a tuple where each entry represents one rule. In our case we register both
- Line 11 registers a second rule using
yuyo.ErrorManger().with_rule33 with the exception
RateLimitedError, which will then call
rety.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 return
False, which will cause
Backoffto manage the next retry for us.
- Line 15 starts the
async forloop for
- Line 16 invokes the
ErrorManagercontext manager. Now
ErrorManagerwill 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 ↩
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 ↩