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.
Files changed (115) hide show
  1. package/README.md +5 -3
  2. package/dist/assets/public.js +4 -4
  3. package/dist/cli/build.d.ts +6 -2
  4. package/dist/cli/build.js +17 -4
  5. package/dist/cli/dev.js +37 -6
  6. package/dist/cli/env.d.ts +5 -0
  7. package/dist/cli/env.js +39 -3
  8. package/dist/cli/project.d.ts +61 -9
  9. package/dist/cli/project.js +250 -44
  10. package/dist/cli/redis.d.ts +23 -0
  11. package/dist/cli/redis.js +69 -0
  12. package/dist/cli/start.js +54 -18
  13. package/dist/cli/test.js +112 -18
  14. package/dist/commands/command.d.ts +140 -5
  15. package/dist/commands/command.js +17 -2
  16. package/dist/commands/declaration.d.ts +17 -4
  17. package/dist/commands/declaration.js +128 -2
  18. package/dist/commands/router.js +78 -18
  19. package/dist/commands/tree.d.ts +25 -4
  20. package/dist/commands/tree.js +95 -26
  21. package/dist/commands/twins.d.ts +8 -0
  22. package/dist/commands/twins.js +24 -6
  23. package/dist/components/button.d.ts +8 -3
  24. package/dist/components/declaration.d.ts +2 -2
  25. package/dist/components/declaration.js +16 -12
  26. package/dist/components/render.js +5 -0
  27. package/dist/components/router.js +41 -0
  28. package/dist/components/select.d.ts +12 -5
  29. package/dist/core/bot.d.ts +7 -0
  30. package/dist/core/bot.js +44 -0
  31. package/dist/discord/api.d.ts +2 -0
  32. package/dist/discord/api.js +2 -0
  33. package/dist/discord/permissions.d.ts +4 -4
  34. package/dist/discord/permissions.js +13 -0
  35. package/dist/events/registry.d.ts +265 -243
  36. package/dist/events/registry.js +131 -100
  37. package/dist/index.d.ts +7 -4
  38. package/dist/index.js +2 -1
  39. package/dist/interactions/dispatch.js +4 -2
  40. package/dist/loader/loader.d.ts +16 -0
  41. package/dist/loader/loader.js +52 -7
  42. package/dist/loader/usage.d.ts +15 -0
  43. package/dist/loader/usage.js +40 -0
  44. package/dist/loader/watch.d.ts +5 -3
  45. package/dist/loader/watch.js +88 -17
  46. package/dist/messages/commands.js +3 -0
  47. package/dist/messages/messages.d.ts +1 -0
  48. package/dist/messages/translate.js +5 -1
  49. package/dist/presence/declaration.d.ts +49 -2
  50. package/dist/presence/declaration.js +122 -39
  51. package/dist/presence/presence.d.ts +65 -6
  52. package/dist/presence/presence.js +11 -0
  53. package/dist/presence/rotation.d.ts +32 -0
  54. package/dist/presence/rotation.js +167 -0
  55. package/dist/ranking/declaration.d.ts +12 -0
  56. package/dist/ranking/declaration.js +48 -0
  57. package/dist/ranking/ranking.d.ts +102 -0
  58. package/dist/ranking/ranking.js +129 -0
  59. package/dist/redis/client.d.ts +36 -0
  60. package/dist/redis/client.js +252 -0
  61. package/dist/redis/protocol.d.ts +22 -0
  62. package/dist/redis/protocol.js +99 -0
  63. package/dist/redis/url.d.ts +24 -0
  64. package/dist/redis/url.js +50 -0
  65. package/dist/rest/errors.d.ts +0 -1
  66. package/dist/rest/errors.js +15 -1
  67. package/dist/rest/rest.js +10 -1
  68. package/dist/store/backend.d.ts +49 -4
  69. package/dist/store/backend.js +66 -11
  70. package/dist/store/declaration.d.ts +3 -0
  71. package/dist/store/declaration.js +8 -3
  72. package/dist/store/file-backend.d.ts +3 -2
  73. package/dist/store/file-backend.js +47 -1
  74. package/dist/store/ipc-backend.d.ts +24 -1
  75. package/dist/store/ipc-backend.js +49 -0
  76. package/dist/store/memory-backend.d.ts +4 -2
  77. package/dist/store/memory-backend.js +27 -1
  78. package/dist/store/ranking-backend.d.ts +45 -0
  79. package/dist/store/ranking-backend.js +82 -0
  80. package/dist/store/redis-backend.d.ts +38 -0
  81. package/dist/store/redis-backend.js +245 -0
  82. package/dist/store/redis-ranking.d.ts +15 -0
  83. package/dist/store/redis-ranking.js +69 -0
  84. package/dist/store/scope.d.ts +17 -1
  85. package/dist/store/scope.js +32 -3
  86. package/dist/store/store.d.ts +39 -5
  87. package/dist/store/store.js +79 -32
  88. package/dist/structures/channel.d.ts +13 -0
  89. package/dist/structures/channel.js +24 -0
  90. package/dist/structures/context.d.ts +3 -0
  91. package/dist/structures/entities.js +1 -0
  92. package/dist/structures/interaction.d.ts +6 -0
  93. package/dist/structures/interaction.js +20 -0
  94. package/dist/structures/invite.d.ts +6 -0
  95. package/dist/structures/invite.js +11 -0
  96. package/dist/structures/message.d.ts +17 -0
  97. package/dist/structures/message.js +33 -0
  98. package/dist/structures/wait.d.ts +105 -0
  99. package/dist/structures/wait.js +85 -0
  100. package/dist/testing/harness.d.ts +48 -8
  101. package/dist/testing/harness.js +156 -36
  102. package/dist/testing/index.d.ts +1 -1
  103. package/dist/testing/smoke.js +112 -1
  104. package/dist/testing/world.js +51 -2
  105. package/dist/util/call-site.d.ts +4 -0
  106. package/dist/util/call-site.js +22 -0
  107. package/dist/voice/connection.d.ts +2 -0
  108. package/dist/voice/connection.js +15 -1
  109. package/dist/voice/usage.d.ts +3 -2
  110. package/dist/voice/usage.js +7 -19
  111. package/dist/waiting/usage.d.ts +5 -0
  112. package/dist/waiting/usage.js +19 -0
  113. package/dist/waiting/waiters.d.ts +48 -0
  114. package/dist/waiting/waiters.js +107 -0
  115. 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
- - **Storage**: `store<Warning[]>({ expires: '30d' })` keeps data from one restart to the next, per server, typed, in a JSON file written for you.
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.
@@ -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 by the time it is watched
73
- // (files copied with it, or written right after) is all new.
74
- watchInside(Promise.resolve(''));
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();
@@ -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
- /** Whether the files join voice channels (an intent to ask for). */
16
- voice: boolean;
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 { sourcesJoinVoice } from '../voice/usage.js';
18
- import { createProject, hasSources, writeTypes } from './project.js';
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
- const sources = createProject({ cwd, version: options.version, log });
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
- voice: await sourcesJoinVoice(cwd),
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, commandName, privateOnly } from '../commands/tree.js';
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(`/${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.`);
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), commandName(entry.command.path));
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('Commands updated on Discord');
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('Commands updated on Discord');
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
- return problems.length > 0 ? { problems } : { env: { token, devGuildId } };
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
- return problems.length > 0
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
- : { env: { token, devGuildId: devGuildId === '' ? null : devGuildId } };
122
+ : {
123
+ env: {
124
+ token,
125
+ devGuildId: devGuildId === '' ? null : devGuildId,
126
+ redis: redis.redis,
127
+ },
128
+ };
93
129
  }
@@ -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 StoreBackend } from '../store/backend.js';
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
- * Whether the files join voice channels, as `chapterjs build` found.
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
- joinsVoice?: boolean;
36
+ uses?: FoundUses;
33
37
  /**
34
- * Where the stores of the project keep their data. A file per store in
35
- * the `data/` folder of the project by default; the first process of a
36
- * cluster, through IPC, for the other processes.
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
- /** Where the stores keep their data: what a cluster serves to its other processes. */
115
- readonly storeBackend: StoreBackend;
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;