chapterjs 0.4.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 +3 -1
- package/dist/assets/public.js +4 -4
- package/dist/cli/build.d.ts +6 -2
- package/dist/cli/build.js +3 -3
- package/dist/cli/dev.js +8 -5
- package/dist/cli/project.d.ts +31 -2
- package/dist/cli/project.js +166 -34
- package/dist/cli/redis.js +14 -2
- package/dist/cli/start.js +11 -1
- package/dist/cli/test.js +108 -16
- 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 +19 -10
- package/dist/events/registry.js +10 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -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/rest/errors.d.ts +0 -1
- package/dist/rest/errors.js +15 -1
- package/dist/rest/rest.js +10 -1
- package/dist/store/file-backend.d.ts +0 -1
- package/dist/store/file-backend.js +18 -1
- package/dist/store/scope.d.ts +6 -1
- package/dist/store/scope.js +9 -3
- 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/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
|
@@ -34,10 +34,12 @@ The framework loads every file of `src/`, wherever it is and whatever it is call
|
|
|
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.
|
|
42
|
+
- **Presence**: one activity, or several in turn, with texts computed when shown.
|
|
41
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).
|
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,9 +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 {
|
|
17
|
+
import { scanUses } from '../loader/usage.js';
|
|
18
18
|
import { MemoryStoreBackend } from '../store/memory-backend.js';
|
|
19
|
-
import { createProject, hasSources, writeTypes } from './project.js';
|
|
19
|
+
import { CODE_USES, createProject, hasSources, writeTypes } from './project.js';
|
|
20
20
|
/** Where the build of a project is, from its folder. */
|
|
21
21
|
export const buildDir = (cwd) => join(cwd, '.chapterjs', 'build');
|
|
22
22
|
/** The file the whole bot is compiled into, in the build. */
|
|
@@ -206,7 +206,7 @@ export async function build(options) {
|
|
|
206
206
|
sources: await fingerprint(src),
|
|
207
207
|
commands: built.commands.size,
|
|
208
208
|
events: built.events.size,
|
|
209
|
-
|
|
209
|
+
uses: await scanUses(cwd, CODE_USES),
|
|
210
210
|
};
|
|
211
211
|
await writeFile(join(out, 'build.json'), JSON.stringify(info, null, 2));
|
|
212
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';
|
|
@@ -150,7 +150,7 @@ export async function dev(options) {
|
|
|
150
150
|
if (saidTwice.has(name))
|
|
151
151
|
continue;
|
|
152
152
|
saidTwice.add(name);
|
|
153
|
-
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.`);
|
|
154
154
|
}
|
|
155
155
|
try {
|
|
156
156
|
return await registerCommands({
|
|
@@ -176,6 +176,7 @@ export async function dev(options) {
|
|
|
176
176
|
project.ready(bot);
|
|
177
177
|
// Tasks follow the bot through reconnections: they read the current one.
|
|
178
178
|
project.startTasks(() => bot);
|
|
179
|
+
project.startPresence(() => bot);
|
|
179
180
|
// Said once per file: what only happens in private messages can't be
|
|
180
181
|
// tried here. Discord only offers the commands of a server in that
|
|
181
182
|
// server, and private messages are answered by the bot in production.
|
|
@@ -188,7 +189,7 @@ export async function dev(options) {
|
|
|
188
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.`);
|
|
189
190
|
};
|
|
190
191
|
for (const entry of privateOnly([...project.commands.values()])) {
|
|
191
|
-
note(siteName(entry),
|
|
192
|
+
note(siteName(entry), describeCommand(entry.command));
|
|
192
193
|
}
|
|
193
194
|
for (const entry of project.events.values()) {
|
|
194
195
|
const { event } = entry;
|
|
@@ -251,7 +252,7 @@ export async function dev(options) {
|
|
|
251
252
|
bot = await connect();
|
|
252
253
|
log.success(`Reconnected, ${project.summary()}`);
|
|
253
254
|
if (await syncCommands())
|
|
254
|
-
log.reload(
|
|
255
|
+
log.reload(`Commands updated on ${guild.name}`);
|
|
255
256
|
return;
|
|
256
257
|
}
|
|
257
258
|
const ms = Math.max(1, Math.round(performance.now() - started));
|
|
@@ -262,7 +263,7 @@ export async function dev(options) {
|
|
|
262
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`);
|
|
263
264
|
}
|
|
264
265
|
if (updated)
|
|
265
|
-
log.reload(
|
|
266
|
+
log.reload(`Commands updated on ${guild.name}`);
|
|
266
267
|
})
|
|
267
268
|
.catch(error => {
|
|
268
269
|
if (!signal.aborted)
|
|
@@ -277,6 +278,7 @@ export async function dev(options) {
|
|
|
277
278
|
const code = await ended;
|
|
278
279
|
await reloading.catch(() => { });
|
|
279
280
|
project.stopTasks();
|
|
281
|
+
project.stopPresence();
|
|
280
282
|
watcher.close();
|
|
281
283
|
assets.close();
|
|
282
284
|
memory.stop();
|
|
@@ -289,6 +291,7 @@ export async function dev(options) {
|
|
|
289
291
|
}
|
|
290
292
|
catch (error) {
|
|
291
293
|
project.stopTasks();
|
|
294
|
+
project.stopPresence();
|
|
292
295
|
watcher?.close();
|
|
293
296
|
assets.close();
|
|
294
297
|
memory.stop();
|
package/dist/cli/project.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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';
|
|
@@ -27,10 +29,11 @@ export interface ProjectOptions {
|
|
|
27
29
|
*/
|
|
28
30
|
built?: readonly BuiltFile[];
|
|
29
31
|
/**
|
|
30
|
-
*
|
|
32
|
+
* What the files do that only their code shows (`CODE_USES`: joining
|
|
33
|
+
* voice, waiting for messages or reactions), as `chapterjs build` found.
|
|
31
34
|
* Without it, the files of `src` are read on every load.
|
|
32
35
|
*/
|
|
33
|
-
|
|
36
|
+
uses?: FoundUses;
|
|
34
37
|
/**
|
|
35
38
|
* Where the stores of the project keep their data, by storage. `file`: a
|
|
36
39
|
* file per store in the `data/` folder of the project by default, the
|
|
@@ -53,6 +56,11 @@ export interface ProjectObserver {
|
|
|
53
56
|
interaction?(raw: RawInteraction, done: Promise<void>): void;
|
|
54
57
|
/** A gateway event was given to the files; `done` once they handled it. */
|
|
55
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;
|
|
56
64
|
}
|
|
57
65
|
/** What the types of a project are written from, once its files ran. */
|
|
58
66
|
export interface TypesInfo {
|
|
@@ -106,6 +114,8 @@ export interface ConnectOptions {
|
|
|
106
114
|
globalLimit?: number | undefined;
|
|
107
115
|
/** Only tests change this. */
|
|
108
116
|
identifyInterval?: number | undefined;
|
|
117
|
+
/** Where nothing can play (`chapterjs test`): what `join()` says at once. */
|
|
118
|
+
voiceUnavailable?: string | undefined;
|
|
109
119
|
/** Discord refused the bot for good. */
|
|
110
120
|
onFatal: (bot: Bot) => void;
|
|
111
121
|
}
|
|
@@ -162,6 +172,20 @@ export interface Project {
|
|
|
162
172
|
* change since the bot was given one.
|
|
163
173
|
*/
|
|
164
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>;
|
|
165
189
|
/**
|
|
166
190
|
* Starts the tasks of the project, with the bot `current` returns (none
|
|
167
191
|
* while it reconnects). Only one process of a bot runs them.
|
|
@@ -181,6 +205,11 @@ export interface Project {
|
|
|
181
205
|
/** Writes what the stores still have to write. Nothing is kept after this. */
|
|
182
206
|
closeStores(): Promise<void>;
|
|
183
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];
|
|
184
213
|
/**
|
|
185
214
|
* A promise of the files that nobody waited for (`store.set()` without
|
|
186
215
|
* `await`, in a `run` that is not `async`) rejects with nobody to tell:
|
package/dist/cli/project.js
CHANGED
|
@@ -7,7 +7,10 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { setPublicDir } from '../assets/asset.js';
|
|
8
8
|
import { listPublic, publicDeclarations } from '../assets/public.js';
|
|
9
9
|
import { MissingForEvent } from '../events/registry.js';
|
|
10
|
-
import {
|
|
10
|
+
import { VOICE_USE } from '../voice/usage.js';
|
|
11
|
+
import { MESSAGES_USE, REACTIONS_USE } from '../waiting/usage.js';
|
|
12
|
+
import { MemoryWaiters } from '../waiting/waiters.js';
|
|
13
|
+
import { scanUses, usesIntents } from '../loader/usage.js';
|
|
11
14
|
import { writeGenerated } from '../loader/generated.js';
|
|
12
15
|
import { commandDeclaration, scanCommands } from '../commands/declaration.js';
|
|
13
16
|
import { CommandRouter } from '../commands/router.js';
|
|
@@ -19,7 +22,9 @@ import { storeDeclaration } from '../store/declaration.js';
|
|
|
19
22
|
import { rankingDeclaration, } from '../ranking/declaration.js';
|
|
20
23
|
import { nowhereToKeep, NowhereToKeepError, setStoreBackend, STORAGES, } from '../store/backend.js';
|
|
21
24
|
import { DATA_FOLDER, FileStoreBackend } from '../store/file-backend.js';
|
|
22
|
-
import { DEFAULT_PRESENCE, presenceDeclaration, } from '../presence/declaration.js';
|
|
25
|
+
import { checkText, DEFAULT_PRESENCE, presenceDeclaration, readActivities, } from '../presence/declaration.js';
|
|
26
|
+
import { PresenceRotation } from '../presence/rotation.js';
|
|
27
|
+
import { GetCurrentApplication } from '../discord/endpoints.js';
|
|
23
28
|
import { assembleMessages, commandsDeclarations, languageDeclaration, languageDefaultsDeclaration, messagesDeclarations, scanLanguages, } from '../messages/declaration.js';
|
|
24
29
|
import { translation } from '../messages/translate.js';
|
|
25
30
|
import { translateCommand, unknownCommands } from '../messages/commands.js';
|
|
@@ -30,7 +35,7 @@ import { GatewayIntent } from '../discord/intents.js';
|
|
|
30
35
|
import { eventDeclaration } from '../events/declaration.js';
|
|
31
36
|
import { EventRouter, intentsFor, intentsOf, limitsFor, } from '../events/router.js';
|
|
32
37
|
import { GatewayFatalError, SessionLimitError } from '../gateway/errors.js';
|
|
33
|
-
import { loadSources, readDeclarations, siteName, } from '../loader/loader.js';
|
|
38
|
+
import { loadSources, bySite, readDeclarations, siteKey, siteName, } from '../loader/loader.js';
|
|
34
39
|
import { locate, messageOf } from '../loader/locate.js';
|
|
35
40
|
import { DiscordApiError, DiscordUnavailableError, InvalidTokenError, } from '../rest/errors.js';
|
|
36
41
|
import { PreflightFailure } from './preflight.js';
|
|
@@ -73,6 +78,11 @@ export const DECLARATIONS = [
|
|
|
73
78
|
presenceDeclaration,
|
|
74
79
|
languageDeclaration,
|
|
75
80
|
];
|
|
81
|
+
/**
|
|
82
|
+
* What the files may do that only their code shows, each with the intents
|
|
83
|
+
* it needs: read in `src/` (dev, at every load) or kept by the build.
|
|
84
|
+
*/
|
|
85
|
+
export const CODE_USES = [VOICE_USE, MESSAGES_USE, REACTIONS_USE];
|
|
76
86
|
/**
|
|
77
87
|
* A promise of the files that nobody waited for (`store.set()` without
|
|
78
88
|
* `await`, in a `run` that is not `async`) rejects with nobody to tell:
|
|
@@ -107,23 +117,15 @@ const PRIVATE_INTENTS = GatewayIntent.DirectMessages |
|
|
|
107
117
|
GatewayIntent.DirectMessagePolls;
|
|
108
118
|
const megabytes = (bytes) => `${Math.round(bytes / 1024 / 1024)} MB`;
|
|
109
119
|
/** What tells one declaration from another: its file and its export. */
|
|
110
|
-
const keyOf =
|
|
120
|
+
const keyOf = siteKey;
|
|
111
121
|
/**
|
|
112
122
|
* Two declarations with the same name, where the name is the export's
|
|
113
123
|
* (a task): the later one is left out, with a message saying so.
|
|
114
124
|
*/
|
|
115
|
-
function sameNames(entries, nameOf, what) {
|
|
125
|
+
function sameNames(entries, nameOf, what, running) {
|
|
116
126
|
const seen = new Map();
|
|
117
127
|
const conflicts = [];
|
|
118
|
-
const valid =
|
|
119
|
-
.sort((a, b) => a.file === b.file
|
|
120
|
-
? a.export < b.export
|
|
121
|
-
? -1
|
|
122
|
-
: 1
|
|
123
|
-
: a.file < b.file
|
|
124
|
-
? -1
|
|
125
|
-
: 1)
|
|
126
|
-
.filter(entry => {
|
|
128
|
+
const valid = bySite(entries, running).filter(entry => {
|
|
127
129
|
const first = seen.get(nameOf(entry));
|
|
128
130
|
if (!first) {
|
|
129
131
|
seen.set(nameOf(entry), entry);
|
|
@@ -183,19 +185,78 @@ export function createProject(options) {
|
|
|
183
185
|
let stores = new Map();
|
|
184
186
|
let rankings = new Map();
|
|
185
187
|
let presence = null;
|
|
188
|
+
/** Where the presence is declared, for what its functions do wrong. */
|
|
189
|
+
let presenceSite = { file: 'the presence', export: 'default' };
|
|
186
190
|
/** The files the presence was declared in, to keep it when they break. */
|
|
187
191
|
let presenceFiles = new Set();
|
|
188
|
-
// Joining voice
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
// Joining voice, waiting for messages or reactions: intents the code
|
|
193
|
+
// needs without any declaration saying it.
|
|
194
|
+
let uses = options.uses ?? {};
|
|
195
|
+
const neededIntents = () => intentsFor(events.values()) | usesIntents(CODE_USES, uses);
|
|
191
196
|
let messages = null;
|
|
192
197
|
/** The last version of each language that loaded. */
|
|
193
198
|
let languages = new Map();
|
|
194
199
|
let languageDefaults = [];
|
|
200
|
+
/** Whether the files were loaded before: what a reload compares with. */
|
|
201
|
+
let loadedOnce = false;
|
|
195
202
|
/** The bot running the files now, to give it what a reload changes. */
|
|
196
203
|
let running = null;
|
|
197
204
|
/** The presence each bot was given last, to send only what changed. */
|
|
198
205
|
const given = new WeakMap();
|
|
206
|
+
/** What each bot shows now, to send only what changed. */
|
|
207
|
+
const shown = new WeakMap();
|
|
208
|
+
/** Whether the running bot is only part of the bot (a cluster). */
|
|
209
|
+
let partial = false;
|
|
210
|
+
/** How many servers the whole bot is in, as Discord counted it last. */
|
|
211
|
+
let counted = null;
|
|
212
|
+
let counting = false;
|
|
213
|
+
/**
|
|
214
|
+
* How many servers the bot is in: what this process knows when it runs
|
|
215
|
+
* every shard; else what Discord counts for the application, asked at
|
|
216
|
+
* most every 10 minutes.
|
|
217
|
+
* @see https://docs.discord.com/developers/resources/application#application-object-application-structure
|
|
218
|
+
*/
|
|
219
|
+
const guildCount = (bot) => {
|
|
220
|
+
const local = bot.ctx.cache.guilds.size;
|
|
221
|
+
if (!partial)
|
|
222
|
+
return local;
|
|
223
|
+
if (!counting && (!counted || Date.now() - counted.at > 600_000)) {
|
|
224
|
+
counting = true;
|
|
225
|
+
bot.ctx.rest
|
|
226
|
+
.request(GetCurrentApplication, [])
|
|
227
|
+
.then(application => {
|
|
228
|
+
const guilds = application.approximate_guild_count;
|
|
229
|
+
if (guilds !== undefined)
|
|
230
|
+
counted = { guilds, at: Date.now() };
|
|
231
|
+
})
|
|
232
|
+
.catch(() => { })
|
|
233
|
+
.finally(() => (counting = false));
|
|
234
|
+
}
|
|
235
|
+
return counted?.guilds ?? local;
|
|
236
|
+
};
|
|
237
|
+
let presenceBot = () => null;
|
|
238
|
+
const rotation = new PresenceRotation({
|
|
239
|
+
apply(raw) {
|
|
240
|
+
const bot = presenceBot();
|
|
241
|
+
if (!bot)
|
|
242
|
+
return;
|
|
243
|
+
const text = JSON.stringify(raw);
|
|
244
|
+
if (shown.get(bot) === text)
|
|
245
|
+
return;
|
|
246
|
+
shown.set(bot, text);
|
|
247
|
+
bot.setPresence(raw);
|
|
248
|
+
},
|
|
249
|
+
context() {
|
|
250
|
+
const bot = presenceBot();
|
|
251
|
+
if (!bot?.ctx.self)
|
|
252
|
+
return null;
|
|
253
|
+
return {
|
|
254
|
+
guilds: guildCount(bot),
|
|
255
|
+
...translation(bot.ctx, null),
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
onError: error => report({ ...presenceSite, error }),
|
|
259
|
+
});
|
|
199
260
|
let lost = false;
|
|
200
261
|
const count = (size, one, many) => `${size} ${size === 1 ? one : many}`;
|
|
201
262
|
return {
|
|
@@ -243,7 +304,10 @@ export function createProject(options) {
|
|
|
243
304
|
})),
|
|
244
305
|
...(messages ? { defaultLocale: messages.default } : {}),
|
|
245
306
|
languageDefaults,
|
|
246
|
-
|
|
307
|
+
// Only slash commands have texts in the language files.
|
|
308
|
+
commands: [...commands.values()]
|
|
309
|
+
.filter(({ command }) => command.type === 'slash')
|
|
310
|
+
.map(({ command }) => ({
|
|
247
311
|
name: command.path.join(' '),
|
|
248
312
|
described: command.described,
|
|
249
313
|
})),
|
|
@@ -251,8 +315,8 @@ export function createProject(options) {
|
|
|
251
315
|
report,
|
|
252
316
|
async load() {
|
|
253
317
|
const { built } = options;
|
|
254
|
-
if (options.
|
|
255
|
-
|
|
318
|
+
if (options.uses === undefined) {
|
|
319
|
+
uses = await scanUses(cwd, CODE_USES);
|
|
256
320
|
}
|
|
257
321
|
// Every file of src/ runs (or ran, in a build); what each exports is
|
|
258
322
|
// then read, kind by kind.
|
|
@@ -292,13 +356,14 @@ export function createProject(options) {
|
|
|
292
356
|
event: item.value,
|
|
293
357
|
}));
|
|
294
358
|
router.set(events.values());
|
|
359
|
+
// A name two declarations share stays with the one that ran before.
|
|
295
360
|
const { valid, conflicts } = findConflicts([
|
|
296
361
|
...next(commands, commandItems, item => ({
|
|
297
362
|
file: item.file,
|
|
298
363
|
export: item.export,
|
|
299
364
|
command: item.value,
|
|
300
365
|
})).values(),
|
|
301
|
-
]);
|
|
366
|
+
], new Set(commands.keys()));
|
|
302
367
|
commands = new Map(valid.map(entry => [keyOf(entry), entry]));
|
|
303
368
|
const unique = findDuplicates([
|
|
304
369
|
...next(components, componentItems, item => ({
|
|
@@ -306,7 +371,7 @@ export function createProject(options) {
|
|
|
306
371
|
export: item.export,
|
|
307
372
|
component: item.value,
|
|
308
373
|
})).values(),
|
|
309
|
-
]);
|
|
374
|
+
], new Set(components.keys()));
|
|
310
375
|
components = new Map(unique.valid.map(entry => [keyOf(entry), entry]));
|
|
311
376
|
componentRouter.set(components.values());
|
|
312
377
|
const sameTask = sameNames([
|
|
@@ -315,7 +380,7 @@ export function createProject(options) {
|
|
|
315
380
|
export: item.export,
|
|
316
381
|
task: item.value,
|
|
317
382
|
})).values(),
|
|
318
|
-
], entry => entry.task.name, 'task');
|
|
383
|
+
], entry => entry.task.name, 'task', new Set(tasks.keys()));
|
|
319
384
|
tasks = new Map(sameTask.valid.map(entry => [keyOf(entry), entry]));
|
|
320
385
|
scheduler.set(tasks.values());
|
|
321
386
|
const keptOf = (entry) => 'store' in entry ? entry.store : entry.ranking;
|
|
@@ -330,7 +395,7 @@ export function createProject(options) {
|
|
|
330
395
|
export: item.export,
|
|
331
396
|
ranking: item.value,
|
|
332
397
|
})).values(),
|
|
333
|
-
], entry => keptOf(entry).name, entry => ('store' in entry ? 'store' : 'ranking'));
|
|
398
|
+
], entry => keptOf(entry).name, entry => ('store' in entry ? 'store' : 'ranking'), new Set([...stores.keys(), ...rankings.keys()]));
|
|
334
399
|
// What is kept where nothing keeps it (Redis without REDIS_URL) is
|
|
335
400
|
// reported and left out: it says so itself when used.
|
|
336
401
|
const unkept = sameStore.valid.filter(entry => !storeBackends.has(keptOf(entry).storage));
|
|
@@ -348,8 +413,10 @@ export function createProject(options) {
|
|
|
348
413
|
}));
|
|
349
414
|
// One presence. A second one is left out.
|
|
350
415
|
const [first, ...extra] = presenceItems.loaded;
|
|
351
|
-
if (first)
|
|
416
|
+
if (first) {
|
|
352
417
|
presence = first.value;
|
|
418
|
+
presenceSite = { file: first.file, export: first.export };
|
|
419
|
+
}
|
|
353
420
|
else if (presenceItems.failed.length === 0 &&
|
|
354
421
|
![...broken].some(file => presenceFiles.has(file))) {
|
|
355
422
|
presence = null;
|
|
@@ -376,14 +443,20 @@ export function createProject(options) {
|
|
|
376
443
|
languages = nextLanguages;
|
|
377
444
|
let assembled = assembleMessages([...languages.values()]);
|
|
378
445
|
// A language that no longer matches the others keeps its last version
|
|
379
|
-
// that did, like a declaration that no longer loads
|
|
446
|
+
// that did, like a declaration that no longer loads; one that was not
|
|
447
|
+
// there before is left out. What already ran is never what breaks
|
|
448
|
+
// the languages: a second default: true saved in a new file must not
|
|
449
|
+
// take the texts of every command away.
|
|
380
450
|
const restored = assembled.failed.filter(failure => {
|
|
381
451
|
const key = keyOf({
|
|
382
452
|
file: failure.file,
|
|
383
453
|
export: failure.export ?? 'default',
|
|
384
454
|
});
|
|
455
|
+
const current = languages.get(key);
|
|
456
|
+
if (current === undefined)
|
|
457
|
+
return false;
|
|
385
458
|
const previous = previousLanguages.get(key);
|
|
386
|
-
return previous
|
|
459
|
+
return previous === undefined ? loadedOnce : previous !== current;
|
|
387
460
|
});
|
|
388
461
|
if (restored.length > 0) {
|
|
389
462
|
for (const failure of restored) {
|
|
@@ -391,7 +464,11 @@ export function createProject(options) {
|
|
|
391
464
|
file: failure.file,
|
|
392
465
|
export: failure.export ?? 'default',
|
|
393
466
|
});
|
|
394
|
-
|
|
467
|
+
const previous = previousLanguages.get(key);
|
|
468
|
+
if (previous)
|
|
469
|
+
languages.set(key, previous);
|
|
470
|
+
else
|
|
471
|
+
languages.delete(key);
|
|
395
472
|
}
|
|
396
473
|
const again = assembleMessages([...languages.values()]);
|
|
397
474
|
assembled = {
|
|
@@ -403,6 +480,7 @@ export function createProject(options) {
|
|
|
403
480
|
};
|
|
404
481
|
}
|
|
405
482
|
messages = assembled.messages;
|
|
483
|
+
loadedOnce = true;
|
|
406
484
|
if (running)
|
|
407
485
|
running.ctx.messages = messages;
|
|
408
486
|
// A command has its texts in its file, or in the language files: the
|
|
@@ -410,7 +488,9 @@ export function createProject(options) {
|
|
|
410
488
|
// be described is left out.
|
|
411
489
|
const badTexts = [...assembled.failed];
|
|
412
490
|
if (messages) {
|
|
413
|
-
const names = new Set([...commands.values()]
|
|
491
|
+
const names = new Set([...commands.values()]
|
|
492
|
+
.filter(entry => entry.command.type === 'slash')
|
|
493
|
+
.map(entry => entry.command.path.join(' ')));
|
|
414
494
|
for (const { file, path } of unknownCommands(messages, names)) {
|
|
415
495
|
badTexts.push({
|
|
416
496
|
file,
|
|
@@ -492,9 +572,12 @@ export function createProject(options) {
|
|
|
492
572
|
messages === null,
|
|
493
573
|
// A file declaring several events that need the intent is named once.
|
|
494
574
|
filesNeeding: intent => [
|
|
495
|
-
...new Set([
|
|
496
|
-
.
|
|
497
|
-
|
|
575
|
+
...new Set([
|
|
576
|
+
...[...events.values()]
|
|
577
|
+
.filter(({ event }) => (intentsOf(event) & GatewayIntent[intent]) !== 0)
|
|
578
|
+
.map(({ file }) => file),
|
|
579
|
+
...CODE_USES.filter(use => (use.intents & GatewayIntent[intent]) !== 0).flatMap(use => uses[use.name] ?? []),
|
|
580
|
+
]),
|
|
498
581
|
],
|
|
499
582
|
intents: (privateEvents = true) => neededIntents() & (privateEvents ? ~0 : ~PRIVATE_INTENTS),
|
|
500
583
|
async connect(connection) {
|
|
@@ -506,6 +589,7 @@ export function createProject(options) {
|
|
|
506
589
|
guildFilter: connection.guildFilter,
|
|
507
590
|
privateEvents: connection.privateEvents,
|
|
508
591
|
presence: presence?.raw,
|
|
592
|
+
waiters: new MemoryWaiters(options.observe?.waits ? { watch: options.observe.waits } : {}),
|
|
509
593
|
shards: connection.shards,
|
|
510
594
|
identifyGate: connection.identifyGate,
|
|
511
595
|
...(connection.identifyInterval === undefined
|
|
@@ -514,7 +598,12 @@ export function createProject(options) {
|
|
|
514
598
|
// Only what the files of the project use is remembered.
|
|
515
599
|
cache: { limits: limitsFor(events.values()) },
|
|
516
600
|
// A local fake of Discord (tests) serves voice without TLS too.
|
|
517
|
-
voice: {
|
|
601
|
+
voice: {
|
|
602
|
+
secure: !connection.apiUrl?.startsWith('http://'),
|
|
603
|
+
...(connection.voiceUnavailable === undefined
|
|
604
|
+
? {}
|
|
605
|
+
: { unavailable: connection.voiceUnavailable }),
|
|
606
|
+
},
|
|
518
607
|
rest: {
|
|
519
608
|
...(connection.apiUrl ? { baseUrl: connection.apiUrl } : {}),
|
|
520
609
|
...(connection.globalLimit === undefined
|
|
@@ -558,19 +647,62 @@ export function createProject(options) {
|
|
|
558
647
|
},
|
|
559
648
|
});
|
|
560
649
|
given.set(created, presence?.key ?? '');
|
|
650
|
+
shown.set(created, JSON.stringify(presence?.raw ?? DEFAULT_PRESENCE));
|
|
651
|
+
partial =
|
|
652
|
+
connection.shards !== undefined &&
|
|
653
|
+
connection.shards.ids.length < connection.shards.count;
|
|
561
654
|
created.ctx.messages = messages;
|
|
562
655
|
running = created;
|
|
563
656
|
await created.connect();
|
|
564
657
|
return created;
|
|
565
658
|
},
|
|
566
659
|
applyPresence(bot) {
|
|
660
|
+
// Always the functions of the files as they are now: a file they
|
|
661
|
+
// import may have changed. What is shown is only sent when it differs.
|
|
662
|
+
rotation.set(presence);
|
|
567
663
|
const key = presence?.key ?? '';
|
|
568
664
|
if (given.get(bot) === key)
|
|
569
665
|
return false;
|
|
570
666
|
given.set(bot, key);
|
|
571
|
-
bot.setPresence(presence?.raw ?? DEFAULT_PRESENCE);
|
|
572
667
|
return true;
|
|
573
668
|
},
|
|
669
|
+
startPresence(current) {
|
|
670
|
+
presenceBot = current;
|
|
671
|
+
rotation.set(presence);
|
|
672
|
+
rotation.start();
|
|
673
|
+
},
|
|
674
|
+
stopPresence: () => rotation.stop(),
|
|
675
|
+
get presenceFile() {
|
|
676
|
+
return presence ? presenceSite.file : null;
|
|
677
|
+
},
|
|
678
|
+
async runPresence(bot) {
|
|
679
|
+
if (!presence)
|
|
680
|
+
return;
|
|
681
|
+
const context = {
|
|
682
|
+
guilds: guildCount(bot),
|
|
683
|
+
...translation(bot.ctx, null),
|
|
684
|
+
};
|
|
685
|
+
const failed = (error) => report({ ...presenceSite, error });
|
|
686
|
+
let activities = presence.activities;
|
|
687
|
+
if (typeof activities === 'function') {
|
|
688
|
+
try {
|
|
689
|
+
activities = readActivities(await activities(context));
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
return failed(error);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
for (const { name } of activities) {
|
|
696
|
+
if (typeof name !== 'function')
|
|
697
|
+
continue;
|
|
698
|
+
try {
|
|
699
|
+
checkText(await name(context));
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
failed(error);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
},
|
|
574
706
|
ready(bot) {
|
|
575
707
|
const user = bot.ctx.cache.users.get(bot.ctx.self.userId);
|
|
576
708
|
router.emit('ready', {
|
package/dist/cli/redis.js
CHANGED
|
@@ -10,11 +10,19 @@ import { RedisStoreBackend } from '../store/redis-backend.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export function redisStores(address, log, options = {}) {
|
|
12
12
|
const client = new RedisClient(address, {
|
|
13
|
-
|
|
13
|
+
// While checkRedis() asks, a failure is what stops the bot, said once
|
|
14
|
+
// by it: "trying again" would not be true.
|
|
15
|
+
onError: message => {
|
|
16
|
+
if (!checking.has(backend))
|
|
17
|
+
log.warn(message);
|
|
18
|
+
},
|
|
14
19
|
onReconnect: () => log.reload(`Redis at ${client.where} is back`),
|
|
15
20
|
});
|
|
16
|
-
|
|
21
|
+
const backend = new RedisStoreBackend(client, options);
|
|
22
|
+
return backend;
|
|
17
23
|
}
|
|
24
|
+
/** The backends `checkRedis()` is asking right now. */
|
|
25
|
+
const checking = new WeakSet();
|
|
18
26
|
/** `a`, `a and b`, `a, b and c`. */
|
|
19
27
|
const and = (items) => items.length <= 1
|
|
20
28
|
? (items[0] ?? '')
|
|
@@ -46,6 +54,7 @@ export async function checkRedis(project, backend, log) {
|
|
|
46
54
|
const { stores, rankings } = project.redisStores();
|
|
47
55
|
if (!backend || stores.length + rankings.length === 0)
|
|
48
56
|
return true;
|
|
57
|
+
checking.add(backend);
|
|
49
58
|
try {
|
|
50
59
|
await backend.client.ping();
|
|
51
60
|
return true;
|
|
@@ -54,4 +63,7 @@ export async function checkRedis(project, backend, log) {
|
|
|
54
63
|
log.error(`${error instanceof Error ? error.message.replace(/ Trying again in \d+ s\.$/, '') : String(error)}`);
|
|
55
64
|
return false;
|
|
56
65
|
}
|
|
66
|
+
finally {
|
|
67
|
+
checking.delete(backend);
|
|
68
|
+
}
|
|
57
69
|
}
|