pkgs/router/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 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# 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.required("the user to get information about"),
ephemeral: option.boolean.optional("whether the response should be ephemeral"),
},
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.required("the setting key"),
value: option.string.required("the value"),
},
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.required("the value"),
},
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.required("the user to ban"),
},
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!
|