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) - [interactive components](#interactive-components) - [adding autocomplete](#adding-autocomplete) - [using middleware](#using-middleware) - [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}`); }, }); ``` ## 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})`); }, }); ``` ## 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], buttons: [closeTicketButton], }); ``` ## all done!