DESIGN.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 |
this file maps out the design of kitten
my goal with kitten is to write a light abstraction atop discord.js to provide
a simple but powerful sort of interaction routing system. and it's fully type-safe,
too.
## commands
we define a command with `kitten.command`, options are typed dynamically so
you get awesome typesafety in the `run` callback.
```ts
const whois = 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 to hide the response"),
},
async run(interaction, { user, ephemeral }) {
// user: GuildMember/User
// ephemeral: boolean | undefined
},
});
```
subcommands are similar.
```ts
const command = kitten.command("command", {
description: "a command with subcommands",
});
command.subcommand("sub1", {
description: "the first subcommand",
options: {
user: option.user("a user option", { required: true }),
},
async run(interaction, { user }) {
// user: GuildMember/User
},
});
```
kitten will handle registering commands and running commands when they're invoked.
## component routing
buttons, modals, and select menus work similarly.
```ts
const banButton = kitten.button("close-ticket", {
options: {
ticketId: option.string(),
},
async run(interaction, { ticketId }) {
// ticketId: string | undefined
},
});
const button = new ButtonBuilder().setCustomId(
banButton.id({ ticketId: "1234" }),
);
```
this would serialise to `close-ticket:1234`
## autocomplete
you can deifne autocomplete handlers inside of optino definitions.
```ts
const repoSearch = kitten.command("repo", {
description: "search a github repository",
options: {
name: option.string("the repository name", {
required: true,
async autocomplete(interaction, value) {
const matches = await searchRepos(value);
return matches.map((r) => ({ name: r.name, value: r.id }));
},
}),
},
async run(interaction, { name }) {},
});
```
## middleware
```ts
const base = kitten.builder();
const authed = base
.use(async (interaction) => {
const dbUser = await db.getUser(interaction.user.id);
if (!dbUser) {
await interaction.reply({ content: "no account found", ephemeral: true });
throw new HaltExecution();
}
return { dbUser };
})
.use(async (interaction, ctx) => {
// ctx.dbUser: DbUser
const isPremium = ctx.dbUser.subscription === "premium";
return { isPremium };
});
const premiumStats = authed.command("meow", {
description: "meow",
async run(interaction, args, ctx) {
// ctx.dbUser is typed
// ctx.isPremium is typed
},
});
```
## usage
```ts
import { Client, GatewayIntentBits } from "discord.js";
import { Kitten } from "kitten";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const kitten = new Kitten(client);
import * as commands from "./commands";
import * as buttons from "./components";
kitten.register({ commands, components });
client.once("ready", async () => {
const isDev = process.env.NODE_ENV === "development";
try {
await kitten.sync({
guildId: isDev ? process.env.GUILD_ID : undefined,
});
} catch (error) {
console.error("failed to sync commands:", error);
}
});
await client.login(process.env.BOT_TOKEN);
```
|