chapterjs 0.1.4 → 0.2.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/dist/cli/build.js +9 -10
- package/dist/cli/dev.js +17 -16
- package/dist/cli/project.d.ts +38 -25
- package/dist/cli/project.js +199 -134
- package/dist/commands/command.d.ts +25 -11
- package/dist/commands/command.js +5 -5
- package/dist/commands/{convention.d.ts → declaration.d.ts} +24 -11
- package/dist/commands/{convention.js → declaration.js} +61 -33
- package/dist/commands/tree.d.ts +8 -4
- package/dist/commands/tree.js +16 -6
- package/dist/components/button.d.ts +13 -13
- package/dist/components/button.js +9 -11
- package/dist/components/component.d.ts +1 -1
- package/dist/components/component.js +1 -1
- package/dist/components/custom-id.d.ts +4 -4
- package/dist/components/custom-id.js +12 -12
- package/dist/components/declaration.d.ts +42 -0
- package/dist/components/{convention.js → declaration.js} +76 -73
- package/dist/components/declared.d.ts +29 -0
- package/dist/components/declared.js +49 -0
- package/dist/components/embed.d.ts +13 -12
- package/dist/components/embed.js +6 -6
- package/dist/components/instance.d.ts +3 -3
- package/dist/components/instance.js +2 -2
- package/dist/components/modal.d.ts +11 -10
- package/dist/components/modal.js +7 -8
- package/dist/components/router.d.ts +3 -2
- package/dist/components/router.js +14 -19
- package/dist/components/select.d.ts +14 -12
- package/dist/components/select.js +4 -4
- package/dist/events/declaration.d.ts +16 -0
- package/dist/events/declaration.js +80 -0
- package/dist/events/event.d.ts +42 -5
- package/dist/events/event.js +25 -11
- package/dist/events/registry.d.ts +1 -10
- package/dist/events/router.d.ts +3 -2
- package/dist/index.d.ts +11 -9
- package/dist/index.js +2 -0
- package/dist/loader/generated.d.ts +5 -32
- package/dist/loader/generated.js +21 -55
- package/dist/loader/loader.d.ts +57 -44
- package/dist/loader/loader.js +73 -89
- package/dist/main.d.ts +0 -1
- package/dist/main.js +2 -3
- package/dist/messages/commands.d.ts +3 -3
- package/dist/messages/commands.js +29 -10
- package/dist/messages/declaration.d.ts +89 -0
- package/dist/messages/declaration.js +455 -0
- package/dist/messages/messages.d.ts +65 -43
- package/dist/messages/messages.js +11 -10
- package/dist/messages/phrases.js +1 -1
- package/dist/messages/translate.d.ts +4 -4
- package/dist/messages/translate.js +4 -4
- package/dist/presence/declaration.d.ts +19 -0
- package/dist/presence/{convention.js → declaration.js} +13 -28
- package/dist/presence/presence.d.ts +7 -7
- package/dist/presence/presence.js +5 -5
- package/dist/structures/context.d.ts +1 -1
- package/dist/structures/interaction.d.ts +2 -2
- package/dist/structures/interaction.js +3 -3
- package/dist/structures/payload.d.ts +4 -4
- package/dist/structures/payload.js +4 -4
- package/dist/tasks/declaration.d.ts +16 -0
- package/dist/tasks/{convention.js → declaration.js} +14 -36
- package/dist/tasks/scheduler.d.ts +3 -1
- package/dist/tasks/scheduler.js +4 -4
- package/dist/tasks/task.d.ts +8 -9
- package/dist/tasks/task.js +6 -7
- package/package.json +1 -1
- package/dist/components/convention.d.ts +0 -35
- package/dist/components/file.d.ts +0 -30
- package/dist/components/file.js +0 -52
- package/dist/events/convention.d.ts +0 -25
- package/dist/events/convention.js +0 -97
- package/dist/events/types.d.ts +0 -7
- package/dist/events/types.js +0 -48
- package/dist/messages/convention.d.ts +0 -89
- package/dist/messages/convention.js +0 -347
- package/dist/presence/convention.d.ts +0 -26
- package/dist/tasks/convention.d.ts +0 -26
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// of Discord, so a mistake is explained before Discord refuses it.
|
|
1
|
+
// What a file declares with `command()`, checked when it loads against the
|
|
2
|
+
// limits of Discord, so a mistake is explained before Discord refuses it.
|
|
4
3
|
// https://docs.discord.com/developers/interactions/application-commands
|
|
5
4
|
import { ALL_PERMISSIONS } from '../discord/permissions.js';
|
|
6
5
|
import { ChannelType } from '../discord/types/channel.js';
|
|
7
6
|
import { Locale } from '../discord/types/common.js';
|
|
8
7
|
import { PermissionFlags } from '../discord/types/permissions.js';
|
|
9
8
|
import { COOLDOWN_SCOPES, NO_COOLDOWN, } from '../interactions/cooldown.js';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { listSources } from '../loader/loader.js';
|
|
10
11
|
import { DURATION_EXAMPLE, parseDuration } from '../util/duration.js';
|
|
11
|
-
import {
|
|
12
|
+
import { isCommandDeclaration, } from './command.js';
|
|
12
13
|
/**
|
|
13
14
|
* The names of commands and options: lowercase letters of any language,
|
|
14
15
|
* digits, `-` and `_`, 32 characters at most.
|
|
@@ -39,6 +40,7 @@ const OPTION_KEYS = {
|
|
|
39
40
|
channel: ['channelTypes'],
|
|
40
41
|
};
|
|
41
42
|
const COMMAND_KEYS = [
|
|
43
|
+
'name',
|
|
42
44
|
'description',
|
|
43
45
|
'options',
|
|
44
46
|
'locales',
|
|
@@ -388,41 +390,45 @@ function checkPermissions(permissions) {
|
|
|
388
390
|
}
|
|
389
391
|
return bits & ALL_PERMISSIONS;
|
|
390
392
|
}
|
|
393
|
+
const EXAMPLE = `command({ name: 'ping', description: '...', run({ interaction }) { ... } })`;
|
|
391
394
|
/**
|
|
392
|
-
*
|
|
393
|
-
* One
|
|
394
|
-
*
|
|
395
|
-
* nothing deeper.
|
|
395
|
+
* The parts of the name of a command: `'mod ban'` is `['mod', 'ban']`.
|
|
396
|
+
* One part is a command, two a subcommand, three a subcommand in a group:
|
|
397
|
+
* Discord allows nothing deeper.
|
|
396
398
|
*/
|
|
397
|
-
export
|
|
398
|
-
|
|
399
|
+
export function readCommandName(name) {
|
|
400
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
401
|
+
return fail(`This command has no "name": what people type after the /, like ${EXAMPLE}. A subcommand is written with its parents: name: 'mod ban' is /mod ban.`);
|
|
402
|
+
}
|
|
403
|
+
const parts = name.trim().split(/\s+/);
|
|
404
|
+
if (parts.length > 3) {
|
|
405
|
+
fail(`The name ${JSON.stringify(name)} has ${parts.length} words: Discord allows a command, a group and a subcommand, so 3 at most ('mod roles add' is /mod roles add).`);
|
|
406
|
+
}
|
|
407
|
+
for (const part of parts) {
|
|
408
|
+
if (!isName(part)) {
|
|
409
|
+
fail(`"${part}" can't be in the name of a command: use lowercase letters, digits, - and _ only, 32 characters at most.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return parts;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A slash command, declared anywhere in `src/` with `command()`: `name` is
|
|
416
|
+
* what people type after the `/`, with its parents for a subcommand
|
|
417
|
+
* (`'mod ban'` is `/mod ban`, `'mod roles add'` is `/mod roles add`). A
|
|
418
|
+
* file may declare several.
|
|
419
|
+
*/
|
|
420
|
+
export const commandDeclaration = {
|
|
399
421
|
one: 'command',
|
|
400
422
|
many: 'commands',
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
for (const part of parts) {
|
|
407
|
-
if (!isName(part)) {
|
|
408
|
-
fail(`"${part}" can't be in the name of a command: use lowercase letters, digits, - and _ only, 32 characters at most. The path of the file is the name of the command.`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
},
|
|
412
|
-
read(exports, path) {
|
|
413
|
-
const file = exports.default;
|
|
414
|
-
const parts = path.replace(/\.[^./]+$/, '').split('/');
|
|
415
|
-
if (!isCommandFile(file)) {
|
|
416
|
-
const example = `import { command } from 'chapterjs'; export default command({ description: '...', run({ interaction }) { ... } })`;
|
|
417
|
-
return fail('default' in exports
|
|
418
|
-
? `The default export of this file must be what command() returns: ${example}`
|
|
419
|
-
: `This file has no default export. It should look like: ${example}`);
|
|
420
|
-
}
|
|
421
|
-
const config = file.config;
|
|
423
|
+
is: isCommandDeclaration,
|
|
424
|
+
list: true,
|
|
425
|
+
read(value) {
|
|
426
|
+
const config = value.config;
|
|
422
427
|
if (!isRecord(config)) {
|
|
423
|
-
return fail(`command() needs an object:
|
|
428
|
+
return fail(`command() needs an object: ${EXAMPLE}`);
|
|
424
429
|
}
|
|
425
430
|
checkKeys('a command', config, COMMAND_KEYS);
|
|
431
|
+
const parts = readCommandName(config.name);
|
|
426
432
|
// No description: the language files describe the command, options
|
|
427
433
|
// included, and hold its translations.
|
|
428
434
|
const described = config.description !== undefined;
|
|
@@ -430,7 +436,7 @@ export const commandsConvention = {
|
|
|
430
436
|
? checkDescription('this command', config.description)
|
|
431
437
|
: '';
|
|
432
438
|
if (!described && config.locales !== undefined) {
|
|
433
|
-
fail(`This command has no "description", so its texts come from the language files
|
|
439
|
+
fail(`This command has no "description", so its texts come from the language files: put its "locales" there too (commands: { ... }), or give it a description here.`);
|
|
434
440
|
}
|
|
435
441
|
if (typeof config.run !== 'function') {
|
|
436
442
|
fail(`This command has no "run": the function to run when someone uses it, like async run({ interaction }) { await interaction.reply('Pong!'); }`);
|
|
@@ -462,3 +468,25 @@ export const commandsConvention = {
|
|
|
462
468
|
});
|
|
463
469
|
},
|
|
464
470
|
};
|
|
471
|
+
/** `command({ ... name: '...' ... })`, as written in a file. */
|
|
472
|
+
const DECLARED = /\bcommand\s*\(\s*\{[^]*?\bname\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
473
|
+
/**
|
|
474
|
+
* The names of the commands of a project, found without running its
|
|
475
|
+
* files: a text search for `command({ name: '...' })`. Approximate on
|
|
476
|
+
* purpose, for `sync`, which runs nothing: `dev` and `build` then write
|
|
477
|
+
* the exact ones. Whether a command is described is not known: none is.
|
|
478
|
+
*/
|
|
479
|
+
export async function scanCommands(cwd) {
|
|
480
|
+
const names = new Set();
|
|
481
|
+
for (const { path } of await listSources(cwd)) {
|
|
482
|
+
const code = await readFile(path, 'utf8').catch(() => '');
|
|
483
|
+
if (!code.includes('command('))
|
|
484
|
+
continue;
|
|
485
|
+
for (const match of code.matchAll(DECLARED)) {
|
|
486
|
+
const name = match[1].trim().split(/\s+/).join(' ');
|
|
487
|
+
if (name !== '')
|
|
488
|
+
names.add(name);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return [...names].sort();
|
|
492
|
+
}
|
package/dist/commands/tree.d.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { type BulkOverwriteGuildApplicationCommandsJSONParams } from '../discord/types/application-command.js';
|
|
2
|
-
import type { LoadedCommand } from './
|
|
2
|
+
import type { LoadedCommand } from './declaration.js';
|
|
3
3
|
/** A command with the file it comes from. */
|
|
4
4
|
export interface CommandEntry {
|
|
5
5
|
file: string;
|
|
6
|
+
/** The export it is: `'default'`, or its name. */
|
|
7
|
+
export: string;
|
|
6
8
|
command: LoadedCommand;
|
|
7
9
|
}
|
|
8
|
-
/** Two
|
|
10
|
+
/** Two declarations that can't both be commands. */
|
|
9
11
|
export interface CommandConflict {
|
|
10
12
|
file: string;
|
|
13
|
+
export: string;
|
|
11
14
|
message: string;
|
|
12
15
|
}
|
|
13
16
|
export type CommandPayload = BulkOverwriteGuildApplicationCommandsJSONParams;
|
|
14
17
|
/** `/mod ban` for `['mod', 'ban']`. */
|
|
15
18
|
export declare const commandName: (path: readonly string[]) => string;
|
|
16
19
|
/**
|
|
17
|
-
* Finds the
|
|
18
|
-
*
|
|
20
|
+
* Finds the declarations that can't be commands together, and leaves them
|
|
21
|
+
* out:
|
|
22
|
+
* - two commands with the same name;
|
|
19
23
|
* - a command that also has subcommands: Discord only lets people use the
|
|
20
24
|
* subcommands.
|
|
21
25
|
*/
|
package/dist/commands/tree.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Turns the
|
|
1
|
+
// Turns the commands of a project into what Discord registers: one
|
|
2
2
|
// application command per top-level name, with its subcommands and groups.
|
|
3
3
|
// https://docs.discord.com/developers/interactions/application-commands#subcommands-and-subcommand-groups
|
|
4
4
|
import { ApplicationCommandOptionType, ApplicationCommandType, } from '../discord/types/application-command.js';
|
|
5
5
|
import { ApplicationIntegrationType } from '../discord/types/application.js';
|
|
6
6
|
import { InteractionContextType } from '../discord/types/interaction.js';
|
|
7
|
+
import { siteName } from '../loader/loader.js';
|
|
7
8
|
const TYPES = {
|
|
8
9
|
string: ApplicationCommandOptionType.String,
|
|
9
10
|
integer: ApplicationCommandOptionType.Integer,
|
|
@@ -76,8 +77,9 @@ function toOption(option, locales, autocomplete) {
|
|
|
76
77
|
/** `/mod ban` for `['mod', 'ban']`. */
|
|
77
78
|
export const commandName = (path) => `/${path.join(' ')}`;
|
|
78
79
|
/**
|
|
79
|
-
* Finds the
|
|
80
|
-
*
|
|
80
|
+
* Finds the declarations that can't be commands together, and leaves them
|
|
81
|
+
* out:
|
|
82
|
+
* - two commands with the same name;
|
|
81
83
|
* - a command that also has subcommands: Discord only lets people use the
|
|
82
84
|
* subcommands.
|
|
83
85
|
*/
|
|
@@ -85,7 +87,13 @@ export function findConflicts(entries) {
|
|
|
85
87
|
const conflicts = [];
|
|
86
88
|
const seen = new Map();
|
|
87
89
|
const unique = [...entries]
|
|
88
|
-
.sort((a, b) =>
|
|
90
|
+
.sort((a, b) => a.file === b.file
|
|
91
|
+
? a.export < b.export
|
|
92
|
+
? -1
|
|
93
|
+
: 1
|
|
94
|
+
: a.file < b.file
|
|
95
|
+
? -1
|
|
96
|
+
: 1)
|
|
89
97
|
.filter(entry => {
|
|
90
98
|
const key = entry.command.path.join(' ');
|
|
91
99
|
const first = seen.get(key);
|
|
@@ -95,7 +103,8 @@ export function findConflicts(entries) {
|
|
|
95
103
|
}
|
|
96
104
|
conflicts.push({
|
|
97
105
|
file: entry.file,
|
|
98
|
-
|
|
106
|
+
export: entry.export,
|
|
107
|
+
message: `${commandName(entry.command.path)} is already declared in ${siteName(first)}: two commands can't have the same name.`,
|
|
99
108
|
});
|
|
100
109
|
return false;
|
|
101
110
|
});
|
|
@@ -108,7 +117,8 @@ export function findConflicts(entries) {
|
|
|
108
117
|
return true;
|
|
109
118
|
conflicts.push({
|
|
110
119
|
file: entry.file,
|
|
111
|
-
|
|
120
|
+
export: entry.export,
|
|
121
|
+
message: `${commandName(path)} can't be a command and have subcommands (${siteName(child)} is ${commandName(child.command.path)}): Discord only lets people use the subcommands. Make this one a subcommand too, for example name: '${[...path, 'run'].join(' ')}'.`,
|
|
112
122
|
});
|
|
113
123
|
return false;
|
|
114
124
|
});
|
|
@@ -29,30 +29,29 @@ export interface ButtonLook<Data extends DataShape = DataShape> {
|
|
|
29
29
|
}
|
|
30
30
|
/** What `run` receives when the button is clicked. */
|
|
31
31
|
export type ButtonContext<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> = InteractiveContext<Data> & TranslationContext & PlaceOf<Where, ComponentInteraction>;
|
|
32
|
-
/** What a
|
|
32
|
+
/** What a file gives to `button()`. */
|
|
33
33
|
export interface ButtonConfig<Data extends DataShape = {}, Where extends ComponentWhere = 'guild'> extends ButtonLook<Data>, InteractiveConfig<Data, Where> {
|
|
34
34
|
/** What to do when someone clicks the button. */
|
|
35
35
|
run: (context: ButtonContext<Data, Where>) => unknown;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* What `button()` returns:
|
|
39
|
-
* where you send a message, and call it with the data
|
|
40
|
-
* (`ban({ userId })`); a button without data is used as
|
|
41
|
-
* called with a look (`confirm({ disabled: true })`).
|
|
38
|
+
* What `button()` returns: a declaration the framework finds in the exports
|
|
39
|
+
* of a file. Import it where you send a message, and call it with the data
|
|
40
|
+
* the button carries (`ban({ userId })`); a button without data is used as
|
|
41
|
+
* is (`confirm`), or called with a look (`confirm({ disabled: true })`).
|
|
42
42
|
*/
|
|
43
|
-
export type
|
|
43
|
+
export type ButtonDeclaration<Data extends DataShape = {}> = {
|
|
44
44
|
/** What the file gave to `button()`, not checked yet. */
|
|
45
45
|
readonly config: unknown;
|
|
46
46
|
} & ({} extends Data ? ((look?: ButtonLook) => ButtonComponent) & ButtonComponent : {} extends DataInputOf<Data> ? ((data?: DataInputOf<Data>, look?: ButtonLook) => ButtonComponent) & ButtonComponent : (data: DataInputOf<Data>, look?: ButtonLook) => ButtonComponent);
|
|
47
47
|
/**
|
|
48
|
-
* Declares a button. Export the result
|
|
49
|
-
*
|
|
50
|
-
* button apart, so you never write an id.
|
|
48
|
+
* Declares a button. Export the result from any file of `src/`: the name
|
|
49
|
+
* of the export is what tells the button apart, so you never write an id.
|
|
51
50
|
*
|
|
52
51
|
* ```ts
|
|
53
52
|
* import { button } from 'chapterjs';
|
|
54
53
|
*
|
|
55
|
-
* export
|
|
54
|
+
* export const confirm = button({
|
|
56
55
|
* label: 'Confirm',
|
|
57
56
|
* style: 'success',
|
|
58
57
|
* async run({ interaction }) {
|
|
@@ -61,11 +60,12 @@ export type ButtonFile<Data extends DataShape = {}> = {
|
|
|
61
60
|
* });
|
|
62
61
|
* ```
|
|
63
62
|
*/
|
|
64
|
-
export declare function button<const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ButtonConfig<Data, Where>):
|
|
65
|
-
/** A button
|
|
63
|
+
export declare function button<const Data extends DataShape = {}, const Where extends ComponentWhere = 'guild'>(config: ButtonConfig<Data, Where>): ButtonDeclaration<Data>;
|
|
64
|
+
/** A button, checked. */
|
|
66
65
|
export interface LoadedButton {
|
|
67
66
|
readonly kind: 'button';
|
|
68
|
-
|
|
67
|
+
/** The name of the export that declares it: its id for Discord. */
|
|
68
|
+
readonly name: string;
|
|
69
69
|
readonly look: Required<Pick<ButtonLook, 'style' | 'disabled'>> & Pick<ButtonLook, 'label' | 'emoji'>;
|
|
70
70
|
readonly data: DataShape;
|
|
71
71
|
readonly where: ComponentWhere;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// `button()`, as
|
|
2
|
-
//
|
|
3
|
-
// when it is clicked.
|
|
1
|
+
// `button()`, as user files import it from 'chapterjs': what the button
|
|
2
|
+
// looks like, what it carries, and what to do when it is clicked.
|
|
4
3
|
// https://docs.discord.com/developers/components/reference#button
|
|
5
4
|
import { Limits } from '../discord/api.js';
|
|
6
5
|
import { ButtonStyle, ComponentType, } from '../discord/types/component.js';
|
|
7
6
|
import { isDynamic, resolveText } from './component.js';
|
|
8
7
|
import { checkData, encodeCustomId, } from './custom-id.js';
|
|
9
|
-
import {
|
|
8
|
+
import { createComponent } from './declared.js';
|
|
10
9
|
import { emojiOf } from './instance.js';
|
|
11
10
|
export const BUTTON_STYLES = {
|
|
12
11
|
primary: ButtonStyle.Primary,
|
|
@@ -15,14 +14,13 @@ export const BUTTON_STYLES = {
|
|
|
15
14
|
danger: ButtonStyle.Danger,
|
|
16
15
|
};
|
|
17
16
|
/**
|
|
18
|
-
* Declares a button. Export the result
|
|
19
|
-
*
|
|
20
|
-
* button apart, so you never write an id.
|
|
17
|
+
* Declares a button. Export the result from any file of `src/`: the name
|
|
18
|
+
* of the export is what tells the button apart, so you never write an id.
|
|
21
19
|
*
|
|
22
20
|
* ```ts
|
|
23
21
|
* import { button } from 'chapterjs';
|
|
24
22
|
*
|
|
25
|
-
* export
|
|
23
|
+
* export const confirm = button({
|
|
26
24
|
* label: 'Confirm',
|
|
27
25
|
* style: 'success',
|
|
28
26
|
* async run({ interaction }) {
|
|
@@ -32,7 +30,7 @@ export const BUTTON_STYLES = {
|
|
|
32
30
|
* ```
|
|
33
31
|
*/
|
|
34
32
|
export function button(config) {
|
|
35
|
-
return
|
|
33
|
+
return createComponent('button', config, {
|
|
36
34
|
asPiece: true,
|
|
37
35
|
pieceKind: 'button',
|
|
38
36
|
});
|
|
@@ -74,7 +72,7 @@ export function readLook(what, look) {
|
|
|
74
72
|
/** The button as Discord takes it, for the data and the look given. */
|
|
75
73
|
export function renderButton(loaded, args) {
|
|
76
74
|
const hasData = Object.keys(loaded.data).length > 0;
|
|
77
|
-
const name = loaded
|
|
75
|
+
const { name } = loaded;
|
|
78
76
|
if (args.length > (hasData ? 2 : 1)) {
|
|
79
77
|
fail(`${name} takes ${hasData ? 'its data, then a look' : 'a look'} at most: ${hasData ? `${name}({ ... }, { disabled: true })` : `${name}({ disabled: true })`}.`);
|
|
80
78
|
}
|
|
@@ -86,7 +84,7 @@ export function renderButton(loaded, args) {
|
|
|
86
84
|
const base = {
|
|
87
85
|
type: ComponentType.Button,
|
|
88
86
|
style: BUTTON_STYLES[look.style],
|
|
89
|
-
custom_id: encodeCustomId(loaded.
|
|
87
|
+
custom_id: encodeCustomId(loaded.name, loaded.data, values),
|
|
90
88
|
};
|
|
91
89
|
if (look.emoji !== undefined)
|
|
92
90
|
base.emoji = emojiOf(look.emoji, name);
|
|
@@ -12,7 +12,7 @@ import type { Translator } from '../messages/messages.js';
|
|
|
12
12
|
* component carries.
|
|
13
13
|
*/
|
|
14
14
|
export interface TextContext<Data extends DataShape = DataShape> {
|
|
15
|
-
/** The texts of
|
|
15
|
+
/** The texts of the language files, in the language of who will read the message. */
|
|
16
16
|
t: Translator;
|
|
17
17
|
/** What the component carries, as declared in `data`. */
|
|
18
18
|
data: [DataShape] extends [Data] ? Record<string, string | number | boolean> : DataValuesOf<Data>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What every interactive component
|
|
1
|
+
// What every interactive component (button, menu, form) shares: where it
|
|
2
2
|
// works, who may use it, what it carries, and what its `run` receives
|
|
3
3
|
// about the place it was used in.
|
|
4
4
|
/** Whether a text is computed when the message is sent. */
|
|
@@ -53,14 +53,14 @@ export declare const needsNoData: (shape: DataShape) => boolean;
|
|
|
53
53
|
/** Checks the values a file gives to a component against its `data`. */
|
|
54
54
|
export declare function checkData(what: string, shape: DataShape, values: unknown): Record<string, string | number | boolean>;
|
|
55
55
|
/**
|
|
56
|
-
* The `custom_id` of a component: its
|
|
56
|
+
* The `custom_id` of a component: its name, then its values in the order
|
|
57
57
|
* of the declaration. Discord accepts 100 characters: when the data does
|
|
58
58
|
* not fit, the error says by how much.
|
|
59
59
|
*/
|
|
60
|
-
export declare function encodeCustomId(
|
|
61
|
-
/** The
|
|
60
|
+
export declare function encodeCustomId(name: string, shape: DataShape, values: Record<string, string | number | boolean>): string;
|
|
61
|
+
/** The name and the encoded values of a `custom_id` the framework wrote. */
|
|
62
62
|
export declare function decodeCustomId(id: string): {
|
|
63
|
-
|
|
63
|
+
name: string;
|
|
64
64
|
parts: string[];
|
|
65
65
|
};
|
|
66
66
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The `custom_id` of a component is never written by users: it is the
|
|
2
|
-
// of the
|
|
3
|
-
// component carries, encoded by the framework. Stateless by design: it
|
|
1
|
+
// The `custom_id` of a component is never written by users: it is the name
|
|
2
|
+
// of the export that declares the component (`ban`), followed by the data
|
|
3
|
+
// the component carries, encoded by the framework. Stateless by design: it
|
|
4
4
|
// survives a restart, and every process of the bot reads it the same way.
|
|
5
5
|
// https://docs.discord.com/developers/components/reference#anatomy-of-a-component-custom-id
|
|
6
6
|
import { Limits } from '../discord/api.js';
|
|
@@ -76,24 +76,24 @@ export function checkData(what, shape, values) {
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* The `custom_id` of a component: its
|
|
79
|
+
* The `custom_id` of a component: its name, then its values in the order
|
|
80
80
|
* of the declaration. Discord accepts 100 characters: when the data does
|
|
81
81
|
* not fit, the error says by how much.
|
|
82
82
|
*/
|
|
83
|
-
export function encodeCustomId(
|
|
84
|
-
const parts = [
|
|
85
|
-
for (const
|
|
86
|
-
parts.push(escape(String(values[
|
|
83
|
+
export function encodeCustomId(name, shape, values) {
|
|
84
|
+
const parts = [name];
|
|
85
|
+
for (const field of Object.keys(shape))
|
|
86
|
+
parts.push(escape(String(values[field])));
|
|
87
87
|
const id = parts.join(SEPARATOR);
|
|
88
88
|
if (id.length > Limits.CustomId) {
|
|
89
|
-
throw new RangeError(`The data of ${
|
|
89
|
+
throw new RangeError(`The data of ${name} is ${id.length - name.length - 1} characters long once encoded, and Discord leaves ${Limits.CustomId - name.length - 1} for it (its id is ${name}, ${name.length} characters, out of ${Limits.CustomId}). Carry less: an id instead of a name, or a shorter export name.`);
|
|
90
90
|
}
|
|
91
91
|
return id;
|
|
92
92
|
}
|
|
93
|
-
/** The
|
|
93
|
+
/** The name and the encoded values of a `custom_id` the framework wrote. */
|
|
94
94
|
export function decodeCustomId(id) {
|
|
95
|
-
const [
|
|
96
|
-
return {
|
|
95
|
+
const [name = '', ...parts] = split(id);
|
|
96
|
+
return { name, parts };
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
99
|
* The values a `custom_id` carries, typed as the component declares them.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Declaration } from '../loader/loader.js';
|
|
2
|
+
import { type LoadedButton } from './button.js';
|
|
3
|
+
import { isEmbedDeclaration } from './embed.js';
|
|
4
|
+
import { type ComponentKind } from './declared.js';
|
|
5
|
+
import { type LoadedModal } from './modal.js';
|
|
6
|
+
import { type LoadedSelect } from './select.js';
|
|
7
|
+
/** An embed, checked (when it is static). */
|
|
8
|
+
export interface LoadedEmbed {
|
|
9
|
+
readonly kind: 'embed';
|
|
10
|
+
/** The name of the export that declares it. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
/** A component, checked: what the router works with. */
|
|
14
|
+
export type LoadedComponent = LoadedButton | LoadedSelect | LoadedModal | LoadedEmbed;
|
|
15
|
+
/** What each kind is called in messages. */
|
|
16
|
+
export declare const WHAT: Record<ComponentKind, string>;
|
|
17
|
+
/**
|
|
18
|
+
* A component (a button, a select menu, a form or an embed), declared
|
|
19
|
+
* anywhere in `src/` with `button()`, `select()`, `modal()` or `embed()`,
|
|
20
|
+
* and exported: the name of the export is its id, so a file may declare
|
|
21
|
+
* several, each under its own export.
|
|
22
|
+
*/
|
|
23
|
+
export declare const componentDeclaration: Declaration<LoadedComponent>;
|
|
24
|
+
/** Two components of the same kind with the same name: the later one is left out. */
|
|
25
|
+
export declare function findDuplicates<T extends {
|
|
26
|
+
file: string;
|
|
27
|
+
export: string;
|
|
28
|
+
component: {
|
|
29
|
+
kind: ComponentKind;
|
|
30
|
+
name: string;
|
|
31
|
+
};
|
|
32
|
+
}>(entries: readonly T[]): {
|
|
33
|
+
valid: T[];
|
|
34
|
+
conflicts: {
|
|
35
|
+
file: string;
|
|
36
|
+
export: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
/** Whether a declaration is an embed: nothing to route. */
|
|
41
|
+
export declare const isEmbedLoaded: (component: LoadedComponent) => component is LoadedEmbed;
|
|
42
|
+
export { isEmbedDeclaration };
|