chapterjs 0.2.3 → 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/README.md +65 -0
- package/dist/cli/build.js +14 -1
- package/dist/cli/cli.js +41 -6
- package/dist/cli/dev.js +29 -1
- package/dist/cli/env.d.ts +5 -0
- package/dist/cli/env.js +39 -3
- package/dist/cli/project.d.ts +49 -7
- package/dist/cli/project.js +103 -19
- package/dist/cli/redis.d.ts +23 -0
- package/dist/cli/redis.js +57 -0
- package/dist/cli/start.js +43 -17
- package/dist/cli/test.d.ts +29 -0
- package/dist/cli/test.js +446 -0
- package/dist/commands/autocomplete.d.ts +1 -1
- package/dist/commands/autocomplete.js +37 -41
- package/dist/commands/router.d.ts +4 -1
- package/dist/commands/router.js +15 -17
- package/dist/components/router.d.ts +7 -2
- package/dist/components/router.js +16 -16
- package/dist/discord/types/channel.d.ts +9 -1
- package/dist/discord/types/guild.d.ts +5 -5
- package/dist/discord/types/permissions.d.ts +6 -0
- package/dist/discord/types/poll.d.ts +7 -1
- package/dist/events/registry.d.ts +252 -239
- package/dist/events/registry.js +125 -100
- package/dist/events/router.d.ts +9 -4
- package/dist/events/router.js +24 -11
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/interactions/dispatch.d.ts +5 -1
- package/dist/interactions/dispatch.js +6 -2
- package/dist/loader/hot.js +22 -2
- package/dist/loader/loader.d.ts +7 -0
- package/dist/loader/loader.js +22 -12
- package/dist/ranking/declaration.d.ts +12 -0
- package/dist/ranking/declaration.js +48 -0
- package/dist/ranking/ranking.d.ts +102 -0
- package/dist/ranking/ranking.js +129 -0
- package/dist/redis/client.d.ts +36 -0
- package/dist/redis/client.js +252 -0
- package/dist/redis/protocol.d.ts +22 -0
- package/dist/redis/protocol.js +99 -0
- package/dist/redis/url.d.ts +24 -0
- package/dist/redis/url.js +50 -0
- package/dist/store/backend.d.ts +49 -4
- package/dist/store/backend.js +66 -11
- package/dist/store/declaration.d.ts +3 -0
- package/dist/store/declaration.js +8 -3
- package/dist/store/file-backend.d.ts +3 -1
- package/dist/store/file-backend.js +29 -0
- package/dist/store/ipc-backend.d.ts +24 -1
- package/dist/store/ipc-backend.js +49 -0
- package/dist/store/memory-backend.d.ts +17 -0
- package/dist/store/memory-backend.js +82 -0
- package/dist/store/ranking-backend.d.ts +45 -0
- package/dist/store/ranking-backend.js +82 -0
- package/dist/store/redis-backend.d.ts +38 -0
- package/dist/store/redis-backend.js +245 -0
- package/dist/store/redis-ranking.d.ts +15 -0
- package/dist/store/redis-ranking.js +69 -0
- package/dist/store/scope.d.ts +11 -0
- package/dist/store/scope.js +23 -0
- package/dist/store/store.d.ts +39 -5
- package/dist/store/store.js +83 -33
- package/dist/structures/invite.d.ts +6 -0
- package/dist/structures/invite.js +11 -0
- package/dist/tasks/scheduler.d.ts +6 -0
- package/dist/tasks/scheduler.js +9 -3
- package/dist/testing/failure.d.ts +9 -0
- package/dist/testing/failure.js +20 -0
- package/dist/testing/fake-discord.d.ts +59 -0
- package/dist/testing/fake-discord.js +120 -0
- package/dist/testing/fake-gateway.d.ts +59 -0
- package/dist/testing/fake-gateway.js +123 -0
- package/dist/testing/harness.d.ts +387 -0
- package/dist/testing/harness.js +1418 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/scenario.d.ts +39 -0
- package/dist/testing/scenario.js +58 -0
- package/dist/testing/smoke.d.ts +20 -0
- package/dist/testing/smoke.js +659 -0
- package/dist/testing/websocket.d.ts +34 -0
- package/dist/testing/websocket.js +167 -0
- package/dist/testing/world.d.ts +145 -0
- package/dist/testing/world.js +1479 -0
- package/package.json +20 -3
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ChapterJS
|
|
2
|
+
|
|
3
|
+
**The Discord framework: ultra simple, but ultra customizable.**
|
|
4
|
+
|
|
5
|
+
Drop files into `src/`, and the `chapterjs` CLI does everything else: the connection, the intents, the registration of commands, hot reload, sharding and several processes for large bots. Everything you write is typed end to end, without a single type to write.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm create chapterjs my-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
📖 Documentation, in English and in French: **[chapterjs.org](https://www.chapterjs.org)**
|
|
12
|
+
|
|
13
|
+
## A file is what it exports
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// src/commands/hello.ts
|
|
17
|
+
import { command } from 'chapterjs';
|
|
18
|
+
|
|
19
|
+
export default command({
|
|
20
|
+
name: 'hello',
|
|
21
|
+
description: 'Says hello to someone',
|
|
22
|
+
cooldown: '10s',
|
|
23
|
+
options: {
|
|
24
|
+
who: { type: 'user', description: 'Who to greet', required: true },
|
|
25
|
+
},
|
|
26
|
+
async run({ interaction, options }) {
|
|
27
|
+
// options.who is a User: typed from the options above.
|
|
28
|
+
await interaction.reply(`Hello ${options.who}!`);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The framework loads every file of `src/`, wherever it is and whatever it is called, and runs what the files export: `command()`, `event()`, `button()`, `select()`, `modal()`, `embed()`, `task()`, `store()`, `ranking()`, `presence()`, `language()`. The folders are yours to organise with.
|
|
34
|
+
|
|
35
|
+
## What it does
|
|
36
|
+
|
|
37
|
+
- **Slash commands** with typed options, choices, autocomplete, permissions, cooldowns and translations.
|
|
38
|
+
- **Events**: the intents follow from the files you write, privileged ones included, with a direct link when one must be enabled.
|
|
39
|
+
- **Components**: buttons, select menus, forms and embeds, with typed `data` carried in the message and no id to write.
|
|
40
|
+
- **Tasks**: every so often or with a cron, in the timezone you choose.
|
|
41
|
+
- **Storage**: `store<Warning[]>({ expires: '30d' })` keeps data from one restart to the next, per server, typed, in a JSON file written for you, or in Redis with `storage: 'redis'` and a `REDIS_URL`; `ranking()` keeps a score per member in order, for leaderboards.
|
|
42
|
+
- **Translated messages**: `t('welcome', { name })` speaks the language of who will read it.
|
|
43
|
+
- **Voice**: join a channel and play a file, an attachment or a link, end-to-end encrypted (DAVE).
|
|
44
|
+
- **Tests**: `chapterjs test` runs the bot against a Discord in memory, no token needed: every file is used once by itself, then your scenarios (`scenario('ping answers Pong', async bot => { … })`) say what the bot should answer.
|
|
45
|
+
|
|
46
|
+
## Four commands
|
|
47
|
+
|
|
48
|
+
- `chapterjs dev` runs the bot on a test server and reloads each file when you save it. A broken file is reported with its line and never stops the bot.
|
|
49
|
+
- `chapterjs test` checks the bot without Discord, with nothing written and with your scenarios.
|
|
50
|
+
- `chapterjs build` checks every file and the types, then compiles the bot into one JavaScript file.
|
|
51
|
+
- `chapterjs start` runs it in production, on several processes when the bot is large.
|
|
52
|
+
|
|
53
|
+
Nothing to install to run a bot: the framework only uses what Node has (Node 24 or later). Discord's rate limits, payload sizes and reconnections are handled for you.
|
|
54
|
+
|
|
55
|
+
## Versions
|
|
56
|
+
|
|
57
|
+
`chapterjs`, `create-chapterjs` and `create-chapter` are always released with the same version, and follow [semantic versioning](https://semver.org): what a bot's files use (the functions exported by `chapterjs`, their options, what your functions receive, the files and env vars of a project, the CLI commands) only changes in a major version. A new feature comes in a minor one, a fix in a patch. The [changelog](https://www.chapterjs.org/changelog) says what each version brings, and `chapterjs dev` tells you when a new one is out.
|
|
58
|
+
|
|
59
|
+
## Contributing
|
|
60
|
+
|
|
61
|
+
The source, the issues and the documentation live in [github.com/chapterjs/chapterjs](https://github.com/chapterjs/chapterjs). Read [CONTRIBUTING.md](https://github.com/chapterjs/chapterjs/blob/master/CONTRIBUTING.md) to get started.
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
[MIT](./LICENSE)
|
package/dist/cli/build.js
CHANGED
|
@@ -15,6 +15,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
15
15
|
import { enableProjectLoader } from '../loader/hot.js';
|
|
16
16
|
import { listSources } from '../loader/loader.js';
|
|
17
17
|
import { sourcesJoinVoice } from '../voice/usage.js';
|
|
18
|
+
import { MemoryStoreBackend } from '../store/memory-backend.js';
|
|
18
19
|
import { createProject, hasSources, writeTypes } from './project.js';
|
|
19
20
|
/** Where the build of a project is, from its folder. */
|
|
20
21
|
export const buildDir = (cwd) => join(cwd, '.chapterjs', 'build');
|
|
@@ -105,7 +106,18 @@ export async function build(options) {
|
|
|
105
106
|
};
|
|
106
107
|
// 1. Every file must run, one by one: each says what is wrong with it.
|
|
107
108
|
enableProjectLoader(src, { reload: false });
|
|
108
|
-
|
|
109
|
+
// The files run once and keep nothing: a store, wherever it says it
|
|
110
|
+
// keeps its data, has nowhere to go (so a build needs no Redis).
|
|
111
|
+
const nothing = {
|
|
112
|
+
file: new MemoryStoreBackend(),
|
|
113
|
+
redis: new MemoryStoreBackend(),
|
|
114
|
+
};
|
|
115
|
+
const sources = createProject({
|
|
116
|
+
cwd,
|
|
117
|
+
version: options.version,
|
|
118
|
+
log,
|
|
119
|
+
stores: nothing,
|
|
120
|
+
});
|
|
109
121
|
const failures = await sources.load();
|
|
110
122
|
// 2. Types: what the editor underlines, for the whole project. Written
|
|
111
123
|
// once the files ran, so a language only offers the commands they do
|
|
@@ -181,6 +193,7 @@ export async function build(options) {
|
|
|
181
193
|
version: options.version,
|
|
182
194
|
log,
|
|
183
195
|
built: await loadBundle(cwd),
|
|
196
|
+
stores: nothing,
|
|
184
197
|
});
|
|
185
198
|
const broken = await built.load();
|
|
186
199
|
if (broken.length > 0) {
|
package/dist/cli/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { writeTypes } from './project.js';
|
|
|
7
7
|
import { createLog } from './log.js';
|
|
8
8
|
import { nodeTooOld } from './node-version.js';
|
|
9
9
|
import { start } from './start.js';
|
|
10
|
+
import { test } from './test.js';
|
|
10
11
|
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
11
12
|
const USAGE = `Usage: chapterjs <command>
|
|
12
13
|
|
|
@@ -14,6 +15,7 @@ Commands:
|
|
|
14
15
|
dev Run your bot on your dev server and reload it when you save a file
|
|
15
16
|
build Check your bot and prepare it for production
|
|
16
17
|
start Run for everyone the bot that was built
|
|
18
|
+
test Check your bot without Discord: every file once, then your scenarios
|
|
17
19
|
sync Write the types of your project (dev does it too)
|
|
18
20
|
|
|
19
21
|
Options of start:
|
|
@@ -29,19 +31,40 @@ if (tooOld) {
|
|
|
29
31
|
const [command, ...rest] = process.argv.slice(2);
|
|
30
32
|
/**
|
|
31
33
|
* Ctrl+C and a stop asked by the system both end the session cleanly, so
|
|
32
|
-
* the bot goes offline at once. A second one
|
|
34
|
+
* the bot goes offline at once. A second one, pressed while the first is
|
|
35
|
+
* still being handled, does not wait.
|
|
36
|
+
*
|
|
37
|
+
* One Ctrl+C reaches the bot twice when a package manager runs it (`pnpm
|
|
38
|
+
* dev`): once from the terminal, which signals every process of the
|
|
39
|
+
* foreground group, and once forwarded by the package manager. Two signals
|
|
40
|
+
* within a second are that one Ctrl+C, not a second one.
|
|
33
41
|
*/
|
|
34
42
|
function stopSignal() {
|
|
35
43
|
const controller = new AbortController();
|
|
44
|
+
let askedAt = 0;
|
|
36
45
|
for (const name of ['SIGINT', 'SIGTERM']) {
|
|
37
46
|
process.on(name, () => {
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (controller.signal.aborted) {
|
|
49
|
+
if (now - askedAt >= 1000)
|
|
50
|
+
process.exit(1);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
askedAt = now;
|
|
40
54
|
controller.abort();
|
|
41
55
|
});
|
|
42
56
|
}
|
|
43
57
|
return controller.signal;
|
|
44
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Ends the process once everything printed has reached the terminal. When
|
|
61
|
+
* the output is a pipe (`pnpm test` puts one between itself and the
|
|
62
|
+
* command), writes are asynchronous on macOS and Windows: `process.exit()`
|
|
63
|
+
* right after the last line would cut it.
|
|
64
|
+
*/
|
|
65
|
+
function exit() {
|
|
66
|
+
process.stdout.write('', () => process.exit());
|
|
67
|
+
}
|
|
45
68
|
/** `--processes 4` or `--processes=4`; `null` when it is not a count. */
|
|
46
69
|
function readProcesses(args) {
|
|
47
70
|
const index = args.findIndex(arg => arg === '--processes' || arg.startsWith('--processes='));
|
|
@@ -66,7 +89,7 @@ if (command === 'dev') {
|
|
|
66
89
|
signal: controller.signal,
|
|
67
90
|
});
|
|
68
91
|
// Nothing may keep the process alive once the bot is disconnected.
|
|
69
|
-
|
|
92
|
+
exit();
|
|
70
93
|
}
|
|
71
94
|
else if (command === 'start') {
|
|
72
95
|
const processes = readProcesses(rest);
|
|
@@ -91,7 +114,7 @@ else if (command === 'start') {
|
|
|
91
114
|
script: process.argv[1],
|
|
92
115
|
args: ['start'],
|
|
93
116
|
});
|
|
94
|
-
|
|
117
|
+
exit();
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
else if (command === 'sync') {
|
|
@@ -108,7 +131,19 @@ else if (command === undefined || command === '--help' || command === '-h') {
|
|
|
108
131
|
}
|
|
109
132
|
else if (command === 'build') {
|
|
110
133
|
process.exitCode = await build({ cwd: process.cwd(), version, log });
|
|
111
|
-
|
|
134
|
+
exit();
|
|
135
|
+
}
|
|
136
|
+
else if (command === 'test') {
|
|
137
|
+
// How long a scenario may take: only tests shorten it.
|
|
138
|
+
const scenarioTimeout = Number(process.env.CHAPTERJS_SCENARIO_TIMEOUT);
|
|
139
|
+
process.exitCode = await test({
|
|
140
|
+
cwd: process.cwd(),
|
|
141
|
+
version,
|
|
142
|
+
log,
|
|
143
|
+
...(scenarioTimeout > 0 ? { scenarioTimeout } : {}),
|
|
144
|
+
progress: process.stdout.isTTY === true,
|
|
145
|
+
});
|
|
146
|
+
exit();
|
|
112
147
|
}
|
|
113
148
|
else {
|
|
114
149
|
log.error(`"${command}" is not a command.\n${USAGE}`);
|
package/dist/cli/dev.js
CHANGED
|
@@ -16,7 +16,9 @@ import { snapshotFolder, watchFolder } from '../loader/watch.js';
|
|
|
16
16
|
import { RestClient } from '../rest/rest.js';
|
|
17
17
|
import { readDevEnv } from './env.js';
|
|
18
18
|
import { describeIntents, ensureInDevGuild, ensurePrivilegedIntents, fetchApplication, } from './preflight.js';
|
|
19
|
-
import { createProject, explain, hasSources, writeTypes } from './project.js';
|
|
19
|
+
import { createProject, explain, hasSources, reportLooseRejections, writeTypes, } from './project.js';
|
|
20
|
+
import { checkRedis, describeRedisStores, redisStores } from './redis.js';
|
|
21
|
+
import { DEV_PREFIX } from '../store/redis-backend.js';
|
|
20
22
|
import { checkForUpdate, describeUpdate } from './update-check.js';
|
|
21
23
|
/** Runs until stopped. Resolves with the exit code of the process. */
|
|
22
24
|
export async function dev(options) {
|
|
@@ -30,8 +32,16 @@ export async function dev(options) {
|
|
|
30
32
|
}
|
|
31
33
|
const { token, devGuildId } = read.env;
|
|
32
34
|
const src = join(cwd, 'src');
|
|
35
|
+
// Every key written from here starts with `dev:`: a Redis shared with
|
|
36
|
+
// production, which the docs advise against, is never mixed up.
|
|
37
|
+
const redis = read.env.redis
|
|
38
|
+
? redisStores(read.env.redis, log, { prefix: DEV_PREFIX })
|
|
39
|
+
: null;
|
|
33
40
|
if (!(await hasSources(cwd, log)))
|
|
34
41
|
return 1;
|
|
42
|
+
// A promise of the files left alone is an error of the file, not the end
|
|
43
|
+
// of the bot.
|
|
44
|
+
const loose = reportLooseRejections(cwd, log);
|
|
35
45
|
// Asked now, said once the bot is online: the registry answers while the
|
|
36
46
|
// files load and Discord is checked, so nobody waits for it.
|
|
37
47
|
const update = checkForUpdate({
|
|
@@ -44,6 +54,7 @@ export async function dev(options) {
|
|
|
44
54
|
cwd,
|
|
45
55
|
version: options.version,
|
|
46
56
|
log,
|
|
57
|
+
...(redis ? { stores: { redis } } : {}),
|
|
47
58
|
...(options.deferAfter === undefined
|
|
48
59
|
? {}
|
|
49
60
|
: { deferAfter: options.deferAfter }),
|
|
@@ -57,6 +68,12 @@ export async function dev(options) {
|
|
|
57
68
|
// Now that the files ran, a language only offers the commands they do
|
|
58
69
|
// not describe, and `t` is typed from the default language.
|
|
59
70
|
await writeTypes(cwd, project.typesInfo());
|
|
71
|
+
// A store kept in Redis needs it now, not when someone uses the bot.
|
|
72
|
+
if (!(await checkRedis(project, redis, log))) {
|
|
73
|
+
await project.closeStores();
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
let redisLine = describeRedisStores(project, redis);
|
|
60
77
|
// 3. What Discord must agree with before connecting.
|
|
61
78
|
const apiUrl = options.env.CHAPTERJS_API_URL;
|
|
62
79
|
const rest = new RestClient({
|
|
@@ -154,6 +171,8 @@ export async function dev(options) {
|
|
|
154
171
|
log.success(`Connected to ${guild.name} as ${self.username}`);
|
|
155
172
|
log.info(`Intents computed from your files: ${describeIntents(connectedIntents)}`);
|
|
156
173
|
(project.isEmpty() ? log.info : log.success)(project.summary());
|
|
174
|
+
if (redisLine)
|
|
175
|
+
log.info(redisLine);
|
|
157
176
|
project.ready(bot);
|
|
158
177
|
// Tasks follow the bot through reconnections: they read the current one.
|
|
159
178
|
project.startTasks(() => bot);
|
|
@@ -215,6 +234,13 @@ export async function dev(options) {
|
|
|
215
234
|
// offer it, or not.
|
|
216
235
|
if ((await writeTypes(cwd, project.typesInfo())) > 0)
|
|
217
236
|
log.reload('Types updated');
|
|
237
|
+
// A store moved to Redis, or out of it, is said once.
|
|
238
|
+
const kept = describeRedisStores(project, redis);
|
|
239
|
+
if (kept !== redisLine) {
|
|
240
|
+
redisLine = kept;
|
|
241
|
+
if (kept)
|
|
242
|
+
log.info(kept);
|
|
243
|
+
}
|
|
218
244
|
const intents = project.intents(false);
|
|
219
245
|
if ((intents & ~connectedIntents) !== 0 && bot) {
|
|
220
246
|
// Intents are given when connecting: new ones need a new session.
|
|
@@ -256,6 +282,7 @@ export async function dev(options) {
|
|
|
256
282
|
memory.stop();
|
|
257
283
|
await bot?.close();
|
|
258
284
|
await project.closeStores();
|
|
285
|
+
loose();
|
|
259
286
|
if (code === 0)
|
|
260
287
|
log.success('Disconnected');
|
|
261
288
|
return code;
|
|
@@ -267,6 +294,7 @@ export async function dev(options) {
|
|
|
267
294
|
memory.stop();
|
|
268
295
|
await bot?.close();
|
|
269
296
|
await project.closeStores();
|
|
297
|
+
loose();
|
|
270
298
|
if (signal.aborted)
|
|
271
299
|
return 0;
|
|
272
300
|
explain(error, log);
|
package/dist/cli/env.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { type RedisAddress } from '../redis/url.js';
|
|
1
2
|
export interface DevEnv {
|
|
2
3
|
token: string;
|
|
3
4
|
devGuildId: string;
|
|
5
|
+
/** Where the stores with `storage: 'redis'` go, when `REDIS_URL` is set. */
|
|
6
|
+
redis: RedisAddress | null;
|
|
4
7
|
}
|
|
5
8
|
/** What `chapterjs start` runs with. */
|
|
6
9
|
export interface StartEnv {
|
|
7
10
|
token: string;
|
|
8
11
|
/** The dev server, when the project has one: production leaves it alone. */
|
|
9
12
|
devGuildId: string | null;
|
|
13
|
+
/** Where the stores with `storage: 'redis'` go, when `REDIS_URL` is set. */
|
|
14
|
+
redis: RedisAddress | null;
|
|
10
15
|
}
|
|
11
16
|
/**
|
|
12
17
|
* Reads what `chapterjs dev` needs from the `.env` file of the project (the
|
package/dist/cli/env.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parseEnv } from 'node:util';
|
|
4
4
|
import { isSnowflake } from '../discord/snowflake.js';
|
|
5
|
+
import { parseRedisUrl } from '../redis/url.js';
|
|
5
6
|
/**
|
|
6
7
|
* Values people leave in a `.env` copied from an example. Whole values only:
|
|
7
8
|
* a real token may start with any letters.
|
|
@@ -24,6 +25,27 @@ async function readEnv(projectDir, processEnv) {
|
|
|
24
25
|
hasFile,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* `REDIS_URL` and `REDIS_PASSWORD`, when they are there: nothing is needed
|
|
30
|
+
* unless a store asks for Redis, which the files say once they loaded.
|
|
31
|
+
*/
|
|
32
|
+
function redisProblem(read, where) {
|
|
33
|
+
const url = read('REDIS_URL');
|
|
34
|
+
const password = read('REDIS_PASSWORD');
|
|
35
|
+
if (url === '') {
|
|
36
|
+
if (password !== '') {
|
|
37
|
+
return {
|
|
38
|
+
problem: `${where}: REDIS_PASSWORD is set, but REDIS_URL is not.\nAdd REDIS_URL, the address of your Redis (like redis://localhost:6379), or remove REDIS_PASSWORD.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { redis: null };
|
|
42
|
+
}
|
|
43
|
+
const parsed = parseRedisUrl(url, password);
|
|
44
|
+
if ('problem' in parsed) {
|
|
45
|
+
return { problem: `${where}: REDIS_URL ${parsed.problem}` };
|
|
46
|
+
}
|
|
47
|
+
return { redis: parsed.address };
|
|
48
|
+
}
|
|
27
49
|
const TOKEN_HELP = 'Copy the token of your bot from https://discord.com/developers/applications → your application → Bot → Reset Token.';
|
|
28
50
|
/** What is wrong with a token, if anything. */
|
|
29
51
|
function tokenProblem(token, where) {
|
|
@@ -61,7 +83,12 @@ export async function readDevEnv(projectDir, processEnv) {
|
|
|
61
83
|
else if (!isSnowflake(devGuildId)) {
|
|
62
84
|
problems.push(`.env: DEV_GUILD_ID is "${devGuildId}", which is not a server ID (an ID only has digits).\nIn Discord, right-click your test server → Copy Server ID.`);
|
|
63
85
|
}
|
|
64
|
-
|
|
86
|
+
const redis = redisProblem(read, '.env');
|
|
87
|
+
if ('problem' in redis)
|
|
88
|
+
problems.push(redis.problem);
|
|
89
|
+
return problems.length > 0 || 'problem' in redis
|
|
90
|
+
? { problems }
|
|
91
|
+
: { env: { token, devGuildId, redis: redis.redis } };
|
|
65
92
|
}
|
|
66
93
|
/**
|
|
67
94
|
* Reads what `chapterjs start` needs. In production the values often come
|
|
@@ -87,7 +114,16 @@ export async function readStartEnv(projectDir, processEnv) {
|
|
|
87
114
|
if (devGuildId !== '' && !isSnowflake(devGuildId)) {
|
|
88
115
|
problems.push(`DEV_GUILD_ID is "${devGuildId}", which is not a server ID (an ID only has digits).\nRemove it, or copy it again: in Discord, right-click your test server → Copy Server ID.`);
|
|
89
116
|
}
|
|
90
|
-
|
|
117
|
+
const redis = redisProblem(read, hasFile ? '.env' : 'Environment');
|
|
118
|
+
if ('problem' in redis)
|
|
119
|
+
problems.push(redis.problem);
|
|
120
|
+
return problems.length > 0 || 'problem' in redis
|
|
91
121
|
? { problems }
|
|
92
|
-
: {
|
|
122
|
+
: {
|
|
123
|
+
env: {
|
|
124
|
+
token,
|
|
125
|
+
devGuildId: devGuildId === '' ? null : devGuildId,
|
|
126
|
+
redis: redis.redis,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
93
129
|
}
|
package/dist/cli/project.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type CommandEntry } from '../commands/tree.js';
|
|
2
2
|
import { type ComponentEntry } from '../components/router.js';
|
|
3
3
|
import { type LoadedStore } from '../store/declaration.js';
|
|
4
|
-
import { type
|
|
4
|
+
import { type LoadedRanking } from '../ranking/declaration.js';
|
|
5
|
+
import { type StoreBackend, type StoreStorage } from '../store/backend.js';
|
|
5
6
|
import { type LoadedPresence } from '../presence/declaration.js';
|
|
6
7
|
import { type DeclaredCommand, type DeclaredLanguage } from '../messages/declaration.js';
|
|
7
8
|
import { type LoadedMessages } from '../messages/translate.js';
|
|
@@ -9,6 +10,8 @@ import type { Locale } from '../discord/types/common.js';
|
|
|
9
10
|
import { type TaskEntry } from '../tasks/scheduler.js';
|
|
10
11
|
import { type Bot, type BotOptions } from '../core/bot.js';
|
|
11
12
|
import { type GatewayIntentName } from '../discord/intents.js';
|
|
13
|
+
import type { GatewayDispatchEventName } from '../discord/types/gateway-events.js';
|
|
14
|
+
import type { RawInteraction } from '../discord/types/interaction.js';
|
|
12
15
|
import { type LoadedEvent } from '../events/router.js';
|
|
13
16
|
import { type BuiltFile, type Declaration, type FailedFile } from '../loader/loader.js';
|
|
14
17
|
import type { Log } from './log.js';
|
|
@@ -29,13 +32,27 @@ export interface ProjectOptions {
|
|
|
29
32
|
*/
|
|
30
33
|
joinsVoice?: boolean;
|
|
31
34
|
/**
|
|
32
|
-
* Where the stores of the project keep their data.
|
|
33
|
-
* the `data/` folder of the project by default
|
|
34
|
-
* cluster
|
|
35
|
+
* Where the stores of the project keep their data, by storage. `file`: a
|
|
36
|
+
* file per store in the `data/` folder of the project by default, the
|
|
37
|
+
* first process of a cluster through IPC for the other processes.
|
|
38
|
+
* `redis`: what the CLI made of `REDIS_URL`; without one, a store that
|
|
39
|
+
* asks for Redis is reported and left out.
|
|
35
40
|
*/
|
|
36
|
-
stores?: StoreBackend
|
|
41
|
+
stores?: Partial<Record<StoreStorage, StoreBackend>>;
|
|
37
42
|
/** Only tests change this. */
|
|
38
43
|
deferAfter?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Told what the bot is doing with the files, and when it is done with
|
|
46
|
+
* it: what `chapterjs test` waits for. Nothing of the bot waits.
|
|
47
|
+
*/
|
|
48
|
+
observe?: ProjectObserver;
|
|
49
|
+
}
|
|
50
|
+
/** What `chapterjs test` watches of a running project. */
|
|
51
|
+
export interface ProjectObserver {
|
|
52
|
+
/** An interaction was given to the files; `done` once they answered it. */
|
|
53
|
+
interaction?(raw: RawInteraction, done: Promise<void>): void;
|
|
54
|
+
/** A gateway event was given to the files; `done` once they handled it. */
|
|
55
|
+
event?(event: GatewayDispatchEventName, data: unknown, done: Promise<void>): void;
|
|
39
56
|
}
|
|
40
57
|
/** What the types of a project are written from, once its files ran. */
|
|
41
58
|
export interface TypesInfo {
|
|
@@ -63,6 +80,12 @@ export interface TypesInfo {
|
|
|
63
80
|
export declare function writeTypes(cwd: string, info?: TypesInfo): Promise<number>;
|
|
64
81
|
/** The kinds of declarations a project is made of: one per feature. */
|
|
65
82
|
export declare const DECLARATIONS: readonly Declaration<unknown>[];
|
|
83
|
+
/** A ranking, with where it is declared. */
|
|
84
|
+
export interface RankingEntry {
|
|
85
|
+
file: string;
|
|
86
|
+
export: string;
|
|
87
|
+
ranking: LoadedRanking;
|
|
88
|
+
}
|
|
66
89
|
/** A store, with where it is declared. */
|
|
67
90
|
export interface StoreEntry {
|
|
68
91
|
file: string;
|
|
@@ -97,8 +120,14 @@ export interface Project {
|
|
|
97
120
|
readonly components: ReadonlyMap<string, ComponentEntry>;
|
|
98
121
|
readonly tasks: ReadonlyMap<string, TaskEntry>;
|
|
99
122
|
readonly stores: ReadonlyMap<string, StoreEntry>;
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
readonly rankings: ReadonlyMap<string, RankingEntry>;
|
|
124
|
+
/** Where the stores keep their data, by storage: what a cluster serves to its other processes. */
|
|
125
|
+
readonly storeBackends: ReadonlyMap<StoreStorage, StoreBackend>;
|
|
126
|
+
/** What is kept in Redis: the names of the stores and of the rankings, sorted. */
|
|
127
|
+
redisStores(): {
|
|
128
|
+
stores: string[];
|
|
129
|
+
rankings: string[];
|
|
130
|
+
};
|
|
102
131
|
/** The presence declared, or `null` without one. */
|
|
103
132
|
readonly presence: LoadedPresence | null;
|
|
104
133
|
/** The languages of the project, assembled, or `null` without any. */
|
|
@@ -140,6 +169,11 @@ export interface Project {
|
|
|
140
169
|
startTasks(current: () => Bot | null): void;
|
|
141
170
|
/** Stops the tasks: nothing runs after this. */
|
|
142
171
|
stopTasks(): void;
|
|
172
|
+
/**
|
|
173
|
+
* Runs one task now, as the scheduler would when its time comes, with
|
|
174
|
+
* the bot given. Resolves once it is done; an error is reported.
|
|
175
|
+
*/
|
|
176
|
+
runTask(entry: TaskEntry, bot: Bot, now?: Date): Promise<void>;
|
|
143
177
|
/** Looks after the memory of the bot `current` returns. */
|
|
144
178
|
watchMemory(current: () => Bot | null): {
|
|
145
179
|
stop(): void;
|
|
@@ -147,6 +181,14 @@ export interface Project {
|
|
|
147
181
|
/** Writes what the stores still have to write. Nothing is kept after this. */
|
|
148
182
|
closeStores(): Promise<void>;
|
|
149
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* A promise of the files that nobody waited for (`store.set()` without
|
|
186
|
+
* `await`, in a `run` that is not `async`) rejects with nobody to tell:
|
|
187
|
+
* Node would stop the bot. Reported instead, with the file and the line,
|
|
188
|
+
* and what to write. A rejection from outside the files stays what it is:
|
|
189
|
+
* a bug of the framework, loud. Returns what stops watching.
|
|
190
|
+
*/
|
|
191
|
+
export declare function reportLooseRejections(cwd: string, log: Log): () => void;
|
|
150
192
|
/** Whether the folder is a project: it has a `src` folder. */
|
|
151
193
|
export declare function hasSources(cwd: string, log: Log): Promise<boolean>;
|
|
152
194
|
export declare function createProject(options: ProjectOptions): Project;
|