chapterjs 0.3.0 → 0.5.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 +5 -3
- package/dist/assets/public.js +4 -4
- package/dist/cli/build.d.ts +6 -2
- package/dist/cli/build.js +17 -4
- package/dist/cli/dev.js +37 -6
- package/dist/cli/env.d.ts +5 -0
- package/dist/cli/env.js +39 -3
- package/dist/cli/project.d.ts +61 -9
- package/dist/cli/project.js +250 -44
- package/dist/cli/redis.d.ts +23 -0
- package/dist/cli/redis.js +69 -0
- package/dist/cli/start.js +54 -18
- package/dist/cli/test.js +112 -18
- package/dist/commands/command.d.ts +140 -5
- package/dist/commands/command.js +17 -2
- package/dist/commands/declaration.d.ts +17 -4
- package/dist/commands/declaration.js +128 -2
- package/dist/commands/router.js +78 -18
- package/dist/commands/tree.d.ts +25 -4
- package/dist/commands/tree.js +95 -26
- package/dist/commands/twins.d.ts +8 -0
- package/dist/commands/twins.js +24 -6
- package/dist/components/button.d.ts +8 -3
- package/dist/components/declaration.d.ts +2 -2
- package/dist/components/declaration.js +16 -12
- package/dist/components/render.js +5 -0
- package/dist/components/router.js +41 -0
- package/dist/components/select.d.ts +12 -5
- package/dist/core/bot.d.ts +7 -0
- package/dist/core/bot.js +44 -0
- package/dist/discord/api.d.ts +2 -0
- package/dist/discord/api.js +2 -0
- package/dist/discord/permissions.d.ts +4 -4
- package/dist/discord/permissions.js +13 -0
- package/dist/events/registry.d.ts +265 -243
- package/dist/events/registry.js +131 -100
- package/dist/index.d.ts +7 -4
- package/dist/index.js +2 -1
- package/dist/interactions/dispatch.js +4 -2
- package/dist/loader/loader.d.ts +16 -0
- package/dist/loader/loader.js +52 -7
- package/dist/loader/usage.d.ts +15 -0
- package/dist/loader/usage.js +40 -0
- package/dist/loader/watch.d.ts +5 -3
- package/dist/loader/watch.js +88 -17
- package/dist/messages/commands.js +3 -0
- package/dist/messages/messages.d.ts +1 -0
- package/dist/messages/translate.js +5 -1
- package/dist/presence/declaration.d.ts +49 -2
- package/dist/presence/declaration.js +122 -39
- package/dist/presence/presence.d.ts +65 -6
- package/dist/presence/presence.js +11 -0
- package/dist/presence/rotation.d.ts +32 -0
- package/dist/presence/rotation.js +167 -0
- 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/rest/errors.d.ts +0 -1
- package/dist/rest/errors.js +15 -1
- package/dist/rest/rest.js +10 -1
- 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 -2
- package/dist/store/file-backend.js +47 -1
- package/dist/store/ipc-backend.d.ts +24 -1
- package/dist/store/ipc-backend.js +49 -0
- package/dist/store/memory-backend.d.ts +4 -2
- package/dist/store/memory-backend.js +27 -1
- 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 +17 -1
- package/dist/store/scope.js +32 -3
- package/dist/store/store.d.ts +39 -5
- package/dist/store/store.js +79 -32
- package/dist/structures/channel.d.ts +13 -0
- package/dist/structures/channel.js +24 -0
- package/dist/structures/context.d.ts +3 -0
- package/dist/structures/entities.js +1 -0
- package/dist/structures/interaction.d.ts +6 -0
- package/dist/structures/interaction.js +20 -0
- package/dist/structures/invite.d.ts +6 -0
- package/dist/structures/invite.js +11 -0
- package/dist/structures/message.d.ts +17 -0
- package/dist/structures/message.js +33 -0
- package/dist/structures/wait.d.ts +105 -0
- package/dist/structures/wait.js +85 -0
- package/dist/testing/harness.d.ts +48 -8
- package/dist/testing/harness.js +156 -36
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/smoke.js +112 -1
- package/dist/testing/world.js +51 -2
- package/dist/util/call-site.d.ts +4 -0
- package/dist/util/call-site.js +22 -0
- package/dist/voice/connection.d.ts +2 -0
- package/dist/voice/connection.js +15 -1
- package/dist/voice/usage.d.ts +3 -2
- package/dist/voice/usage.js +7 -19
- package/dist/waiting/usage.d.ts +5 -0
- package/dist/waiting/usage.js +19 -0
- package/dist/waiting/waiters.d.ts +48 -0
- package/dist/waiting/waiters.js +107 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,15 +30,17 @@ export default command({
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
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()`, `presence()`, `language()`. The folders are yours to organise with.
|
|
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
34
|
|
|
35
35
|
## What it does
|
|
36
36
|
|
|
37
|
-
- **Slash commands** with typed options, choices, autocomplete, permissions, cooldowns and translations.
|
|
37
|
+
- **Slash commands** with typed options, choices, autocomplete, permissions, cooldowns and translations, and **menu commands** on the right click of a message or a person.
|
|
38
38
|
- **Events**: the intents follow from the files you write, privileged ones included, with a direct link when one must be enabled.
|
|
39
39
|
- **Components**: buttons, select menus, forms and embeds, with typed `data` carried in the message and no id to write.
|
|
40
|
+
- **Waiting for an answer**: `await channel.waitForMessage({ matches: 'paris', within: '30s' })`, a click or a reaction, right where your function asked.
|
|
40
41
|
- **Tasks**: every so often or with a cron, in the timezone you choose.
|
|
41
|
-
- **
|
|
42
|
+
- **Presence**: one activity, or several in turn, with texts computed when shown.
|
|
43
|
+
- **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
44
|
- **Translated messages**: `t('welcome', { name })` speaks the language of who will read it.
|
|
43
45
|
- **Voice**: join a channel and play a file, an attachment or a link, end-to-end encrypted (DAVE).
|
|
44
46
|
- **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.
|
package/dist/assets/public.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, watch } from 'node:fs';
|
|
5
5
|
import { readdir } from 'node:fs/promises';
|
|
6
6
|
import { join, relative, sep } from 'node:path';
|
|
7
|
-
import { watchFolder } from '../loader/watch.js';
|
|
7
|
+
import { snapshotFolder, watchFolder } from '../loader/watch.js';
|
|
8
8
|
/** The folder, at the root of the project. */
|
|
9
9
|
export const PUBLIC_FOLDER = 'public';
|
|
10
10
|
/**
|
|
@@ -69,9 +69,9 @@ export function watchPublic(projectDir, onChange) {
|
|
|
69
69
|
const appeared = () => {
|
|
70
70
|
if (inner)
|
|
71
71
|
return;
|
|
72
|
-
// The folder was not there: what it has
|
|
73
|
-
//
|
|
74
|
-
watchInside(
|
|
72
|
+
// The folder was not there: what it has now (files copied with it) is
|
|
73
|
+
// told at once, and what is written after it by the watch.
|
|
74
|
+
watchInside(snapshotFolder(root));
|
|
75
75
|
if (!inner)
|
|
76
76
|
return;
|
|
77
77
|
outer?.close();
|
package/dist/cli/build.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BuiltFile } from '../loader/loader.js';
|
|
2
|
+
import { type FoundUses } from '../loader/usage.js';
|
|
2
3
|
import type { Log } from './log.js';
|
|
3
4
|
/** Where the build of a project is, from its folder. */
|
|
4
5
|
export declare const buildDir: (cwd: string) => string;
|
|
@@ -12,8 +13,11 @@ export interface BuildInfo {
|
|
|
12
13
|
sources: string;
|
|
13
14
|
commands: number;
|
|
14
15
|
events: number;
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* What the files do that only their code shows, with the files doing it
|
|
18
|
+
* (`CODE_USES`: intents to ask for).
|
|
19
|
+
*/
|
|
20
|
+
uses: FoundUses;
|
|
17
21
|
}
|
|
18
22
|
/** One value for everything in a folder: it changes when a file does. */
|
|
19
23
|
export declare function fingerprint(dir: string): Promise<string>;
|
package/dist/cli/build.js
CHANGED
|
@@ -14,8 +14,9 @@ import { join, relative, sep } from 'node:path';
|
|
|
14
14
|
import { pathToFileURL } from 'node:url';
|
|
15
15
|
import { enableProjectLoader } from '../loader/hot.js';
|
|
16
16
|
import { listSources } from '../loader/loader.js';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { scanUses } from '../loader/usage.js';
|
|
18
|
+
import { MemoryStoreBackend } from '../store/memory-backend.js';
|
|
19
|
+
import { CODE_USES, 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');
|
|
21
22
|
/** The file the whole bot is compiled into, in the 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) {
|
|
@@ -193,7 +206,7 @@ export async function build(options) {
|
|
|
193
206
|
sources: await fingerprint(src),
|
|
194
207
|
commands: built.commands.size,
|
|
195
208
|
events: built.events.size,
|
|
196
|
-
|
|
209
|
+
uses: await scanUses(cwd, CODE_USES),
|
|
197
210
|
};
|
|
198
211
|
await writeFile(join(out, 'build.json'), JSON.stringify(info, null, 2));
|
|
199
212
|
if (built.isEmpty())
|
package/dist/cli/dev.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// server, and reloads every file of the project when it is saved.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { registerCommands } from '../commands/register.js';
|
|
5
|
-
import { buildCommands,
|
|
5
|
+
import { buildCommands, capitalize, describeCommand, privateOnly, } from '../commands/tree.js';
|
|
6
6
|
import { withoutGlobalTwins } from '../commands/twins.js';
|
|
7
7
|
import { configure } from '../core/memory.js';
|
|
8
8
|
import { GetGlobalApplicationCommands } from '../discord/endpoints.js';
|
|
@@ -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({
|
|
@@ -133,7 +150,7 @@ export async function dev(options) {
|
|
|
133
150
|
if (saidTwice.has(name))
|
|
134
151
|
continue;
|
|
135
152
|
saidTwice.add(name);
|
|
136
|
-
log.info(
|
|
153
|
+
log.info(`${capitalize(name)} is not the same here as in production, so ${guild.name} shows it twice: yours, and the one everyone has. It is shown once again when production runs your version.`);
|
|
137
154
|
}
|
|
138
155
|
try {
|
|
139
156
|
return await registerCommands({
|
|
@@ -154,9 +171,12 @@ 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);
|
|
179
|
+
project.startPresence(() => bot);
|
|
160
180
|
// Said once per file: what only happens in private messages can't be
|
|
161
181
|
// tried here. Discord only offers the commands of a server in that
|
|
162
182
|
// server, and private messages are answered by the bot in production.
|
|
@@ -169,7 +189,7 @@ export async function dev(options) {
|
|
|
169
189
|
log.info(`${file} ${what} only works in private messages, and chapterjs dev only runs your bot in ${guild.name}. Try it with chapterjs start.`);
|
|
170
190
|
};
|
|
171
191
|
for (const entry of privateOnly([...project.commands.values()])) {
|
|
172
|
-
note(siteName(entry),
|
|
192
|
+
note(siteName(entry), describeCommand(entry.command));
|
|
173
193
|
}
|
|
174
194
|
for (const entry of project.events.values()) {
|
|
175
195
|
const { event } = entry;
|
|
@@ -215,6 +235,13 @@ export async function dev(options) {
|
|
|
215
235
|
// offer it, or not.
|
|
216
236
|
if ((await writeTypes(cwd, project.typesInfo())) > 0)
|
|
217
237
|
log.reload('Types updated');
|
|
238
|
+
// A store moved to Redis, or out of it, is said once.
|
|
239
|
+
const kept = describeRedisStores(project, redis);
|
|
240
|
+
if (kept !== redisLine) {
|
|
241
|
+
redisLine = kept;
|
|
242
|
+
if (kept)
|
|
243
|
+
log.info(kept);
|
|
244
|
+
}
|
|
218
245
|
const intents = project.intents(false);
|
|
219
246
|
if ((intents & ~connectedIntents) !== 0 && bot) {
|
|
220
247
|
// Intents are given when connecting: new ones need a new session.
|
|
@@ -225,7 +252,7 @@ export async function dev(options) {
|
|
|
225
252
|
bot = await connect();
|
|
226
253
|
log.success(`Reconnected, ${project.summary()}`);
|
|
227
254
|
if (await syncCommands())
|
|
228
|
-
log.reload(
|
|
255
|
+
log.reload(`Commands updated on ${guild.name}`);
|
|
229
256
|
return;
|
|
230
257
|
}
|
|
231
258
|
const ms = Math.max(1, Math.round(performance.now() - started));
|
|
@@ -236,7 +263,7 @@ export async function dev(options) {
|
|
|
236
263
|
log.warn(`Reloaded with ${failures.length === 1 ? 'an error' : `${failures.length} errors`}: ${failures.length === 1 ? 'that file keeps' : 'those files keep'} running ${failures.length === 1 ? 'its' : 'their'} last working version`);
|
|
237
264
|
}
|
|
238
265
|
if (updated)
|
|
239
|
-
log.reload(
|
|
266
|
+
log.reload(`Commands updated on ${guild.name}`);
|
|
240
267
|
})
|
|
241
268
|
.catch(error => {
|
|
242
269
|
if (!signal.aborted)
|
|
@@ -251,22 +278,26 @@ export async function dev(options) {
|
|
|
251
278
|
const code = await ended;
|
|
252
279
|
await reloading.catch(() => { });
|
|
253
280
|
project.stopTasks();
|
|
281
|
+
project.stopPresence();
|
|
254
282
|
watcher.close();
|
|
255
283
|
assets.close();
|
|
256
284
|
memory.stop();
|
|
257
285
|
await bot?.close();
|
|
258
286
|
await project.closeStores();
|
|
287
|
+
loose();
|
|
259
288
|
if (code === 0)
|
|
260
289
|
log.success('Disconnected');
|
|
261
290
|
return code;
|
|
262
291
|
}
|
|
263
292
|
catch (error) {
|
|
264
293
|
project.stopTasks();
|
|
294
|
+
project.stopPresence();
|
|
265
295
|
watcher?.close();
|
|
266
296
|
assets.close();
|
|
267
297
|
memory.stop();
|
|
268
298
|
await bot?.close();
|
|
269
299
|
await project.closeStores();
|
|
300
|
+
loose();
|
|
270
301
|
if (signal.aborted)
|
|
271
302
|
return 0;
|
|
272
303
|
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,10 @@
|
|
|
1
|
+
import { type WaitWatcher } from '../waiting/waiters.js';
|
|
2
|
+
import { type FoundUses } from '../loader/usage.js';
|
|
1
3
|
import { type CommandEntry } from '../commands/tree.js';
|
|
2
4
|
import { type ComponentEntry } from '../components/router.js';
|
|
3
5
|
import { type LoadedStore } from '../store/declaration.js';
|
|
4
|
-
import { type
|
|
6
|
+
import { type LoadedRanking } from '../ranking/declaration.js';
|
|
7
|
+
import { type StoreBackend, type StoreStorage } from '../store/backend.js';
|
|
5
8
|
import { type LoadedPresence } from '../presence/declaration.js';
|
|
6
9
|
import { type DeclaredCommand, type DeclaredLanguage } from '../messages/declaration.js';
|
|
7
10
|
import { type LoadedMessages } from '../messages/translate.js';
|
|
@@ -26,16 +29,19 @@ export interface ProjectOptions {
|
|
|
26
29
|
*/
|
|
27
30
|
built?: readonly BuiltFile[];
|
|
28
31
|
/**
|
|
29
|
-
*
|
|
32
|
+
* What the files do that only their code shows (`CODE_USES`: joining
|
|
33
|
+
* voice, waiting for messages or reactions), as `chapterjs build` found.
|
|
30
34
|
* Without it, the files of `src` are read on every load.
|
|
31
35
|
*/
|
|
32
|
-
|
|
36
|
+
uses?: FoundUses;
|
|
33
37
|
/**
|
|
34
|
-
* Where the stores of the project keep their data.
|
|
35
|
-
* the `data/` folder of the project by default
|
|
36
|
-
* cluster
|
|
38
|
+
* Where the stores of the project keep their data, by storage. `file`: a
|
|
39
|
+
* file per store in the `data/` folder of the project by default, the
|
|
40
|
+
* first process of a cluster through IPC for the other processes.
|
|
41
|
+
* `redis`: what the CLI made of `REDIS_URL`; without one, a store that
|
|
42
|
+
* asks for Redis is reported and left out.
|
|
37
43
|
*/
|
|
38
|
-
stores?: StoreBackend
|
|
44
|
+
stores?: Partial<Record<StoreStorage, StoreBackend>>;
|
|
39
45
|
/** Only tests change this. */
|
|
40
46
|
deferAfter?: number;
|
|
41
47
|
/**
|
|
@@ -50,6 +56,11 @@ export interface ProjectObserver {
|
|
|
50
56
|
interaction?(raw: RawInteraction, done: Promise<void>): void;
|
|
51
57
|
/** A gateway event was given to the files; `done` once they handled it. */
|
|
52
58
|
event?(event: GatewayDispatchEventName, data: unknown, done: Promise<void>): void;
|
|
59
|
+
/**
|
|
60
|
+
* Told when a run of the files starts and stops waiting for what comes
|
|
61
|
+
* next (`waitForMessage()`...): its `done` waits for the test to act.
|
|
62
|
+
*/
|
|
63
|
+
waits?: WaitWatcher;
|
|
53
64
|
}
|
|
54
65
|
/** What the types of a project are written from, once its files ran. */
|
|
55
66
|
export interface TypesInfo {
|
|
@@ -77,6 +88,12 @@ export interface TypesInfo {
|
|
|
77
88
|
export declare function writeTypes(cwd: string, info?: TypesInfo): Promise<number>;
|
|
78
89
|
/** The kinds of declarations a project is made of: one per feature. */
|
|
79
90
|
export declare const DECLARATIONS: readonly Declaration<unknown>[];
|
|
91
|
+
/** A ranking, with where it is declared. */
|
|
92
|
+
export interface RankingEntry {
|
|
93
|
+
file: string;
|
|
94
|
+
export: string;
|
|
95
|
+
ranking: LoadedRanking;
|
|
96
|
+
}
|
|
80
97
|
/** A store, with where it is declared. */
|
|
81
98
|
export interface StoreEntry {
|
|
82
99
|
file: string;
|
|
@@ -97,6 +114,8 @@ export interface ConnectOptions {
|
|
|
97
114
|
globalLimit?: number | undefined;
|
|
98
115
|
/** Only tests change this. */
|
|
99
116
|
identifyInterval?: number | undefined;
|
|
117
|
+
/** Where nothing can play (`chapterjs test`): what `join()` says at once. */
|
|
118
|
+
voiceUnavailable?: string | undefined;
|
|
100
119
|
/** Discord refused the bot for good. */
|
|
101
120
|
onFatal: (bot: Bot) => void;
|
|
102
121
|
}
|
|
@@ -111,8 +130,14 @@ export interface Project {
|
|
|
111
130
|
readonly components: ReadonlyMap<string, ComponentEntry>;
|
|
112
131
|
readonly tasks: ReadonlyMap<string, TaskEntry>;
|
|
113
132
|
readonly stores: ReadonlyMap<string, StoreEntry>;
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
readonly rankings: ReadonlyMap<string, RankingEntry>;
|
|
134
|
+
/** Where the stores keep their data, by storage: what a cluster serves to its other processes. */
|
|
135
|
+
readonly storeBackends: ReadonlyMap<StoreStorage, StoreBackend>;
|
|
136
|
+
/** What is kept in Redis: the names of the stores and of the rankings, sorted. */
|
|
137
|
+
redisStores(): {
|
|
138
|
+
stores: string[];
|
|
139
|
+
rankings: string[];
|
|
140
|
+
};
|
|
116
141
|
/** The presence declared, or `null` without one. */
|
|
117
142
|
readonly presence: LoadedPresence | null;
|
|
118
143
|
/** The languages of the project, assembled, or `null` without any. */
|
|
@@ -147,6 +172,20 @@ export interface Project {
|
|
|
147
172
|
* change since the bot was given one.
|
|
148
173
|
*/
|
|
149
174
|
applyPresence(bot: Bot): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Shows the activities of the presence in turn on the bot `current`
|
|
177
|
+
* returns, and runs its functions when their time comes.
|
|
178
|
+
*/
|
|
179
|
+
startPresence(current: () => Bot | null): void;
|
|
180
|
+
/** Stops the presence: nothing of it runs after this. */
|
|
181
|
+
stopPresence(): void;
|
|
182
|
+
/** The file of the presence, or `null` without one. */
|
|
183
|
+
readonly presenceFile: string | null;
|
|
184
|
+
/**
|
|
185
|
+
* Runs every function of the presence once (the list, then each text),
|
|
186
|
+
* as the rotation would, with the bot given. What goes wrong is reported.
|
|
187
|
+
*/
|
|
188
|
+
runPresence(bot: Bot): Promise<void>;
|
|
150
189
|
/**
|
|
151
190
|
* Starts the tasks of the project, with the bot `current` returns (none
|
|
152
191
|
* while it reconnects). Only one process of a bot runs them.
|
|
@@ -166,6 +205,19 @@ export interface Project {
|
|
|
166
205
|
/** Writes what the stores still have to write. Nothing is kept after this. */
|
|
167
206
|
closeStores(): Promise<void>;
|
|
168
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* What the files may do that only their code shows, each with the intents
|
|
210
|
+
* it needs: read in `src/` (dev, at every load) or kept by the build.
|
|
211
|
+
*/
|
|
212
|
+
export declare const CODE_USES: readonly [import("../loader/usage.js").CodeUse, import("../loader/usage.js").CodeUse, import("../loader/usage.js").CodeUse];
|
|
213
|
+
/**
|
|
214
|
+
* A promise of the files that nobody waited for (`store.set()` without
|
|
215
|
+
* `await`, in a `run` that is not `async`) rejects with nobody to tell:
|
|
216
|
+
* Node would stop the bot. Reported instead, with the file and the line,
|
|
217
|
+
* and what to write. A rejection from outside the files stays what it is:
|
|
218
|
+
* a bug of the framework, loud. Returns what stops watching.
|
|
219
|
+
*/
|
|
220
|
+
export declare function reportLooseRejections(cwd: string, log: Log): () => void;
|
|
169
221
|
/** Whether the folder is a project: it has a `src` folder. */
|
|
170
222
|
export declare function hasSources(cwd: string, log: Log): Promise<boolean>;
|
|
171
223
|
export declare function createProject(options: ProjectOptions): Project;
|