all repos — kitten @ 84532fc0726e827829bfb3b608a7194c272dcdd1

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
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);
```