Yuyo: Make Components Easy

Components are the cool new feature in Discord. Dropdowns, various Buttons and more! While Yuyo isn't a requirement and Hikari can do this all by itself, Yuyo definitely makes it easier to manage, let's take a look.

By KatieKat Asterisk

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 on hikari 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.ComponentClient9. 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.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
import asyncio

from hikari import ButtonStyle
import tanjun
import yuyo


component = tanjun.Component()

@component.with_command
@tanjun.as_slash_command("wait-for", "Test the yuyo waitfor executor")
async def test_waitfor_exec(
    ctx: tanjun.abc.SlashContext,
    component_client: yuyo.ComponentClient = tanjun.injected(type=yuyo.ComponentClient),
):
    row = (
        ctx.rest.build_action_row()
        .add_button(ButtonStyle.PRIMARY, "primary_button")
        .set_label("Primary!")
        .add_to_container()
        .add_button(ButtonStyle.SECONDARY, "secondary_button")
        .set_label("Second!")
        .add_to_container()
    )

    message = await ctx.respond(
        "Only works once!",
        component=row,
        ensure_result=True,
    )
    executor = yuyo.components.WaitFor(authors=(ctx.author.id,), timeout=datetime.timedelta(seconds=30))
    component_client.set_executor(message.id, executor)

    try:
        result = await executor.wait_for()
        custom_id = result.interaction.custom_id
    except asyncio.TimeoutError:
        await ctx.respond("timed out")
    else:
        await result.respond(f"The custom id was {custom_id}")
  • 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=True so we are guaranteed a message id, as well as component=row so our buttons are added.
  • Line 31 creates the yuyo.WaitFor() and targets ctx.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 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.InteractionCreateEvent meeting the passed in requirements.
  • Line 37 - 38 catches an asnycio.TimeoutError() exception. This will occur if a member does not cause that InteractionCreateEvent withing the provided WaitFor 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.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.

MultiComponentExecutor

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
import datetime

import hikari
import tanjun
from hikari import ButtonStyle
import yuyo


async def callback_prim(ctx: yuyo.ComponentContext):
    await ctx.respond("Clicked Primary!")


async def callback_sec(ctx: yuyo.ComponentContext):
    await ctx.respond("Clicked Secondary!")


@test_grp.with_command
@tanjun.as_slash_command("multi", "Test the yuyo multi component executor")
async def test_multi_component_exec(
    ctx: tanjun.abc.SlashContext,
    component_client: yuyo.ComponentClient = tanjun.injected(type=yuyo.ComponentClient),
):
    executor = (
        yuyo.MultiComponentExecutor()
        .add_action_row()
        .add_button(ButtonStyle.PRIMARY, callback_prim)
        .set_emoji("👍")
        .add_to_container()
        .add_button(ButtonStyle.SECONDARY, callback_sec)
        .set_emoji("💫")
        .add_to_container()
        .add_to_parent()
    )
    message = await ctx.respond("Test!", components=executor.builders, ensure_result=True)
    component_client.set_executor(message, executor)
  • 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 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 callback_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 to MultiComponentExecutor. If you wanted, you could repeat this process again starting at line 25 to add more rows and buttons.
  • 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 MultiComponentExecutor and message to the ComponentClient.

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 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.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
import datetime

import hikari
import tanjun
import yuyo


component = tanjun.Component()

@component.with_command
@tanjun.as_slash_command("paginator", "Test the yuyo paginator")
async def test_paginator(
    ctx: tanjun.abc.SlashContext,
    component_client: yuyo.ComponentClient = tanjun.injected(type=yuyo.ComponentClient),
):
    entries = (
        "Entry 1",
        "Entry 2",
        "Entry 3",
    )
    iterator = (
        (
            f"`{entry}`",
            hikari.Embed(
                color=16711680,
                title=f"Text selected: `{entry}`",
            ),
        )
        for entry in entries
    )
    paginator = yuyo.ComponentPaginator(iterator, authors=(ctx.author,), timeout=datetime.timedelta(minutes=3))
    if first_response := await paginator.get_next_entry():
        content, embed = first_response
        message = await ctx.respond(content=content, component=paginator, embed=embed, ensure_result=True)
        component_client.add_executor(message, paginator)
        return
    await ctx.respond("Entry not found")

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 assumed Message.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.
  • 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
from hikari import StartedEvent, ButtonStyle
import tanjun
import yuyo


component = tanjun.Component()

async def persistent_callback(ctx: yuyo.ComponentContext):
    await ctx.respond("Clicked!")


@component.with_listener(hikari.StartedEvent)
async def on_bot_ready(
    _: hikari.StartedEvent,
    component_client: yuyo.ComponentClient = tanjun.injected(type=yuyo.ComponentClient),
):
    component_client.set_constant_id("click-me-id", persistent_callback)


@component.with_command
@tanjun.as_slash_command("persistent-button", "Test the yuyo waitfor executor")
async def test_persistent_button(
    ctx: tanjun.abc.SlashContext,
):
    row = (
        ctx.rest.build_action_row()
        .add_button(ButtonStyle.PRIMARY, "click-me-id")
        .set_label("Click Me!")
        .add_to_container()
    )

    await ctx.respond(
        "Persistent!",
        component=row,
        ensure_result=True,
    )

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 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.ComponentClient will automatically set persistent_callback to be run when ahikari.InteractionCreateEvent with a custom_id of click-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
import hikari
from hikari import errors
import tanjun
import yuyo


component = tanjun.Component()

@component.with_command
@tanjun.as_slash_command("back-off", "Test the yuyo backoff.")
async def test_backoff(
    ctx: tanjun.abc.SlashContext,
):
    channel_id: hikari.Snowflake = <channel_id>
    message_id: hikari.Snowflake = <message_id>

    backoff = yuyo.Backoff()
    async for _ in backoff:
        try:
            message = await bot.rest.fetch_message(channel_id, message_id)
            await message.delete()
        except errors.RateLimitedError as exc:
            backoff.set_next_backoff(exc.retry_after)
        except errors.InternalServerError:
            pass
        except errors.NotFoundError:
            break
        else:
            break
  • 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.RateLimitedError29 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 case yoyu.Backoff.set_next_backoff(exec.retry_after)30 is called. This tells Backoff to use exec.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 simply pass. Now yuyo.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 and break if the exception is raised. break will cause yuyo.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
channel_id: hikari.Snowflake = <channel_id>
message_id: hikari.Snowflake = <message_id>

backoff = yuyo.Backoff()
while not message:
    try:
        message = await bot.rest.fetch_message(channel_id, message_id)
        mawait message.delete()
    except errors.RateLimitedError as exc:
        await backoff.backoff(exc.retry_after)
    except errors.InternalServerError:
        await backoff.backoff()
    except errors.NotFoundError:
        pass

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.ErrorManager32. 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
channel_id: hikari.Snowflake = <channel_id>
message_id: hikari.Snowflake = <message_id>

retry = yuyo.Backoff()

error_handler = (
    yuyo.ErrorManager(
        (
            (errors.NotFoundError, errors.ForbiddenError), lambda _: retry.finish()
        )
    ).with_rule((errors.RateLimitedError,), lambda exc: retry.set_next_backoff(exc.retry_after))
     .with_rule((errors.InternalServerError,), lambda _: False)       
)

async for _ in retry:
    with error_handler:
        message = await bot.rest.fetch_message(channel_id, message_id)
        mawait message.delete()
        break

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 NotFoundError and ForbiddenError to call retry.finish.
  • Line 11 registers a second rule using yuyo.ErrorManger().with_rule33 with the exception RateLimitedError, 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 return False, which will cause Backoff to manage the next retry for us.
  • Line 15 starts the async for loop for yuyo.Backoff
  • Line 16 invokes the ErrorManager context manager. Now ErrorManager will handle all exceptions raised by this code block.
  • Lines 17 - 19 again attempts to fetch and delete a message from Discord.

Footnotes & Links


  1. Building an Interative Embed WYSIWYG Editor in Discord, Patchwork Collective, https://patchwork.systems/programming/hikari-discord-bot/embed-plugin-dependency-injection-components.html 

  2. Tanjun Github, FasterSpeeding, https://github.com/FasterSpeeding/Tanjun 

  3. Lightbulb Github, Thommohttps://github.com/tandemdude/hikari-lightbulb 

  4. Lavalink-rs Github, Vicky5124, https://github.com/vicky5124/lavalink-rs 

  5. Songbird-Py Github, Magpie-Dev, https://github.com/magpie-dev/Songbird-Py 

  6. Sake Github, FasterSpeeding, https://github.com/FasterSpeeding/Sake 

  7. Yuyo Github, FasterSpeeding, https://pypi.org/project/hikari-yuyo/ 

  8. Miru Github, HyperGH, https://github.com/HyperGH/hikari-miru 

  9. Yuyo Docs yuyo.components, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html 

  10. Yuyo Docs yuyo.components.WaitFor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#WaitFor 

  11. Python3 Docs datetime.timedelta, PSF, https://docs.python.org/3/library/datetime.html#datetime.timedelta 

  12. Hikari Docs hikari.InteractionCreateEvent, Davfsa, https://www.hikari-py.dev/hikari/events/interaction_events.html#hikari.events.interaction_events.InteractionCreateEvent 

  13. Yuyo Docs yuyo.components.MultiComponentExecutor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#MultiComponentExecutor 

  14. Embeds and Dependency Injection, Patchwork Collective, https://patchwork.systems/programming/hikari-discord-bot/embed-plugin-dependency-injection-components.html 

  15. Yuyo Docs yuyo.components.MultiComponentExecutor().add_action_row(), FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#MultiComponentExecutor.add_action_row 

  16. Yuyo Docs yuyo.components.ChildActionRowExecutor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ChildActionRowExecutor 

  17. Hikari Docs hikari.messages.ButtonComponent, Davfsahttps://www.hikari-py.dev/hikari/messages.html#hikari.messages.ButtonComponent 

  18. Yuyo Docs yuyo.components.ActionRowExecutor, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ActionRowExecutor.add_button 

  19. Yyuo Docs yuyo.components.MultiComponentExecutor.builders, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#MultiComponentExecutor.builders 

  20. Yuyo Docs yuyo.components.ComponentContext.interaction, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ComponentContext.interaction 

  21. Hikari Docs hikari.interactions.component_interactions.ComponentInteraction, Davfsa, https://www.hikari-py.dev/hikari/interactions/component_interactions.html#hikari.interactions.component_interactions.ComponentInteraction 

  22. Yuyo Docs yuyo.components.ComponentPaginator, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ComponentPaginator 

  23. Discord Developer Portal Rate Limits, Discord, https://discord.com/developers/docs/resources/channel#create-message 

  24. Discord Developer Portal Embed Limtis, Discord, https://discord.com/developers/docs/resources/channel#embed-limits 

  25. Deeper Look at Dependency Injection in Tanjun, Patchwork Collective, https://patchwork.systems/programming/hikari-discord-bot/deeper-look-at-dependency-injection-tanjun.html 

  26. Hikari Docs hikari.StartedEvent, Davfsa, https://www.hikari-py.dev/hikari/events/lifetime_events.html#hikari.events.lifetime_events.StartedEvent 

  27. Yuyo Docs yuyo.ComponentClient.set_constant_id, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo/components.html#ComponentClient.set_constant_id 

  28. Yuyo Docs yuyo.Backoff, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#Backoff 

  29. Hikari Docs hikari.errors.RateLimitedError, FasterSpeeding, https://www.hikari-py.dev/hikari/errors.html#hikari.errors.RateLimitedError 

  30. Yuyo Docs yuyo.Backoff.set_next_backoff, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#Backoff.set_next_backoff 

  31. Yuyo Docs yuyo.Backoff.backoff, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#Backoff.backoff 

  32. Yuyo Docs yuyo.ErrorManger, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#ErrorManager 

  33. Yuyo Docs yuyo.ErrorManger.with_rule, FasterSpeeding, https://yuyo.cursed.solutions/release/yuyo.html#ErrorManager.with_rule 

We appreciate Donations & Tips!

We would much rather be making cool tutorials, streaming code, and making accessibility tools. Unfortunately we have bills and a job. Hopefully one day, with enough Patrons and donations this can be fulltime!

Authors

Katie is the Social Protector for the Patchwork Collective and the first Part to start communicating with Aster in early 2021. Most of Katie's System Role revolves around handling social situations, work, and dealing with external strangers. Once the internal walls came down Katie quickly found her interests in research, programming, and writing.

>> Learn More about Katie and The Patchwork Collective

Asterisk

Role: Host Apparently Normal Part

Asters's current pfp

Pre-2020 Aster consider themself a very neurotypical male. As the COVID pandemic hit it magnified many issues Aster had been able to hide or compensate for. Since diagnosis Aster now tries to use their previous skills with teaching and speaking to help spread education and awareness about DID/OSDD. In their spare time Aster enjoys programming, teaching, and helping build the Official Game Theory Discord.

>> Learn More about Aster and The Patchwork Collective