# Getting Started with Kitten hi! welcome to kitten, this guide will help you get started. ## table of contents - [bootstrapping](#bootstrapping) - [defining your first command](#defining-your-first-command) - [adding subcommands](#adding-subcommands) - [context menus](#context-menus) - [interactive components](#interactive-components) - [adding autocomplete](#adding-autocomplete) - [using middleware](#using-middleware) - [handling errors](#handling-errors) - [registering our commands and components](#registering-our-commands-and-components) ## bootstrapping to get started, we need to initialise a new discord.js Client as you normally would, and then pass it to a new Kitten instance. ```ts import { Client, GatewayIntentBits } from "discord.js"; import { Kitten } from "kitten"; const client = new Client({ intents: [GatewayIntentBits.Guilds] }); const kitten = new Kitten(client); ``` next, we'll attach an event listener to the client's `clientReady` event, and call `kitten.sync()` to register our commands with the Discord API, we'll also log the client in. ```ts client.once("clientReady", async () => { // in development, it's recommened to sync commands to a single guild for // faster updates const isDev = process.env.NODE_ENV === "development"; await kitten.sync({ guildId: isDev ? process.env.GUILD_ID : undefined, }); }); await client.login(process.env.DISCORD_TOKEN); ``` and we're ready to start building out some commands and components! ## defining your first command commands are defined using `kitten.command`, options are defined dynamically which gives you type-safe parameters directly inside of your command's `run` callback, so you can do away with `interaction.options.getString("param")`. ```ts const whoisCommand = kitten.command("whois", { description: "get information about a user", options: { user: option.user("the user to get information about", { required: true }), ephemeral: option.boolean("whether the response should be ephemeral", { required: false }), }, async run(interaction, { user, ephemeral }) { await interaction.reply({ content: `User ID: ${user.id}`, ephemeral: ephemeral ?? false, }); }, }); ``` if you view the type definitions for `user` and `ephemeral`, you'll notice that they're both appropriately typed. ## adding subcommands if your command has multiple sub-actions, you can chain `.subcommand` definitions. ```ts const configCommand = kitten.command("config", { description: "configure bot settings", }); configCommand.subcommand("set", { description: "Set a configuration value", options: { key: option.string("the setting key", { required: true }), value: option.string("the value", { required: true }), }, async run(interaction, { key, value }) { await interaction.reply(`set ${key} to ${value}`); }, }); ``` ## context menus kitten supports both User and Message context menu commands with strict type inference for your targets. ```ts const reportUser = kitten.userContextMenu("Report User", { async run(interaction, ctx) { const targetUser = interaction.targetUser; // typed as User await interaction.reply({ content: `reporting ${targetUser.username}...` }); }, }); const bookmarkMessage = kitten.messageContextMenu("bookmark message", { async run(interaction, ctx) { const targetMessage = interaction.targetMessage; // typed as Message await interaction.reply({ content: `bookmarked: "${targetMessage.content}"` }); }, }); ``` ## interactive components kitten simplifies routing and parsing custom IDs for buttons, select menus, and modals. when you define a component, you define the schema of the payload you want to store in its `customId`. ```ts const closeTicketButton = kitten.button("close-ticket", { options: { ticketId: option.string(), }, async run(interaction, { ticketId }) { // ticketId: strign | undefined await interaction.reply(`closing ticket: ${ticketId}`); }, }); const button = new ButtonBuilder() .setLabel("close ticket") .setCustomId(closeTicketButton.id({ ticketId: "67" })); // serialises to `close-ticket:67` ``` kitten will automatically match incoming interactions to the `close-ticket` handler, and parse the `ticketId` parameter for you. just like with commands. modals and select menus work similarly. ```ts const modal = new ModalBuilder() // .setCustomId(myModal.id({ foo: "bar" })); // serialises to `my-modal:bar` const selectMenu = new StringSelectMenuBuilder() // .setCustomId(mySelectMenu.id({ foo: "baz" })); // serialises to `my-select-menu:baz` ``` note! custom IDs must be 100 characters or less, so be mindful of how much data you store in them. kitten will throw a `CustomIdTooLongError` if you exceed this limit. ## adding autocomplete you can define autocomplete logic directly within your option definitions. kitten handles routing the autocomplete interaction and running your helper. let's go back to our `config set` subcommand and add an autocomplete helper to the `key` option. ```ts configCommand.subcommand("set", { description: "Set a configuration value", options: { key: option.string("the option key", { required: true, async autocomplete(interaction, value) { // `value` is the current string the user has typed const keys = ["prefix", "welcomeMessage", "modLogChannel"]; const matches = keys.filter((key) => key.startsWith(value)).slice(0, 25); // and we return an array of objects with `name` and `value` properties // with a max of 25 results. return matches.map((key) => ({ name: key, value: key })); }, }), value: option.string("the value", { required: true }), }, async run(interaction, { key, value }) { await interaction.reply(`set ${key} to ${value}`); }, }); ``` ## using middleware when building larger bots, you often need to fetch database records, check permissions, or rate limit commands. kitten's middleware builder lets you chain pre-execution checks and pass their results down as typed context. ```ts const base = kitten.builder(); const authorised = base .use(async (interaction) => { const user = await db.getUser(interaction.user.id); if (!user) { await interaction.reply({ content: "no account found.", ephemeral: true }); throw new HaltExecution(); } return { user }; }) .use(async (interaction, context) => { if (!context.user.moderator) { await interaction.reply({ content: "you are not authorised to use this command.", ephemeral: true, }); throw new HaltExecution(); } return {}; }); ``` ```ts const banCommand = authorised.command("ban", { description: "ban a user", options: { user: option.user("the user to ban", { required: true }), }, async run(interaction, args, context) { // context: { user: { id: string; moderator: boolean; ... } } await banUser(args.user.id, context.user.id); await interaction.reply(`banned ${args.user.username} (${args.user.id})`); }, }); ``` ## handling errors kitten implements a multi-level, context-aware error propagation pipeline. if an error is thrown in middleware or command, your accumulated context up to that point is preserved and supplied to the handler. kitten searches for handlers up the chain: 1. local error handlers defined on the command/component configuration. 2. builder-level error handlers configured via `.onError()`. 3. global error handler configured on your `Kitten` instance. if a handler resolves without throwing, propagation stops. if it throws, that new error is propagated up to the next tier. if kitten experiences an uncaught error, it will log it to the console and reply with a generic error message to the user. ### global error handler note that context here is just typed as `any` since it's not known what the context will be at this point. though, we could possibly make use of type narrowing here in future. ```ts const kitten = new Kitten(client, { onError(err, interaction, ctx) { console.error("uncaught global exception:", err); }, }); kitten.onError((err, interaction, ctx) => { console.error("uncaught global exception:", err); }); ``` ### builder-level error handler ```ts const accountGroup = kitten .builder() .use(async (interaction) => { const account = await whatever(); return { account }; }) .onError(async (err, interaction, ctx) => { // ctx.account is accessible if the error occurred after resolved. console.error(`failure on account: ${ctx.account?.id}`, err); if (interaction.isRepliable()) { await interaction.reply({ content: "command could not be processed.", flags: ["Ephemeral"], }); } }); ``` ### local error handler ```ts accountGroup.command("account", { description: "manage your account", options: { username: option.string({ description: "your username", required: true, }), }, onError(err, interaction, ctx) { // handles only failures specific to this command. console.warn(`failed to execute command for ${ctx.account?.id}`, err); }, async run(interaction, { amount }, ctx) { await whatever2(); await interaction.reply("Invoice paid successfully!"); }, }); ``` ## registering our commands and components and we're almost there! the final step is to register our commands and components with kitten. you should register all of your commands and components before calling `kitten.sync()`, otherwise they won't be registered with the Discord API. ```ts kitten.register({ commands: [whoisCommand, configCommand, banCommand, reportUser, bookmarkMessage], components: [closeTicketButton], }); ``` ## all done!