@went.tf/discord-bot-framework 2.0.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/LICENSE +21 -0
- package/README.md +550 -0
- package/build/.tsbuildinfo +1 -0
- package/build/api-client/api-client.d.ts +13 -0
- package/build/api-client/api-client.d.ts.map +1 -0
- package/build/api-client/api-client.js +147 -0
- package/build/api-client/api-client.js.map +1 -0
- package/build/api-client/api-http-exception.d.ts +7 -0
- package/build/api-client/api-http-exception.d.ts.map +1 -0
- package/build/api-client/api-http-exception.js +12 -0
- package/build/api-client/api-http-exception.js.map +1 -0
- package/build/api-client/index.d.ts +4 -0
- package/build/api-client/index.d.ts.map +1 -0
- package/build/api-client/index.js +4 -0
- package/build/api-client/index.js.map +1 -0
- package/build/api-client/types.d.ts +105 -0
- package/build/api-client/types.d.ts.map +1 -0
- package/build/api-client/types.js +8 -0
- package/build/api-client/types.js.map +1 -0
- package/build/client/create-bot-client.d.ts +14 -0
- package/build/client/create-bot-client.d.ts.map +1 -0
- package/build/client/create-bot-client.js +19 -0
- package/build/client/create-bot-client.js.map +1 -0
- package/build/client/create-shard-manager.d.ts +17 -0
- package/build/client/create-shard-manager.d.ts.map +1 -0
- package/build/client/create-shard-manager.js +26 -0
- package/build/client/create-shard-manager.js.map +1 -0
- package/build/client/index.d.ts +3 -0
- package/build/client/index.d.ts.map +1 -0
- package/build/client/index.js +3 -0
- package/build/client/index.js.map +1 -0
- package/build/commands/build-application-commands-body.d.ts +39 -0
- package/build/commands/build-application-commands-body.d.ts.map +1 -0
- package/build/commands/build-application-commands-body.js +127 -0
- package/build/commands/build-application-commands-body.js.map +1 -0
- package/build/commands/fixed-reply-command-factory.d.ts +10 -0
- package/build/commands/fixed-reply-command-factory.d.ts.map +1 -0
- package/build/commands/fixed-reply-command-factory.js +19 -0
- package/build/commands/fixed-reply-command-factory.js.map +1 -0
- package/build/commands/index.d.ts +4 -0
- package/build/commands/index.d.ts.map +1 -0
- package/build/commands/index.js +4 -0
- package/build/commands/index.js.map +1 -0
- package/build/commands/registration.d.ts +24 -0
- package/build/commands/registration.d.ts.map +1 -0
- package/build/commands/registration.js +69 -0
- package/build/commands/registration.js.map +1 -0
- package/build/commands/schema/application-command-leaf-option.schema.d.ts +230 -0
- package/build/commands/schema/application-command-leaf-option.schema.d.ts.map +1 -0
- package/build/commands/schema/application-command-leaf-option.schema.js +129 -0
- package/build/commands/schema/application-command-leaf-option.schema.js.map +1 -0
- package/build/commands/schema/application-command-leaf-option.schema.json +258 -0
- package/build/commands/schema/application-command-option-choice.schema.d.ts +18 -0
- package/build/commands/schema/application-command-option-choice.schema.d.ts.map +1 -0
- package/build/commands/schema/application-command-option-choice.schema.js +12 -0
- package/build/commands/schema/application-command-option-choice.schema.js.map +1 -0
- package/build/commands/schema/application-command-option-choice.schema.json +23 -0
- package/build/commands/schema/application-command-option.schema.d.ts +12 -0
- package/build/commands/schema/application-command-option.schema.d.ts.map +1 -0
- package/build/commands/schema/application-command-option.schema.js +13 -0
- package/build/commands/schema/application-command-option.schema.js.map +1 -0
- package/build/commands/schema/application-command-option.schema.json +14 -0
- package/build/commands/schema/application-command-subcommand-group.schema.d.ts +28 -0
- package/build/commands/schema/application-command-subcommand-group.schema.d.ts.map +1 -0
- package/build/commands/schema/application-command-subcommand-group.schema.js +22 -0
- package/build/commands/schema/application-command-subcommand-group.schema.js.map +1 -0
- package/build/commands/schema/application-command-subcommand-group.schema.json +26 -0
- package/build/commands/schema/application-command-subcommand.schema.d.ts +29 -0
- package/build/commands/schema/application-command-subcommand.schema.d.ts.map +1 -0
- package/build/commands/schema/application-command-subcommand.schema.js +23 -0
- package/build/commands/schema/application-command-subcommand.schema.js.map +1 -0
- package/build/commands/schema/application-command-subcommand.schema.json +26 -0
- package/build/commands/schema/application-command-type.schema.d.ts +6 -0
- package/build/commands/schema/application-command-type.schema.d.ts.map +1 -0
- package/build/commands/schema/application-command-type.schema.js +6 -0
- package/build/commands/schema/application-command-type.schema.js.map +1 -0
- package/build/commands/schema/application-command-type.schema.json +9 -0
- package/build/commands/schema/application-integration-type.schema.d.ts +6 -0
- package/build/commands/schema/application-integration-type.schema.d.ts.map +1 -0
- package/build/commands/schema/application-integration-type.schema.js +6 -0
- package/build/commands/schema/application-integration-type.schema.js.map +1 -0
- package/build/commands/schema/application-integration-type.schema.json +8 -0
- package/build/commands/schema/channel-type.schema.d.ts +6 -0
- package/build/commands/schema/channel-type.schema.d.ts.map +1 -0
- package/build/commands/schema/channel-type.schema.js +6 -0
- package/build/commands/schema/channel-type.schema.js.map +1 -0
- package/build/commands/schema/channel-type.schema.json +16 -0
- package/build/commands/schema/chat-input-command.schema.d.ts +45 -0
- package/build/commands/schema/chat-input-command.schema.d.ts.map +1 -0
- package/build/commands/schema/chat-input-command.schema.js +34 -0
- package/build/commands/schema/chat-input-command.schema.js.map +1 -0
- package/build/commands/schema/chat-input-command.schema.json +46 -0
- package/build/commands/schema/commands-file.schema.d.ts +18 -0
- package/build/commands/schema/commands-file.schema.d.ts.map +1 -0
- package/build/commands/schema/commands-file.schema.js +16 -0
- package/build/commands/schema/commands-file.schema.js.map +1 -0
- package/build/commands/schema/commands-file.schema.json +14 -0
- package/build/commands/schema/context-menu-command.schema.d.ts +40 -0
- package/build/commands/schema/context-menu-command.schema.d.ts.map +1 -0
- package/build/commands/schema/context-menu-command.schema.js +32 -0
- package/build/commands/schema/context-menu-command.schema.js.map +1 -0
- package/build/commands/schema/context-menu-command.schema.json +40 -0
- package/build/commands/schema/context-menu-name.schema.d.ts +7 -0
- package/build/commands/schema/context-menu-name.schema.d.ts.map +1 -0
- package/build/commands/schema/context-menu-name.schema.js +7 -0
- package/build/commands/schema/context-menu-name.schema.js.map +1 -0
- package/build/commands/schema/context-menu-name.schema.json +6 -0
- package/build/commands/schema/default-member-permissions.schema.d.ts +7 -0
- package/build/commands/schema/default-member-permissions.schema.d.ts.map +1 -0
- package/build/commands/schema/default-member-permissions.schema.js +7 -0
- package/build/commands/schema/default-member-permissions.schema.js.map +1 -0
- package/build/commands/schema/default-member-permissions.schema.json +6 -0
- package/build/commands/schema/index.d.ts +77 -0
- package/build/commands/schema/index.d.ts.map +1 -0
- package/build/commands/schema/index.js +48 -0
- package/build/commands/schema/index.js.map +1 -0
- package/build/commands/schema/interaction-context-type.schema.d.ts +6 -0
- package/build/commands/schema/interaction-context-type.schema.d.ts.map +1 -0
- package/build/commands/schema/interaction-context-type.schema.js +6 -0
- package/build/commands/schema/interaction-context-type.schema.js.map +1 -0
- package/build/commands/schema/interaction-context-type.schema.json +9 -0
- package/build/commands/schema/option-name.schema.d.ts +7 -0
- package/build/commands/schema/option-name.schema.d.ts.map +1 -0
- package/build/commands/schema/option-name.schema.js +7 -0
- package/build/commands/schema/option-name.schema.js.map +1 -0
- package/build/commands/schema/option-name.schema.json +6 -0
- package/build/commands/schema/parse-commands-file.d.ts +19 -0
- package/build/commands/schema/parse-commands-file.d.ts.map +1 -0
- package/build/commands/schema/parse-commands-file.js +19 -0
- package/build/commands/schema/parse-commands-file.js.map +1 -0
- package/build/db/create-postgres-prisma-db.d.ts +20 -0
- package/build/db/create-postgres-prisma-db.d.ts.map +1 -0
- package/build/db/create-postgres-prisma-db.js +17 -0
- package/build/db/create-postgres-prisma-db.js.map +1 -0
- package/build/db/index.d.ts +2 -0
- package/build/db/index.d.ts.map +1 -0
- package/build/db/index.js +2 -0
- package/build/db/index.js.map +1 -0
- package/build/dev/create-handler-watcher.d.ts +23 -0
- package/build/dev/create-handler-watcher.d.ts.map +1 -0
- package/build/dev/create-handler-watcher.js +55 -0
- package/build/dev/create-handler-watcher.js.map +1 -0
- package/build/dev/create-source-reloader.d.ts +37 -0
- package/build/dev/create-source-reloader.d.ts.map +1 -0
- package/build/dev/create-source-reloader.js +42 -0
- package/build/dev/create-source-reloader.js.map +1 -0
- package/build/dev/index.d.ts +3 -0
- package/build/dev/index.d.ts.map +1 -0
- package/build/dev/index.js +3 -0
- package/build/dev/index.js.map +1 -0
- package/build/dev/reload-loader.d.ts +11 -0
- package/build/dev/reload-loader.d.ts.map +1 -0
- package/build/dev/reload-loader.js +28 -0
- package/build/dev/reload-loader.js.map +1 -0
- package/build/env/define-env.d.ts +25 -0
- package/build/env/define-env.d.ts.map +1 -0
- package/build/env/define-env.js +24 -0
- package/build/env/define-env.js.map +1 -0
- package/build/env/helpers.d.ts +9 -0
- package/build/env/helpers.d.ts.map +1 -0
- package/build/env/helpers.js +9 -0
- package/build/env/helpers.js.map +1 -0
- package/build/env/index.d.ts +3 -0
- package/build/env/index.d.ts.map +1 -0
- package/build/env/index.js +3 -0
- package/build/env/index.js.map +1 -0
- package/build/i18n/create-command-localizer.d.ts +27 -0
- package/build/i18n/create-command-localizer.d.ts.map +1 -0
- package/build/i18n/create-command-localizer.js +40 -0
- package/build/i18n/create-command-localizer.js.map +1 -0
- package/build/i18n/create-i18n-initializer.d.ts +18 -0
- package/build/i18n/create-i18n-initializer.d.ts.map +1 -0
- package/build/i18n/create-i18n-initializer.js +34 -0
- package/build/i18n/create-i18n-initializer.js.map +1 -0
- package/build/i18n/index.d.ts +3 -0
- package/build/i18n/index.d.ts.map +1 -0
- package/build/i18n/index.js +3 -0
- package/build/i18n/index.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +12 -0
- package/build/index.js.map +1 -0
- package/build/interactions/custom-id.d.ts +9 -0
- package/build/interactions/custom-id.d.ts.map +1 -0
- package/build/interactions/custom-id.js +15 -0
- package/build/interactions/custom-id.js.map +1 -0
- package/build/interactions/dispatch.d.ts +35 -0
- package/build/interactions/dispatch.d.ts.map +1 -0
- package/build/interactions/dispatch.js +87 -0
- package/build/interactions/dispatch.js.map +1 -0
- package/build/interactions/handle-interaction-error.d.ts +22 -0
- package/build/interactions/handle-interaction-error.d.ts.map +1 -0
- package/build/interactions/handle-interaction-error.js +65 -0
- package/build/interactions/handle-interaction-error.js.map +1 -0
- package/build/interactions/index.d.ts +7 -0
- package/build/interactions/index.d.ts.map +1 -0
- package/build/interactions/index.js +7 -0
- package/build/interactions/index.js.map +1 -0
- package/build/interactions/registry.d.ts +32 -0
- package/build/interactions/registry.d.ts.map +1 -0
- package/build/interactions/registry.js +49 -0
- package/build/interactions/registry.js.map +1 -0
- package/build/interactions/router.d.ts +28 -0
- package/build/interactions/router.d.ts.map +1 -0
- package/build/interactions/router.js +48 -0
- package/build/interactions/router.js.map +1 -0
- package/build/interactions/types.d.ts +37 -0
- package/build/interactions/types.d.ts.map +1 -0
- package/build/interactions/types.js +2 -0
- package/build/interactions/types.js.map +1 -0
- package/build/logger/create-logger.d.ts +27 -0
- package/build/logger/create-logger.d.ts.map +1 -0
- package/build/logger/create-logger.js +36 -0
- package/build/logger/create-logger.js.map +1 -0
- package/build/logger/dev-null-logger.d.ts +11 -0
- package/build/logger/dev-null-logger.d.ts.map +1 -0
- package/build/logger/dev-null-logger.js +19 -0
- package/build/logger/dev-null-logger.js.map +1 -0
- package/build/logger/discord-webhook-batcher.d.ts +38 -0
- package/build/logger/discord-webhook-batcher.d.ts.map +1 -0
- package/build/logger/discord-webhook-batcher.js +87 -0
- package/build/logger/discord-webhook-batcher.js.map +1 -0
- package/build/logger/discord-webhook-transport.d.ts +9 -0
- package/build/logger/discord-webhook-transport.d.ts.map +1 -0
- package/build/logger/discord-webhook-transport.js +20 -0
- package/build/logger/discord-webhook-transport.js.map +1 -0
- package/build/logger/index.d.ts +5 -0
- package/build/logger/index.d.ts.map +1 -0
- package/build/logger/index.js +5 -0
- package/build/logger/index.js.map +1 -0
- package/build/logger/logger.d.ts +30 -0
- package/build/logger/logger.d.ts.map +1 -0
- package/build/logger/logger.js +84 -0
- package/build/logger/logger.js.map +1 -0
- package/build/logger/pino-pretty-options.d.ts +12 -0
- package/build/logger/pino-pretty-options.d.ts.map +1 -0
- package/build/logger/pino-pretty-options.js +17 -0
- package/build/logger/pino-pretty-options.js.map +1 -0
- package/build/logger/types.d.ts +6 -0
- package/build/logger/types.d.ts.map +1 -0
- package/build/logger/types.js +2 -0
- package/build/logger/types.js.map +1 -0
- package/build/utils/discord-lookups.d.ts +16 -0
- package/build/utils/discord-lookups.d.ts.map +1 -0
- package/build/utils/discord-lookups.js +39 -0
- package/build/utils/discord-lookups.js.map +1 -0
- package/build/utils/get-git-data.d.ts +8 -0
- package/build/utils/get-git-data.d.ts.map +1 -0
- package/build/utils/get-git-data.js +18 -0
- package/build/utils/get-git-data.js.map +1 -0
- package/build/utils/index.d.ts +7 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +7 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/messaging.d.ts +15 -0
- package/build/utils/messaging.d.ts.map +1 -0
- package/build/utils/messaging.js +71 -0
- package/build/utils/messaging.js.map +1 -0
- package/build/utils/promises.d.ts +6 -0
- package/build/utils/promises.d.ts.map +1 -0
- package/build/utils/promises.js +13 -0
- package/build/utils/promises.js.map +1 -0
- package/build/utils/run-attempts.d.ts +6 -0
- package/build/utils/run-attempts.d.ts.map +1 -0
- package/build/utils/run-attempts.js +16 -0
- package/build/utils/run-attempts.js.map +1 -0
- package/build/utils/strings.d.ts +7 -0
- package/build/utils/strings.d.ts.map +1 -0
- package/build/utils/strings.js +39 -0
- package/build/utils/strings.js.map +1 -0
- package/package.json +146 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WentTheFox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
# @went.tf/discord-bot-framework
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@went.tf/discord-bot-framework)
|
|
4
|
+
|
|
5
|
+
Shared building blocks for discord.js-based Discord bots: a nestable console
|
|
6
|
+
logger, zod-based env validation, a generic HTTP API client, a slash-command
|
|
7
|
+
interaction dispatcher, command registration helpers, and thin client/shard
|
|
8
|
+
bootstrap wrappers — plus optional Postgres (Prisma) and i18next helpers for
|
|
9
|
+
bots that want them.
|
|
10
|
+
|
|
11
|
+
Extracted from [HammerTimeBot](https://github.com/WentTheFox/HammerTimeBot),
|
|
12
|
+
[Fantastick](https://github.com/WentTheFox/Fantastick), and
|
|
13
|
+
[PennyCurve](https://github.com/MLP-VectorClub/PennyCurve), which had each
|
|
14
|
+
independently reimplemented the same architecture. See `CLAUDE.md` for the
|
|
15
|
+
design rationale and module-to-source mapping.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm add @went.tf/discord-bot-framework zod discord.js @discordjs/rest discord-api-types
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`zod` and `ajv` are real dependencies of this package but must also be
|
|
24
|
+
listed by consumers directly (peer resolution quirk of subpath-only usage)
|
|
25
|
+
if you use `defineEnv` or compile your own commands schema at your own top
|
|
26
|
+
level. `prisma`/`@prisma/client`/`@prisma/adapter-pg` and
|
|
27
|
+
`i18next`/`i18next-fs-backend` are **optional** peers — only install them if
|
|
28
|
+
you import `@went.tf/discord-bot-framework/db` or `/i18n`.
|
|
29
|
+
|
|
30
|
+
## Subpaths
|
|
31
|
+
|
|
32
|
+
Everything is available from the package root **except** `./db`, `./i18n`, and
|
|
33
|
+
`./dev`. `./db`/`./i18n` are kept as separate subpaths so bots that don't use
|
|
34
|
+
Postgres/Prisma or i18next never need to install those peer dependencies.
|
|
35
|
+
`./dev` is excluded for a different reason — it has no extra peer
|
|
36
|
+
dependencies, but it's dev-only tooling that shouldn't leak into every
|
|
37
|
+
consumer's root import surface.
|
|
38
|
+
|
|
39
|
+
### `@went.tf/discord-bot-framework/logger`
|
|
40
|
+
|
|
41
|
+
Backed by [pino](https://getpino.io). Plain `new Logger(prefix)` /
|
|
42
|
+
`Logger.fromShardInfo(...)` stay simple, console-only, worker-thread-free
|
|
43
|
+
constructors:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Logger, NestableLogger, DevNullLogger } from '@went.tf/discord-bot-framework/logger';
|
|
47
|
+
|
|
48
|
+
const logger = new Logger('Bot');
|
|
49
|
+
const interactionLogger = logger.nest(`Interaction#${interaction.id}`);
|
|
50
|
+
const shardLogger = Logger.fromShardInfo(process.env.SHARDS);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To additionally fan logs out to a Discord webhook (in batches, respecting
|
|
54
|
+
Discord's per-webhook rate limits), use `createLogger` instead — it builds one
|
|
55
|
+
pino instance with the requested transport targets (console + optional
|
|
56
|
+
webhook), and `nest()` on the result shares that same instance rather than
|
|
57
|
+
spawning a new worker thread per call:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createLogger } from '@went.tf/discord-bot-framework/logger';
|
|
61
|
+
|
|
62
|
+
const logger = createLogger({
|
|
63
|
+
prefix: 'Bot',
|
|
64
|
+
discordWebhook: {
|
|
65
|
+
url: env.LOG_WEBHOOK_URL,
|
|
66
|
+
level: 'warn', // only warn/error/fatal are sent to Discord; default 'warn'
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `@went.tf/discord-bot-framework/env`
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { defineEnv, boolFromString } from '@went.tf/discord-bot-framework/env';
|
|
75
|
+
import { z } from 'zod';
|
|
76
|
+
|
|
77
|
+
export const env = defineEnv({
|
|
78
|
+
DISCORD_BOT_TOKEN: z.string().min(1),
|
|
79
|
+
API_URL: z.string().url(),
|
|
80
|
+
LOCAL: boolFromString().default(false),
|
|
81
|
+
SUPPORT_SERVER_ID: z.string().optional().default(''),
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Throws one formatted `Error` listing every failing key. Pass `{ dotenv: false }`
|
|
86
|
+
to skip loading a `.env` file, or `{ source }` to validate a fixture object
|
|
87
|
+
(useful in tests).
|
|
88
|
+
|
|
89
|
+
### `@went.tf/discord-bot-framework/api-client`
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { ApiClient, ApiAuthType } from '@went.tf/discord-bot-framework/api-client';
|
|
93
|
+
|
|
94
|
+
const apiClient = new ApiClient(logger, {
|
|
95
|
+
baseUrl: `${env.API_URL}/api`,
|
|
96
|
+
authentication: { type: ApiAuthType.AUTHORIZATION_HEADER, getValue: () => env.API_TOKEN },
|
|
97
|
+
userAgent: env.UA_STRING,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const { response } = await apiClient.request({
|
|
101
|
+
path: '/things',
|
|
102
|
+
validator: typia.createValidate<Thing[]>(), // optional; omit for `response: unknown`
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `@went.tf/discord-bot-framework/interactions`
|
|
107
|
+
|
|
108
|
+
Commands/components/modals are self-describing — put the name/id directly on
|
|
109
|
+
the object (as `name` or `id`) and pass an array to a `createXRegistry()`
|
|
110
|
+
helper instead of hand-writing a `Record<Enum, Handler>` map. The registry
|
|
111
|
+
derives the literal name/id union from the array itself (TS 5 `const` type
|
|
112
|
+
params), so there's no separate enum to keep in sync, and `registry.byName`
|
|
113
|
+
is a drop-in `commands`/`components`/`modals` value for
|
|
114
|
+
`createInteractionRouter`/the `dispatch*` functions below.
|
|
115
|
+
|
|
116
|
+
A command's *wire definition* (name, description, options, permissions) no
|
|
117
|
+
longer lives on this object — it lives in your `commands.json` file, see
|
|
118
|
+
`./commands` below. This object is purely the handler side:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { createChatInputCommandRegistry, createComponentRegistry, createInteractionRouter, handleInteractionError } from '@went.tf/discord-bot-framework/interactions';
|
|
122
|
+
|
|
123
|
+
const pingCommand = { name: 'ping', handle: (interaction) => interaction.reply('pong') };
|
|
124
|
+
|
|
125
|
+
const chatInputCommandRegistry = createChatInputCommandRegistry([pingCommand /* , ... */]);
|
|
126
|
+
const componentRegistry = createComponentRegistry([/* ... */]);
|
|
127
|
+
|
|
128
|
+
const router = createInteractionRouter({
|
|
129
|
+
commands: chatInputCommandRegistry.byName,
|
|
130
|
+
components: componentRegistry.byName,
|
|
131
|
+
buildContext: async (interaction, baseContext) => ({ ...baseContext, t: await buildT(interaction) }),
|
|
132
|
+
onError: (interaction, context, error) =>
|
|
133
|
+
handleInteractionError(interaction, context, { buildMessage: () => context.t('errors.unexpected') }),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
client.on(Events.InteractionCreate, (interaction) => router(interaction, baseContext));
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Bots that need to run logic between a command handler and error handling
|
|
140
|
+
(e.g. telemetry) can call `dispatchChatInputCommand`/`dispatchAutocomplete`/
|
|
141
|
+
`dispatchComponent`/`dispatchModal`/`dispatchContextMenu` directly instead of
|
|
142
|
+
the combined router — both take the same `registry.byName` maps.
|
|
143
|
+
|
|
144
|
+
There's also `createContextMenuCommandRegistry`/`createModalRegistry` for the
|
|
145
|
+
other two interaction kinds, and `flattenCommandModals(chatInputRegistry)`
|
|
146
|
+
for bots that nest a `.modal` map directly on the owning chat-input command
|
|
147
|
+
(rather than registering modals as a standalone top-level registry) — it
|
|
148
|
+
synthesizes a flat `Registry<string, BotModal<Ctx>>` view so `dispatchModal`
|
|
149
|
+
can consume it unchanged.
|
|
150
|
+
|
|
151
|
+
### `@went.tf/discord-bot-framework/commands`
|
|
152
|
+
|
|
153
|
+
**Every command's wire definition (name, description, options, permissions)
|
|
154
|
+
lives in one `commands.json` file per bot** — a flat array mirroring
|
|
155
|
+
Discord's bulk-overwrite PUT body exactly, so it's directly postable to
|
|
156
|
+
Discord's API as-is (translations aside, see below). This replaces the old
|
|
157
|
+
per-command `getDefinition()` function: handler objects (`{ name, handle,
|
|
158
|
+
autocomplete?, modal? }`) no longer describe their own wire shape at all.
|
|
159
|
+
|
|
160
|
+
1. Author `commands.json`, validated against your own JSON Schema composed
|
|
161
|
+
over this package's generic fragments (see "JSON Schema fragments"
|
|
162
|
+
below):
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
[
|
|
166
|
+
{ "type": 1, "name": "ping", "description": "Replies with pong" },
|
|
167
|
+
{
|
|
168
|
+
"type": 1,
|
|
169
|
+
"name": "search",
|
|
170
|
+
"description": "Search for something",
|
|
171
|
+
"options": [
|
|
172
|
+
{ "type": 3, "name": "query", "description": "Query string", "required": true }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
2. Parse and validate it before doing anything else with it:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { Ajv } from 'ajv';
|
|
182
|
+
import { parseCommandsFile, registerFrameworkSchemas } from '@went.tf/discord-bot-framework/commands/schema';
|
|
183
|
+
import myCommandsSchema from './commands.schema.json' with { type: 'json' };
|
|
184
|
+
import commandsData from './commands.json' with { type: 'json' };
|
|
185
|
+
|
|
186
|
+
const ajv = new Ajv({ allErrors: true, allowUnionTypes: true });
|
|
187
|
+
registerFrameworkSchemas(ajv);
|
|
188
|
+
const validate = ajv.compile(myCommandsSchema);
|
|
189
|
+
|
|
190
|
+
const commandsFile = parseCommandsFile(commandsData, { validate });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
3. Build the Discord-ready body and register it — unchanged from before,
|
|
194
|
+
just fed by `commandsFile` + your handler registries instead of
|
|
195
|
+
`getDefinition()`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { buildApplicationCommandsBody, createCommandRegistrar, fixedReplyCommandFactory } from '@went.tf/discord-bot-framework/commands';
|
|
199
|
+
|
|
200
|
+
const registrar = createCommandRegistrar({ rest, applicationId: env.DISCORD_CLIENT_ID, logger });
|
|
201
|
+
|
|
202
|
+
const commandBodies = buildApplicationCommandsBody(
|
|
203
|
+
commandsFile,
|
|
204
|
+
{ chatInput: chatInputCommandRegistry, contextMenu: contextMenuCommandRegistry },
|
|
205
|
+
{ sharedMetadata: { integration_types: [...], contexts: [...] } },
|
|
206
|
+
);
|
|
207
|
+
await registrar.updateGlobalCommands(commandBodies);
|
|
208
|
+
|
|
209
|
+
const pingCommand = { name: 'ping', ...fixedReplyCommandFactory('pong') };
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`buildApplicationCommandsBody` walks `commandsFile` (its order drives the
|
|
213
|
+
output order, not registry insertion order), matches each entry to a handler
|
|
214
|
+
by name, applies `registerCondition` filtering, merges `sharedMetadata`
|
|
215
|
+
(the commands.json entry's own fields win on conflict), and stably sorts
|
|
216
|
+
every options array (including nested subcommand/subcommand-group options)
|
|
217
|
+
so required options precede optional ones, matching Discord's API
|
|
218
|
+
requirement automatically. It also enforces two invariants **before ever
|
|
219
|
+
calling Discord's API**, each collecting every offender into one thrown
|
|
220
|
+
error rather than failing on the first: every `commands.json` entry must
|
|
221
|
+
have a matching handler, every handler must have a matching `commands.json`
|
|
222
|
+
entry, and every command/option must end up with a non-empty `description`
|
|
223
|
+
(from the file directly, or via `resolveDescription` — see "Localizing
|
|
224
|
+
command names/descriptions" below).
|
|
225
|
+
|
|
226
|
+
`fixedReplyCommandFactory(content, ephemeral?)` now only returns `{ handle }`
|
|
227
|
+
— pair it with a registry entry that supplies `name`, and a `commands.json`
|
|
228
|
+
entry that supplies `name`/`description`.
|
|
229
|
+
|
|
230
|
+
#### JSON Schema fragments
|
|
231
|
+
|
|
232
|
+
This package ships only the **generic, reusable JSON Schema building
|
|
233
|
+
blocks** mirroring `discord-api-types`' command/option shapes — it does not
|
|
234
|
+
dictate one rigid schema for your whole `commands.json` file. Compose your
|
|
235
|
+
own schema on top via `$ref`/`allOf`, e.g. to narrow `name` to an enum of
|
|
236
|
+
your bot's actual command names:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"$id": "https://schema.your-bot.example/commands-file.json",
|
|
241
|
+
"type": "array",
|
|
242
|
+
"items": {
|
|
243
|
+
"oneOf": [
|
|
244
|
+
{
|
|
245
|
+
"allOf": [
|
|
246
|
+
{ "$ref": "https://schema.went.tf/discord-bot-framework/chat-input-command.json" },
|
|
247
|
+
{ "properties": { "name": { "enum": ["ping", "search"] } } }
|
|
248
|
+
]
|
|
249
|
+
},
|
|
250
|
+
{ "$ref": "https://schema.went.tf/discord-bot-framework/context-menu-command.json" }
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Call `registerFrameworkSchemas(ajv)` before compiling your own schema so its
|
|
257
|
+
`$ref`s resolve. The fragments this package ships (all under
|
|
258
|
+
`@went.tf/discord-bot-framework/commands/schema`, and as real standalone
|
|
259
|
+
`.json` files under `build/commands/schema/` for non-TS tooling):
|
|
260
|
+
`commands-file`, `chat-input-command`, `context-menu-command`,
|
|
261
|
+
`application-command-option` (and its `application-command-leaf-option`/
|
|
262
|
+
`application-command-subcommand`/`application-command-subcommand-group`
|
|
263
|
+
building blocks), `application-command-option-choice`,
|
|
264
|
+
`default-member-permissions`, `option-name`, `context-menu-name`,
|
|
265
|
+
`application-command-type`, `interaction-context-type`,
|
|
266
|
+
`application-integration-type`, `channel-type`.
|
|
267
|
+
|
|
268
|
+
Base fragments use `additionalProperties: false` for strictness — if your
|
|
269
|
+
bot needs a genuinely new top-level field per command entry, you'll need
|
|
270
|
+
`unevaluatedProperties`-based composition instead of `allOf`, since
|
|
271
|
+
`additionalProperties: false` only evaluates a schema's own declared
|
|
272
|
+
properties, not fields declared on sibling `allOf` members.
|
|
273
|
+
|
|
274
|
+
Command/option **names are required** in `commands.json`, but
|
|
275
|
+
**descriptions are optional** — a description can be authored directly in
|
|
276
|
+
the file, or left out and filled in at submission time (see below). Nothing
|
|
277
|
+
in `commands.json` is ever localized by hand: no `name_localizations`/
|
|
278
|
+
`description_localizations` fields exist in this schema at all.
|
|
279
|
+
|
|
280
|
+
#### Localizing command names/descriptions
|
|
281
|
+
|
|
282
|
+
`createCommandLocalizer` (from `@went.tf/discord-bot-framework/i18n`)
|
|
283
|
+
generically resolves descriptions and builds `name_localizations`/
|
|
284
|
+
`description_localizations` dictionaries from an i18next `TFunction`, keyed
|
|
285
|
+
by the same path convention `buildApplicationCommandsBody` uses internally
|
|
286
|
+
(`commands.<name>.description`, `commands.<name>.options.<option>.description`,
|
|
287
|
+
and one level deeper for subcommand options):
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { createCommandLocalizer } from '@went.tf/discord-bot-framework/i18n';
|
|
291
|
+
|
|
292
|
+
const localizer = createCommandLocalizer({ locales: SUPPORTED_LANGUAGES, baseLocale: DEFAULT_LANGUAGE, t: i18nextInstance.t });
|
|
293
|
+
|
|
294
|
+
const commandBodies = buildApplicationCommandsBody(commandsFile, registries, {
|
|
295
|
+
resolveDescription: localizer.resolveDescription,
|
|
296
|
+
localizeNames: localizer.localizeName,
|
|
297
|
+
localizeDescriptions: localizer.localizeDescription,
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
If a command/option has no `description` in `commands.json` **and** no
|
|
302
|
+
`resolveDescription` hook is wired in (or the hook can't find a translation
|
|
303
|
+
either), `buildApplicationCommandsBody` throws before anything is sent to
|
|
304
|
+
Discord — it never silently registers a command with a missing description.
|
|
305
|
+
|
|
306
|
+
To derive TS types from your own composed schema, install
|
|
307
|
+
[`json-schema-to-ts`](https://www.npmjs.com/package/json-schema-to-ts)
|
|
308
|
+
yourself (it's a devDependency of this package, type-only, not re-exported)
|
|
309
|
+
and use its `FromSchema` the same way this package's own
|
|
310
|
+
`commands/schema/index.ts` does — pass every `$ref`-ed fragment (yours and
|
|
311
|
+
this package's) in the `references` option.
|
|
312
|
+
|
|
313
|
+
### `@went.tf/discord-bot-framework/client`
|
|
314
|
+
|
|
315
|
+
Sharding is entirely opt-in. Most bots — anything single-guild or otherwise
|
|
316
|
+
small enough not to need multiple discord.js shards — should just use
|
|
317
|
+
`createBotClient` and never touch `createShardManager` or anything
|
|
318
|
+
shard-related at all:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
import { createBotClient } from '@went.tf/discord-bot-framework/client';
|
|
322
|
+
|
|
323
|
+
const client = await createBotClient({ intents: [GatewayIntentBits.Guilds], token, onInteraction });
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Only reach for `createShardManager` if your bot actually runs across
|
|
327
|
+
multiple discord.js shards (large multi-guild bots). It's a separate,
|
|
328
|
+
independent function — pulling it in doesn't require any sharding-specific
|
|
329
|
+
config elsewhere in the framework:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { createShardManager } from '@went.tf/discord-bot-framework/client';
|
|
333
|
+
|
|
334
|
+
const manager = await createShardManager({
|
|
335
|
+
token, botScriptPath, logger,
|
|
336
|
+
beforeSpawn: () => startupCommandsUpdate(logger),
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### `@went.tf/discord-bot-framework/dev`
|
|
341
|
+
|
|
342
|
+
Live-reloads compiled command/interaction handler *implementations* during
|
|
343
|
+
local development, without restarting the process or re-registering commands
|
|
344
|
+
with Discord for every code change. `createHandlerWatcher` is a small,
|
|
345
|
+
dependency-free primitive built on native `fs.watch` — it only watches paths,
|
|
346
|
+
debounces/coalesces filesystem events per file, and invokes your `onChange`
|
|
347
|
+
callback (catching and logging anything it throws so a bad reload never
|
|
348
|
+
crashes the bot). It deliberately does not know how to re-import a module or
|
|
349
|
+
merge it into a registry, since that depends on each bot's own file layout:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { createHandlerWatcher } from '@went.tf/discord-bot-framework/dev';
|
|
353
|
+
import { pathToFileURL } from 'node:url';
|
|
354
|
+
import { basename, extname } from 'node:path';
|
|
355
|
+
|
|
356
|
+
if (env.DEV_WATCH) {
|
|
357
|
+
const watcher = createHandlerWatcher({
|
|
358
|
+
paths: ['./build/commands'],
|
|
359
|
+
logger,
|
|
360
|
+
onChange: async (filePath) => {
|
|
361
|
+
const commandName = basename(filePath, extname(filePath));
|
|
362
|
+
if (!chatInputCommandRegistry.isKnown(commandName)) return;
|
|
363
|
+
// The `?t=` query busts Node's ESM module cache, which keys on the
|
|
364
|
+
// resolved URL — deriving the registry key and writing it back into
|
|
365
|
+
// `byName` is bot-side glue, not something this package standardizes.
|
|
366
|
+
const fresh = await import(`${pathToFileURL(filePath).href}?t=${Date.now()}`);
|
|
367
|
+
chatInputCommandRegistry.byName[commandName] = fresh.default;
|
|
368
|
+
logger.log(`Reloaded command handler: ${commandName}`);
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
process.on('SIGINT', () => {
|
|
373
|
+
watcher.close();
|
|
374
|
+
process.exit(0);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
This works because `dispatch*`/`createInteractionRouter` always read
|
|
380
|
+
`registry.byName[key]` live on every interaction — mutating an entry in place
|
|
381
|
+
is picked up on the very next interaction with no other wiring.
|
|
382
|
+
|
|
383
|
+
**Limitations:** this only reloads handler implementations already sitting in
|
|
384
|
+
a registry's `byName`. It does **not** re-run command registration — changing
|
|
385
|
+
a command's `commands.json` entry (name, description, options schema) still
|
|
386
|
+
requires re-running `buildApplicationCommandsBody` + `createCommandRegistrar`
|
|
387
|
+
and a full process restart, and a brand-new command file that wasn't in the
|
|
388
|
+
registry at startup isn't picked up without one either. It also assumes a parallel `tsc --watch` (or equivalent) process
|
|
389
|
+
is running, since this package has no bundler and watches compiled `build/`
|
|
390
|
+
output, not `src/`. Gate it behind your own dev-only flag (e.g. a `DEV_WATCH`
|
|
391
|
+
env var via `boolFromString()`) — this package intentionally has no built-in
|
|
392
|
+
concept of a dev/prod mode.
|
|
393
|
+
|
|
394
|
+
If your `onChange` callback re-imports only the *one file that changed* (as
|
|
395
|
+
in the example above), it correctly picks up edits to a command file itself,
|
|
396
|
+
but **not** edits to a shared module that command statically imports — a
|
|
397
|
+
modal handler, a util, anything under a second file. Node's ESM cache keys on
|
|
398
|
+
resolved URL: giving the changed file a fresh cache-busted URL doesn't affect
|
|
399
|
+
how its own `import './some-util.js'` statement resolves, so that nested
|
|
400
|
+
import still returns the stale cached instance. Reimporting one small
|
|
401
|
+
aggregator module that pulls in your whole command/component tree (its
|
|
402
|
+
`registry.byName` values in particular) instead of one file at a time avoids
|
|
403
|
+
this — see `createSourceReloader` below.
|
|
404
|
+
|
|
405
|
+
#### `createSourceReloader`
|
|
406
|
+
|
|
407
|
+
Re-imports a module — and everything it transitively imports from under a
|
|
408
|
+
given root directory — as brand-new instances on every call, without
|
|
409
|
+
restarting the process. Unlike the single-file `?t=` trick above, this
|
|
410
|
+
correctly picks up changes to *any* file in the reloaded subtree, not just
|
|
411
|
+
the one directly re-imported, by tagging every module resolved under
|
|
412
|
+
`rootDir` with a shared epoch via a `module.register()` hook, and bumping
|
|
413
|
+
that epoch before each `reimport()`:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { createHandlerWatcher, createSourceReloader } from '@went.tf/discord-bot-framework/dev';
|
|
417
|
+
import { join } from 'node:path';
|
|
418
|
+
|
|
419
|
+
if (env.DEV_WATCH) {
|
|
420
|
+
const reloader = createSourceReloader({ rootDir: currentFolder, logger });
|
|
421
|
+
const interactionsPath = join(currentFolder, 'utils', 'interactions.ts');
|
|
422
|
+
|
|
423
|
+
const watcher = createHandlerWatcher({
|
|
424
|
+
paths: [join(currentFolder, 'commands'), join(currentFolder, 'components'), join(currentFolder, 'utils')],
|
|
425
|
+
filter: filePath => filePath.endsWith('.ts'),
|
|
426
|
+
logger,
|
|
427
|
+
onChange: async () => {
|
|
428
|
+
const fresh = await reloader.reimport(interactionsPath);
|
|
429
|
+
// `registry.byName` is what dispatch reads live — merge into the existing
|
|
430
|
+
// registry object in place; the binding in the module that declared it
|
|
431
|
+
// (and everything that imported it) can't be swapped from out here.
|
|
432
|
+
Object.assign(chatInputCommandRegistry.byName, fresh.chatInputCommandRegistry.byName);
|
|
433
|
+
Object.assign(componentRegistry.byName, fresh.componentRegistry.byName);
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
process.on('SIGINT', () => watcher.close());
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Anything resolved **outside** `rootDir` — `node_modules`, this framework,
|
|
442
|
+
compiled output elsewhere — is left completely alone, on Node's normal
|
|
443
|
+
module cache. That's the property that makes this safe to use for a Discord
|
|
444
|
+
bot: as long as your gateway client and DB pool are created (and imported
|
|
445
|
+
from) outside `rootDir` — true for the shard-script shape shown under
|
|
446
|
+
`createShardManager` below, where the client is built directly in `bot.ts`
|
|
447
|
+
and never re-imported by the reloaded `interactions.ts` subtree — a reload
|
|
448
|
+
never reconnects the client or reopens the pool. Reloading a module *with*
|
|
449
|
+
top-level side effects (one that opens a connection, starts a timer) will
|
|
450
|
+
duplicate those side effects on every call; keep whatever you reload
|
|
451
|
+
side-effect-free (a thin aggregator of plain object exports, like
|
|
452
|
+
`interactions.ts` above).
|
|
453
|
+
|
|
454
|
+
`reimport()`'s epoch tag lives on a `SharedArrayBuffer`, so it needs
|
|
455
|
+
`--allow-worker` under Node's permission model, same as `module.register()`
|
|
456
|
+
itself.
|
|
457
|
+
|
|
458
|
+
**Combining with `createShardManager`:** the example above assumes the
|
|
459
|
+
process calling `createHandlerWatcher` is also the one holding the
|
|
460
|
+
registries — true for `createBotClient` bots, and true for a
|
|
461
|
+
`createShardManager` bot's *shard* process (the file at `botScriptPath`),
|
|
462
|
+
**not** the top-level process that calls `createShardManager` itself. Put the
|
|
463
|
+
`if (env.DEV_WATCH) { ... }` block in the shard script, after the client is
|
|
464
|
+
created, not in the file that spawns the `ShardingManager`.
|
|
465
|
+
|
|
466
|
+
If you skip `tsc --watch` and instead run the shard script directly from
|
|
467
|
+
source (`tsx`/`ts-node`/similar) to avoid a separate compile step, two things
|
|
468
|
+
that are easy to get wrong:
|
|
469
|
+
|
|
470
|
+
- `botScriptPath` must point at the actual file being executed (e.g. `bot.ts`),
|
|
471
|
+
not a `build/`-compiled path that was never written.
|
|
472
|
+
- discord.js's `ShardingManager` does **not** inherit the parent process's
|
|
473
|
+
CLI flags for spawned shards — both `'process'` (`child_process.fork`) and
|
|
474
|
+
`'worker'` (`worker_threads.Worker`) modes are given an explicit
|
|
475
|
+
`execArgv: []` unless you pass your own `execArgv` to `createShardManager`.
|
|
476
|
+
If the parent process is only able to run TypeScript because of loader
|
|
477
|
+
flags injected by a tool like `tsx` (visible in `process.execArgv`), those
|
|
478
|
+
flags are silently dropped for every shard unless you forward them
|
|
479
|
+
yourself — the shard process/thread then fails to load a `.ts` entry file
|
|
480
|
+
at all. Forward them explicitly:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
const isTsDevMode = process.env.npm_lifecycle_script?.includes('.ts') ?? false;
|
|
484
|
+
await createShardManager({
|
|
485
|
+
token, logger,
|
|
486
|
+
botScriptPath: `${currentFolder}/bot.${isTsDevMode ? 'ts' : 'js'}`,
|
|
487
|
+
mode: isTsDevMode ? 'worker' : 'process',
|
|
488
|
+
execArgv: isTsDevMode ? process.execArgv : undefined,
|
|
489
|
+
beforeSpawn: () => startupCommandsUpdate(logger),
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
This has no effect on watched paths: `createHandlerWatcher`'s `filter`
|
|
494
|
+
option still needs updating to match `.ts` instead of the default
|
|
495
|
+
`.js`/`.mjs`/`.cjs`, since there's no `build/` output to watch in this mode.
|
|
496
|
+
|
|
497
|
+
### `@went.tf/discord-bot-framework/utils`
|
|
498
|
+
|
|
499
|
+
`runAttempts`, `getGitData`, `queueLazyPromises`, `condenseStringArray`,
|
|
500
|
+
`sendMessageSlices`, `loadAllMessages`, `getUserIdentifier`,
|
|
501
|
+
`stringifyChannelName`, `stringifyOptionsData`, and generic guild/member/role/
|
|
502
|
+
channel lookups (`getServer`, `findServerTextChannelByName`,
|
|
503
|
+
`findServerRoleByName`, `findServerMember`, `getServerMemberRole`,
|
|
504
|
+
`serverMemberHasRole`, `isSameObject`).
|
|
505
|
+
|
|
506
|
+
### `@went.tf/discord-bot-framework/db` (optional)
|
|
507
|
+
|
|
508
|
+
Requires `@prisma/client` and `@prisma/adapter-pg` (Postgres only).
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
import { createPostgresPrismaDb } from '@went.tf/discord-bot-framework/db';
|
|
512
|
+
import { PrismaClient } from './generated/prisma/client.js';
|
|
513
|
+
|
|
514
|
+
export const db = createPostgresPrismaDb(PrismaClient, { connectionString: env.DATABASE_URL });
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Bots that only talk to an externally-managed database (or no database at
|
|
518
|
+
all) never need to import this subpath or install its peer dependencies.
|
|
519
|
+
|
|
520
|
+
### `@went.tf/discord-bot-framework/i18n` (optional)
|
|
521
|
+
|
|
522
|
+
Requires `i18next` and `i18next-fs-backend`.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
import { createI18nInitializer } from '@went.tf/discord-bot-framework/i18n';
|
|
526
|
+
|
|
527
|
+
const initI18next = createI18nInitializer({
|
|
528
|
+
localesDir: './src/locales',
|
|
529
|
+
supportedLngs: SUPPORTED_LANGUAGES,
|
|
530
|
+
fallbackLng: DEFAULT_LANGUAGE,
|
|
531
|
+
debug: env.DEBUG_I18N,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const i18nextInstance = await initI18next(logger);
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Locale file content, translation-credit generation, and any custom eslint
|
|
538
|
+
i18n-key-validation rules stay entirely bot-side.
|
|
539
|
+
|
|
540
|
+
`createCommandLocalizer` also lives here — see "Localizing command
|
|
541
|
+
names/descriptions" under `./commands` above.
|
|
542
|
+
|
|
543
|
+
## Development
|
|
544
|
+
|
|
545
|
+
```sh
|
|
546
|
+
pnpm install
|
|
547
|
+
pnpm test
|
|
548
|
+
pnpm run lint
|
|
549
|
+
pnpm run build
|
|
550
|
+
```
|