---
name: "purrkit"
description: "a meowing, fully-typesafe set of packages for interacting with the discord API"

languages: ["TypeScript"]
stub: false

links:
  [
    { type: "source", label: "source", href: "https://tangled.org/vt3e.cat/purrkit.git" },
    { type: "website", label: "npm", href: "https://www.npmjs.com/package/@purrkit" },
  ]

messages:
  {
    author: "april",
    avatarUrl: "/avatar.webp",
    timestamp: "02:15",
    content:
      [
        "im writing my own and its gonna be so clean and im gonna do type magic god i love doing type magic",
        "its gonanbe called KITTEN and its gonna MEOW",
      ],
  }
---

import DiscordMessages from "../../components/DiscordMessages.astro";

<section>
  <header>
    <h2>what's a purrkit</h2>
    <p>purrkit fixes this™</p>
  </header>
  <div class="content">
        i write a lot of discord stuff, and every time i started to write a new bot i'd end up
        writing the exact same helpers, command loaders and interaction handlers ooover and ooooover
        again.

        it got very tiring to do this, so i started looking around for some discord.js frameworks,
        the main two i found were [sapphire](https://sapphirejs.dev/) and [commandkit](https://commandkit.dev/).

        i did start out writing sapphire, it had a fair amount of niceities, but
        there's SO. MUCH. BOILERPLATE. why am i making an entire class just to
        listen for new messages . hoe come commands have a nice pretty central
        registry but for buttons, modals, and etc. you're on your own? did they
        just give up? also maybe i was stupid but it seemed to just freaking
        swallow errors.

        this is the point where i decided i would Write my Own framework . to quote
        verbatim what i said:

        <DiscordMessages message={frontmatter.messages} />

        at that point however a friend directed me to commandkit, i had a quick
        look and i did like it, what's really interesting is that they use JSX
        for components, i really liked that approach. but you were still on your
        own for handling options and such . and there was no type magic . im a big
        fan of type magic..

    </div>

</section>

<section>
  <header>
    <h2>the type magic of slash options</h2>
    <p>"but i beloved non-null asserting Everything"</p>
  </header>
  <div class="content">
    if you've written discord.js slash commands before then you know the pain of
    options. vanilla discord.js just gives you `interaction.options.getString("name")`,
    `getInteger(...)`, `getUser(...)`, and so on.

    the problem is this returns `(string | User | number | ...) | null`. doesn't
    matter that you configured the option as required, typescript has NO idea,
    discord.js ALSO has NO idea. so you're doing `?? ""` or slapping `!` on everything
    which is just a bit nasty isn't it. THEN multiply that by 5-6 options per
    command and it's, it's just pure misery. it's gross.

    purrkit fixes this™ by making you declare your options up front as a schema
    instead of imperatively fishing for them:

    ```ts
    const whois = kitten.command("whois", {
        description: "get info about a user",
        options: {
            user: option.user.required("the user"),
            ephemeral: option.boolean.optional("hide response"),
        },
        async run(interaction, { user, ephemeral }) {
            // `user` is strictly typed as User
            // `ephemeral` is strictly typed as boolean | undefined
        },
    });
    ```

    ### how it work

    i wrote a mapped type, `InferOptions<O>`, that chews through your options object.
    checks if `required: true`, and if so rips out the inner type (`User`, `string`,
    whatever). if it's not required it unions with `undefined`.

    then at runtime kitten intercepts the command, walks the same schema, calls the
    right discord.js getter (`getUser`, etc), and just. hands you a plain object that
    matches the type EXACTLY. and it just works and it's beautiful as you can see above.

  </div>
</section>

<section>
  <header>
    <h2>the stateless component problem</h2>
    <p>"how should i store this button's data" the convenient uri-encoded custom id:</p>
  </header>
  <div class="content">
    discord components (so modals, select menus, modals and etc) are stateless
    lil kittens. all discord gives you is a `customId` string with up to 100 chars,
    which, to be fair, is probably how it should be.

    your options are pretty much:
    * generate a unique id for each component and store the state in redis or whatever,
    * or mash the state into a string like `ticket_close:ticketId`, untyped, and fragile,
      and breaks if your data has an underscore in it.

    purrkit however treats your components like commands, you provide an optiosn schema,
    and purrkit handles the encode/decode for you.

    ```ts
    const ticketBtn = kitten.button("close-ticket", {
        options: { ticketId: option.string() },
        async run(interaction, { ticketId }) {
            await interaction.reply(`closing ticket ${ticketId}...`);
        },
    });

    const button = ticketBtn.button({ ticketId: "6767" }).setLabel("close");
    ```

    now this won't necesarrily cut it for all use cases, but those use cases i
    believe are fairly rare? i haven't encountered them, for example.

    i would like to implement custom \{de,\}serialisers for custom IDs in future.

    ### how it work

    call `.button({ ticketId: "6767" })` and purrkit walks your schema
    keys, `encodeURIComponent`s each value, joins with `:`. so you get `"close-ticket:6767"`.

    purrkit also conveniently checks the lengths of the resulting custom IDs, if
    they're larger than 100 chars, it\'ll throw a `CustomIdTooLong` error.

    when the click comes back in, kitten splits on the colon, reads the schema,
    casts everything back to proper types (`"true"` -> boolean, `"67"` -> number)
    and then hands it to your `run`.

  </div>
</section>

<section>
    <header>
        <h2>mutating context without the `any`</h2>
        <p>believe it or not . purrkit fixes this™ too</p>
    </header>
    <div class="content">
        instead of middleware fetching something and throwing it into the `interaction`
        object or whatever, like SOME web frameworks, purrkit accumulates it into a fully
        typed `context` object.

        the context accumulation here is basically stolen from koa (`ctx` threading through
        middleware) crossed with trpc's procedure builder (chaining `.use()`, narrowing types
        with each call). i wanted that shape for purrkit's middleware.

        ```ts
        const authed = kitten.builder().use(async (interaction) => {
            const dbUser = await db.users.find(interaction.user.id);
            if (!dbUser) throw new HaltExecution({ content: "unauthorized" });
            return { dbUser };
        });

        authed.command("balance", {
            description: "check your balance",
            async run(interaction, args, ctx) {
                // ctx.dbUser is fully typed and guaranteed to exist here
                await interaction.reply(`balance: $${ctx.dbUser.balance}`);
            },
        });
        ```

        ### how it work

        `CommandBuilder` is generic over its context, `CommandBuilder<Ctx>`.
        every `.use(middleware)` call looks at what your middleware returns
        (`NewCtx`) and hands you back a new builder typed as `CommandBuilder<Ctx & NewCtx>`.
        intersection after intersection, it accumulates, down the whole chain,
        like a snowball, or an onion, like an ogre. layers of types.

        so by the time `.command()` gets called, `ctx` in your `run` is the
        intersection of everything every middleware in that chain returned. with
        No Globals harmed in the making.

        but what if we need to stop! you can `throw new HaltExecution()`. kitten
        catches it, optionally fires a reply, and aborts the chain.
    </div>

</section>

<section>
  <header>
    <h2>a rest client that doesn't throw</h2>
    <p class="subtitle">@purrkit/rest</p>
  </header>
  <div class="content">
    outside the router i still needed to just. talk to the discord api directly sometimes. and most fetch wrappers `throw` on 400/500 which is SO annoying because an http error code is not always a code exception

    like, if i ask for a user and they deleted their account, a 404 is a completely valid expected state, not a catastrophe. i don't want to wrap every call in a giant `try/catch` and have my error variable collapse into `unknown` mush

    so, `@purrkit/rest`. discriminated unions, my beloved:

```ts
import { RESTClient } from "@purrkit/rest";

const client = RESTClient({ token: process.env.DISCORD_TOKEN });

const user = await client.users.me.get();

// typescript forces you to check `.ok` before accessing `.data`
if (user.ok) {
  console.log(user.data.username); // UserPIIResponse
} else if (user.status === 429) {
  console.log(user.data.retry_after); // RatelimitedResponse
} else {
  console.error(user.data.message); // ErrorResponse
}
```

    **how it works:** every method resolves to either `{ ok: true, status: 200, data: T }` or `{ ok: false, status: number, data: E }`. control flow analysis means `if (user.ok)` is a real type guard, inside it `data` is narrowed to the success shape, in the `else` it's narrowed to the error shape. no casting, no praying, typescript just KNOWS

    all the response types come straight from `@purrkit/types`, autogenerated off discord's own OpenAPI spec, so it's never out of sync with reality (unless discord lies to us, which, fair, has happened)

  </div>
</section>
