spearkit 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/spearkit/SKILL.md +236 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +218 -0
- package/AGENTS.md +164 -0
- package/README.md +13 -0
- package/docs/README.md +63 -0
- package/docs/api-reference.md +589 -0
- package/docs/client.md +207 -0
- package/docs/commands.md +198 -0
- package/docs/components.md +274 -0
- package/docs/context.md +201 -0
- package/docs/cooldown.md +124 -0
- package/docs/env.md +130 -0
- package/docs/events.md +152 -0
- package/docs/getting-started.md +147 -0
- package/docs/loading.md +142 -0
- package/docs/logging.md +195 -0
- package/docs/migration.md +160 -0
- package/docs/options.md +163 -0
- package/docs/plugins.md +116 -0
- package/docs/prefix.md +180 -0
- package/docs/scheduler.md +87 -0
- package/docs/usage.md +178 -0
- package/llms-full.txt +3367 -0
- package/llms.txt +39 -0
- package/package.json +9 -3
package/docs/client.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Client
|
|
2
|
+
|
|
3
|
+
`SpearClient` is a discord.js `Client` with command, event and component
|
|
4
|
+
registries — plus interaction routing — wired up for you. You construct it the
|
|
5
|
+
same way you construct a discord.js client, register your handlers, log in, and
|
|
6
|
+
(optionally) push your slash commands to Discord.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { SpearClient, Intents } from "spearkit";
|
|
10
|
+
|
|
11
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Constructing a client
|
|
15
|
+
|
|
16
|
+
`new SpearClient(options?)` takes the same options as discord.js'
|
|
17
|
+
`ClientOptions`, except `intents` may be omitted: it defaults to
|
|
18
|
+
`Intents.default` (just the `Guilds` intent, enough for slash commands and
|
|
19
|
+
interactions).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { SpearClient, Intents } from "spearkit";
|
|
23
|
+
|
|
24
|
+
// Explicit preset.
|
|
25
|
+
const a = new SpearClient({ intents: Intents.messages });
|
|
26
|
+
|
|
27
|
+
// Omitted — falls back to Intents.default.
|
|
28
|
+
const b = new SpearClient();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The options type is exported as `SpearClientOptions` (`Partial<ClientOptions>`),
|
|
32
|
+
so every other discord.js option (`partials`, `presence`, `sweepers`, …) is
|
|
33
|
+
available.
|
|
34
|
+
|
|
35
|
+
### Intents presets
|
|
36
|
+
|
|
37
|
+
`Intents` is a set of ready-made arrays of `GatewayIntentBits`. Pass one as
|
|
38
|
+
`intents`, or compose your own array of `GatewayIntentBits` if you need
|
|
39
|
+
something in between.
|
|
40
|
+
|
|
41
|
+
| Preset | Contents |
|
|
42
|
+
| ------ | -------- |
|
|
43
|
+
| `Intents.none` | `[]` |
|
|
44
|
+
| `Intents.default` | `[Guilds]` |
|
|
45
|
+
| `Intents.guilds` | `[Guilds, GuildMembers]` |
|
|
46
|
+
| `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
|
|
47
|
+
| `Intents.all` | Every intent, including privileged ones. |
|
|
48
|
+
|
|
49
|
+
`Intents.messages` includes `MessageContent`, and `Intents.guilds` includes
|
|
50
|
+
`GuildMembers` — both are **privileged intents**. You must enable them in the
|
|
51
|
+
Discord developer portal for your application, otherwise the gateway will reject
|
|
52
|
+
the connection. `Intents.all` includes every privileged intent for the same
|
|
53
|
+
reason.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { SpearClient, GatewayIntentBits } from "spearkit";
|
|
57
|
+
|
|
58
|
+
// A custom intent set, mixing a preset idea with explicit bits.
|
|
59
|
+
const client = new SpearClient({
|
|
60
|
+
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## The three registries
|
|
65
|
+
|
|
66
|
+
Every client owns three registries, each populated by `register` (or `load`):
|
|
67
|
+
|
|
68
|
+
| Registry | Property | Holds |
|
|
69
|
+
| -------- | -------- | ----- |
|
|
70
|
+
| `CommandRegistry` | `client.commands` | Slash commands; dispatches chat-input and autocomplete interactions. |
|
|
71
|
+
| `EventRegistry` | `client.events` | Event listeners; attached to the client automatically. |
|
|
72
|
+
| `ComponentRegistry` | `client.components` | Buttons, selects and modals; routes component interactions by custom id. |
|
|
73
|
+
|
|
74
|
+
You rarely touch them directly — `register` routes items into the right one —
|
|
75
|
+
but they are public if you need to inspect or manipulate them (e.g.
|
|
76
|
+
`client.commands.size`, `client.commands.toJSON()`).
|
|
77
|
+
|
|
78
|
+
## Registering handlers
|
|
79
|
+
|
|
80
|
+
`client.register(...items)` accepts commands, events and components in a single
|
|
81
|
+
call and routes each to its registry by kind. The accepted union is exported as
|
|
82
|
+
`Registerable` (`SlashCommand | EventDef | ComponentDef`). It returns the client
|
|
83
|
+
for chaining.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { SpearClient, command, event, button, option } from "spearkit";
|
|
87
|
+
|
|
88
|
+
const client = new SpearClient();
|
|
89
|
+
|
|
90
|
+
const greet = command({
|
|
91
|
+
name: "greet",
|
|
92
|
+
description: "Greet someone",
|
|
93
|
+
options: { who: option.user({ description: "Who", required: true }) },
|
|
94
|
+
run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const ready = event("clientReady", (c) => {
|
|
98
|
+
console.log(`Logged in as ${c.user.tag}`); // c: Client<true>
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const ping = button({
|
|
102
|
+
id: "ping:{n}",
|
|
103
|
+
label: "Ping",
|
|
104
|
+
run: (ctx) => ctx.reply(`pong #${ctx.params.n}`), // n: string
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Commands, events and components in one call.
|
|
108
|
+
client.register(greet, ready, ping);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Plugins
|
|
112
|
+
|
|
113
|
+
`client.use(...plugins)` installs one or more plugins, awaiting each plugin's
|
|
114
|
+
`setup`. It is async and returns the client.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { SpearClient } from "spearkit";
|
|
118
|
+
import { statsPlugin } from "./plugins/stats.js";
|
|
119
|
+
|
|
120
|
+
const client = new SpearClient();
|
|
121
|
+
await client.use(statsPlugin);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
See [Plugins](./plugins.md) for authoring `SpearPlugin`s.
|
|
125
|
+
|
|
126
|
+
## File-based loading
|
|
127
|
+
|
|
128
|
+
`client.load(dir, options?)` recursively imports a directory and registers every
|
|
129
|
+
command, event and component it exports. It returns the number of items
|
|
130
|
+
registered.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { SpearClient } from "spearkit";
|
|
134
|
+
|
|
135
|
+
const client = new SpearClient();
|
|
136
|
+
const count = await client.load("./src/commands");
|
|
137
|
+
console.log(`Loaded ${count} handlers`);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
See [File-based loading](./loading.md) for the layout and `LoadOptions`.
|
|
141
|
+
|
|
142
|
+
## Starting and deploying
|
|
143
|
+
|
|
144
|
+
`client.start(token?)` logs in. If you omit the token it falls back to the
|
|
145
|
+
`DISCORD_TOKEN` environment variable, and throws if neither is present.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { SpearClient } from "spearkit";
|
|
149
|
+
|
|
150
|
+
const client = new SpearClient();
|
|
151
|
+
|
|
152
|
+
// Pass a token explicitly…
|
|
153
|
+
await client.start("your-token");
|
|
154
|
+
|
|
155
|
+
// …or set DISCORD_TOKEN and call start() with no argument.
|
|
156
|
+
await client.start();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`client.deployCommands({ guildId })` pushes the registered slash commands to
|
|
160
|
+
Discord using the client's own authenticated REST connection — there is no
|
|
161
|
+
separate token or application id to supply. Because it reads the application id
|
|
162
|
+
from the logged-in client, it **must run after the client is ready**. Pass a
|
|
163
|
+
`guildId` to deploy instantly to a single guild (ideal for development); omit it
|
|
164
|
+
to deploy globally.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { SpearClient, Intents } from "spearkit";
|
|
168
|
+
|
|
169
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
170
|
+
// …register commands…
|
|
171
|
+
|
|
172
|
+
await client.start(); // uses DISCORD_TOKEN
|
|
173
|
+
|
|
174
|
+
// Deploy once the client is ready.
|
|
175
|
+
client.once("clientReady", async () => {
|
|
176
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Everything discord.js still works
|
|
181
|
+
|
|
182
|
+
`SpearClient` extends discord.js `Client`, so the full client surface is
|
|
183
|
+
available unchanged. spearkit adds registries on top — it never hides what is
|
|
184
|
+
underneath:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { SpearClient } from "spearkit";
|
|
188
|
+
|
|
189
|
+
const client = new SpearClient();
|
|
190
|
+
|
|
191
|
+
client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
|
|
192
|
+
client.ws.on("VOICE_SERVER_UPDATE", () => {});
|
|
193
|
+
|
|
194
|
+
await client.start();
|
|
195
|
+
|
|
196
|
+
console.log(client.application?.id); // application
|
|
197
|
+
console.log(client.user?.tag); // user
|
|
198
|
+
console.log(client.rest); // REST manager (used by deployCommands)
|
|
199
|
+
|
|
200
|
+
await client.destroy(); // graceful shutdown
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## See also
|
|
204
|
+
|
|
205
|
+
- [Commands](./commands.md) — defining slash commands you register here.
|
|
206
|
+
- [Plugins](./plugins.md) — bundling features for `client.use`.
|
|
207
|
+
- [File-based loading](./loading.md) — populating the client from a directory.
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
Slash commands in spearkit are defined as a single object: the metadata, the typed
|
|
4
|
+
options, and the handler all live together. spearkit serialises them for discord
|
|
5
|
+
and routes incoming interactions to the right handler for you.
|
|
6
|
+
|
|
7
|
+
## A first command
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { command } from "spearkit";
|
|
11
|
+
|
|
12
|
+
export const ping = command({
|
|
13
|
+
name: "ping",
|
|
14
|
+
description: "Check latency",
|
|
15
|
+
run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Register it on a client (`client.register(ping)`) and deploy it (see
|
|
20
|
+
[Deployment](#deployment)). That's the whole loop.
|
|
21
|
+
|
|
22
|
+
## The command context
|
|
23
|
+
|
|
24
|
+
The handler receives a `CommandContext`. It wraps the discord.js
|
|
25
|
+
`ChatInputCommandInteraction` and adds ergonomic accessors and reply helpers.
|
|
26
|
+
|
|
27
|
+
| Member | Description |
|
|
28
|
+
| ------ | ----------- |
|
|
29
|
+
| `ctx.options` | Resolved, fully-typed option values (see [Options](./options.md)). |
|
|
30
|
+
| `ctx.commandName` | The invoked command name. |
|
|
31
|
+
| `ctx.subcommand` | The invoked subcommand name, or `null`. |
|
|
32
|
+
| `ctx.showModal(modal)` | Present a modal in response. |
|
|
33
|
+
| `ctx.user` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` / `ctx.locale` | Actor and location accessors. |
|
|
34
|
+
| `ctx.reply` / `ctx.replyEphemeral` / `ctx.defer` / `ctx.editReply` / `ctx.followUp` / `ctx.send` / `ctx.error` | Reply helpers (see [Contexts](./context.md)). |
|
|
35
|
+
| `ctx.interaction` | The raw discord.js interaction, for anything not wrapped. |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { command, option } from "spearkit";
|
|
39
|
+
|
|
40
|
+
export const echo = command({
|
|
41
|
+
name: "echo",
|
|
42
|
+
description: "Repeat a message",
|
|
43
|
+
options: {
|
|
44
|
+
text: option.string({ description: "What to say", required: true }),
|
|
45
|
+
times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
|
|
46
|
+
},
|
|
47
|
+
run: (ctx) => {
|
|
48
|
+
ctx.options.text; // string
|
|
49
|
+
ctx.options.times; // number | undefined
|
|
50
|
+
return ctx.reply({
|
|
51
|
+
content: ctx.options.text.repeat(ctx.options.times ?? 1),
|
|
52
|
+
ephemeral: true,
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Options are covered in depth in [Options](./options.md).
|
|
59
|
+
|
|
60
|
+
## Command metadata
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { command, PermissionFlagsBits } from "spearkit";
|
|
64
|
+
|
|
65
|
+
export const purge = command({
|
|
66
|
+
name: "purge",
|
|
67
|
+
description: "Delete recent messages",
|
|
68
|
+
guildOnly: true, // only usable in guilds
|
|
69
|
+
nsfw: false, // age-restricted command
|
|
70
|
+
defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
|
|
71
|
+
nameLocalizations: { tr: "temizle" }, // localized name
|
|
72
|
+
descriptionLocalizations: { tr: "Mesajları sil" },
|
|
73
|
+
run: (ctx) => ctx.reply("…"),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Field | Type | Effect |
|
|
78
|
+
| ----- | ---- | ------ |
|
|
79
|
+
| `guildOnly` | `boolean` | Restricts the command to guild contexts. |
|
|
80
|
+
| `nsfw` | `boolean` | Marks the command age-restricted. |
|
|
81
|
+
| `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate (members without it don't see the command). |
|
|
82
|
+
| `nameLocalizations` / `descriptionLocalizations` | `LocalizationMap` | Per-locale name/description. |
|
|
83
|
+
|
|
84
|
+
## Subcommands and groups
|
|
85
|
+
|
|
86
|
+
For commands with subcommands, use `commandGroup` together with `subcommand`
|
|
87
|
+
and (optionally) `subcommandGroup`. Each subcommand has its own typed options
|
|
88
|
+
and handler; spearkit routes to the right one automatically.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";
|
|
92
|
+
|
|
93
|
+
export const admin = commandGroup({
|
|
94
|
+
name: "admin",
|
|
95
|
+
description: "Administration",
|
|
96
|
+
guildOnly: true,
|
|
97
|
+
// Direct subcommands: /admin say
|
|
98
|
+
subcommands: {
|
|
99
|
+
say: subcommand({
|
|
100
|
+
description: "Make the bot say something",
|
|
101
|
+
options: { message: option.string({ description: "Message", required: true }) },
|
|
102
|
+
run: (ctx) => ctx.reply(ctx.options.message),
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
// Grouped subcommands: /admin users ban
|
|
106
|
+
groups: {
|
|
107
|
+
users: subcommandGroup({
|
|
108
|
+
description: "Manage users",
|
|
109
|
+
subcommands: {
|
|
110
|
+
ban: subcommand({
|
|
111
|
+
description: "Ban a member",
|
|
112
|
+
options: {
|
|
113
|
+
target: option.user({ description: "Member", required: true }),
|
|
114
|
+
reason: option.string({ description: "Reason" }),
|
|
115
|
+
},
|
|
116
|
+
run: (ctx) =>
|
|
117
|
+
ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Inside a subcommand handler, `ctx.options` is typed from *that subcommand's*
|
|
126
|
+
options. There is no `switch (subcommand)` to write — spearkit dispatches by the
|
|
127
|
+
invoked subcommand group/name.
|
|
128
|
+
|
|
129
|
+
## The command registry
|
|
130
|
+
|
|
131
|
+
`client.commands` is a `CommandRegistry`. You usually feed it through
|
|
132
|
+
`client.register(...)`, but you can use it directly:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { CommandRegistry } from "spearkit";
|
|
136
|
+
|
|
137
|
+
const registry = new CommandRegistry();
|
|
138
|
+
registry.add(ping, echo, admin);
|
|
139
|
+
|
|
140
|
+
registry.get("ping"); // SlashCommand | undefined
|
|
141
|
+
registry.names; // string[]
|
|
142
|
+
registry.size; // number
|
|
143
|
+
registry.remove("ping"); // boolean
|
|
144
|
+
registry.toJSON(); // REST payloads for all commands
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`SpearClient` calls `registry.handle(interaction)` and
|
|
148
|
+
`registry.handleAutocomplete(interaction)` for you on every interaction.
|
|
149
|
+
|
|
150
|
+
### Error handling
|
|
151
|
+
|
|
152
|
+
If a handler throws, spearkit catches it. By default it emits the client's `error`
|
|
153
|
+
event and replies with an ephemeral "something went wrong" message. Override
|
|
154
|
+
that:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
client.commands.onError((error, interaction) => {
|
|
158
|
+
console.error(`/${interaction.commandName} failed`, error);
|
|
159
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
160
|
+
return interaction.reply({ content: "Command failed.", ephemeral: true });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Deployment
|
|
166
|
+
|
|
167
|
+
Commands must be registered with discord before they appear. spearkit gives you two
|
|
168
|
+
ways.
|
|
169
|
+
|
|
170
|
+
**From the client** (uses the client's authenticated REST; call after ready):
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
174
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for global
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Standalone** (a separate deploy script, no running client needed):
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { CommandRegistry } from "spearkit";
|
|
181
|
+
|
|
182
|
+
const registry = new CommandRegistry().add(ping, echo, admin);
|
|
183
|
+
await registry.deploy({
|
|
184
|
+
token: process.env.DISCORD_TOKEN,
|
|
185
|
+
applicationId: process.env.DISCORD_APP_ID,
|
|
186
|
+
guildId: process.env.GUILD_ID, // optional
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Guild deploys apply **instantly** and are ideal during development. Global
|
|
191
|
+
deploys (no `guildId`) can take up to an hour to propagate.
|
|
192
|
+
|
|
193
|
+
## See also
|
|
194
|
+
|
|
195
|
+
- [Options](./options.md) — typed option builders, choices, autocomplete.
|
|
196
|
+
- [Components](./components.md) — buttons, selects, modals.
|
|
197
|
+
- [Client](./client.md) — registering and deploying from the client.
|
|
198
|
+
- [Contexts](./context.md) — the reply helpers every handler shares.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
Buttons, select menus and modals in spearkit follow one pattern: define the
|
|
4
|
+
appearance, the **custom-id pattern**, and the handler in one place; register
|
|
5
|
+
it; then `build()` the discord.js component to put in a message. spearkit decodes
|
|
6
|
+
incoming interactions and routes them to your handler — no `interactionCreate`
|
|
7
|
+
switch statements, no manual custom-id parsing.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { button, row } from "spearkit";
|
|
11
|
+
|
|
12
|
+
const vote = button({
|
|
13
|
+
id: "vote:{choice}",
|
|
14
|
+
label: "Yes",
|
|
15
|
+
style: "Success",
|
|
16
|
+
run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // ctx.params.choice: string
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
client.register(vote); // or client.components.add(vote)
|
|
20
|
+
|
|
21
|
+
await channel.send({
|
|
22
|
+
content: "Cast your vote:",
|
|
23
|
+
components: [row(vote.build({ choice: "yes" }))], // build() requires { choice }
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Custom-id patterns
|
|
28
|
+
|
|
29
|
+
The `id` is a pattern with the grammar `name` or `name:{param}` or
|
|
30
|
+
`name:{a}:{b}`. The leading `name` is the routing **namespace**; each `{param}`
|
|
31
|
+
becomes a positional value carried in the custom-id.
|
|
32
|
+
|
|
33
|
+
- In the handler, params are available as a typed object: `ctx.params.choice`.
|
|
34
|
+
- `build(params)` requires **exactly** those params and encodes them into the
|
|
35
|
+
custom-id.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const page = button({
|
|
39
|
+
id: "page:{id}:{dir}",
|
|
40
|
+
label: "Next",
|
|
41
|
+
run: (ctx) => ctx.update(`item ${ctx.params.id}, going ${ctx.params.dir}`),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
page.build({ id: "42", dir: "next" }); // custom-id "page:42:next"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
spearkit percent-escapes param values, so they may safely contain `:`. Custom-ids
|
|
48
|
+
are limited to 100 characters (`MAX_CUSTOM_ID_LENGTH`); `build()` throws if you
|
|
49
|
+
exceed it.
|
|
50
|
+
|
|
51
|
+
For advanced use, the codec is exported directly: `compilePattern`,
|
|
52
|
+
`buildCustomId`, `parseCustomId`, and `paramsFromValues`.
|
|
53
|
+
|
|
54
|
+
## Buttons
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { button, linkButton, ButtonStyle } from "spearkit";
|
|
58
|
+
|
|
59
|
+
const confirm = button({
|
|
60
|
+
id: "confirm:{action}",
|
|
61
|
+
label: "Confirm",
|
|
62
|
+
style: ButtonStyle.Danger, // or the string "Danger"
|
|
63
|
+
emoji: "⚠️",
|
|
64
|
+
disabled: false,
|
|
65
|
+
run: (ctx) => ctx.update(`Confirmed: ${ctx.params.action}`),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Link buttons have no handler and no custom-id:
|
|
69
|
+
const docs = linkButton({ url: "https://example.com", label: "Docs" });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`style` accepts the string names `"Primary"`, `"Secondary"`, `"Success"`,
|
|
73
|
+
`"Danger"`, or the `ButtonStyle` enum. It defaults to `"Secondary"`.
|
|
74
|
+
|
|
75
|
+
The `ButtonContext` adds, on top of the shared [reply helpers](./context.md):
|
|
76
|
+
|
|
77
|
+
| Member | Description |
|
|
78
|
+
| ------ | ----------- |
|
|
79
|
+
| `ctx.params` | Decoded custom-id params. |
|
|
80
|
+
| `ctx.update(input)` | Edit the message the button is on. |
|
|
81
|
+
| `ctx.deferUpdate()` | Acknowledge without editing yet. |
|
|
82
|
+
| `ctx.showModal(modal)` | Open a modal in response. |
|
|
83
|
+
| `ctx.message` | The message the button belongs to. |
|
|
84
|
+
| `ctx.customId` | The raw custom-id. |
|
|
85
|
+
|
|
86
|
+
## Select menus
|
|
87
|
+
|
|
88
|
+
There are five select builders. All share `placeholder`, `minValues`,
|
|
89
|
+
`maxValues`, and `disabled`; the string select additionally takes `options`,
|
|
90
|
+
and the channel select takes `channelTypes`.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { stringSelect, channelSelect, ChannelType } from "spearkit";
|
|
94
|
+
|
|
95
|
+
const colour = stringSelect({
|
|
96
|
+
id: "colour",
|
|
97
|
+
placeholder: "Pick a colour",
|
|
98
|
+
minValues: 1,
|
|
99
|
+
maxValues: 1,
|
|
100
|
+
options: [
|
|
101
|
+
{ label: "Red", value: "red" },
|
|
102
|
+
{ label: "Green", value: "green", description: "the calm one" },
|
|
103
|
+
{ label: "Blue", value: "blue", default: true },
|
|
104
|
+
],
|
|
105
|
+
run: (ctx) => ctx.reply({ content: `You picked ${ctx.values.join(", ")}`, ephemeral: true }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const pickChannel = channelSelect({
|
|
109
|
+
id: "pick-channel",
|
|
110
|
+
channelTypes: [ChannelType.GuildText],
|
|
111
|
+
run: (ctx) => ctx.reply({ content: `${ctx.values.length} channel(s)`, ephemeral: true }),
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each select context exposes the relevant resolved data:
|
|
116
|
+
|
|
117
|
+
| Builder | Context | Extra accessors |
|
|
118
|
+
| ------- | ------- | --------------- |
|
|
119
|
+
| `stringSelect` | `StringSelectContext` | `values: string[]`, `value: string \| undefined` |
|
|
120
|
+
| `userSelect` | `UserSelectContext` | `values`, `users`, `members` |
|
|
121
|
+
| `roleSelect` | `RoleSelectContext` | `values`, `roles` |
|
|
122
|
+
| `channelSelect` | `ChannelSelectContext` | `values`, `channels` |
|
|
123
|
+
| `mentionableSelect` | `MentionableSelectContext` | `values`, `users`, `roles`, `members` |
|
|
124
|
+
|
|
125
|
+
Select contexts also have `ctx.params`, `ctx.update`, `ctx.deferUpdate`,
|
|
126
|
+
`ctx.showModal`, and the shared reply helpers.
|
|
127
|
+
|
|
128
|
+
## Modals
|
|
129
|
+
|
|
130
|
+
A modal declares its `fields` as a map of name → `textInput`. The submit handler
|
|
131
|
+
receives the submitted values in `ctx.fields`, keyed (and typed) by those names,
|
|
132
|
+
plus any custom-id params in `ctx.params`.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { modal, textInput } from "spearkit";
|
|
136
|
+
|
|
137
|
+
const feedback = modal({
|
|
138
|
+
id: "feedback:{ticket}",
|
|
139
|
+
title: "Feedback",
|
|
140
|
+
fields: {
|
|
141
|
+
summary: textInput({ label: "Summary", required: true }),
|
|
142
|
+
detail: textInput({ label: "Details", style: "Paragraph", maxLength: 2000 }),
|
|
143
|
+
},
|
|
144
|
+
run: (ctx) =>
|
|
145
|
+
ctx.reply({
|
|
146
|
+
// ctx.params.ticket: string, ctx.fields.summary / ctx.fields.detail: string
|
|
147
|
+
content: `#${ctx.params.ticket}: ${ctx.fields.summary}`,
|
|
148
|
+
ephemeral: true,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`textInput` config: `label` (required), `style` (`"Short"` default, or
|
|
154
|
+
`"Paragraph"`, or a `TextInputStyle`), `placeholder`, `required`, `minLength`,
|
|
155
|
+
`maxLength`, `value`.
|
|
156
|
+
|
|
157
|
+
Open a modal from a command or a component handler with `showModal` — modals
|
|
158
|
+
cannot be the *response* to another modal, but they can follow a command or a
|
|
159
|
+
button/select:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { command } from "spearkit";
|
|
163
|
+
|
|
164
|
+
const ask = command({
|
|
165
|
+
name: "ask",
|
|
166
|
+
description: "Open the feedback form",
|
|
167
|
+
run: (ctx) => ctx.showModal(feedback.build({ ticket: "1234" })),
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Action rows
|
|
172
|
+
|
|
173
|
+
`row(...components)` wraps builders in an `ActionRowBuilder`. A row holds up to
|
|
174
|
+
five buttons, or exactly one select menu.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { row } from "spearkit";
|
|
178
|
+
|
|
179
|
+
const components = [
|
|
180
|
+
row(confirm.build({ action: "delete" }), docs),
|
|
181
|
+
row(colour.build()),
|
|
182
|
+
];
|
|
183
|
+
await channel.send({ content: "Choose:", components });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Registering and routing
|
|
187
|
+
|
|
188
|
+
Register components like anything else:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
client.register(vote, colour, feedback);
|
|
192
|
+
// equivalently:
|
|
193
|
+
client.components.add(vote, colour, feedback);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`SpearClient` routes every button, select and modal interaction to the matching
|
|
197
|
+
namespace automatically. The `ComponentRegistry` API:
|
|
198
|
+
|
|
199
|
+
| Member | Description |
|
|
200
|
+
| ------ | ----------- |
|
|
201
|
+
| `add(...defs)` | Register components (override by namespace). |
|
|
202
|
+
| `size` | Number registered. |
|
|
203
|
+
| `onError(handler)` | Set the error handler. |
|
|
204
|
+
| `handle(interaction)` | Route an interaction; returns `true` if matched. |
|
|
205
|
+
|
|
206
|
+
### Error handling
|
|
207
|
+
|
|
208
|
+
By default a throwing handler emits the client `error` event and replies with an
|
|
209
|
+
ephemeral message. Customise it:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
client.components.onError((error, interaction) => {
|
|
213
|
+
console.error("component failed", error);
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## End-to-end example
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import {
|
|
221
|
+
SpearClient,
|
|
222
|
+
Intents,
|
|
223
|
+
command,
|
|
224
|
+
button,
|
|
225
|
+
stringSelect,
|
|
226
|
+
modal,
|
|
227
|
+
textInput,
|
|
228
|
+
row,
|
|
229
|
+
} from "spearkit";
|
|
230
|
+
|
|
231
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
232
|
+
|
|
233
|
+
const open = button({
|
|
234
|
+
id: "open-form:{topic}",
|
|
235
|
+
label: "Open form",
|
|
236
|
+
style: "Primary",
|
|
237
|
+
run: (ctx) => ctx.showModal(form.build({ topic: ctx.params.topic })),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const rating = stringSelect({
|
|
241
|
+
id: "rating",
|
|
242
|
+
placeholder: "Rate us",
|
|
243
|
+
options: [
|
|
244
|
+
{ label: "Good", value: "good" },
|
|
245
|
+
{ label: "Bad", value: "bad" },
|
|
246
|
+
],
|
|
247
|
+
run: (ctx) => ctx.reply({ content: `Thanks: ${ctx.value}`, ephemeral: true }),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const form = modal({
|
|
251
|
+
id: "form:{topic}",
|
|
252
|
+
title: "Tell us more",
|
|
253
|
+
fields: { body: textInput({ label: "Message", style: "Paragraph", required: true }) },
|
|
254
|
+
run: (ctx) => ctx.reply({ content: `[${ctx.params.topic}] ${ctx.fields.body}`, ephemeral: true }),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const panel = command({
|
|
258
|
+
name: "panel",
|
|
259
|
+
description: "Show the panel",
|
|
260
|
+
run: (ctx) =>
|
|
261
|
+
ctx.reply({
|
|
262
|
+
content: "How was it?",
|
|
263
|
+
components: [row(open.build({ topic: "support" })), row(rating.build())],
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
client.register(panel, open, rating, form);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## See also
|
|
271
|
+
|
|
272
|
+
- [Commands](./commands.md) — opening components from commands.
|
|
273
|
+
- [Contexts](./context.md) — the reply/update helpers contexts share.
|
|
274
|
+
- [Client](./client.md) — registration and routing.
|