GET-STARTED.md (view raw)
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
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!
|