spearkit 0.3.1 → 0.4.0
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 +11 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +117 -6
- package/AGENTS.md +98 -1
- package/README.md +10 -3
- package/dist/index.cjs +599 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -2
- package/dist/index.d.ts +524 -2
- package/dist/index.js +576 -19
- package/dist/index.js.map +1 -1
- package/docs/README.md +21 -12
- package/docs/api-reference.md +222 -34
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +60 -22
- package/docs/collectors.md +65 -0
- package/docs/commands.md +5 -0
- package/docs/components.md +7 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +94 -2
- package/docs/cooldown.md +2 -1
- package/docs/errors.md +73 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +7 -5
- package/docs/messages.md +35 -0
- package/docs/permissions.md +68 -0
- package/docs/prefix.md +54 -0
- package/docs/scheduler.md +26 -2
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +20 -10
- package/llms-full.txt +1337 -85
- package/llms.txt +91 -3
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# spearkit v0.
|
|
1
|
+
# spearkit v0.4.0 — full documentation for LLMs
|
|
2
2
|
|
|
3
3
|
> discord.js++ — a developer-experience-first Discord library. Drop-in compatible with discord.js, with ergonomic events, slash commands, and interactive components.
|
|
4
4
|
|
|
@@ -6,6 +6,87 @@ This file concatenates every spearkit guide and the complete API reference. It i
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## Use cases — reach for
|
|
10
|
+
|
|
11
|
+
**Bot setup & lifecycle**
|
|
12
|
+
|
|
13
|
+
| Want to… | Reach for |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| Start a bot and connect | `new SpearClient({ intents })` + `await client.start(token)` |
|
|
16
|
+
| Choose gateway intents | `Intents.none / default / guilds / messages / all` |
|
|
17
|
+
| Wire up handlers | `client.register(...)`; one file per handler → `client.load(dir)` |
|
|
18
|
+
| Push commands to Discord | `client.deployCommands({ guildId })`; slash + context menus, safe CI → `client.deployAllCommands({ strategy: "diff", dryRun })` |
|
|
19
|
+
| Package a reusable feature | `definePlugin(...)` + `client.use(...)` |
|
|
20
|
+
| Migrate an existing discord.js bot | import from `"spearkit"`, swap `Client` → `SpearClient` |
|
|
21
|
+
|
|
22
|
+
**Commands & input**
|
|
23
|
+
|
|
24
|
+
| Want to… | Reach for |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| A slash command | `command({ name, description, run })` |
|
|
27
|
+
| Typed inputs to a command | `options: { x: option.string/integer/number/boolean/user/channel/role/mentionable/attachment(...) }` |
|
|
28
|
+
| Group many commands under one name | `commandGroup` + `subcommand` / `subcommandGroup` |
|
|
29
|
+
| Suggest values while the user types | `option.string({ autocomplete })` |
|
|
30
|
+
| A right-click "Apps" action on a user/message | `userCommand` / `messageCommand` |
|
|
31
|
+
| A classic `!text` command | `prefixCommand(...)` + `new SpearClient({ prefix })` |
|
|
32
|
+
| Parse `!cmd` arguments into typed values | `args: (a) => a.snowflake().duration().rest()` → `ctx.options` |
|
|
33
|
+
| Avoid `Unknown interaction` (10062) on slow work | `command({ autoDefer: true })` / `new SpearClient({ autoDefer: true })` |
|
|
34
|
+
|
|
35
|
+
**Interactivity (components)**
|
|
36
|
+
|
|
37
|
+
| Want to… | Reach for |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| A clickable button | `button({ id, run })` → `row(btn.build(...))` |
|
|
40
|
+
| A URL button (no handler) | `linkButton` |
|
|
41
|
+
| A dropdown of fixed options | `stringSelect` |
|
|
42
|
+
| Pick users / roles / channels / mentionables | `userSelect` / `roleSelect` / `channelSelect` / `mentionableSelect` |
|
|
43
|
+
| A form with text fields | `modal` + `textInput` |
|
|
44
|
+
| Carry data through a component | custom-id params `id: "x:{id}"` → `ctx.params.id` |
|
|
45
|
+
| A paged list with next/prev | `paginate(...)` |
|
|
46
|
+
| An "Are you sure?" yes/no gate | `confirm(...)` |
|
|
47
|
+
|
|
48
|
+
**Replies & UX**
|
|
49
|
+
|
|
50
|
+
| Want to… | Reach for |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| Reply, public or hidden | `ctx.reply(...)` / `ctx.replyEphemeral(...)` |
|
|
53
|
+
| Work that takes >3s | `ctx.defer()` then `ctx.editReply(...)` |
|
|
54
|
+
| A styled success/error/info/warn embed | `ctx.success/error/info/warn(...)` |
|
|
55
|
+
| "Reply, edit, or follow-up — whichever fits" | `ctx.send(...)` |
|
|
56
|
+
|
|
57
|
+
**Cross-cutting concerns**
|
|
58
|
+
|
|
59
|
+
| Want to… | Reach for |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| React to gateway events | `event(name, run)`; once on startup → `event("clientReady", ...)` |
|
|
62
|
+
| Rate-limit a command/handler | `cooldown` (per-command or client-wide) |
|
|
63
|
+
| Restrict by role / permission / owner / guild | guards: `requireAnyRole` / `requireUserPermissions` / `requireOwner` / `guildOnly` |
|
|
64
|
+
| Run jobs on cron or interval | `task({ cron \| interval })` / `client.schedule(...)` |
|
|
65
|
+
| Delay once / staged follow-ups / recover on restart | `client.scheduler.delay` / `followUp` / `reconcile` |
|
|
66
|
+
| Structured logs to file/webhook | `client.logger` + `consoleSink` / `jsonlSink` / `webhookSink` |
|
|
67
|
+
| Track who used what | `new SpearClient({ usage })` + `MemoryUsageStore` / `JsonFileUsageStore` |
|
|
68
|
+
| Read typed env / load `.env` | `env.string/number/boolean/require` (auto-loaded on `start()`) |
|
|
69
|
+
| Shut down cleanly on SIGINT/SIGTERM | `client.enableGracefulShutdown({ onShutdown })` |
|
|
70
|
+
| Permission / role-hierarchy preflight | `moderationCheck(...)`, `missingPermissions(...)`, `canActOn(...)`, `ctx.botMissing(...)` |
|
|
71
|
+
| Wait for a reply / click / modal submission | `ctx.awaitMessageFrom(...)` / `ctx.awaitModal(...)` / `awaitComponent(...)` |
|
|
72
|
+
| Branch on a Discord API error | `isDiscordError(err, DiscordErrorCode.X)` / `explainDiscordError(err)` |
|
|
73
|
+
| Per-guild prefix from a store | `prefix: { dynamic: (message) => ... }` |
|
|
74
|
+
|
|
75
|
+
**Utilities (primitives)**
|
|
76
|
+
|
|
77
|
+
| Want to… | Reach for |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| Stop concurrent runs per key (e.g. per user) | `KeyedLock` |
|
|
80
|
+
| Fetch that returns `null` instead of throwing | `safeFetch.{member,channel,message,user,guild,role}` |
|
|
81
|
+
| Format/parse `"1h30m"` durations | `formatDuration` / `parseDuration` |
|
|
82
|
+
| Render `<t:…>` Discord timestamps | `discordTimestamp` / `relativeTimestamp` |
|
|
83
|
+
| In-memory cache / counters / rate-limit window | `MemoryCache` |
|
|
84
|
+
| Load JSON/JSON5/YAML config | `loadConfig` |
|
|
85
|
+
| Persist key-value data / per-guild settings | `MemoryStore` / `JsonStore` + `createSettings({ store, defaults })` |
|
|
86
|
+
| Split text to Discord's 2000-char limit | `chunkMessage(text)` / `truncate(text, max)` |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
9
90
|
# spearkit documentation
|
|
10
91
|
|
|
11
92
|
**discord.js++** — a developer-experience-first layer over discord.js. spearkit
|
|
@@ -20,18 +101,27 @@ components.
|
|
|
20
101
|
3. [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — slash commands, subcommands, permissions, deployment.
|
|
21
102
|
4. [Options](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/options.md) — typed option builders, choices, autocomplete.
|
|
22
103
|
5. [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — buttons, selects, modals, custom-id routing.
|
|
23
|
-
6. [
|
|
24
|
-
7. [
|
|
25
|
-
8. [
|
|
26
|
-
9. [
|
|
27
|
-
10. [
|
|
28
|
-
11. [
|
|
29
|
-
12. [
|
|
30
|
-
13. [
|
|
31
|
-
14. [
|
|
32
|
-
15. [
|
|
33
|
-
16. [
|
|
34
|
-
17. [
|
|
104
|
+
6. [Context menus](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context-menus.md) — user and message "Apps" commands.
|
|
105
|
+
7. [Events](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/events.md) — the `event()` helper and the event registry.
|
|
106
|
+
8. [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md) — reply helpers shared by every handler.
|
|
107
|
+
9. [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md) — role/permission/owner/guild preconditions.
|
|
108
|
+
10. [Auto-defer](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/auto-defer.md) — beat the 3-second `Unknown interaction` error.
|
|
109
|
+
11. [Permissions & hierarchy](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/permissions.md) — moderation preflight checks.
|
|
110
|
+
12. [Discord API errors](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/errors.md) — recognise and recover from `DiscordAPIError`.
|
|
111
|
+
13. [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) — per-user/role/guild rate limiting.
|
|
112
|
+
14. [Scheduled tasks](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/scheduler.md) — cron and interval jobs.
|
|
113
|
+
15. [Prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md) — classic `!text` commands.
|
|
114
|
+
16. [Collectors](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/collectors.md) — await messages, modals and component clicks.
|
|
115
|
+
17. [Key-value store & settings](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/store.md) — persist per-guild config + dynamic prefix.
|
|
116
|
+
18. [Messages & limits](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/messages.md) — split long output, truncate text.
|
|
117
|
+
19. [Logging](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md) — structured, leveled, scoped logging.
|
|
118
|
+
20. [Usage tracking](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/usage.md) — record who used what (store + Discord channel).
|
|
119
|
+
21. [Environment & dotenv](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/env.md) — load `.env` and read typed env vars.
|
|
120
|
+
22. [Graceful shutdown](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/shutdown.md) — close cleanly on `SIGINT`/`SIGTERM`.
|
|
121
|
+
23. [Plugins](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/plugins.md) — bundling features into reusable units.
|
|
122
|
+
24. [File-based loading](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/loading.md) — one file per command/event/component.
|
|
123
|
+
25. [Migrating from discord.js](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/migration.md) — the drop-in path.
|
|
124
|
+
26. [API reference](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md) — every exported symbol.
|
|
35
125
|
|
|
36
126
|
## Why spearkit
|
|
37
127
|
|
|
@@ -415,9 +505,10 @@ const a = new SpearClient({ intents: Intents.messages });
|
|
|
415
505
|
const b = new SpearClient();
|
|
416
506
|
```
|
|
417
507
|
|
|
418
|
-
The options type is exported as `SpearClientOptions`
|
|
419
|
-
|
|
420
|
-
available
|
|
508
|
+
The options type is exported as `SpearClientOptions` — `Partial<ClientOptions> &
|
|
509
|
+
SpearOptions`. Every discord.js option (`partials`, `presence`, `sweepers`, …) is
|
|
510
|
+
available, plus spearkit's own: `logger`, `dotenv`, `cooldown`, `prefix`, `usage`,
|
|
511
|
+
`embeds`, `guards` and `autoDefer` (each covered in its own guide).
|
|
421
512
|
|
|
422
513
|
### Intents presets
|
|
423
514
|
|
|
@@ -448,26 +539,38 @@ const client = new SpearClient({
|
|
|
448
539
|
});
|
|
449
540
|
```
|
|
450
541
|
|
|
451
|
-
##
|
|
542
|
+
## Registries and subsystems
|
|
452
543
|
|
|
453
|
-
Every client owns
|
|
544
|
+
Every client owns a set of registries and subsystems, each populated by
|
|
545
|
+
`register` (or `load`) or configured by an option:
|
|
454
546
|
|
|
455
|
-
|
|
|
456
|
-
|
|
|
457
|
-
| `
|
|
458
|
-
| `
|
|
459
|
-
| `
|
|
547
|
+
| Member | Type | Holds / does |
|
|
548
|
+
| ------ | ---- | ------------ |
|
|
549
|
+
| `client.commands` | `CommandRegistry` | Slash commands; dispatches chat-input and autocomplete interactions. |
|
|
550
|
+
| `client.events` | `EventRegistry` | Event listeners; attached to the client automatically. |
|
|
551
|
+
| `client.components` | `ComponentRegistry` | Buttons, selects and modals; routed by custom-id namespace. |
|
|
552
|
+
| `client.contextMenus` | `ContextMenuRegistry` | User / message context-menu ("Apps") commands. |
|
|
553
|
+
| `client.prefix` | `PrefixRegistry` | Prefix (text) commands, dispatched from `messageCreate`. |
|
|
554
|
+
| `client.scheduler` | `TaskScheduler` | Cron / interval tasks; started on ready, stopped on `destroy`. |
|
|
555
|
+
| `client.cooldowns` | `CooldownManager` | Shared rate-limit state across commands and prefix commands. |
|
|
556
|
+
| `client.usage` | `UsageTracker` | Records who used what to a store and/or channel. |
|
|
557
|
+
| `client.logger` | `Logger` | Structured, scoped logger used across spearkit. |
|
|
558
|
+
| `client.embeds` | `Embeds` | Preset embed factory behind `ctx.success/error/...`. |
|
|
460
559
|
|
|
461
|
-
You rarely touch
|
|
462
|
-
but they are public
|
|
463
|
-
`client.commands.size`, `client.commands.toJSON()`).
|
|
560
|
+
You rarely touch the registries directly — `register` routes items into the right
|
|
561
|
+
one — but they are public for inspection and advanced control (e.g.
|
|
562
|
+
`client.commands.size`, `client.commands.toJSON()`). Each subsystem has its own
|
|
563
|
+
guide: [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md), [Scheduled tasks](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/scheduler.md),
|
|
564
|
+
[Prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md), [Context menus](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context-menus.md),
|
|
565
|
+
[Logging](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md), [Usage tracking](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/usage.md), [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md).
|
|
464
566
|
|
|
465
567
|
## Registering handlers
|
|
466
568
|
|
|
467
|
-
`client.register(...items)` accepts commands, events
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
569
|
+
`client.register(...items)` accepts commands, events, components, context-menu
|
|
570
|
+
commands, prefix commands and scheduled tasks in a single call and routes each to
|
|
571
|
+
its registry by kind. The accepted union is exported as `Registerable`
|
|
572
|
+
(`SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand |
|
|
573
|
+
ContextMenuCommand`). It returns the client for chaining.
|
|
471
574
|
|
|
472
575
|
```ts
|
|
473
576
|
import { SpearClient, command, event, button, option } from "spearkit";
|
|
@@ -513,8 +616,8 @@ See [Plugins](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/plugi
|
|
|
513
616
|
## File-based loading
|
|
514
617
|
|
|
515
618
|
`client.load(dir, options?)` recursively imports a directory and registers every
|
|
516
|
-
|
|
517
|
-
registered.
|
|
619
|
+
spearkit-registrable export it finds — commands, events, components, scheduled
|
|
620
|
+
tasks and prefix commands. It returns the number of items registered.
|
|
518
621
|
|
|
519
622
|
```ts
|
|
520
623
|
import { SpearClient } from "spearkit";
|
|
@@ -564,6 +667,31 @@ client.once("clientReady", async () => {
|
|
|
564
667
|
});
|
|
565
668
|
```
|
|
566
669
|
|
|
670
|
+
## Reliability: auto-defer and graceful shutdown
|
|
671
|
+
|
|
672
|
+
A slow handler that doesn't respond within Discord's 3-second window dies with
|
|
673
|
+
`Unknown interaction` (10062). Set `autoDefer` to have spearkit `deferReply()`
|
|
674
|
+
automatically just before that window closes — per handler (`command({ autoDefer:
|
|
675
|
+
true })`, `userCommand`/`messageCommand`) or for every slash + context-menu
|
|
676
|
+
handler at once:
|
|
677
|
+
|
|
678
|
+
```ts
|
|
679
|
+
const client = new SpearClient({ autoDefer: true });
|
|
680
|
+
// or { ephemeral: true, delayMs: 1500 } for a hidden defer / earlier fire.
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
With auto-defer on, respond via `ctx.send(...)` or `ctx.editReply(...)` — the
|
|
684
|
+
initial reply slot may already be taken by the safety defer.
|
|
685
|
+
|
|
686
|
+
`client.enableGracefulShutdown(options?)` closes the bot cleanly on `SIGINT` /
|
|
687
|
+
`SIGTERM`: it runs an optional `onShutdown` hook, calls `destroy()` (stopping the
|
|
688
|
+
scheduler and gateway), and exits, with a hard timeout so a wedged shutdown can't
|
|
689
|
+
hang. It returns a disposer that removes the signal handlers.
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
client.enableGracefulShutdown({ onShutdown: () => db.close() });
|
|
693
|
+
```
|
|
694
|
+
|
|
567
695
|
## Everything discord.js still works
|
|
568
696
|
|
|
569
697
|
`SpearClient` extends discord.js `Client`, so the full client surface is
|
|
@@ -677,6 +805,9 @@ export const purge = command({
|
|
|
677
805
|
| `nsfw` | `boolean` | Marks the command age-restricted. |
|
|
678
806
|
| `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate (members without it don't see the command). |
|
|
679
807
|
| `nameLocalizations` / `descriptionLocalizations` | `LocalizationMap` | Per-locale name/description. |
|
|
808
|
+
| `cooldown` | `number \| CooldownConfig` | Rate-limit the command (a number is milliseconds). See [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md). |
|
|
809
|
+
| `guards` | `readonly Guard[]` | Preconditions run before the handler. See [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md). |
|
|
810
|
+
| `autoDefer` | `boolean \| { ephemeral?, delayMs? }` | Auto-`deferReply()` if the handler is slow (>~2s), preventing `Unknown interaction`. Respond via `ctx.send`/`ctx.editReply`. |
|
|
680
811
|
|
|
681
812
|
## Subcommands and groups
|
|
682
813
|
|
|
@@ -793,6 +924,8 @@ deploys (no `guildId`) can take up to an hour to propagate.
|
|
|
793
924
|
- [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — buttons, selects, modals.
|
|
794
925
|
- [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — registering and deploying from the client.
|
|
795
926
|
- [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md) — the reply helpers every handler shares.
|
|
927
|
+
- [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) — rate-limit a command with `cooldown`.
|
|
928
|
+
- [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md) — gate a command with `guards`.
|
|
796
929
|
|
|
797
930
|
---
|
|
798
931
|
|
|
@@ -1036,6 +1169,10 @@ const docs = linkButton({ url: "https://example.com", label: "Docs" });
|
|
|
1036
1169
|
`style` accepts the string names `"Primary"`, `"Secondary"`, `"Success"`,
|
|
1037
1170
|
`"Danger"`, or the `ButtonStyle` enum. It defaults to `"Secondary"`.
|
|
1038
1171
|
|
|
1172
|
+
All component builders (`button`, the five selects, and `modal`) also accept
|
|
1173
|
+
`guards?: readonly Guard[]` — preconditions evaluated before the handler runs.
|
|
1174
|
+
See [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md).
|
|
1175
|
+
|
|
1039
1176
|
The `ButtonContext` adds, on top of the shared [reply helpers](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md):
|
|
1040
1177
|
|
|
1041
1178
|
| Member | Description |
|
|
@@ -1166,6 +1303,9 @@ namespace automatically. The `ComponentRegistry` API:
|
|
|
1166
1303
|
| `size` | Number registered. |
|
|
1167
1304
|
| `onError(handler)` | Set the error handler. |
|
|
1168
1305
|
| `handle(interaction)` | Route an interaction; returns `true` if matched. |
|
|
1306
|
+
| `setDefaultGuards(guards)` | Guards run before each component's own guards. |
|
|
1307
|
+
|
|
1308
|
+
`setLogger` and `setUsageHook` also exist; the client wires all three for you.
|
|
1169
1309
|
|
|
1170
1310
|
### Error handling
|
|
1171
1311
|
|
|
@@ -1239,6 +1379,130 @@ client.register(panel, open, rating, form);
|
|
|
1239
1379
|
|
|
1240
1380
|
---
|
|
1241
1381
|
|
|
1382
|
+
# Context-menu commands
|
|
1383
|
+
|
|
1384
|
+
Context-menu commands are the right-click **"Apps"** actions Discord shows on a
|
|
1385
|
+
user or a message. spearkit makes them first-class: define one with `userCommand`
|
|
1386
|
+
or `messageCommand`, register it like anything else, and deploy it alongside your
|
|
1387
|
+
slash commands. The handler gets a typed `targetUser` or `targetMessage`.
|
|
1388
|
+
|
|
1389
|
+
```ts
|
|
1390
|
+
import { userCommand } from "spearkit";
|
|
1391
|
+
|
|
1392
|
+
export const whois = userCommand({
|
|
1393
|
+
name: "Who is this?",
|
|
1394
|
+
run: (ctx) => ctx.replyEphemeral(`That's ${ctx.targetUser.tag}.`),
|
|
1395
|
+
});
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
`name` is the label shown in the Apps menu (no description — Discord does not show
|
|
1399
|
+
one for context-menu commands).
|
|
1400
|
+
|
|
1401
|
+
## User vs message commands
|
|
1402
|
+
|
|
1403
|
+
| Builder | Appears on | Target context |
|
|
1404
|
+
| ------- | ---------- | -------------- |
|
|
1405
|
+
| `userCommand` | a user (right-click → Apps) | `ctx.targetUser`, `ctx.targetMember` |
|
|
1406
|
+
| `messageCommand` | a message (right-click → Apps) | `ctx.targetMessage` |
|
|
1407
|
+
|
|
1408
|
+
```ts
|
|
1409
|
+
import { messageCommand } from "spearkit";
|
|
1410
|
+
|
|
1411
|
+
export const report = messageCommand({
|
|
1412
|
+
name: "Report message",
|
|
1413
|
+
run: (ctx) =>
|
|
1414
|
+
ctx.replyEphemeral(`Reported message ${ctx.targetMessage.id}.`),
|
|
1415
|
+
});
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
Both handler contexts extend the shared [`BaseContext`](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md), so
|
|
1419
|
+
`ctx.reply`, `ctx.replyEphemeral`, `ctx.defer`, `ctx.success/error/...` and the
|
|
1420
|
+
usual accessors are all available.
|
|
1421
|
+
|
|
1422
|
+
## Metadata, cooldowns and guards
|
|
1423
|
+
|
|
1424
|
+
Both builders accept the same metadata, plus a `cooldown` and `guards`:
|
|
1425
|
+
|
|
1426
|
+
| Field | Type | Effect |
|
|
1427
|
+
| ----- | ---- | ------ |
|
|
1428
|
+
| `name` | `string` | The Apps-menu label. |
|
|
1429
|
+
| `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate. |
|
|
1430
|
+
| `nsfw` | `boolean` | Marks the command age-restricted. |
|
|
1431
|
+
| `guildOnly` | `boolean` | Restricts it to guild contexts. |
|
|
1432
|
+
| `nameLocalizations` | `LocalizationMap` | Per-locale label. |
|
|
1433
|
+
| `cooldown` | `number \| CooldownConfig` | Rate limit (shares `client.cooldowns`). |
|
|
1434
|
+
| `guards` | `readonly Guard[]` | Preconditions run before the handler. |
|
|
1435
|
+
| `autoDefer` | `boolean \| { ephemeral?, delayMs? }` | Auto-`deferReply()` if the handler is slow, preventing `Unknown interaction`. |
|
|
1436
|
+
|
|
1437
|
+
```ts
|
|
1438
|
+
import { userCommand, guildOnly, requireUserPermissions, PermissionFlagsBits } from "spearkit";
|
|
1439
|
+
|
|
1440
|
+
export const warn = userCommand({
|
|
1441
|
+
name: "Warn user",
|
|
1442
|
+
guildOnly: true,
|
|
1443
|
+
cooldown: 5_000,
|
|
1444
|
+
guards: [requireUserPermissions(PermissionFlagsBits.ModerateMembers)],
|
|
1445
|
+
run: (ctx) => ctx.replyEphemeral(`Warned ${ctx.targetUser.tag}.`),
|
|
1446
|
+
});
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
See [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) and [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md) for the shared options.
|
|
1450
|
+
|
|
1451
|
+
## Registering and deploying
|
|
1452
|
+
|
|
1453
|
+
Register context-menu commands like everything else with `client.register(...)`.
|
|
1454
|
+
They route automatically — spearkit dispatches user- and message-context-menu
|
|
1455
|
+
interactions to the matching command.
|
|
1456
|
+
|
|
1457
|
+
```ts
|
|
1458
|
+
client.register(whois, report, warn);
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
Because context menus and slash commands deploy to the same Discord endpoint,
|
|
1462
|
+
push them together with `deployAllCommands` once you mix the two — it sends both
|
|
1463
|
+
in a single request. (`deployCommands` is slash-only.)
|
|
1464
|
+
|
|
1465
|
+
```ts
|
|
1466
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
1467
|
+
await client.deployAllCommands({ guildId: process.env.GUILD_ID }); // slash + menus
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
`deployAllCommands` also supports a `dryRun` flag and a `strategy: "diff"` that
|
|
1471
|
+
skips the PUT when the remote set already matches — handy in CI:
|
|
1472
|
+
|
|
1473
|
+
```ts
|
|
1474
|
+
// Preview without touching Discord:
|
|
1475
|
+
const result = await client.deployAllCommands({ guildId, dryRun: true });
|
|
1476
|
+
// → { skipped: true, reason: "dry-run", body: [...] }
|
|
1477
|
+
|
|
1478
|
+
// Only deploy when something changed:
|
|
1479
|
+
await client.deployAllCommands({ guildId, strategy: "diff" });
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
## The registry
|
|
1483
|
+
|
|
1484
|
+
`client.contextMenus` is a `ContextMenuRegistry`. The client wires it to the
|
|
1485
|
+
logger and cooldown manager and routes interactions for you, so you rarely touch
|
|
1486
|
+
it directly:
|
|
1487
|
+
|
|
1488
|
+
```ts
|
|
1489
|
+
client.contextMenus.size; // number registered
|
|
1490
|
+
client.contextMenus.all(); // ContextMenuCommand[]
|
|
1491
|
+
client.contextMenus.toJSON(); // REST payloads (also included by deployAllCommands)
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
Note: context-menu commands are **not** picked up by `client.load(...)`
|
|
1495
|
+
directory loading — register them explicitly with `client.register(...)`.
|
|
1496
|
+
|
|
1497
|
+
## See also
|
|
1498
|
+
|
|
1499
|
+
- [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — slash commands.
|
|
1500
|
+
- [Client](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/client.md) — `deployAllCommands` and registration.
|
|
1501
|
+
- [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md) / [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) — the shared preconditions.
|
|
1502
|
+
- [Contexts](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context.md) — the reply helpers every handler shares.
|
|
1503
|
+
|
|
1504
|
+
---
|
|
1505
|
+
|
|
1242
1506
|
# Events
|
|
1243
1507
|
|
|
1244
1508
|
`event()` defines a reusable, loadable discord.js event listener with a
|
|
@@ -1426,7 +1690,9 @@ rest extend `BaseContext`, adding their own specifics (e.g. `ctx.options`,
|
|
|
1426
1690
|
| `editReply(input)` | `Promise<Message>` | Edit the original (or deferred) response. |
|
|
1427
1691
|
| `followUp(input)` | `Promise<Message>` | Add a message after the initial response. |
|
|
1428
1692
|
| `send(input)` | `Promise<void>` | State-aware: replies, edits, or follows up automatically. |
|
|
1429
|
-
| `error(
|
|
1693
|
+
| `error(input, options?)` | `Promise<void>` | State-aware preset error embed; ephemeral by default. |
|
|
1694
|
+
| `success` / `info` / `warn` `(input, options?)` | `Promise<void>` | State-aware preset embeds. |
|
|
1695
|
+
| `replyError` / `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `Promise<InteractionResponse>` | Initial-reply preset embeds. |
|
|
1430
1696
|
|
|
1431
1697
|
```ts
|
|
1432
1698
|
import { command } from "spearkit";
|
|
@@ -1468,7 +1734,8 @@ export default command({
|
|
|
1468
1734
|
|
|
1469
1735
|
### `error` for ephemeral failures
|
|
1470
1736
|
|
|
1471
|
-
`error(
|
|
1737
|
+
`error(input, options?)` sends a state-aware preset **error embed** — ephemeral
|
|
1738
|
+
by default (pass `{ ephemeral: false }` to make it public) — perfect for
|
|
1472
1739
|
validation failures that only the invoking user should see.
|
|
1473
1740
|
|
|
1474
1741
|
```ts
|
|
@@ -1485,6 +1752,40 @@ export default command({
|
|
|
1485
1752
|
});
|
|
1486
1753
|
```
|
|
1487
1754
|
|
|
1755
|
+
## Preset embeds
|
|
1756
|
+
|
|
1757
|
+
`BaseContext` builds consistent, colored embeds from `client.embeds` (or a shared
|
|
1758
|
+
default). Each takes an `EmbedPresetInput` — a plain string, or a structured body
|
|
1759
|
+
(`{ title?, description?, fields?, footer?, ... }`) — and an optional
|
|
1760
|
+
`{ ephemeral? }`.
|
|
1761
|
+
|
|
1762
|
+
| Method | Sends via | Default visibility |
|
|
1763
|
+
| ------ | --------- | ------------------ |
|
|
1764
|
+
| `success(input, options?)` | `send` (state-aware) | public |
|
|
1765
|
+
| `info(input, options?)` | `send` (state-aware) | public |
|
|
1766
|
+
| `warn(input, options?)` | `send` (state-aware) | public |
|
|
1767
|
+
| `error(input, options?)` | `send` (state-aware) | **ephemeral** |
|
|
1768
|
+
| `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `reply` (initial only) | public |
|
|
1769
|
+
| `replyError(input, options?)` | `reply` (initial only) | **ephemeral** |
|
|
1770
|
+
|
|
1771
|
+
```ts
|
|
1772
|
+
import { command } from "spearkit";
|
|
1773
|
+
|
|
1774
|
+
export default command({
|
|
1775
|
+
name: "save",
|
|
1776
|
+
description: "Save settings",
|
|
1777
|
+
run: async (ctx) => {
|
|
1778
|
+
await ctx.success("Settings saved."); // green embed, public
|
|
1779
|
+
await ctx.warn({ title: "Heads up", description: "Quota is almost full." });
|
|
1780
|
+
// error defaults to ephemeral; make it public with { ephemeral: false }:
|
|
1781
|
+
// await ctx.error("Failed to save.", { ephemeral: false });
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
Configure the colors/icons with the client `embeds` option; see the
|
|
1787
|
+
[API reference](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md#embeds--preset-replies).
|
|
1788
|
+
|
|
1488
1789
|
## The `{ ephemeral: true }` shortcut
|
|
1489
1790
|
|
|
1490
1791
|
discord.js represents an ephemeral reply with `flags: MessageFlags.Ephemeral`.
|
|
@@ -1560,6 +1861,7 @@ asEphemeral("hidden");
|
|
|
1560
1861
|
| `locale` | The user's locale. |
|
|
1561
1862
|
| `deferred` | Whether the interaction is already deferred. |
|
|
1562
1863
|
| `replied` | Whether the interaction already received an initial response. |
|
|
1864
|
+
| `botPermissions` | The bot's resolved permissions in the channel (`PermissionsBitField`, zero-fetch). |
|
|
1563
1865
|
|
|
1564
1866
|
```ts
|
|
1565
1867
|
import { command } from "spearkit";
|
|
@@ -1591,6 +1893,60 @@ export default button({
|
|
|
1591
1893
|
});
|
|
1592
1894
|
```
|
|
1593
1895
|
|
|
1896
|
+
## Permission preflights
|
|
1897
|
+
|
|
1898
|
+
`BaseContext` reads the permissions Discord already attached to the interaction —
|
|
1899
|
+
no extra fetches — so you can check before attempting a privileged action:
|
|
1900
|
+
|
|
1901
|
+
```ts
|
|
1902
|
+
import { command, PermissionFlagsBits } from "spearkit";
|
|
1903
|
+
|
|
1904
|
+
export default command({
|
|
1905
|
+
name: "slowmode",
|
|
1906
|
+
description: "Set slowmode",
|
|
1907
|
+
run: async (ctx) => {
|
|
1908
|
+
const missing = ctx.botMissing(PermissionFlagsBits.ManageChannels);
|
|
1909
|
+
if (missing.length > 0) return ctx.error(`I'm missing: ${missing.join(", ")}`);
|
|
1910
|
+
// …apply slowmode…
|
|
1911
|
+
},
|
|
1912
|
+
});
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
- `ctx.botPermissions` — the bot's `PermissionsBitField` in the current channel.
|
|
1916
|
+
- `ctx.botMissing(required)` — permission names the bot lacks here (`[]` if none).
|
|
1917
|
+
- `ctx.userMissing(required)` — permission names the invoking user lacks here.
|
|
1918
|
+
|
|
1919
|
+
For role-hierarchy and moderation preflights (acting on self/owner, comparing top
|
|
1920
|
+
roles) see `moderationCheck` and the permission helpers in the
|
|
1921
|
+
[API reference](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md#permissions--moderation).
|
|
1922
|
+
|
|
1923
|
+
## Awaiting input
|
|
1924
|
+
|
|
1925
|
+
When a flow needs a follow-up message or a modal, the context wraps discord.js
|
|
1926
|
+
collectors so you skip the boilerplate. Both resolve to `null` on timeout.
|
|
1927
|
+
|
|
1928
|
+
```ts
|
|
1929
|
+
import { command, modal, textInput } from "spearkit";
|
|
1930
|
+
|
|
1931
|
+
const nameModal = modal({ id: "name", title: "Your name", fields: { name: textInput({ label: "Name" }) }, run: () => {} });
|
|
1932
|
+
|
|
1933
|
+
export default command({
|
|
1934
|
+
name: "setup",
|
|
1935
|
+
description: "Interactive setup",
|
|
1936
|
+
run: async (ctx) => {
|
|
1937
|
+
// Wait for the user to type an answer in this channel:
|
|
1938
|
+
const reply = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
|
|
1939
|
+
if (reply === null) return ctx.error("Timed out.");
|
|
1940
|
+
// Or show a modal and await its submission:
|
|
1941
|
+
const submission = await ctx.awaitModal(nameModal);
|
|
1942
|
+
if (submission !== null) await submission.reply(`Hi, ${submission.fields.getTextInputValue("name")}!`);
|
|
1943
|
+
},
|
|
1944
|
+
});
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
The standalone `awaitMessage`, `awaitComponent` and `showAndAwaitModal` helpers
|
|
1948
|
+
are also exported; see the [API reference](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md#collectors).
|
|
1949
|
+
|
|
1594
1950
|
## See also
|
|
1595
1951
|
|
|
1596
1952
|
- [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — `CommandContext`, options and `showModal`.
|
|
@@ -1598,9 +1954,307 @@ export default button({
|
|
|
1598
1954
|
|
|
1599
1955
|
---
|
|
1600
1956
|
|
|
1957
|
+
# Guards
|
|
1958
|
+
|
|
1959
|
+
Guards are declarative **preconditions** that run before a handler. They work
|
|
1960
|
+
uniformly across slash commands, components (buttons, selects, modals), prefix
|
|
1961
|
+
commands and context-menu commands — and can also be applied client-wide. A
|
|
1962
|
+
guard returns `true` to allow the handler, or a denial (with an optional reason)
|
|
1963
|
+
to block it; spearkit replies with the reason and the handler never runs.
|
|
1964
|
+
|
|
1965
|
+
```ts
|
|
1966
|
+
import { command, requireUserPermissions, PermissionFlagsBits } from "spearkit";
|
|
1967
|
+
|
|
1968
|
+
export const purge = command({
|
|
1969
|
+
name: "purge",
|
|
1970
|
+
description: "Bulk-delete messages",
|
|
1971
|
+
guards: [requireUserPermissions(PermissionFlagsBits.ManageMessages)],
|
|
1972
|
+
run: (ctx) => ctx.reply("Purged."),
|
|
1973
|
+
});
|
|
1974
|
+
```
|
|
1975
|
+
|
|
1976
|
+
## Where guards attach
|
|
1977
|
+
|
|
1978
|
+
Pass `guards: [...]` to any handler definition, or set client-wide defaults that
|
|
1979
|
+
run before every handler's own guards.
|
|
1980
|
+
|
|
1981
|
+
```ts
|
|
1982
|
+
import {
|
|
1983
|
+
SpearClient,
|
|
1984
|
+
command,
|
|
1985
|
+
button,
|
|
1986
|
+
prefixCommand,
|
|
1987
|
+
userCommand,
|
|
1988
|
+
guildOnly,
|
|
1989
|
+
} from "spearkit";
|
|
1990
|
+
|
|
1991
|
+
// Per-handler — on commands, components, prefix and context-menu commands.
|
|
1992
|
+
command({ name: "kick", description: "…", guards: [guildOnly()], run: () => {} });
|
|
1993
|
+
button({ id: "del:{id}", guards: [guildOnly()], run: () => {} });
|
|
1994
|
+
prefixCommand({ name: "ban", guards: [guildOnly()], run: () => {} });
|
|
1995
|
+
userCommand({ name: "Report", guards: [guildOnly()], run: () => {} });
|
|
1996
|
+
|
|
1997
|
+
// Client-wide — applied before each handler's own guards.
|
|
1998
|
+
const client = new SpearClient({ guards: [guildOnly()] });
|
|
1999
|
+
```
|
|
2000
|
+
|
|
2001
|
+
Client-wide guards run first; if they pass, the handler's own guards run next.
|
|
2002
|
+
The first denial short-circuits the rest.
|
|
2003
|
+
|
|
2004
|
+
## Built-in guards
|
|
2005
|
+
|
|
2006
|
+
Each built-in returns a `Guard` and accepts an optional custom `reason`. When
|
|
2007
|
+
omitted, a sensible default message is used (shown below).
|
|
2008
|
+
|
|
2009
|
+
| Guard | Denies unless… | Default reason |
|
|
2010
|
+
| ----- | -------------- | -------------- |
|
|
2011
|
+
| `guildOnly(reason?)` | used inside a guild | `"This can only be used in a server."` |
|
|
2012
|
+
| `dmOnly(reason?)` | used in a DM | `"This can only be used in DMs."` |
|
|
2013
|
+
| `requireAnyRole(roleIds, reason?)` | the member holds **any** of `roleIds` | `"You don't have permission to use this."` |
|
|
2014
|
+
| `requireAllRoles(roleIds, reason?)` | the member holds **every** id in `roleIds` | `"You're missing one of the required roles."` |
|
|
2015
|
+
| `requireOwner(ownerIds, reason?)` | the user id is in `ownerIds` | `"This is owner-only."` |
|
|
2016
|
+
| `requireUserPermissions(permission, reason?)` | the member has the Discord `permission` | `"You don't have permission to use this."` |
|
|
2017
|
+
| `requireBotPermissions(permission, reason?)` | the bot's member has the Discord `permission` | `"I don't have permission to do that here."` |
|
|
2018
|
+
|
|
2019
|
+
```ts
|
|
2020
|
+
import {
|
|
2021
|
+
command,
|
|
2022
|
+
requireAnyRole,
|
|
2023
|
+
requireBotPermissions,
|
|
2024
|
+
PermissionFlagsBits,
|
|
2025
|
+
} from "spearkit";
|
|
2026
|
+
|
|
2027
|
+
export const announce = command({
|
|
2028
|
+
name: "announce",
|
|
2029
|
+
description: "Post an announcement",
|
|
2030
|
+
guards: [
|
|
2031
|
+
requireAnyRole(["111111111111111111"], "Staff only."),
|
|
2032
|
+
requireBotPermissions(PermissionFlagsBits.SendMessages),
|
|
2033
|
+
],
|
|
2034
|
+
run: (ctx) => ctx.reply("Announced."),
|
|
2035
|
+
});
|
|
2036
|
+
```
|
|
2037
|
+
|
|
2038
|
+
## Custom guards
|
|
2039
|
+
|
|
2040
|
+
`guard(predicate)` wraps an inline predicate so a one-off check still types as a
|
|
2041
|
+
`Guard`. The predicate receives a `GuardContext` and returns a `GuardResult`;
|
|
2042
|
+
use `denied(reason?)` to build a denial.
|
|
2043
|
+
|
|
2044
|
+
```ts
|
|
2045
|
+
import { command, guard, denied } from "spearkit";
|
|
2046
|
+
|
|
2047
|
+
const cooldownOver = guard((ctx) =>
|
|
2048
|
+
isReady(ctx.user.id) ? true : denied("Still warming up — try again soon."),
|
|
2049
|
+
);
|
|
2050
|
+
|
|
2051
|
+
export const cast = command({
|
|
2052
|
+
name: "cast",
|
|
2053
|
+
description: "Cast a spell",
|
|
2054
|
+
guards: [cooldownOver],
|
|
2055
|
+
run: (ctx) => ctx.reply("✨"),
|
|
2056
|
+
});
|
|
2057
|
+
```
|
|
2058
|
+
|
|
2059
|
+
`GuardContext` exposes the actor/location fields every handler shares, so the
|
|
2060
|
+
same guard works on commands, components, prefix and context-menu handlers:
|
|
2061
|
+
|
|
2062
|
+
```ts
|
|
2063
|
+
interface GuardContext {
|
|
2064
|
+
client: Client;
|
|
2065
|
+
user: User;
|
|
2066
|
+
member: GuildMember | APIInteractionGuildMember | null;
|
|
2067
|
+
guild: Guild | null;
|
|
2068
|
+
guildId: string | null;
|
|
2069
|
+
channelId: string | null;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
type GuardResult = boolean | { allowed: false; reason?: string };
|
|
2073
|
+
type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
|
|
2074
|
+
```
|
|
2075
|
+
|
|
2076
|
+
## Running guards manually
|
|
2077
|
+
|
|
2078
|
+
`runGuards(ctx, guards)` evaluates a list in order and short-circuits on the
|
|
2079
|
+
first denial — useful if you build your own dispatch on top of spearkit.
|
|
2080
|
+
|
|
2081
|
+
```ts
|
|
2082
|
+
import { runGuards, guildOnly } from "spearkit";
|
|
2083
|
+
|
|
2084
|
+
const result = await runGuards(ctx, [guildOnly()]);
|
|
2085
|
+
if (!result.allowed) {
|
|
2086
|
+
// result.reason is the denial message (or undefined)
|
|
2087
|
+
}
|
|
2088
|
+
```
|
|
2089
|
+
|
|
2090
|
+
`runGuards` resolves to `RunGuardsResult`:
|
|
2091
|
+
|
|
2092
|
+
```ts
|
|
2093
|
+
type RunGuardsResult = { allowed: true } | { allowed: false; reason: string | undefined };
|
|
2094
|
+
```
|
|
2095
|
+
|
|
2096
|
+
## See also
|
|
2097
|
+
|
|
2098
|
+
- [Commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/commands.md) — `guards` on slash commands.
|
|
2099
|
+
- [Components](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/components.md) — `guards` on buttons, selects and modals.
|
|
2100
|
+
- [Prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md) — `guards` on text commands.
|
|
2101
|
+
- [Context menus](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context-menus.md) — `guards` on "Apps" actions.
|
|
2102
|
+
- [Cooldowns](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/cooldown.md) — the other built-in precondition.
|
|
2103
|
+
|
|
2104
|
+
---
|
|
2105
|
+
|
|
2106
|
+
# Permissions & hierarchy
|
|
2107
|
+
|
|
2108
|
+
Moderation commands fail in two predictable ways: the bot lacks a permission in
|
|
2109
|
+
the channel (`Missing Permissions`, 50013), or the target sits above the bot (or
|
|
2110
|
+
the moderator) in the role list. Both are checkable *before* you act, so you can
|
|
2111
|
+
bail out with a clear message instead of a half-finished action and an exception.
|
|
2112
|
+
|
|
2113
|
+
## Did the bot/user get the permissions? (zero-fetch)
|
|
2114
|
+
|
|
2115
|
+
Every interaction carries the bot's and the invoker's resolved permissions for
|
|
2116
|
+
the current channel. `ctx.botMissing(...)` / `ctx.userMissing(...)` read them
|
|
2117
|
+
with no API calls and return the **missing** flag names:
|
|
2118
|
+
|
|
2119
|
+
```ts
|
|
2120
|
+
import { PermissionFlagsBits, command, formatPermissions } from "spearkit";
|
|
2121
|
+
|
|
2122
|
+
export const slowmode = command({
|
|
2123
|
+
name: "slowmode",
|
|
2124
|
+
description: "Set channel slowmode",
|
|
2125
|
+
run: async (ctx) => {
|
|
2126
|
+
const missing = ctx.botMissing(PermissionFlagsBits.ManageChannels);
|
|
2127
|
+
if (missing.length > 0) return ctx.error(`I need: ${formatPermissions(missing)}`);
|
|
2128
|
+
// …
|
|
2129
|
+
},
|
|
2130
|
+
});
|
|
2131
|
+
```
|
|
2132
|
+
|
|
2133
|
+
`formatPermissions(...)` renders flag names as a friendly list
|
|
2134
|
+
(`"Manage Channels, Ban Members"`).
|
|
2135
|
+
|
|
2136
|
+
## Permissions in another channel
|
|
2137
|
+
|
|
2138
|
+
For a channel other than the current one, use the standalone helpers:
|
|
2139
|
+
|
|
2140
|
+
```ts
|
|
2141
|
+
import { botMissingPermissions, hasPermissions, missingPermissions } from "spearkit";
|
|
2142
|
+
|
|
2143
|
+
const missing = botMissingPermissions(targetChannel, [PermissionFlagsBits.SendMessages]);
|
|
2144
|
+
if (missing.length > 0) return ctx.error("I can't post there.");
|
|
2145
|
+
|
|
2146
|
+
// or for a specific member/role:
|
|
2147
|
+
hasPermissions(targetChannel, member, PermissionFlagsBits.ViewChannel); // boolean
|
|
2148
|
+
missingPermissions(targetChannel, role, [PermissionFlagsBits.Connect]); // PermissionsString[]
|
|
2149
|
+
```
|
|
2150
|
+
|
|
2151
|
+
## Role hierarchy
|
|
2152
|
+
|
|
2153
|
+
`moderationCheck(...)` validates both the moderator and the bot against a target,
|
|
2154
|
+
returning a ready-to-show reason on the first failing rule (self, server owner,
|
|
2155
|
+
moderator hierarchy, bot hierarchy):
|
|
2156
|
+
|
|
2157
|
+
```ts
|
|
2158
|
+
import { moderationCheck } from "spearkit";
|
|
2159
|
+
|
|
2160
|
+
const moderator = await ctx.guild!.members.fetch(ctx.user.id);
|
|
2161
|
+
const check = moderationCheck({ moderator, target, action: "ban" });
|
|
2162
|
+
if (!check.ok) return ctx.error(check.reason);
|
|
2163
|
+
await target.ban();
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2166
|
+
The `me` (bot) member defaults to `target.guild.members.me`; pass `me: null` to
|
|
2167
|
+
skip the bot check. `action` is the verb used in messages (default `"moderate"`).
|
|
2168
|
+
|
|
2169
|
+
Lower-level primitives are exported too:
|
|
2170
|
+
|
|
2171
|
+
- `canActOn(actor, target)` — boolean: not self, target isn't the owner, actor is
|
|
2172
|
+
the owner or outranks the target.
|
|
2173
|
+
- `compareRoles(a, b)` — highest-role position comparison (`>0`, `<0`, `0`).
|
|
2174
|
+
|
|
2175
|
+
---
|
|
2176
|
+
|
|
2177
|
+
# Auto-defer
|
|
2178
|
+
|
|
2179
|
+
The single most common discord.js error is
|
|
2180
|
+
`DiscordAPIError[10062]: Unknown interaction`. An interaction token is valid for
|
|
2181
|
+
only **3 seconds** before your first response; any handler that awaits a database
|
|
2182
|
+
query or an HTTP call risks blowing past that window, after which the interaction
|
|
2183
|
+
is dead and every reply throws.
|
|
2184
|
+
|
|
2185
|
+
Auto-defer removes the footgun: spearkit arms a timer when your handler starts
|
|
2186
|
+
and, if you haven't responded in time, calls `deferReply()` for you. The timer is
|
|
2187
|
+
cancelled the instant your handler replies or defers itself.
|
|
2188
|
+
|
|
2189
|
+
## Per command
|
|
2190
|
+
|
|
2191
|
+
```ts
|
|
2192
|
+
import { command, option } from "spearkit";
|
|
2193
|
+
|
|
2194
|
+
export const weather = command({
|
|
2195
|
+
name: "weather",
|
|
2196
|
+
description: "Look up the weather",
|
|
2197
|
+
autoDefer: true, // defers automatically if the handler takes too long
|
|
2198
|
+
options: { city: option.string({ description: "City", required: true }) },
|
|
2199
|
+
run: async (ctx) => {
|
|
2200
|
+
const report = await fetchWeather(ctx.options.city); // slow
|
|
2201
|
+
await ctx.send(`Weather in ${ctx.options.city}: ${report}`);
|
|
2202
|
+
},
|
|
2203
|
+
});
|
|
2204
|
+
```
|
|
2205
|
+
|
|
2206
|
+
> With auto-defer on, respond via `ctx.send(...)` or `ctx.editReply(...)`, not
|
|
2207
|
+
> `ctx.reply(...)` — the initial reply slot may already be taken by the
|
|
2208
|
+
> auto-defer. `ctx.send` is state-aware and always does the right thing.
|
|
2209
|
+
|
|
2210
|
+
## Options
|
|
2211
|
+
|
|
2212
|
+
`autoDefer` accepts `true` (defaults) or an object:
|
|
2213
|
+
|
|
2214
|
+
```ts
|
|
2215
|
+
command({
|
|
2216
|
+
name: "report",
|
|
2217
|
+
description: "Generate a report",
|
|
2218
|
+
autoDefer: { ephemeral: true, delayMs: 1500 },
|
|
2219
|
+
run: async (ctx) => ctx.send("…"),
|
|
2220
|
+
});
|
|
2221
|
+
```
|
|
2222
|
+
|
|
2223
|
+
| Field | Default | Meaning |
|
|
2224
|
+
| --- | --- | --- |
|
|
2225
|
+
| `ephemeral` | `false` | Defer as a hidden ("thinking…") response. |
|
|
2226
|
+
| `delayMs` | `2000` | How long to wait before the safety defer fires. Kept under the 3s cutoff. |
|
|
2227
|
+
|
|
2228
|
+
## Client-wide default
|
|
2229
|
+
|
|
2230
|
+
Apply auto-defer to **every** slash command and context menu; each handler can
|
|
2231
|
+
still override with its own `autoDefer`.
|
|
2232
|
+
|
|
2233
|
+
```ts
|
|
2234
|
+
import { SpearClient } from "spearkit";
|
|
2235
|
+
|
|
2236
|
+
const client = new SpearClient({ autoDefer: true });
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
## Scope
|
|
2240
|
+
|
|
2241
|
+
Auto-defer covers slash commands and context menus (answered with `deferReply`).
|
|
2242
|
+
Component handlers (buttons/selects) usually respond instantly with `update`, so
|
|
2243
|
+
they're not auto-deferred — call `ctx.deferUpdate()` yourself if a component
|
|
2244
|
+
handler does slow work.
|
|
2245
|
+
|
|
2246
|
+
## Lower-level helpers
|
|
2247
|
+
|
|
2248
|
+
`normalizeAutoDefer(input)` resolves `true`/object/`undefined` into an
|
|
2249
|
+
`AutoDeferConfig`; `armAutoDefer(interaction, config)` arms the timer and returns
|
|
2250
|
+
a cancel function. Both are exported for custom dispatch.
|
|
2251
|
+
|
|
2252
|
+
---
|
|
2253
|
+
|
|
1601
2254
|
# Cooldowns
|
|
1602
2255
|
|
|
1603
|
-
Rate-limit commands per user, per
|
|
2256
|
+
Rate-limit commands per user, per guild, per channel, or globally — with per-role
|
|
2257
|
+
and per-user exemptions and overrides.
|
|
1604
2258
|
Cooldowns are enforced automatically by command dispatch: when an actor is
|
|
1605
2259
|
still on cooldown, spearkit replies (ephemerally) with a message and the
|
|
1606
2260
|
handler does not run.
|
|
@@ -1733,7 +2387,7 @@ outlive your bot.
|
|
|
1733
2387
|
|
|
1734
2388
|
## Define a task
|
|
1735
2389
|
|
|
1736
|
-
Provide exactly one of `cron` or `interval
|
|
2390
|
+
Provide exactly one of `cron` or `interval` (if both are set, the interval is used):
|
|
1737
2391
|
|
|
1738
2392
|
```ts
|
|
1739
2393
|
import { task } from "spearkit";
|
|
@@ -1782,7 +2436,7 @@ Each field supports `*`, ranges (`1-5`), lists (`1,3,5`) and steps (`*/15`).
|
|
|
1782
2436
|
When both day-of-month and day-of-week are restricted, a date matches if
|
|
1783
2437
|
**either** does (standard cron behaviour).
|
|
1784
2438
|
|
|
1785
|
-
Aliases: `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`.
|
|
2439
|
+
Aliases: `@yearly`/`@annually`, `@monthly`, `@weekly`, `@daily`/`@midnight`, `@hourly`.
|
|
1786
2440
|
|
|
1787
2441
|
```ts
|
|
1788
2442
|
task({ name: "report", cron: "@daily", run: () => {} });
|
|
@@ -1798,6 +2452,30 @@ import { cron } from "spearkit";
|
|
|
1798
2452
|
const next = cron("*/15 * * * *").next(new Date());
|
|
1799
2453
|
```
|
|
1800
2454
|
|
|
2455
|
+
## One-shot jobs, follow-ups and on-ready recovery
|
|
2456
|
+
|
|
2457
|
+
Beyond recurring tasks, the scheduler runs one-shot timers (they `unref()`
|
|
2458
|
+
themselves, so they never keep the process alive) and a once-on-ready reconciler.
|
|
2459
|
+
|
|
2460
|
+
```ts
|
|
2461
|
+
// Run once after a delay; returns a cancel handle.
|
|
2462
|
+
const handle = client.scheduler.delay("remind", 10 * 60_000, async () => {
|
|
2463
|
+
// …remind the moderator if nothing happened…
|
|
2464
|
+
});
|
|
2465
|
+
handle.cancel(); // true if it was still pending
|
|
2466
|
+
|
|
2467
|
+
// A series of fires measured from "now"; the callback gets the fire index.
|
|
2468
|
+
client.scheduler.followUp("escalate", [10_000, 30_000, 60_000], (i) => {
|
|
2469
|
+
// i = 0, then 1, then 2
|
|
2470
|
+
});
|
|
2471
|
+
|
|
2472
|
+
// Run once the first time the scheduler starts (typically on clientReady) and
|
|
2473
|
+
// never again — ideal for restart recovery.
|
|
2474
|
+
client.scheduler.reconcile("voice-sessions", async (client) => {
|
|
2475
|
+
// …close orphaned voice sessions, reapply cached state…
|
|
2476
|
+
});
|
|
2477
|
+
```
|
|
2478
|
+
|
|
1801
2479
|
## The scheduler
|
|
1802
2480
|
|
|
1803
2481
|
`client.scheduler` is the `TaskScheduler`:
|
|
@@ -1848,6 +2526,25 @@ new SpearClient({
|
|
|
1848
2526
|
});
|
|
1849
2527
|
```
|
|
1850
2528
|
|
|
2529
|
+
### Dynamic (per-guild) prefixes
|
|
2530
|
+
|
|
2531
|
+
Pass `dynamic` to resolve extra prefix(es) per message — for example a custom
|
|
2532
|
+
per-guild prefix from a database or [`createSettings`](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md#persistent-storage).
|
|
2533
|
+
Dynamic prefixes are tried in addition to any static `prefix`; return
|
|
2534
|
+
`null`/`undefined` for none. It runs on every candidate message, so keep it fast
|
|
2535
|
+
(and cached).
|
|
2536
|
+
|
|
2537
|
+
```ts
|
|
2538
|
+
new SpearClient({
|
|
2539
|
+
intents: Intents.messages,
|
|
2540
|
+
prefix: {
|
|
2541
|
+
prefix: "!", // static fallback
|
|
2542
|
+
dynamic: async (message) =>
|
|
2543
|
+
message.guildId ? await settings.get(message.guildId).then((s) => s.prefix) : null,
|
|
2544
|
+
},
|
|
2545
|
+
});
|
|
2546
|
+
```
|
|
2547
|
+
|
|
1851
2548
|
## You need the MessageContent intent
|
|
1852
2549
|
|
|
1853
2550
|
Reading the text of other users' messages is a **privileged** gateway intent.
|
|
@@ -1896,6 +2593,8 @@ client.register(ping);
|
|
|
1896
2593
|
| `aliases` | `string[]` | Extra names that also trigger it. |
|
|
1897
2594
|
| `description` | `string` | Human description, for your own help command. |
|
|
1898
2595
|
| `cooldown` | `number \| CooldownConfig` | Per-user rate limit (a number is milliseconds). |
|
|
2596
|
+
| `guards` | `readonly Guard[]` | Preconditions run before the handler. See [Guards](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md). |
|
|
2597
|
+
| `args` | `(a) => PrefixArgsBuilder` | Typed argument schema; shapes `ctx.options`. See [Typed arguments](#typed-arguments). |
|
|
1899
2598
|
| `run` | `(ctx: PrefixContext) => void \| Promise<void>` | The handler. |
|
|
1900
2599
|
|
|
1901
2600
|
## The prefix context
|
|
@@ -1909,6 +2608,7 @@ adds the parsed arguments plus reply helpers.
|
|
|
1909
2608
|
| `ctx.commandName` | The matched name as the user typed it (an alias if they used one). |
|
|
1910
2609
|
| `ctx.args` | Whitespace-split arguments after the command name (`string[]`). |
|
|
1911
2610
|
| `ctx.rest` | The raw text after the command name (unsplit). |
|
|
2611
|
+
| `ctx.options` | Typed parsed arguments from the `args` schema (`{}` when none). |
|
|
1912
2612
|
| `ctx.author` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` | Actor and location accessors. |
|
|
1913
2613
|
| `ctx.reply(content)` | Reply to the triggering message. |
|
|
1914
2614
|
| `ctx.send(content)` | Send a message to the same channel without a reply reference. |
|
|
@@ -1930,6 +2630,38 @@ export const echo = prefixCommand({
|
|
|
1930
2630
|
`ctx.args` and `ctx.rest` are two views of the same input: `!say hello world`
|
|
1931
2631
|
gives `args === ["hello", "world"]` and `rest === "hello world"`.
|
|
1932
2632
|
|
|
2633
|
+
## Typed arguments
|
|
2634
|
+
|
|
2635
|
+
Pass an `args` schema to parse positional arguments into typed values. Chain
|
|
2636
|
+
builder methods — first token → first arg, second → second, and so on — and read
|
|
2637
|
+
the result from `ctx.options`. Each method requires a name and takes optional
|
|
2638
|
+
settings (`required`, `default`, and per-type bounds).
|
|
2639
|
+
|
|
2640
|
+
```ts
|
|
2641
|
+
import { prefixCommand } from "spearkit";
|
|
2642
|
+
|
|
2643
|
+
export const mute = prefixCommand({
|
|
2644
|
+
name: "mute",
|
|
2645
|
+
description: "Mute a member",
|
|
2646
|
+
args: (a) =>
|
|
2647
|
+
a
|
|
2648
|
+
.snowflake("target", { required: true }) // raw id or <@mention> → string
|
|
2649
|
+
.duration("duration", { required: true }) // "1h30m" → number (ms)
|
|
2650
|
+
.rest("reason", { default: "No reason given" }), // remaining text → string
|
|
2651
|
+
run: (ctx) => {
|
|
2652
|
+
ctx.options.target; // string
|
|
2653
|
+
ctx.options.duration; // number
|
|
2654
|
+
ctx.options.reason; // string
|
|
2655
|
+
return ctx.reply(`Muted <@${ctx.options.target}> for ${ctx.options.duration}ms.`);
|
|
2656
|
+
},
|
|
2657
|
+
});
|
|
2658
|
+
```
|
|
2659
|
+
|
|
2660
|
+
Builder methods: `.string`, `.integer`, `.number`, `.boolean`, `.snowflake`,
|
|
2661
|
+
`.duration`, `.rest`. A missing required argument — or a value that fails to
|
|
2662
|
+
parse — makes spearkit reply with an error and skip the handler. Without an `args`
|
|
2663
|
+
schema, `ctx.options` is `{}`; use `ctx.args` / `ctx.rest` for raw access.
|
|
2664
|
+
|
|
1933
2665
|
## Aliases
|
|
1934
2666
|
|
|
1935
2667
|
List alternative names in `aliases`; any of them triggers the command, and
|
|
@@ -2196,10 +2928,87 @@ const client = new SpearClient({ logger: { level: "debug" } });
|
|
|
2196
2928
|
|
|
2197
2929
|
---
|
|
2198
2930
|
|
|
2931
|
+
# Discord API errors
|
|
2932
|
+
|
|
2933
|
+
discord.js reports REST failures as `DiscordAPIError` with a numeric `code`
|
|
2934
|
+
(`10008` "Unknown Message", `50013` "Missing Permissions", `50007` "Cannot send
|
|
2935
|
+
DMs to this user", …). Catching *everything* turns recoverable failures — a
|
|
2936
|
+
deleted message, a closed DM — into crashes or scary stack traces. spearkit gives
|
|
2937
|
+
you named codes, a type-narrowing predicate, and a friendly explanation.
|
|
2938
|
+
|
|
2939
|
+
## Recognise and recover
|
|
2940
|
+
|
|
2941
|
+
`isDiscordError(err, code?)` narrows the throw and optionally matches a code
|
|
2942
|
+
(or a list). Perfect for "ignore this one, re-throw the rest":
|
|
2943
|
+
|
|
2944
|
+
```ts
|
|
2945
|
+
import { DiscordErrorCode, isDiscordError } from "spearkit";
|
|
2946
|
+
|
|
2947
|
+
try {
|
|
2948
|
+
await message.delete();
|
|
2949
|
+
} catch (err) {
|
|
2950
|
+
if (isDiscordError(err, DiscordErrorCode.UnknownMessage)) return; // already gone
|
|
2951
|
+
throw err;
|
|
2952
|
+
}
|
|
2953
|
+
```
|
|
2954
|
+
|
|
2955
|
+
```ts
|
|
2956
|
+
// match any of several codes
|
|
2957
|
+
if (isDiscordError(err, [DiscordErrorCode.UnknownChannel, DiscordErrorCode.MissingAccess])) {
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
```
|
|
2961
|
+
|
|
2962
|
+
## Friendly messages
|
|
2963
|
+
|
|
2964
|
+
`explainDiscordError(err)` returns an end-user-appropriate sentence for a
|
|
2965
|
+
recognised failure, or `null` otherwise (fall back to a generic message + log):
|
|
2966
|
+
|
|
2967
|
+
```ts
|
|
2968
|
+
import { explainDiscordError } from "spearkit";
|
|
2969
|
+
|
|
2970
|
+
catch (err) {
|
|
2971
|
+
await ctx.error(explainDiscordError(err) ?? "Something went wrong.");
|
|
2972
|
+
}
|
|
2973
|
+
```
|
|
2974
|
+
|
|
2975
|
+
spearkit already routes its own command/context-menu errors through
|
|
2976
|
+
`explainDiscordError`, so a handler that throws `Missing Permissions` shows the
|
|
2977
|
+
user *"I'm missing the permissions needed to do that."* instead of a generic
|
|
2978
|
+
error.
|
|
2979
|
+
|
|
2980
|
+
## Named codes
|
|
2981
|
+
|
|
2982
|
+
`DiscordErrorCode` is a curated map of the codes bots actually hit:
|
|
2983
|
+
|
|
2984
|
+
| Name | Code | When |
|
|
2985
|
+
| --- | --- | --- |
|
|
2986
|
+
| `UnknownChannel` | 10003 | Channel gone/invisible |
|
|
2987
|
+
| `UnknownMessage` | 10008 | Message deleted |
|
|
2988
|
+
| `UnknownMember` | 10007 | Member left |
|
|
2989
|
+
| `UnknownInteraction` | 10062 | Token expired (the 3s window) |
|
|
2990
|
+
| `MissingAccess` | 50001 | No access to the resource |
|
|
2991
|
+
| `CannotSendMessagesToThisUser` | 50007 | DMs closed / blocked |
|
|
2992
|
+
| `MissingPermissions` | 50013 | Missing a permission |
|
|
2993
|
+
| `InteractionHasAlreadyBeenAcknowledged` | 40060 | Double-acked |
|
|
2994
|
+
|
|
2995
|
+
(See the type for the full set — it mirrors discord.js' `RESTJSONErrorCodes`.)
|
|
2996
|
+
|
|
2997
|
+
## Transport & rate-limit errors
|
|
2998
|
+
|
|
2999
|
+
- `isHTTPError(err)` — a transport-level `HTTPError` (timeout, 5xx, aborted): an
|
|
3000
|
+
HTTP status with no Discord JSON code.
|
|
3001
|
+
- `isRateLimitError(err)` — a `DiscordAPIError` with HTTP status `429`.
|
|
3002
|
+
`explainDiscordError` handles this case first, returning a "try again in a
|
|
3003
|
+
moment" message.
|
|
3004
|
+
|
|
3005
|
+
---
|
|
3006
|
+
|
|
2199
3007
|
# Usage tracking
|
|
2200
3008
|
|
|
2201
|
-
Usage tracking records **who used what**: every
|
|
2202
|
-
and prefix-command invocation becomes a `UsageEvent`
|
|
3009
|
+
Usage tracking records **who used what**: every command, component, context-menu
|
|
3010
|
+
and prefix-command invocation — successful or errored — becomes a `UsageEvent`
|
|
3011
|
+
that spearkit can persist to a
|
|
2203
3012
|
store and/or mirror into a Discord channel. Turn it on with the client's `usage`
|
|
2204
3013
|
option.
|
|
2205
3014
|
|
|
@@ -2211,13 +3020,14 @@ independent sinks:
|
|
|
2211
3020
|
| | Logger | Usage tracking |
|
|
2212
3021
|
| --- | --- | --- |
|
|
2213
3022
|
| Question | *What is the bot doing?* (diagnostics) | *Who used which feature?* (audit) |
|
|
2214
|
-
| Content | Free-form messages, levels, errors, internals | Structured `UsageEvent`s for
|
|
3023
|
+
| Content | Free-form messages, levels, errors, internals | Structured `UsageEvent`s for every completed use (with its `outcome`) |
|
|
2215
3024
|
| Sinks | Console / your log pipeline | A database store and/or a Discord channel |
|
|
2216
3025
|
| Configured by | the `logger` option | the `usage` option |
|
|
2217
3026
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
3027
|
+
Both successes and handler errors are recorded as usage events — an error carries
|
|
3028
|
+
`outcome: "error"` and an `errorMessage` — so usage is a complete audit trail.
|
|
3029
|
+
The [logger](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md) is for debugging; usage tracking is for analytics,
|
|
3030
|
+
audit trails, and "top commands" dashboards.
|
|
2221
3031
|
|
|
2222
3032
|
## Enabling it
|
|
2223
3033
|
|
|
@@ -2235,8 +3045,9 @@ const client = new SpearClient({
|
|
|
2235
3045
|
});
|
|
2236
3046
|
```
|
|
2237
3047
|
|
|
2238
|
-
Once enabled, spearkit auto-tracks every
|
|
2239
|
-
command —
|
|
3048
|
+
Once enabled, spearkit auto-tracks every command, component, context-menu and
|
|
3049
|
+
prefix-command invocation — successes and errors alike — with no tracking code in
|
|
3050
|
+
your handlers.
|
|
2240
3051
|
|
|
2241
3052
|
## The usage event
|
|
2242
3053
|
|
|
@@ -2244,15 +3055,22 @@ Each tracked use is a `UsageEvent`:
|
|
|
2244
3055
|
|
|
2245
3056
|
```ts
|
|
2246
3057
|
interface UsageEvent {
|
|
2247
|
-
type: "command" | "prefix" | "component" | "event"
|
|
2248
|
-
name: string; // command/component/event name
|
|
3058
|
+
type: UsageType; // "command" | "prefix" | "component" | "event"
|
|
3059
|
+
name: string; // command / component / event name
|
|
2249
3060
|
userId?: string;
|
|
2250
3061
|
userTag?: string;
|
|
2251
3062
|
guildId?: string | null;
|
|
2252
3063
|
channelId?: string | null;
|
|
2253
3064
|
detail?: string; // free-form extra detail
|
|
3065
|
+
outcome?: UsageOutcome; // "success" | "error"
|
|
3066
|
+
durationMs?: number; // handler wall-clock time
|
|
3067
|
+
options?: Readonly<Record<string, UsageMetaValue>>; // snapshot of typed options
|
|
3068
|
+
errorMessage?: string; // set when outcome === "error"
|
|
2254
3069
|
timestamp: Date;
|
|
2255
3070
|
}
|
|
3071
|
+
type UsageType = "command" | "prefix" | "component" | "event";
|
|
3072
|
+
type UsageOutcome = "success" | "error";
|
|
3073
|
+
type UsageMetaValue = string | number | boolean | null;
|
|
2256
3074
|
```
|
|
2257
3075
|
|
|
2258
3076
|
## Stores (the database)
|
|
@@ -2629,12 +3447,256 @@ Because `use` awaits `setup`, every plugin is fully installed before
|
|
|
2629
3447
|
|
|
2630
3448
|
---
|
|
2631
3449
|
|
|
3450
|
+
# Collectors
|
|
3451
|
+
|
|
3452
|
+
discord.js collectors are powerful but fiddly: you wire an event emitter, set a
|
|
3453
|
+
`time`, write a `filter`, remember that dismissed modals need their own timeout,
|
|
3454
|
+
and translate the "timed out" rejection into something you can branch on.
|
|
3455
|
+
spearkit collapses the common cases to a single `await` that resolves to the
|
|
3456
|
+
result — or `null` on timeout.
|
|
3457
|
+
|
|
3458
|
+
Beyond these, see the [pagination and confirm helpers](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md#pagination--confirmation)
|
|
3459
|
+
for ready-made paged lists and yes/no gates.
|
|
3460
|
+
|
|
3461
|
+
## Wait for a message ("type your answer")
|
|
3462
|
+
|
|
3463
|
+
`ctx.awaitMessageFrom(userId?, options?)` waits for the next message in the
|
|
3464
|
+
current channel from a user (defaults to the invoking user):
|
|
3465
|
+
|
|
3466
|
+
```ts
|
|
3467
|
+
await ctx.reply("What's your favourite colour?");
|
|
3468
|
+
const answer = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
|
|
3469
|
+
if (answer === null) return ctx.followUp("Timed out.");
|
|
3470
|
+
await ctx.followUp(`Nice — ${answer.content}!`);
|
|
3471
|
+
```
|
|
3472
|
+
|
|
3473
|
+
The standalone `awaitMessage(channel, options)` does the same for any text
|
|
3474
|
+
channel; `options` takes `{ filter, time }` (default `time` 60s).
|
|
3475
|
+
|
|
3476
|
+
## Wait for a modal submission
|
|
3477
|
+
|
|
3478
|
+
`ctx.awaitModal(modal, options?)` (on command and component contexts) shows a
|
|
3479
|
+
modal and waits for the submission — scoped to the same user and that modal's
|
|
3480
|
+
custom-id, always bounded — sidestepping the "Unknown interaction after a
|
|
3481
|
+
cancelled modal" trap:
|
|
3482
|
+
|
|
3483
|
+
```ts
|
|
3484
|
+
import { modal, textInput } from "spearkit";
|
|
3485
|
+
|
|
3486
|
+
const form = modal({
|
|
3487
|
+
id: "feedback",
|
|
3488
|
+
title: "Feedback",
|
|
3489
|
+
fields: { text: textInput({ label: "Your feedback", required: true }) },
|
|
3490
|
+
run: (ctx) => ctx.replyEphemeral("thanks"), // routed fallback
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3493
|
+
const submitted = await ctx.awaitModal(form.build(), { time: 120_000 });
|
|
3494
|
+
if (submitted === null) return; // dismissed or timed out
|
|
3495
|
+
await submitted.reply({ content: submitted.fields.getTextInputValue("text"), ephemeral: true });
|
|
3496
|
+
```
|
|
3497
|
+
|
|
3498
|
+
This is the inline alternative to registering a separate modal handler and
|
|
3499
|
+
threading state through its custom-id.
|
|
3500
|
+
|
|
3501
|
+
## Wait for a component click
|
|
3502
|
+
|
|
3503
|
+
`awaitComponent(message, options)` waits for the next button/select interaction
|
|
3504
|
+
on a message. `options` takes `{ filter, time, componentType }`. You must still
|
|
3505
|
+
acknowledge the returned interaction (`update`/`deferUpdate`/`reply`):
|
|
3506
|
+
|
|
3507
|
+
```ts
|
|
3508
|
+
import { awaitComponent } from "spearkit";
|
|
3509
|
+
|
|
3510
|
+
const sent = await ctx.channel!.send({ content: "Pick one", components: [row] });
|
|
3511
|
+
const click = await awaitComponent(sent, { time: 15_000 });
|
|
3512
|
+
if (click === null) return;
|
|
3513
|
+
await click.update("Got it!");
|
|
3514
|
+
```
|
|
3515
|
+
|
|
3516
|
+
---
|
|
3517
|
+
|
|
3518
|
+
# Key-value store & settings
|
|
3519
|
+
|
|
3520
|
+
Almost every community bot needs to remember *something* per guild — a custom
|
|
3521
|
+
prefix, a mod-log channel, a welcome message — and reaches for a database on day
|
|
3522
|
+
one. spearkit ships a dependency-free `KeyValueStore` interface with two
|
|
3523
|
+
backends, plus a typed per-guild settings helper. Swap in Redis/SQL later by
|
|
3524
|
+
implementing the same interface.
|
|
3525
|
+
|
|
3526
|
+
## Stores
|
|
3527
|
+
|
|
3528
|
+
```ts
|
|
3529
|
+
import { JsonStore, MemoryStore } from "spearkit";
|
|
3530
|
+
|
|
3531
|
+
const dev = new MemoryStore(); // in-memory, great for tests
|
|
3532
|
+
const prod = new JsonStore("data/db.json"); // durable JSON file
|
|
3533
|
+
```
|
|
3534
|
+
|
|
3535
|
+
Both implement `KeyValueStore`:
|
|
3536
|
+
|
|
3537
|
+
```ts
|
|
3538
|
+
await store.set("key", { any: "json" });
|
|
3539
|
+
await store.get<{ any: string }>("key"); // typed read, or undefined
|
|
3540
|
+
await store.has("key");
|
|
3541
|
+
await store.delete("key"); // → boolean (existed?)
|
|
3542
|
+
await store.keys(); // → string[]
|
|
3543
|
+
await store.clear();
|
|
3544
|
+
```
|
|
3545
|
+
|
|
3546
|
+
`MemoryStore` deep-clones on read and write, so callers can't mutate stored
|
|
3547
|
+
state. `JsonStore` serves reads from an in-memory cache and commits writes
|
|
3548
|
+
atomically (temp file + rename) through a queue — a crash mid-write can't corrupt
|
|
3549
|
+
the file, and concurrent writes don't interleave.
|
|
3550
|
+
|
|
3551
|
+
## Typed per-guild settings
|
|
3552
|
+
|
|
3553
|
+
`createSettings` wraps a store with defaults. `get` always returns a complete
|
|
3554
|
+
object; `set` persists *only* the overrides, so widening `defaults` later is
|
|
3555
|
+
safe.
|
|
3556
|
+
|
|
3557
|
+
```ts
|
|
3558
|
+
import { JsonStore, createSettings } from "spearkit";
|
|
3559
|
+
|
|
3560
|
+
const settings = createSettings({
|
|
3561
|
+
store: new JsonStore("data/guilds.json"),
|
|
3562
|
+
defaults: { prefix: "!", modLogChannelId: null as string | null },
|
|
3563
|
+
});
|
|
3564
|
+
|
|
3565
|
+
const cfg = await settings.get(guildId); // { prefix, modLogChannelId }
|
|
3566
|
+
await settings.set(guildId, { prefix: "?" }); // shallow-merged + persisted
|
|
3567
|
+
await settings.reset(guildId); // back to defaults
|
|
3568
|
+
```
|
|
3569
|
+
|
|
3570
|
+
Pass `namespace` to keep several settings groups in one store:
|
|
3571
|
+
|
|
3572
|
+
```ts
|
|
3573
|
+
const guilds = createSettings({ store, defaults: { prefix: "!" }, namespace: "guild" });
|
|
3574
|
+
const users = createSettings({ store, defaults: { xp: 0 }, namespace: "user" });
|
|
3575
|
+
```
|
|
3576
|
+
|
|
3577
|
+
## Dynamic per-guild prefix
|
|
3578
|
+
|
|
3579
|
+
A stored prefix is only useful if prefix commands respect it. `prefix.dynamic`
|
|
3580
|
+
resolves extra prefix(es) per message — combine it with `createSettings` for true
|
|
3581
|
+
per-guild prefixes:
|
|
3582
|
+
|
|
3583
|
+
```ts
|
|
3584
|
+
const client = new SpearClient({
|
|
3585
|
+
prefix: {
|
|
3586
|
+
dynamic: async (message) =>
|
|
3587
|
+
message.guildId ? (await settings.get(message.guildId)).prefix : null,
|
|
3588
|
+
},
|
|
3589
|
+
});
|
|
3590
|
+
```
|
|
3591
|
+
|
|
3592
|
+
The resolver runs on every candidate message, so keep it fast (cache or use the
|
|
3593
|
+
in-memory `JsonStore` cache). Returned prefixes are tried *in addition* to any
|
|
3594
|
+
static `prefix`. See [Prefix commands](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md) for the rest of the prefix
|
|
3595
|
+
system.
|
|
3596
|
+
|
|
3597
|
+
## Namespacing a raw store
|
|
3598
|
+
|
|
3599
|
+
`namespaced(store, prefix)` returns a `KeyValueStore` whose keys are
|
|
3600
|
+
transparently prefixed — handy for sharing one file across features:
|
|
3601
|
+
|
|
3602
|
+
```ts
|
|
3603
|
+
import { namespaced } from "spearkit";
|
|
3604
|
+
|
|
3605
|
+
const tags = namespaced(store, "tags");
|
|
3606
|
+
await tags.set("hello", "world"); // stored under "tags:hello"
|
|
3607
|
+
```
|
|
3608
|
+
|
|
3609
|
+
---
|
|
3610
|
+
|
|
3611
|
+
# Messages & limits
|
|
3612
|
+
|
|
3613
|
+
Discord caps a message's `content` at **2000 characters**. Long output — a log
|
|
3614
|
+
dump, a list, an AI response — silently fails or throws unless you split it.
|
|
3615
|
+
spearkit ships two helpers (in addition to the duration/timestamp formatters; see
|
|
3616
|
+
the [API reference](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/api-reference.md)).
|
|
3617
|
+
|
|
3618
|
+
## Split long output
|
|
3619
|
+
|
|
3620
|
+
`chunkMessage(text, options?)` breaks text into chunks that each fit the limit,
|
|
3621
|
+
preferring line boundaries (and word boundaries for an over-long single line) so
|
|
3622
|
+
you never lose the tail:
|
|
3623
|
+
|
|
3624
|
+
```ts
|
|
3625
|
+
import { chunkMessage } from "spearkit";
|
|
3626
|
+
|
|
3627
|
+
const parts = chunkMessage(hugeLog); // default max = 2000
|
|
3628
|
+
await ctx.reply(parts[0] ?? "(empty)");
|
|
3629
|
+
for (const part of parts.slice(1)) await ctx.followUp(part);
|
|
3630
|
+
```
|
|
3631
|
+
|
|
3632
|
+
Pass `{ max }` to target a smaller budget (e.g. inside a code block or embed
|
|
3633
|
+
description). `MESSAGE_CHARACTER_LIMIT` (2000) is exported as the default.
|
|
3634
|
+
|
|
3635
|
+
## Truncate
|
|
3636
|
+
|
|
3637
|
+
`truncate(text, max, suffix?)` cuts text to `max` characters, appending the
|
|
3638
|
+
suffix (default `…`) — the result, suffix included, never exceeds `max`:
|
|
3639
|
+
|
|
3640
|
+
```ts
|
|
3641
|
+
import { truncate } from "spearkit";
|
|
3642
|
+
|
|
3643
|
+
embed.setFooter({ text: truncate(reason, 100) });
|
|
3644
|
+
truncate("a very long reason", 10); // → "a very lo…"
|
|
3645
|
+
```
|
|
3646
|
+
|
|
3647
|
+
---
|
|
3648
|
+
|
|
3649
|
+
# Graceful shutdown
|
|
3650
|
+
|
|
3651
|
+
A `Ctrl-C` or a container stop sends your process a signal. If you don't handle
|
|
3652
|
+
it, the process dies mid-flight — the gateway connection, scheduler timers, and
|
|
3653
|
+
any open database handles are reaped abruptly. Graceful shutdown runs an optional
|
|
3654
|
+
cleanup hook, calls `client.destroy()` (which also stops spearkit's scheduler),
|
|
3655
|
+
then exits — with a hard timeout so a wedged shutdown can't hang forever.
|
|
3656
|
+
|
|
3657
|
+
## On a SpearClient
|
|
3658
|
+
|
|
3659
|
+
```ts
|
|
3660
|
+
client.enableGracefulShutdown({
|
|
3661
|
+
onShutdown: () => db.close(), // flush state before we exit
|
|
3662
|
+
});
|
|
3663
|
+
await client.start();
|
|
3664
|
+
```
|
|
3665
|
+
|
|
3666
|
+
Progress is logged through `client.logger`. The method returns a disposer that
|
|
3667
|
+
removes the signal handlers (useful for tests / hot-reload).
|
|
3668
|
+
|
|
3669
|
+
## Standalone
|
|
3670
|
+
|
|
3671
|
+
`gracefulShutdown(client, options)` works with any object that has a `destroy()`
|
|
3672
|
+
method:
|
|
3673
|
+
|
|
3674
|
+
```ts
|
|
3675
|
+
import { gracefulShutdown } from "spearkit";
|
|
3676
|
+
|
|
3677
|
+
gracefulShutdown(client, { onShutdown: () => db.close() });
|
|
3678
|
+
```
|
|
3679
|
+
|
|
3680
|
+
## Options
|
|
3681
|
+
|
|
3682
|
+
| Field | Default | Meaning |
|
|
3683
|
+
| --- | --- | --- |
|
|
3684
|
+
| `signals` | `["SIGINT", "SIGTERM"]` | Signals to listen for. |
|
|
3685
|
+
| `timeoutMs` | `10000` | Force-exit if shutdown exceeds this. |
|
|
3686
|
+
| `onShutdown` | — | Runs before `destroy()`; receives the signal. |
|
|
3687
|
+
| `exit` | `true` | Call `process.exit()` when done (set `false` in tests). |
|
|
3688
|
+
| `logger` | — | `{ info?, error? }` progress logger. |
|
|
3689
|
+
|
|
3690
|
+
Shutdown runs **once** — repeated signals during teardown are ignored.
|
|
3691
|
+
|
|
3692
|
+
---
|
|
3693
|
+
|
|
2632
3694
|
# File-based loading
|
|
2633
3695
|
|
|
2634
3696
|
Instead of importing and registering every handler by hand, you can keep one
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3697
|
+
handler per file and let spearkit discover them. The loader imports a directory,
|
|
3698
|
+
inspects each module's exports, and registers everything that is a command,
|
|
3699
|
+
event, component, scheduled task or prefix command.
|
|
2638
3700
|
|
|
2639
3701
|
## `client.load`
|
|
2640
3702
|
|
|
@@ -2667,8 +3729,10 @@ await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
|
2667
3729
|
|
|
2668
3730
|
For every imported file, spearkit walks **all** of its exports — default *and*
|
|
2669
3731
|
named — and registers each value that is a command (`command`, `commandGroup`),
|
|
2670
|
-
an event (`event`),
|
|
2671
|
-
|
|
3732
|
+
an event (`event`), a component (`button`, `stringSelect`, `modal`, …), a
|
|
3733
|
+
scheduled task (`task`) or a prefix command (`prefixCommand`). Other exports
|
|
3734
|
+
(helpers, constants, types) are ignored, and context-menu commands are **not**
|
|
3735
|
+
auto-detected — register those explicitly. So both of these are picked up:
|
|
2672
3736
|
|
|
2673
3737
|
```ts
|
|
2674
3738
|
// default export
|
|
@@ -2800,17 +3864,39 @@ new SpearClient(options?: SpearClientOptions)
|
|
|
2800
3864
|
| `commands` | `CommandRegistry` | Slash command registry + dispatcher. |
|
|
2801
3865
|
| `events` | `EventRegistry` | Event listener registry. |
|
|
2802
3866
|
| `components` | `ComponentRegistry` | Button/select/modal router. |
|
|
3867
|
+
| `logger` | `Logger` | Structured logger (`client.logger.child(scope)` for sub-scopes). |
|
|
3868
|
+
| `cooldowns` | `CooldownManager` | Shared cooldown manager (also used by prefix commands). |
|
|
3869
|
+
| `scheduler` | `TaskScheduler` | Cron / interval task scheduler. |
|
|
3870
|
+
| `prefix` | `PrefixRegistry` | Prefix (text) command registry. |
|
|
3871
|
+
| `usage` | `UsageTracker` | Usage tracker — records who used what. |
|
|
3872
|
+
| `embeds` | `Embeds` | Preset embed factory behind `ctx.success/error/...`. |
|
|
3873
|
+
| `contextMenus` | `ContextMenuRegistry` | User / message context-menu registry. |
|
|
2803
3874
|
| `register(...items: Registerable[])` | `this` | Route each item to the matching registry. |
|
|
2804
3875
|
| `use(...plugins: SpearPlugin[])` | `Promise<this>` | Run each plugin's `setup`. |
|
|
2805
3876
|
| `load(dir: string, options?: LoadOptions)` | `Promise<number>` | Import a directory and register its exports. Returns count. |
|
|
2806
3877
|
| `start(token?: string)` | `Promise<this>` | Log in (falls back to `DISCORD_TOKEN`). |
|
|
2807
3878
|
| `deployCommands(options?: { guildId?: string })` | `Promise<DeployResult>` | Push commands using the client's REST. Call after ready. |
|
|
3879
|
+
| `deployAllCommands(options?)` | `Promise<DeployResult \| { skipped: true; reason; body }>` | Deploy slash + context menus together; supports `dryRun` and `strategy: "diff"`. |
|
|
3880
|
+
| `schedule(config: TaskConfig)` | `ScheduledTask` | Define and register a scheduled task in one call. |
|
|
3881
|
+
| `enableGracefulShutdown(options?: GracefulShutdownOptions)` | `() => void` | Tear down cleanly on `SIGINT`/`SIGTERM`; returns a disposer. |
|
|
2808
3882
|
|
|
2809
3883
|
Inherits everything from discord.js `Client` (`on`, `once`, `login`, `ws`, `rest`, `application`, `user`, …).
|
|
2810
3884
|
|
|
2811
|
-
### `type SpearClientOptions = Partial<ClientOptions
|
|
3885
|
+
### `type SpearClientOptions = Partial<ClientOptions> & SpearOptions`
|
|
2812
3886
|
|
|
2813
|
-
|
|
3887
|
+
discord.js `ClientOptions` (with `intents` optional — it defaults to
|
|
3888
|
+
`Intents.default`) intersected with spearkit's own options (`SpearOptions`):
|
|
3889
|
+
|
|
3890
|
+
| Option | Type | Configures |
|
|
3891
|
+
| ------ | ---- | ---------- |
|
|
3892
|
+
| `logger` | `Logger \| LoggerOptions` | The `client.logger`. |
|
|
3893
|
+
| `dotenv` | `boolean \| LoadEnvOptions` | Auto-load `.env` on `start()` (default `true`). |
|
|
3894
|
+
| `cooldown` | `CooldownInput` | Default cooldown applied to every command. |
|
|
3895
|
+
| `prefix` | `string \| readonly string[] \| PrefixOptions` | Enable prefix commands. |
|
|
3896
|
+
| `usage` | `UsageOptions` | Usage-tracking store and/or channel. |
|
|
3897
|
+
| `embeds` | `Embeds \| EmbedsOptions` | Preset embed factory. |
|
|
3898
|
+
| `guards` | `readonly Guard[]` | Default guards run before every handler. |
|
|
3899
|
+
| `autoDefer` | `AutoDeferInput` | Default auto-defer for slash + context-menu handlers. |
|
|
2814
3900
|
|
|
2815
3901
|
### `const Intents`
|
|
2816
3902
|
|
|
@@ -2824,7 +3910,7 @@ Ready-made intent presets (arrays of `GatewayIntentBits`).
|
|
|
2824
3910
|
| `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
|
|
2825
3911
|
| `Intents.all` | Every intent (includes privileged). |
|
|
2826
3912
|
|
|
2827
|
-
### `type Registerable = SlashCommand | EventDef | ComponentDef`
|
|
3913
|
+
### `type Registerable = SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand | ContextMenuCommand`
|
|
2828
3914
|
|
|
2829
3915
|
The union accepted by `SpearClient.register`.
|
|
2830
3916
|
|
|
@@ -2846,6 +3932,9 @@ interface CommandConfig<O extends OptionMap, R> {
|
|
|
2846
3932
|
guildOnly?: boolean;
|
|
2847
3933
|
nameLocalizations?: LocalizationMap;
|
|
2848
3934
|
descriptionLocalizations?: LocalizationMap;
|
|
3935
|
+
cooldown?: CooldownInput;
|
|
3936
|
+
guards?: readonly Guard[];
|
|
3937
|
+
autoDefer?: AutoDeferInput;
|
|
2849
3938
|
run: (ctx: CommandContext<O>) => Awaitable<R>;
|
|
2850
3939
|
}
|
|
2851
3940
|
```
|
|
@@ -2865,6 +3954,9 @@ interface CommandGroupConfig {
|
|
|
2865
3954
|
guildOnly?: boolean;
|
|
2866
3955
|
nameLocalizations?: LocalizationMap;
|
|
2867
3956
|
descriptionLocalizations?: LocalizationMap;
|
|
3957
|
+
cooldown?: CooldownInput;
|
|
3958
|
+
guards?: readonly Guard[];
|
|
3959
|
+
autoDefer?: AutoDeferInput;
|
|
2868
3960
|
}
|
|
2869
3961
|
```
|
|
2870
3962
|
|
|
@@ -2900,6 +3992,9 @@ interface SubcommandGroupConfig {
|
|
|
2900
3992
|
| `toJSON()` | `RESTPostAPIChatInputApplicationCommandsJSONBody` | REST payload. |
|
|
2901
3993
|
| `execute(interaction)` | `Promise<void>` | Run for a chat-input interaction. |
|
|
2902
3994
|
| `autocomplete(interaction)` | `Promise<void>` | Run autocomplete for the focused option. |
|
|
3995
|
+
| `cooldown` | `CooldownConfig \| undefined` | Resolved cooldown, when set. |
|
|
3996
|
+
| `guards` | `readonly Guard[] \| undefined` | Guards run before `execute`. |
|
|
3997
|
+
| `autoDefer` | `AutoDeferConfig \| undefined` | Resolved auto-defer config, when set. |
|
|
2903
3998
|
|
|
2904
3999
|
### `class CommandContext<O> extends BaseContext<ChatInputCommandInteraction>`
|
|
2905
4000
|
|
|
@@ -2909,6 +4004,7 @@ interface SubcommandGroupConfig {
|
|
|
2909
4004
|
| `commandName` | `string` | Invoked command name. |
|
|
2910
4005
|
| `subcommand` | `string \| null` | Invoked subcommand, if any. |
|
|
2911
4006
|
| `showModal(modal)` | `Promise<void>` | Present a modal. |
|
|
4007
|
+
| `awaitModal(modal, options?)` | `Promise<ModalSubmitInteraction \| null>` | Show a modal and await its submission (scoped to this user). |
|
|
2912
4008
|
|
|
2913
4009
|
Plus all `BaseContext` members.
|
|
2914
4010
|
|
|
@@ -2927,6 +4023,10 @@ Plus all `BaseContext` members.
|
|
|
2927
4023
|
| `handle(interaction)` | `Promise<void>` | Dispatch a chat-input interaction. |
|
|
2928
4024
|
| `handleAutocomplete(interaction)` | `Promise<void>` | Dispatch an autocomplete interaction. |
|
|
2929
4025
|
| `deploy(options: DeployOptions)` | `Promise<DeployResult>` | Push commands to discord. |
|
|
4026
|
+
| `setLogger(logger: Logger)` | `this` | Attach a debug logger for dispatch tracing. |
|
|
4027
|
+
| `setCooldowns(manager: CooldownManager, default?: CooldownConfig)` | `this` | Wire a shared cooldown manager and optional default. |
|
|
4028
|
+
| `setDefaultGuards(guards: readonly Guard[])` | `this` | Guards run before each command's own guards. |
|
|
4029
|
+
| `setUsageHook(hook: (event: UsageEvent) => void)` | `this` | Called after each dispatch (success or error). |
|
|
2930
4030
|
|
|
2931
4031
|
```ts
|
|
2932
4032
|
type CommandErrorHandler = (error: Error, interaction: ChatInputCommandInteraction) => Awaitable<void>;
|
|
@@ -3064,6 +4164,7 @@ interface ButtonConfig<P extends string, R> {
|
|
|
3064
4164
|
style?: ButtonStyleInput; // "Primary" | "Secondary" | "Success" | "Danger" | ButtonStyle.*
|
|
3065
4165
|
emoji?: ComponentEmojiResolvable;
|
|
3066
4166
|
disabled?: boolean;
|
|
4167
|
+
guards?: readonly Guard[];
|
|
3067
4168
|
run: (ctx: ButtonContext<Params<P>>) => Awaitable<R>;
|
|
3068
4169
|
}
|
|
3069
4170
|
|
|
@@ -3073,11 +4174,13 @@ interface StringSelectConfig<P extends string, R> {
|
|
|
3073
4174
|
id: P;
|
|
3074
4175
|
options: readonly SelectMenuComponentOptionData[];
|
|
3075
4176
|
placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
4177
|
+
guards?: readonly Guard[];
|
|
3076
4178
|
run: (ctx: StringSelectContext<Params<P>>) => Awaitable<R>;
|
|
3077
4179
|
}
|
|
3078
4180
|
|
|
3079
4181
|
interface EntitySelectConfig<P extends string> {
|
|
3080
4182
|
id: P; placeholder?: string; minValues?: number; maxValues?: number; disabled?: boolean;
|
|
4183
|
+
guards?: readonly Guard[];
|
|
3081
4184
|
}
|
|
3082
4185
|
// user/role/mentionable selects take EntitySelectConfig & { run };
|
|
3083
4186
|
// channelSelect additionally takes { channelTypes?: readonly ChannelType[] }.
|
|
@@ -3092,6 +4195,7 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
|
|
|
3092
4195
|
id: P;
|
|
3093
4196
|
title: string;
|
|
3094
4197
|
fields: F;
|
|
4198
|
+
guards?: readonly Guard[];
|
|
3095
4199
|
run: (ctx: ModalContext<Params<P>, keyof F & string>) => Awaitable<R>;
|
|
3096
4200
|
}
|
|
3097
4201
|
```
|
|
@@ -3100,7 +4204,7 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
|
|
|
3100
4204
|
|
|
3101
4205
|
| Class | Extra members |
|
|
3102
4206
|
| ----- | ------------- |
|
|
3103
|
-
| `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)` (+ BaseContext) |
|
|
4207
|
+
| `MessageComponentContext<P, I>` | `params`, `customId`, `message`, `update(input)`, `deferUpdate()`, `showModal(modal)`, `awaitModal(modal, options?)` (+ BaseContext) |
|
|
3104
4208
|
| `ButtonContext<P>` | — |
|
|
3105
4209
|
| `StringSelectContext<P>` | `values: string[]`, `value: string \| undefined` |
|
|
3106
4210
|
| `UserSelectContext<P>` | `values`, `users`, `members` |
|
|
@@ -3117,6 +4221,9 @@ interface ModalConfig<P extends string, F extends Record<string, TextInputDef>,
|
|
|
3117
4221
|
| `onError(handler: ComponentErrorHandler)` | `this` | Set the error handler. |
|
|
3118
4222
|
| `size` | `number` | Count. |
|
|
3119
4223
|
| `handle(interaction: Interaction)` | `Promise<boolean>` | Route an interaction; `true` if matched. |
|
|
4224
|
+
| `setLogger(logger: Logger)` | `this` | Debug logger for dispatch tracing. |
|
|
4225
|
+
| `setUsageHook(hook: (event: UsageEvent) => void)` | `this` | Called after each component run (success or error). |
|
|
4226
|
+
| `setDefaultGuards(guards: readonly Guard[])` | `this` | Guards run before each component's own guards. |
|
|
3120
4227
|
|
|
3121
4228
|
```ts
|
|
3122
4229
|
type ComponentErrorHandler = (error: Error, interaction: RepliableInteraction) => Awaitable<void>;
|
|
@@ -3156,7 +4263,14 @@ The base for every interaction context.
|
|
|
3156
4263
|
| `editReply(input)` | `Promise<Message>` | Edit the response. |
|
|
3157
4264
|
| `followUp(input)` | `Promise<Message>` | Additional message. |
|
|
3158
4265
|
| `send(input)` | `Promise<void>` | State-aware reply/edit/followUp. |
|
|
3159
|
-
| `error(
|
|
4266
|
+
| `error(input, options?)` | `Promise<void>` | State-aware preset error embed; defaults to ephemeral (pass `{ ephemeral: false }` to override). |
|
|
4267
|
+
| `success` / `info` / `warn` `(input, options?)` | `Promise<void>` | State-aware preset embeds (green / blue / yellow). |
|
|
4268
|
+
| `replyError(input, options?)` | `Promise<InteractionResponse>` | Initial-reply error embed; defaults to ephemeral. |
|
|
4269
|
+
| `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `Promise<InteractionResponse>` | Initial-reply preset embeds. |
|
|
4270
|
+
| `botPermissions` | `Readonly<PermissionsBitField>` | The bot's resolved permissions in the channel (zero-fetch). |
|
|
4271
|
+
| `botMissing(required)` | `PermissionsString[]` | Permission names the bot is missing here. |
|
|
4272
|
+
| `userMissing(required)` | `PermissionsString[]` | Permission names the invoking user is missing here. |
|
|
4273
|
+
| `awaitMessageFrom(userId?, options?)` | `Promise<Message \| null>` | Wait for the next message from a user in this channel. |
|
|
3160
4274
|
|
|
3161
4275
|
```ts
|
|
3162
4276
|
type ReplyData = InteractionReplyOptions & { ephemeral?: boolean };
|
|
@@ -3191,12 +4305,12 @@ function loadInto(client: SpearClient, dir: string, options?: LoadOptions): Prom
|
|
|
3191
4305
|
## Added in 0.2
|
|
3192
4306
|
|
|
3193
4307
|
New subsystems, each with a dedicated guide. The `SpearClient` options
|
|
3194
|
-
`{ logger?, dotenv?, cooldown?, prefix?, usage? }` configure them.
|
|
4308
|
+
`{ logger?, dotenv?, cooldown?, prefix?, usage?, embeds?, guards? }` configure them.
|
|
3195
4309
|
|
|
3196
4310
|
### Logging — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/logging.md)
|
|
3197
4311
|
|
|
3198
4312
|
```ts
|
|
3199
|
-
class Logger { debug/info/warn/error(message: string, options?: { error?: Error; data?: Record<string, LogValue> }): void; child(scope: string): Logger; setLevel(level: LogThreshold): this; enabled(level: LogLevel): boolean; }
|
|
4313
|
+
class Logger { log(level, message, options?): void; debug/info/warn/error(message: string, options?: { error?: Error; data?: Record<string, LogValue> }): void; child(scope: string): Logger; setLevel(level: LogThreshold): this; enabled(level: LogLevel): boolean; addTransport(sink): this; setTransports(sinks): this; }
|
|
3200
4314
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
|
3201
4315
|
type LogThreshold = LogLevel | "silent";
|
|
3202
4316
|
function consoleSink(entry: LogEntry): void;
|
|
@@ -3218,6 +4332,13 @@ const env: { string(k, fallback?); number(k, fallback?); boolean(k, fallback?);
|
|
|
3218
4332
|
```ts
|
|
3219
4333
|
interface CooldownConfig { duration: number; scope?: "user" | "guild" | "channel" | "global"; exempt?: { users?: string[]; roles?: string[] }; overrides?: { users?: Record<string, number>; roles?: Record<string, number> }; message?: string | ((remainingMs: number) => string); }
|
|
3220
4334
|
class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(...); clear(); }
|
|
4335
|
+
type CooldownInput = number | CooldownConfig; // a bare ms duration, or a full config
|
|
4336
|
+
type CooldownScope = "user" | "guild" | "channel" | "global";
|
|
4337
|
+
type CooldownResult = { allowed: true } | { allowed: false; remaining: number };
|
|
4338
|
+
interface CooldownActor { userId; roleIds; guildId; channelId; } // also: CooldownExemptions, CooldownOverrides
|
|
4339
|
+
function normalizeCooldown(input: CooldownInput): CooldownConfig;
|
|
4340
|
+
function effectiveDuration(config: CooldownConfig, actor: CooldownActor): number | null; // null = exempt
|
|
4341
|
+
function formatCooldownMessage(config: CooldownConfig, remainingMs: number): string;
|
|
3221
4342
|
// command({ cooldown: number | CooldownConfig }); new SpearClient({ cooldown }); client.cooldowns
|
|
3222
4343
|
```
|
|
3223
4344
|
|
|
@@ -3226,15 +4347,15 @@ class CooldownManager { consume(bucket, input, actor, now?); peek(...); reset(..
|
|
|
3226
4347
|
```ts
|
|
3227
4348
|
function task(config: { name: string; cron?: string; interval?: number; runOnStart?: boolean; run: (client: SpearClient) => Awaitable<void> }): ScheduledTask;
|
|
3228
4349
|
function cron(expression: string): CronExpression; // .next(from?: Date): Date
|
|
3229
|
-
class TaskScheduler { add/remove/list/size/active/start/stop }
|
|
4350
|
+
class TaskScheduler { add/remove/list/size/active/start/stop/setLogger; delay/followUp/reconcile (see "Scheduler — one-shot + reconcile") }
|
|
3230
4351
|
// client.register(task(...)); client.schedule(config); client.scheduler
|
|
3231
4352
|
```
|
|
3232
4353
|
|
|
3233
4354
|
### Prefix commands — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/prefix.md)
|
|
3234
4355
|
|
|
3235
4356
|
```ts
|
|
3236
|
-
function prefixCommand(config: { name: string; aliases?: string[]; description?: string; cooldown?: CooldownInput; run: (ctx: PrefixContext) => Awaitable<R> }): PrefixCommand;
|
|
3237
|
-
class PrefixContext { message; commandName; args: string[]; rest: string; reply(content); send(content); }
|
|
4357
|
+
function prefixCommand<TArgs, R>(config: { name: string; aliases?: readonly string[]; description?: string; cooldown?: CooldownInput; guards?: readonly Guard[]; args?: (a: PrefixArgsBuilder<{}>) => PrefixArgsBuilder<TArgs>; run: (ctx: PrefixContext<TArgs>) => Awaitable<R> }): PrefixCommand;
|
|
4358
|
+
class PrefixContext<TArgs> { message; commandName; args: string[]; rest: string; options: TArgs; client; author; member; guild; guildId; channel; channelId; reply(content); send(content); }
|
|
3238
4359
|
// new SpearClient({ prefix: "!" | string[] | { prefix, mention?, ignoreBots?, caseInsensitive? } }); client.prefix
|
|
3239
4360
|
// reading others' content needs the privileged MessageContent intent (Intents.messages)
|
|
3240
4361
|
```
|
|
@@ -3242,7 +4363,11 @@ class PrefixContext { message; commandName; args: string[]; rest: string; reply(
|
|
|
3242
4363
|
### Usage tracking — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/usage.md)
|
|
3243
4364
|
|
|
3244
4365
|
```ts
|
|
3245
|
-
interface UsageEvent { type:
|
|
4366
|
+
interface UsageEvent { type: UsageType; name: string; userId?; userTag?; guildId?; channelId?; detail?; outcome?: UsageOutcome; durationMs?: number; options?: Readonly<Record<string, UsageMetaValue>>; errorMessage?: string; timestamp: Date; }
|
|
4367
|
+
type UsageType = "command" | "prefix" | "component" | "event";
|
|
4368
|
+
type UsageOutcome = "success" | "error";
|
|
4369
|
+
type UsageMetaValue = string | number | boolean | null;
|
|
4370
|
+
function formatUsage(event: UsageEvent): string; // default channel-line renderer
|
|
3246
4371
|
interface UsageStore { record(event): Awaitable<void>; all(): Awaitable<readonly UsageEvent[]>; }
|
|
3247
4372
|
class MemoryUsageStore { record; all; size; byUser(id); clear; }
|
|
3248
4373
|
class JsonFileUsageStore { constructor(path: string); record; all; }
|
|
@@ -3262,16 +4387,22 @@ log/usage transports a real Discord bot ends up writing.
|
|
|
3262
4387
|
### Embeds — preset replies
|
|
3263
4388
|
|
|
3264
4389
|
```ts
|
|
3265
|
-
class Embeds { error(input); success(input); info(input); warn(input); build(level, input); }
|
|
3266
|
-
|
|
4390
|
+
class Embeds { constructor(options?: EmbedsOptions); error(input); success(input); info(input); warn(input); build(level, input); readonly colors: EmbedColors; readonly icons: EmbedIcons; }
|
|
4391
|
+
const defaultEmbeds: Embeds; // shared default used when `client.embeds` is unset
|
|
4392
|
+
const DEFAULT_EMBED_COLORS: EmbedColors; // red / green / blue / yellow
|
|
4393
|
+
const DEFAULT_EMBED_ICONS: EmbedIcons; // ⛔ ✅ ℹ️ ⚠️
|
|
3267
4394
|
// SpearClient owns one as `client.embeds`; configure via the `embeds` option.
|
|
3268
4395
|
// BaseContext gains ctx.success/info/warn/error (state-aware send) + replySuccess/replyInfo/replyWarn/replyError.
|
|
3269
4396
|
```
|
|
3270
4397
|
|
|
3271
|
-
### Guards — declarative preconditions
|
|
4398
|
+
### Guards — declarative preconditions — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/guards.md)
|
|
3272
4399
|
|
|
3273
4400
|
```ts
|
|
3274
4401
|
type Guard<TCtx extends GuardContext = GuardContext> = (ctx: TCtx) => Awaitable<GuardResult>;
|
|
4402
|
+
interface GuardContext { client; user; member; guild; guildId; channelId; }
|
|
4403
|
+
type GuardResult = boolean | { allowed: false; reason?: string };
|
|
4404
|
+
type RunGuardsResult = { allowed: true } | { allowed: false; reason: string | undefined };
|
|
4405
|
+
function runGuards<TCtx extends GuardContext>(ctx: TCtx, guards?: readonly Guard<TCtx>[]): Promise<RunGuardsResult>;
|
|
3275
4406
|
function denied(reason?: string): GuardResult;
|
|
3276
4407
|
function guildOnly(reason?: string): Guard;
|
|
3277
4408
|
function dmOnly(reason?: string): Guard;
|
|
@@ -3281,49 +4412,62 @@ function requireOwner(ownerIds: readonly string[], reason?: string): Guard;
|
|
|
3281
4412
|
function requireUserPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
3282
4413
|
function requireBotPermissions(permission: PermissionResolvable, reason?: string): Guard;
|
|
3283
4414
|
function guard<TCtx>(predicate: Guard<TCtx>): Guard<TCtx>;
|
|
4415
|
+
// every built-in guard takes an optional custom `reason`; each has a sensible default message.
|
|
3284
4416
|
// per-handler: command({ guards: [...] }), prefixCommand({ guards }), button({ guards }), userCommand({ guards }), ...
|
|
3285
4417
|
// client-wide: new SpearClient({ guards: [...] })
|
|
3286
4418
|
```
|
|
3287
4419
|
|
|
3288
|
-
### Context-menu commands
|
|
4420
|
+
### Context-menu commands — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/context-menus.md)
|
|
3289
4421
|
|
|
3290
4422
|
```ts
|
|
3291
|
-
|
|
3292
|
-
function
|
|
3293
|
-
|
|
4423
|
+
interface ContextMenuMeta { defaultMemberPermissions?: PermissionResolvable | null; nsfw?: boolean; guildOnly?: boolean; nameLocalizations?: LocalizationMap; cooldown?: CooldownInput; guards?: readonly Guard[]; autoDefer?: AutoDeferInput; }
|
|
4424
|
+
function userCommand<R>(config: ContextMenuMeta & { name: string; run: (ctx: UserContextMenuContext) => Awaitable<R> }): UserContextMenu;
|
|
4425
|
+
function messageCommand<R>(config: ContextMenuMeta & { name: string; run: (ctx: MessageContextMenuContext) => Awaitable<R> }): MessageContextMenu;
|
|
4426
|
+
// UserContextMenuContext adds ctx.targetUser, ctx.targetMember; MessageContextMenuContext adds ctx.targetMessage (+ BaseContext).
|
|
4427
|
+
// ContextMenuCommand = UserContextMenu | MessageContextMenu; client.contextMenus is a ContextMenuRegistry.
|
|
4428
|
+
// Deploy slash commands + menus together with client.deployAllCommands({ guildId }).
|
|
3294
4429
|
```
|
|
3295
4430
|
|
|
3296
4431
|
### Prefix typed arguments
|
|
3297
4432
|
|
|
3298
4433
|
```ts
|
|
3299
4434
|
function prefixArgs(): PrefixArgsBuilder<{}>;
|
|
3300
|
-
// builder methods:
|
|
3301
|
-
//
|
|
4435
|
+
// builder methods — each requires a `name` and takes an optional options object:
|
|
4436
|
+
// .string(name, { required?, minLength?, maxLength?, default? }) -> string
|
|
4437
|
+
// .integer(name, { required?, minValue?, maxValue?, default? }) -> number
|
|
4438
|
+
// .number(name, { required?, minValue?, maxValue?, default? }) -> number
|
|
4439
|
+
// .boolean(name, { required?, default? }) -> boolean
|
|
4440
|
+
// .snowflake(name, { required?, default? }) -> string (accepts raw ids and <@u>/<#c>/<@&r> mentions)
|
|
4441
|
+
// .duration(name, { required?, default? }) -> number ("1h30m" parsed to ms)
|
|
4442
|
+
// .rest(name, { required?, default? }) -> string (remaining text)
|
|
4443
|
+
// prefixCommand({ args: (a) => a.snowflake("target", { required: true }).duration("dur").rest("reason", { default: "No reason" }), run: (ctx) => ctx.options });
|
|
3302
4444
|
```
|
|
3303
4445
|
|
|
3304
4446
|
### Pagination + Confirmation
|
|
3305
4447
|
|
|
3306
4448
|
```ts
|
|
3307
|
-
function paginate<T>(interaction, items, {
|
|
4449
|
+
function paginate<T>(interaction, items, { render, pageSize?, user?, timeoutMs?, controls?: "prev-next" | "first-prev-next-last", ephemeral?, namespace?, labels?: { first?; prev?; next?; last? } }): Promise<void>;
|
|
3308
4450
|
function buildPaginatorPage<T>(items, page, options): Promise<{ payload; pages }>;
|
|
3309
|
-
function confirm(interaction, { title?,
|
|
4451
|
+
function confirm(interaction, { body, title?, confirm?: { label?; style? }, cancel?: { label?; style? }, user?, timeoutMs?, ephemeral?, namespace? }): Promise<{ confirmed: boolean; reason: "confirm" | "cancel" | "timeout"; interaction? }>; // style: "Primary" | "Secondary" | "Success" | "Danger"
|
|
3310
4452
|
```
|
|
3311
4453
|
|
|
3312
4454
|
### Primitives
|
|
3313
4455
|
|
|
3314
4456
|
```ts
|
|
3315
|
-
class KeyedLock { tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); }
|
|
3316
|
-
const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null
|
|
4457
|
+
class KeyedLock { constructor(options?: { ttl?: number; sweep?: number }); tryAcquire(key, ttl?); run(key, fn, { onBusy?, ttl? }); isHeld(key); forget(key); dispose(); readonly size: number; }
|
|
4458
|
+
const safeFetch = { member, channel, message, user, guild, role, try }; // each returns T | null; also exported standalone as fetchMember/fetchChannel/fetchMessage/fetchUser/fetchGuild/fetchRole/safeTry
|
|
3317
4459
|
function withSafeTimeout<T>(p: Promise<T>, ms): Promise<T | null>;
|
|
3318
|
-
function formatDuration(ms, { locale?:
|
|
4460
|
+
function formatDuration(ms, opts?: { locale?: string | UnitLabels; largest?: number; units?: readonly DurationUnit[] }): string; // locale: "en"|"en-US"|"en-GB"|"tr"|"tr-TR" or a custom label set; unknown locales fall back to en
|
|
3319
4461
|
function parseDuration(input: string): number | null;
|
|
3320
4462
|
function discordTimestamp(date, style?: "t"|"T"|"d"|"D"|"f"|"F"|"R"): string;
|
|
3321
4463
|
function relativeTimestamp(date): string;
|
|
3322
4464
|
interface CacheStore { get; set; delete; has; increment; rateLimit; clear; }
|
|
3323
4465
|
class MemoryCache implements CacheStore { /* TTL, counter, fixed-window rate limit */ }
|
|
4466
|
+
function createCache(): CacheStore; // default in-memory cache
|
|
3324
4467
|
function loadConfig<T>({ file, parser?, schema?, encoding? }): T;
|
|
3325
4468
|
function loadConfigAsync<T>(opts): Promise<T>;
|
|
3326
4469
|
function lookup<K, V>(table, resourceName?): (key: K) => V;
|
|
4470
|
+
function lookupOptional<K, V>(table): (key: K) => V | undefined; // non-throwing variant of lookup
|
|
3327
4471
|
```
|
|
3328
4472
|
|
|
3329
4473
|
### Logger transports
|
|
@@ -3332,6 +4476,7 @@ function lookup<K, V>(table, resourceName?): (key: K) => V;
|
|
|
3332
4476
|
new Logger({ level, transports: [consoleSink, jsonlSink("./logs/bot.jsonl"), webhookSink({ url, minLevel: "error" })] });
|
|
3333
4477
|
function jsonlSink(path: string, { minLevel? }?): LogSink;
|
|
3334
4478
|
function webhookSink({ url, minLevel?, username? }): LogSink;
|
|
4479
|
+
function consoleSink(entry: LogEntry): void; // default human-readable console transport
|
|
3335
4480
|
// Logger.addTransport(sink), setTransports([sinks])
|
|
3336
4481
|
```
|
|
3337
4482
|
|
|
@@ -3350,18 +4495,125 @@ client.deployAllCommands({ guildId, dryRun: true }); // returns { ski
|
|
|
3350
4495
|
client.deployAllCommands({ guildId, strategy: "diff" }); // skips PUT when remote matches
|
|
3351
4496
|
client.deployAllCommands({ applicationId: "...", strategy: "diff" }); // explicit app id, no ready required
|
|
3352
4497
|
```
|
|
4498
|
+
---
|
|
3353
4499
|
|
|
3354
|
-
|
|
4500
|
+
## Added in 0.4
|
|
4501
|
+
|
|
4502
|
+
Reliability and moderation helpers distilled from production bots: never lose an
|
|
4503
|
+
interaction to the 3-second window, shut down cleanly, run permission/hierarchy
|
|
4504
|
+
preflights, persist per-guild settings, and await replies without hand-rolled
|
|
4505
|
+
collectors.
|
|
4506
|
+
|
|
4507
|
+
### Auto-defer — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/auto-defer.md)
|
|
3355
4508
|
|
|
3356
4509
|
```ts
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
4510
|
+
type AutoDeferInput = boolean | { ephemeral?: boolean; delayMs?: number };
|
|
4511
|
+
interface AutoDeferConfig { ephemeral: boolean; delayMs: number; }
|
|
4512
|
+
const DEFAULT_AUTO_DEFER_DELAY_MS = 2000;
|
|
4513
|
+
function normalizeAutoDefer(input?: AutoDeferInput): AutoDeferConfig | undefined;
|
|
4514
|
+
function armAutoDefer(interaction, config: AutoDeferConfig): () => void; // returns a cancel fn
|
|
4515
|
+
type AutoDeferrableInteraction = ChatInputCommandInteraction | UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction;
|
|
4516
|
+
// Enable per handler: command({ autoDefer: true }), userCommand({ autoDefer }), messageCommand({ autoDefer })
|
|
4517
|
+
// Or globally: new SpearClient({ autoDefer: true }). With it on, respond via ctx.send / ctx.editReply.
|
|
4518
|
+
// Arms a timer when the handler starts; defers if it hasn't responded by ~2s, preventing "Unknown interaction" (10062).
|
|
4519
|
+
```
|
|
4520
|
+
|
|
4521
|
+
### Graceful shutdown
|
|
4522
|
+
|
|
4523
|
+
```ts
|
|
4524
|
+
interface GracefulShutdownOptions {
|
|
4525
|
+
signals?: readonly NodeJS.Signals[]; // default ["SIGINT", "SIGTERM"]
|
|
4526
|
+
timeoutMs?: number; // force-exit after this; default 10000
|
|
4527
|
+
exit?: boolean; // call process.exit when done; default true
|
|
4528
|
+
onShutdown?: (signal: NodeJS.Signals) => Awaitable<void>; // runs before client.destroy()
|
|
4529
|
+
logger?: { info?(msg): void; error?(msg, meta?): void };
|
|
4530
|
+
}
|
|
4531
|
+
interface Destroyable { destroy(): Awaitable<void>; } // a discord.js Client qualifies
|
|
4532
|
+
interface ShutdownLogger { info?(message: string): void; error?(message: string, meta?: unknown): void; }
|
|
4533
|
+
function gracefulShutdown(client: Destroyable, options?: GracefulShutdownOptions): () => void;
|
|
4534
|
+
// SpearClient.enableGracefulShutdown(options?) wires it with client.logger and returns a disposer.
|
|
4535
|
+
```
|
|
4536
|
+
|
|
4537
|
+
### Permissions & moderation — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/permissions.md)
|
|
4538
|
+
|
|
4539
|
+
```ts
|
|
4540
|
+
type PermissionHolder = GuildMember | Role;
|
|
4541
|
+
function missingPermissions(channel: GuildBasedChannel, who: PermissionHolder, required: PermissionResolvable): PermissionsString[];
|
|
4542
|
+
function botMissingPermissions(channel: GuildBasedChannel, required: PermissionResolvable): PermissionsString[];
|
|
4543
|
+
function hasPermissions(channel: GuildBasedChannel, who: PermissionHolder, required: PermissionResolvable): boolean;
|
|
4544
|
+
function compareRoles(a: GuildMember, b: GuildMember): number; // by highest-role position
|
|
4545
|
+
function canActOn(actor: GuildMember, target: GuildMember): boolean;
|
|
4546
|
+
function formatPermissions(permissions: PermissionResolvable): string; // human, comma-separated
|
|
4547
|
+
|
|
4548
|
+
type ModerationCheckResult = { ok: true } | { ok: false; reason: string };
|
|
4549
|
+
interface ModerationCheckOptions { moderator: GuildMember; target: GuildMember; me?: GuildMember | null; action?: string; }
|
|
4550
|
+
function moderationCheck(options: ModerationCheckOptions): ModerationCheckResult; // self / owner / role-hierarchy preflight
|
|
4551
|
+
```
|
|
4552
|
+
|
|
4553
|
+
### Persistent storage
|
|
4554
|
+
|
|
4555
|
+
```ts
|
|
4556
|
+
interface KeyValueStore {
|
|
4557
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
4558
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
4559
|
+
has(key: string): Promise<boolean>;
|
|
4560
|
+
delete(key: string): Promise<boolean>;
|
|
4561
|
+
keys(): Promise<string[]>;
|
|
4562
|
+
clear(): Promise<void>;
|
|
4563
|
+
}
|
|
4564
|
+
class MemoryStore implements KeyValueStore { /* deep-cloned in-memory */ }
|
|
4565
|
+
class JsonStore implements KeyValueStore { constructor(path: string); /* atomic JSON file */ }
|
|
4566
|
+
function namespaced(store: KeyValueStore, prefix: string): KeyValueStore;
|
|
4567
|
+
|
|
4568
|
+
interface SettingsManager<T> { readonly defaults: T; readonly store: KeyValueStore; get(id): Promise<T>; set(id, patch: Partial<T>): Promise<T>; reset(id): Promise<void>; }
|
|
4569
|
+
interface CreateSettingsOptions<T> { store: KeyValueStore; defaults: T; namespace?: string; } // namespace default "settings"
|
|
4570
|
+
function createSettings<T extends Record<string, unknown>>(options: CreateSettingsOptions<T>): SettingsManager<T>;
|
|
4571
|
+
```
|
|
4572
|
+
|
|
4573
|
+
### Collectors
|
|
4574
|
+
|
|
4575
|
+
```ts
|
|
4576
|
+
interface AwaitMessageOptions { filter?: (m: Message) => boolean; time?: number; } // time default 60000
|
|
4577
|
+
function awaitMessage(channel: CollectableChannel, options?: AwaitMessageOptions): Promise<Message | null>;
|
|
4578
|
+
interface AwaitComponentOptions { filter?; time?; componentType?: ComponentType; } // time default 60000
|
|
4579
|
+
function awaitComponent(message: Message, options?: AwaitComponentOptions): Promise<MessageComponentInteraction | null>;
|
|
4580
|
+
interface AwaitModalOptions { time?: number; filter?: (i: ModalSubmitInteraction) => boolean; } // time default 120000
|
|
4581
|
+
function showAndAwaitModal(interaction: ModalShowingInteraction, modal: ModalLike, options?: AwaitModalOptions): Promise<ModalSubmitInteraction | null>;
|
|
4582
|
+
// Context sugar: ctx.awaitMessageFrom(userId?, options?) and ctx.awaitModal(modal, options?) (command + component contexts).
|
|
4583
|
+
```
|
|
4584
|
+
|
|
4585
|
+
### Discord errors — [guide](https://raw.githubusercontent.com/byigitt/spearkit/main/docs/errors.md)
|
|
4586
|
+
|
|
4587
|
+
```ts
|
|
4588
|
+
const DiscordErrorCode = { UnknownChannel, UnknownGuild, UnknownMember, UnknownMessage, UnknownUser,
|
|
4589
|
+
UnknownInteraction, MissingAccess, CannotExecuteActionOnDMChannel, CannotSendMessagesToThisUser,
|
|
4590
|
+
MissingPermissions, InvalidFormBodyOrContentType, InteractionHasAlreadyBeenAcknowledged,
|
|
4591
|
+
MaximumNumberOfGuildsReached, MaximumNumberOfReactionsReached } as const; // named RESTJSONErrorCodes
|
|
4592
|
+
type DiscordErrorCodeValue = (typeof DiscordErrorCode)[keyof typeof DiscordErrorCode];
|
|
4593
|
+
function isDiscordError(error: unknown, code?: number | string | readonly (number | string)[]): error is DiscordAPIError;
|
|
4594
|
+
function isHTTPError(error: unknown): error is HTTPError;
|
|
4595
|
+
function isRateLimitError(error: unknown): boolean; // HTTP 429
|
|
4596
|
+
function explainDiscordError(error: unknown): string | null; // end-user-friendly sentence, or null
|
|
4597
|
+
// The default command/component error reply uses explainDiscordError(...) when it can.
|
|
4598
|
+
```
|
|
4599
|
+
|
|
4600
|
+
### Message formatting
|
|
4601
|
+
|
|
4602
|
+
```ts
|
|
4603
|
+
const MESSAGE_CHARACTER_LIMIT = 2000;
|
|
4604
|
+
function truncate(text: string, max: number, suffix?: string): string; // suffix default "…"
|
|
4605
|
+
interface ChunkOptions { max?: number; } // default MESSAGE_CHARACTER_LIMIT
|
|
4606
|
+
function chunkMessage(text: string, options?: ChunkOptions): string[]; // splits on line/word boundaries
|
|
4607
|
+
```
|
|
4608
|
+
|
|
4609
|
+
### Dynamic prefixes
|
|
4610
|
+
|
|
4611
|
+
```ts
|
|
4612
|
+
// PrefixOptions gains a per-message resolver (e.g. a per-guild prefix from a store):
|
|
4613
|
+
interface PrefixOptions { /* …prefix, mention, ignoreBots, caseInsensitive… */
|
|
4614
|
+
dynamic?: (message: Message) => Awaitable<string | readonly string[] | null | undefined>;
|
|
3364
4615
|
}
|
|
4616
|
+
// Dynamic prefixes are tried in addition to any static prefix. Keep the resolver fast (cache it).
|
|
3365
4617
|
```
|
|
3366
4618
|
|
|
3367
4619
|
---
|