chapterjs 0.1.2 → 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/assets/asset.d.ts +46 -0
- package/dist/assets/asset.js +86 -0
- package/dist/assets/public.d.ts +19 -0
- package/dist/assets/public.js +120 -0
- package/dist/cache/cache.d.ts +62 -0
- package/dist/cache/cache.js +80 -0
- package/dist/cache/store.d.ts +45 -0
- package/dist/cache/store.js +53 -0
- package/dist/cli/build.d.ts +41 -0
- package/dist/cli/build.js +205 -0
- package/dist/cli/cli.js +108 -25
- package/dist/cli/cluster.d.ts +70 -0
- package/dist/cli/cluster.js +213 -0
- package/dist/cli/dev.d.ts +18 -1
- package/dist/cli/dev.js +253 -198
- package/dist/cli/env.d.ts +31 -0
- package/dist/cli/env.js +93 -0
- package/dist/cli/log.d.ts +13 -0
- package/dist/cli/log.js +17 -0
- package/dist/cli/node-version.d.ts +8 -0
- package/dist/cli/node-version.js +13 -0
- package/dist/cli/preflight.d.ts +36 -0
- package/dist/cli/preflight.js +122 -0
- package/dist/cli/project.d.ts +135 -0
- package/dist/cli/project.js +517 -0
- package/dist/cli/start.d.ts +25 -0
- package/dist/cli/start.js +225 -0
- package/dist/commands/autocomplete.d.ts +13 -0
- package/dist/commands/autocomplete.js +159 -0
- package/dist/commands/command.d.ts +360 -0
- package/dist/commands/command.js +28 -0
- package/dist/commands/declaration.d.ts +77 -0
- package/dist/commands/declaration.js +492 -0
- package/dist/commands/register.d.ts +18 -0
- package/dist/commands/register.js +42 -0
- package/dist/commands/router.d.ts +20 -0
- package/dist/commands/router.js +228 -0
- package/dist/commands/tree.d.ts +47 -0
- package/dist/commands/tree.js +216 -0
- package/dist/commands/twins.d.ts +15 -0
- package/dist/commands/twins.js +78 -0
- package/dist/components/button.d.ts +79 -0
- package/dist/components/button.js +106 -0
- package/dist/components/component.d.ts +140 -0
- package/dist/components/component.js +39 -0
- package/dist/components/custom-id.d.ts +72 -0
- package/dist/components/custom-id.js +128 -0
- package/dist/components/declaration.d.ts +42 -0
- package/dist/components/declaration.js +342 -0
- package/dist/components/declared.d.ts +29 -0
- package/dist/components/declared.js +49 -0
- package/dist/components/embed.d.ts +29 -0
- package/dist/components/embed.js +11 -0
- package/dist/components/instance.d.ts +100 -0
- package/dist/components/instance.js +86 -0
- package/dist/components/layout.d.ts +103 -0
- package/dist/components/layout.js +279 -0
- package/dist/components/modal.d.ts +245 -0
- package/dist/components/modal.js +398 -0
- package/dist/components/options.d.ts +41 -0
- package/dist/components/options.js +101 -0
- package/dist/components/render.d.ts +12 -0
- package/dist/components/render.js +58 -0
- package/dist/components/router.d.ts +19 -0
- package/dist/components/router.js +251 -0
- package/dist/components/select.d.ts +146 -0
- package/dist/components/select.js +179 -0
- package/dist/core/bot.d.ts +99 -0
- package/dist/core/bot.js +174 -0
- package/dist/core/memory.d.ts +66 -0
- package/dist/core/memory.js +115 -0
- package/dist/discord/api.d.ts +112 -0
- package/dist/discord/api.js +115 -0
- package/dist/discord/cdn.d.ts +67 -0
- package/dist/discord/cdn.js +100 -0
- package/dist/discord/codes.d.ts +171 -0
- package/dist/discord/codes.js +421 -0
- package/dist/discord/endpoint.d.ts +56 -0
- package/dist/discord/endpoint.js +53 -0
- package/dist/discord/endpoints.d.ts +1529 -0
- package/dist/discord/endpoints.js +1139 -0
- package/dist/discord/formatting.d.ts +67 -0
- package/dist/discord/formatting.js +87 -0
- package/dist/discord/index.d.ts +10 -0
- package/dist/discord/index.js +13 -0
- package/dist/discord/intents.d.ts +48 -0
- package/dist/discord/intents.js +128 -0
- package/dist/discord/permissions.d.ts +94 -0
- package/dist/discord/permissions.js +185 -0
- package/dist/discord/snowflake.d.ts +21 -0
- package/dist/discord/snowflake.js +36 -0
- package/dist/discord/types/application-command.d.ts +332 -0
- package/dist/discord/types/application-command.js +48 -0
- package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
- package/dist/discord/types/application-role-connection-metadata.js +22 -0
- package/dist/discord/types/application.d.ts +208 -0
- package/dist/discord/types/application.js +48 -0
- package/dist/discord/types/audit-log.d.ts +255 -0
- package/dist/discord/types/audit-log.js +144 -0
- package/dist/discord/types/auto-moderation.d.ts +167 -0
- package/dist/discord/types/auto-moderation.js +52 -0
- package/dist/discord/types/channel.d.ts +557 -0
- package/dist/discord/types/channel.js +88 -0
- package/dist/discord/types/common.d.ts +54 -0
- package/dist/discord/types/common.js +38 -0
- package/dist/discord/types/component.d.ts +775 -0
- package/dist/discord/types/component.js +76 -0
- package/dist/discord/types/emoji.d.ts +74 -0
- package/dist/discord/types/emoji.js +1 -0
- package/dist/discord/types/entitlement.d.ts +84 -0
- package/dist/discord/types/entitlement.js +22 -0
- package/dist/discord/types/gateway-events.d.ts +1148 -0
- package/dist/discord/types/gateway-events.js +54 -0
- package/dist/discord/types/gateway.d.ts +38 -0
- package/dist/discord/types/gateway.js +1 -0
- package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
- package/dist/discord/types/guild-scheduled-event.js +68 -0
- package/dist/discord/types/guild-template.d.ts +51 -0
- package/dist/discord/types/guild-template.js +1 -0
- package/dist/discord/types/guild.d.ts +910 -0
- package/dist/discord/types/guild.js +153 -0
- package/dist/discord/types/index.d.ts +31 -0
- package/dist/discord/types/index.js +31 -0
- package/dist/discord/types/interaction.d.ts +321 -0
- package/dist/discord/types/interaction.js +47 -0
- package/dist/discord/types/invite.d.ts +158 -0
- package/dist/discord/types/invite.js +39 -0
- package/dist/discord/types/message.d.ts +834 -0
- package/dist/discord/types/message.js +159 -0
- package/dist/discord/types/oauth2.d.ts +22 -0
- package/dist/discord/types/oauth2.js +1 -0
- package/dist/discord/types/permissions.d.ts +183 -0
- package/dist/discord/types/permissions.js +118 -0
- package/dist/discord/types/poll.d.ts +106 -0
- package/dist/discord/types/poll.js +8 -0
- package/dist/discord/types/sku.d.ts +47 -0
- package/dist/discord/types/sku.js +26 -0
- package/dist/discord/types/soundboard.d.ts +71 -0
- package/dist/discord/types/soundboard.js +1 -0
- package/dist/discord/types/stage-instance.d.ts +58 -0
- package/dist/discord/types/stage-instance.js +10 -0
- package/dist/discord/types/sticker.d.ts +115 -0
- package/dist/discord/types/sticker.js +20 -0
- package/dist/discord/types/subscription.d.ts +54 -0
- package/dist/discord/types/subscription.js +12 -0
- package/dist/discord/types/team.d.ts +55 -0
- package/dist/discord/types/team.js +21 -0
- package/dist/discord/types/user.d.ts +273 -0
- package/dist/discord/types/user.js +83 -0
- package/dist/discord/types/voice.d.ts +72 -0
- package/dist/discord/types/voice.js +1 -0
- package/dist/discord/types/webhook.d.ts +184 -0
- package/dist/discord/types/webhook.js +12 -0
- package/dist/events/declaration.d.ts +16 -0
- package/dist/events/declaration.js +80 -0
- package/dist/events/event.d.ts +45 -0
- package/dist/events/event.js +34 -0
- package/dist/events/registry.d.ts +697 -0
- package/dist/events/registry.js +839 -0
- package/dist/events/router.d.ts +46 -0
- package/dist/events/router.js +133 -0
- package/dist/gateway/errors.d.ts +18 -0
- package/dist/gateway/errors.js +47 -0
- package/dist/gateway/gateway.d.ts +64 -0
- package/dist/gateway/gateway.js +117 -0
- package/dist/gateway/identify-queue.d.ts +25 -0
- package/dist/gateway/identify-queue.js +29 -0
- package/dist/gateway/shard.d.ts +94 -0
- package/dist/gateway/shard.js +414 -0
- package/dist/gateway/state.d.ts +11 -0
- package/dist/gateway/state.js +184 -0
- package/dist/index.d.ts +57 -18
- package/dist/index.js +40 -12
- package/dist/interactions/cooldown.d.ts +33 -0
- package/dist/interactions/cooldown.js +74 -0
- package/dist/interactions/dispatch.d.ts +91 -0
- package/dist/interactions/dispatch.js +110 -0
- package/dist/loader/generated.d.ts +10 -0
- package/dist/loader/generated.js +80 -0
- package/dist/loader/hot.d.ts +13 -0
- package/dist/loader/hot.js +85 -0
- package/dist/loader/loader.d.ts +85 -0
- package/dist/loader/loader.js +119 -0
- package/dist/loader/locate.d.ts +10 -0
- package/dist/loader/locate.js +36 -0
- package/dist/loader/watch.d.ts +17 -0
- package/dist/loader/watch.js +66 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/messages/commands.d.ts +23 -0
- package/dist/messages/commands.js +142 -0
- package/dist/messages/declaration.d.ts +89 -0
- package/dist/messages/declaration.js +455 -0
- package/dist/messages/messages.d.ts +241 -0
- package/dist/messages/messages.js +31 -0
- package/dist/messages/phrases.d.ts +23 -0
- package/dist/messages/phrases.js +44 -0
- package/dist/messages/translate.d.ts +100 -0
- package/dist/messages/translate.js +249 -0
- package/dist/presence/declaration.d.ts +19 -0
- package/dist/presence/declaration.js +118 -0
- package/dist/presence/presence.d.ts +57 -0
- package/dist/presence/presence.js +27 -0
- package/dist/rest/errors.d.ts +55 -0
- package/dist/rest/errors.js +92 -0
- package/dist/rest/rate-limiter.d.ts +46 -0
- package/dist/rest/rate-limiter.js +153 -0
- package/dist/rest/rest.d.ts +63 -0
- package/dist/rest/rest.js +194 -0
- package/dist/structures/base.d.ts +48 -0
- package/dist/structures/base.js +84 -0
- package/dist/structures/channel.d.ts +380 -0
- package/dist/structures/channel.js +627 -0
- package/dist/structures/context.d.ts +30 -0
- package/dist/structures/context.js +7 -0
- package/dist/structures/emoji.d.ts +46 -0
- package/dist/structures/emoji.js +65 -0
- package/dist/structures/entities.d.ts +62 -0
- package/dist/structures/entities.js +226 -0
- package/dist/structures/guild.d.ts +348 -0
- package/dist/structures/guild.js +466 -0
- package/dist/structures/interaction.d.ts +197 -0
- package/dist/structures/interaction.js +396 -0
- package/dist/structures/invite.d.ts +38 -0
- package/dist/structures/invite.js +57 -0
- package/dist/structures/known.d.ts +28 -0
- package/dist/structures/known.js +58 -0
- package/dist/structures/member.d.ts +140 -0
- package/dist/structures/member.js +260 -0
- package/dist/structures/message.d.ts +197 -0
- package/dist/structures/message.js +268 -0
- package/dist/structures/payload.d.ts +91 -0
- package/dist/structures/payload.js +184 -0
- package/dist/structures/role.d.ts +69 -0
- package/dist/structures/role.js +112 -0
- package/dist/structures/user.d.ts +59 -0
- package/dist/structures/user.js +94 -0
- package/dist/structures/webhook.d.ts +46 -0
- package/dist/structures/webhook.js +79 -0
- package/dist/tasks/declaration.d.ts +16 -0
- package/dist/tasks/declaration.js +56 -0
- package/dist/tasks/schedule.d.ts +54 -0
- package/dist/tasks/schedule.js +236 -0
- package/dist/tasks/scheduler.d.ts +35 -0
- package/dist/tasks/scheduler.js +107 -0
- package/dist/tasks/task.d.ts +75 -0
- package/dist/tasks/task.js +27 -0
- package/dist/util/case.d.ts +13 -0
- package/dist/util/case.js +54 -0
- package/dist/util/duration.d.ts +8 -0
- package/dist/util/duration.js +29 -0
- package/dist/util/websocket.d.ts +9 -0
- package/dist/util/websocket.js +31 -0
- package/dist/voice/connection.d.ts +90 -0
- package/dist/voice/connection.js +494 -0
- package/dist/voice/dave.d.ts +109 -0
- package/dist/voice/dave.js +245 -0
- package/dist/voice/opus.d.ts +19 -0
- package/dist/voice/opus.js +273 -0
- package/dist/voice/session.d.ts +55 -0
- package/dist/voice/session.js +405 -0
- package/dist/voice/source.d.ts +22 -0
- package/dist/voice/source.js +195 -0
- package/dist/voice/transport.d.ts +43 -0
- package/dist/voice/transport.js +140 -0
- package/dist/voice/usage.d.ts +4 -0
- package/dist/voice/usage.js +33 -0
- package/package.json +26 -7
- package/vendor/dave/LICENSE +231 -0
- package/vendor/dave/libdave.d.mts +99 -0
- package/vendor/dave/libdave.mjs +2 -0
- package/vendor/dave/libdave.wasm +0 -0
- package/dist/components.d.ts +0 -97
- package/dist/components.js +0 -260
- package/dist/context.d.ts +0 -72
- package/dist/context.js +0 -273
- package/dist/discord/client.d.ts +0 -35
- package/dist/discord/client.js +0 -66
- package/dist/discord/constants.d.ts +0 -49
- package/dist/discord/constants.js +0 -61
- package/dist/discord/events.d.ts +0 -129
- package/dist/discord/events.js +0 -102
- package/dist/discord/gateway.d.ts +0 -61
- package/dist/discord/gateway.js +0 -320
- package/dist/discord/payloads.d.ts +0 -275
- package/dist/discord/payloads.js +0 -3
- package/dist/discord/presence.d.ts +0 -42
- package/dist/discord/presence.js +0 -51
- package/dist/discord/rest.d.ts +0 -49
- package/dist/discord/rest.js +0 -271
- package/dist/discord/types.d.ts +0 -334
- package/dist/discord/types.js +0 -4
- package/dist/events.d.ts +0 -94
- package/dist/events.js +0 -255
- package/dist/presence.d.ts +0 -92
- package/dist/presence.js +0 -250
- package/dist/structures/Channel.d.ts +0 -110
- package/dist/structures/Channel.js +0 -171
- package/dist/structures/Guild.d.ts +0 -70
- package/dist/structures/Guild.js +0 -105
- package/dist/structures/Interaction.d.ts +0 -113
- package/dist/structures/Interaction.js +0 -274
- package/dist/structures/Member.d.ts +0 -70
- package/dist/structures/Member.js +0 -124
- package/dist/structures/Message.d.ts +0 -87
- package/dist/structures/Message.js +0 -182
- package/dist/structures/Reaction.d.ts +0 -38
- package/dist/structures/Reaction.js +0 -64
- package/dist/structures/Role.d.ts +0 -43
- package/dist/structures/Role.js +0 -77
- package/dist/structures/User.d.ts +0 -41
- package/dist/structures/User.js +0 -79
- package/dist/structures/utils.d.ts +0 -72
- package/dist/structures/utils.js +0 -59
- package/dist/utils/errors.d.ts +0 -5
- package/dist/utils/errors.js +0 -28
- package/dist/utils/modules.d.ts +0 -39
- package/dist/utils/modules.js +0 -156
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// `modal()`, as the files of `src/components/modals/` import it from
|
|
2
|
+
// 'chapterjs': a form that opens in Discord, its fields typed in `run`.
|
|
3
|
+
// https://docs.discord.com/developers/components/using-modal-components
|
|
4
|
+
import { Limits } from '../discord/api.js';
|
|
5
|
+
import { ComponentType, TextInputStyle, } from '../discord/types/component.js';
|
|
6
|
+
import { isDynamic, resolveText, } from './component.js';
|
|
7
|
+
import { checkData, encodeCustomId, } from './custom-id.js';
|
|
8
|
+
import { createComponent } from './declared.js';
|
|
9
|
+
import { hasDynamicOption, readOptions, resolveOptions, } from './options.js';
|
|
10
|
+
import { readChannelTypes, readPlaceholder, readRange, SELECT_TYPES, } from './select.js';
|
|
11
|
+
/**
|
|
12
|
+
* Declares a form. Export the result from any file of `src/`: the name of
|
|
13
|
+
* the export is what tells the form apart, so you never write an id.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { modal } from 'chapterjs';
|
|
17
|
+
*
|
|
18
|
+
* export const feedback = modal({
|
|
19
|
+
* title: 'Feedback',
|
|
20
|
+
* fields: {
|
|
21
|
+
* text: { type: 'text', label: 'What do you think?', style: 'paragraph' },
|
|
22
|
+
* },
|
|
23
|
+
* async run({ interaction, fields }) {
|
|
24
|
+
* await interaction.reply({ content: `Thanks! You wrote: ${fields.text}`, ephemeral: true });
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function modal(config) {
|
|
30
|
+
return createComponent('modal', config, {
|
|
31
|
+
asPiece: true,
|
|
32
|
+
pieceKind: 'modal',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const fail = (message) => {
|
|
36
|
+
throw new TypeError(message);
|
|
37
|
+
};
|
|
38
|
+
const FIELD_TYPES = [
|
|
39
|
+
'text',
|
|
40
|
+
'select',
|
|
41
|
+
'user',
|
|
42
|
+
'role',
|
|
43
|
+
'mentionable',
|
|
44
|
+
'channel',
|
|
45
|
+
'files',
|
|
46
|
+
'radio',
|
|
47
|
+
'checkboxes',
|
|
48
|
+
'checkbox',
|
|
49
|
+
'note',
|
|
50
|
+
];
|
|
51
|
+
/** The keys each kind of field accepts. */
|
|
52
|
+
const FIELD_KEYS = {
|
|
53
|
+
text: ['style', 'placeholder', 'required', 'minLength', 'maxLength', 'value'],
|
|
54
|
+
select: ['options', 'placeholder', 'min', 'max', 'required'],
|
|
55
|
+
user: ['placeholder', 'min', 'max', 'required'],
|
|
56
|
+
role: ['placeholder', 'min', 'max', 'required'],
|
|
57
|
+
mentionable: ['placeholder', 'min', 'max', 'required'],
|
|
58
|
+
channel: ['placeholder', 'min', 'max', 'required', 'channelTypes'],
|
|
59
|
+
files: ['min', 'max', 'required', 'fileTypes'],
|
|
60
|
+
radio: ['options', 'required'],
|
|
61
|
+
checkboxes: ['options', 'min', 'max', 'required'],
|
|
62
|
+
checkbox: ['default'],
|
|
63
|
+
note: ['content'],
|
|
64
|
+
};
|
|
65
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
66
|
+
function checkText(what, value, max, { required = true, min = 1 } = {}) {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return required ? fail(`${what} is missing.`) : undefined;
|
|
69
|
+
// A function is checked when the form opens.
|
|
70
|
+
if (isDynamic(value))
|
|
71
|
+
return undefined;
|
|
72
|
+
if (typeof value !== 'string')
|
|
73
|
+
return fail(`${what} is a text, got ${typeof value}.`);
|
|
74
|
+
if (value.length < min || value.length > max) {
|
|
75
|
+
return fail(`${what} is ${value.length} characters long: Discord accepts between ${min} and ${max}.`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function checkBoolean(what, value, fallback) {
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
return fallback;
|
|
82
|
+
if (typeof value !== 'boolean')
|
|
83
|
+
return fail(`${what} is true or false, got ${JSON.stringify(value) ?? typeof value}.`);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
/** Checks one field of a form. */
|
|
87
|
+
export function readField(name, given) {
|
|
88
|
+
const what = `the field "${name}"`;
|
|
89
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
90
|
+
fail(`"${name}" can't be the name of a field: it is how your code reads it (fields.${name}), so use letters, digits and _ only.`);
|
|
91
|
+
}
|
|
92
|
+
if (!isRecord(given)) {
|
|
93
|
+
return fail(`${what} must be an object like { type: 'text', label: '...' }.`);
|
|
94
|
+
}
|
|
95
|
+
const type = given.type;
|
|
96
|
+
if (!FIELD_TYPES.includes(type)) {
|
|
97
|
+
fail(`The type of ${what} is ${JSON.stringify(given.type)}, which does not exist. Types are: ${FIELD_TYPES.join(', ')}.`);
|
|
98
|
+
}
|
|
99
|
+
for (const key of Object.keys(given)) {
|
|
100
|
+
const allowed = [
|
|
101
|
+
'type',
|
|
102
|
+
...(type === 'note' ? [] : ['label', 'description']),
|
|
103
|
+
...FIELD_KEYS[type],
|
|
104
|
+
];
|
|
105
|
+
if (!allowed.includes(key)) {
|
|
106
|
+
fail(`"${key}" is not something ${what} has (a ${type}). It can have: ${allowed.join(', ')}.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (type === 'note') {
|
|
110
|
+
checkText(`The content of ${what}`, given.content, Limits.TextDisplayTotal);
|
|
111
|
+
return {
|
|
112
|
+
name,
|
|
113
|
+
field: given,
|
|
114
|
+
options: [],
|
|
115
|
+
required: false,
|
|
116
|
+
min: 0,
|
|
117
|
+
max: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
checkText(`The label of ${what}`, given.label, Limits.LabelText);
|
|
121
|
+
checkText(`The description of ${what}`, given.description, Limits.LabelDescription, { required: false });
|
|
122
|
+
const required = checkBoolean(`"required" of ${what}`, given.required, true);
|
|
123
|
+
let options = [];
|
|
124
|
+
let range = { min: 1, max: 1 };
|
|
125
|
+
switch (type) {
|
|
126
|
+
case 'text': {
|
|
127
|
+
if (given.style !== undefined &&
|
|
128
|
+
given.style !== 'short' &&
|
|
129
|
+
given.style !== 'paragraph') {
|
|
130
|
+
fail(`The style of ${what} is 'short' or 'paragraph', got ${JSON.stringify(given.style)}.`);
|
|
131
|
+
}
|
|
132
|
+
checkText(`The placeholder of ${what}`, given.placeholder, Limits.TextInputPlaceholder, { required: false });
|
|
133
|
+
checkText(`The value of ${what}`, given.value, Limits.TextInputLength, {
|
|
134
|
+
required: false,
|
|
135
|
+
min: 0,
|
|
136
|
+
});
|
|
137
|
+
range = readRange(what, { min: given.minLength, max: given.maxLength }, Limits.TextInputLength, { min: 0, max: Limits.TextInputLength });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'select':
|
|
141
|
+
options = readOptions(what, given.options);
|
|
142
|
+
readPlaceholder(what, given.placeholder);
|
|
143
|
+
range = readRange(what, given, Limits.SelectValues, { min: 1, max: 1 });
|
|
144
|
+
break;
|
|
145
|
+
case 'user':
|
|
146
|
+
case 'role':
|
|
147
|
+
case 'mentionable':
|
|
148
|
+
case 'channel':
|
|
149
|
+
readPlaceholder(what, given.placeholder);
|
|
150
|
+
range = readRange(what, given, Limits.SelectValues, { min: 1, max: 1 });
|
|
151
|
+
if (type === 'channel')
|
|
152
|
+
readChannelTypes(what, given.channelTypes);
|
|
153
|
+
break;
|
|
154
|
+
case 'files':
|
|
155
|
+
range = readRange(what, given, Limits.FileUploadMax, { min: 1, max: 1 });
|
|
156
|
+
if (given.fileTypes !== undefined) {
|
|
157
|
+
const types = given.fileTypes;
|
|
158
|
+
if (!Array.isArray(types) ||
|
|
159
|
+
types.length === 0 ||
|
|
160
|
+
types.length > Limits.FileUploadTypes ||
|
|
161
|
+
!types.every(one => typeof one === 'string' &&
|
|
162
|
+
/^(image|video|audio|\.[\w-]+)$/.test(one))) {
|
|
163
|
+
fail(`"fileTypes" of ${what} is a list of 'image', 'video', 'audio' or extensions like '.pdf' (${Limits.FileUploadTypes} at most).`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
case 'radio':
|
|
168
|
+
options = readOptions(what, given.options, {
|
|
169
|
+
min: Limits.RadioOptionsMin,
|
|
170
|
+
max: Limits.RadioOptions,
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
case 'checkboxes':
|
|
174
|
+
options = readOptions(what, given.options, {
|
|
175
|
+
min: 1,
|
|
176
|
+
max: Limits.CheckboxOptions,
|
|
177
|
+
});
|
|
178
|
+
range = readRange(what, given, Limits.CheckboxOptions, {
|
|
179
|
+
min: 1,
|
|
180
|
+
max: options.length,
|
|
181
|
+
});
|
|
182
|
+
if (range.max > options.length) {
|
|
183
|
+
fail(`"max" of ${what} is ${range.max}, but it only has ${options.length} options.`);
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case 'checkbox':
|
|
187
|
+
checkBoolean(`"default" of ${what}`, given.default, false);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
name,
|
|
192
|
+
field: given,
|
|
193
|
+
options,
|
|
194
|
+
required,
|
|
195
|
+
...range,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** The label and the input of one field, as Discord takes them. */
|
|
199
|
+
function renderField(loaded, prefill, context) {
|
|
200
|
+
const { name, field, required, min, max } = loaded;
|
|
201
|
+
const what = `the field "${name}"`;
|
|
202
|
+
if (field.type === 'note') {
|
|
203
|
+
return {
|
|
204
|
+
type: ComponentType.TextDisplay,
|
|
205
|
+
content: resolveText(`The content of ${what}`, field.content, context, {
|
|
206
|
+
max: Limits.TextDisplayTotal,
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const placeholder = 'placeholder' in field
|
|
211
|
+
? resolveText(`The placeholder of ${what}`, field.placeholder, context, {
|
|
212
|
+
max: field.type === 'text'
|
|
213
|
+
? Limits.TextInputPlaceholder
|
|
214
|
+
: Limits.SelectPlaceholder,
|
|
215
|
+
})
|
|
216
|
+
: undefined;
|
|
217
|
+
const options = resolveOptions(what, loaded.options, context);
|
|
218
|
+
const base = { custom_id: name };
|
|
219
|
+
const picked = Array.isArray(prefill)
|
|
220
|
+
? prefill
|
|
221
|
+
: prefill !== undefined
|
|
222
|
+
? [prefill]
|
|
223
|
+
: undefined;
|
|
224
|
+
const withDefaults = (options) => options.map(option => {
|
|
225
|
+
const { default: initial, ...rest } = option;
|
|
226
|
+
const isDefault = picked ? picked.includes(option.value) : initial;
|
|
227
|
+
return isDefault ? { ...rest, default: true } : rest;
|
|
228
|
+
});
|
|
229
|
+
let component;
|
|
230
|
+
switch (field.type) {
|
|
231
|
+
case 'text':
|
|
232
|
+
component = {
|
|
233
|
+
type: ComponentType.TextInput,
|
|
234
|
+
...base,
|
|
235
|
+
style: field.style === 'paragraph'
|
|
236
|
+
? TextInputStyle.Paragraph
|
|
237
|
+
: TextInputStyle.Short,
|
|
238
|
+
...(min !== 0 ? { min_length: min } : {}),
|
|
239
|
+
...(max !== Limits.TextInputLength ? { max_length: max } : {}),
|
|
240
|
+
...(required ? {} : { required: false }),
|
|
241
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
242
|
+
...((prefill ?? field.value) !== undefined
|
|
243
|
+
? { value: (prefill ?? field.value) }
|
|
244
|
+
: {}),
|
|
245
|
+
};
|
|
246
|
+
break;
|
|
247
|
+
case 'select':
|
|
248
|
+
component = {
|
|
249
|
+
type: ComponentType.StringSelect,
|
|
250
|
+
...base,
|
|
251
|
+
options: withDefaults(options),
|
|
252
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
253
|
+
...(min !== 1 ? { min_values: min } : {}),
|
|
254
|
+
...(max !== 1 ? { max_values: max } : {}),
|
|
255
|
+
...(required ? {} : { required: false }),
|
|
256
|
+
};
|
|
257
|
+
break;
|
|
258
|
+
case 'user':
|
|
259
|
+
case 'role':
|
|
260
|
+
case 'mentionable':
|
|
261
|
+
case 'channel':
|
|
262
|
+
component = {
|
|
263
|
+
type: SELECT_TYPES[field.type],
|
|
264
|
+
...base,
|
|
265
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
266
|
+
...(min !== 1 ? { min_values: min } : {}),
|
|
267
|
+
...(max !== 1 ? { max_values: max } : {}),
|
|
268
|
+
...(required ? {} : { required: false }),
|
|
269
|
+
...(field.type === 'channel' && field.channelTypes
|
|
270
|
+
? { channel_types: [...field.channelTypes] }
|
|
271
|
+
: {}),
|
|
272
|
+
};
|
|
273
|
+
break;
|
|
274
|
+
case 'files':
|
|
275
|
+
component = {
|
|
276
|
+
type: ComponentType.FileUpload,
|
|
277
|
+
...base,
|
|
278
|
+
...(min !== 1 ? { min_values: min } : {}),
|
|
279
|
+
...(max !== 1 ? { max_values: max } : {}),
|
|
280
|
+
...(required ? {} : { required: false }),
|
|
281
|
+
...(field.fileTypes ? { file_types: [...field.fileTypes] } : {}),
|
|
282
|
+
};
|
|
283
|
+
break;
|
|
284
|
+
case 'radio':
|
|
285
|
+
component = {
|
|
286
|
+
type: ComponentType.RadioGroup,
|
|
287
|
+
...base,
|
|
288
|
+
options: withDefaults(options),
|
|
289
|
+
...(required ? {} : { required: false }),
|
|
290
|
+
};
|
|
291
|
+
break;
|
|
292
|
+
case 'checkboxes':
|
|
293
|
+
component = {
|
|
294
|
+
type: ComponentType.CheckboxGroup,
|
|
295
|
+
...base,
|
|
296
|
+
options: withDefaults(options),
|
|
297
|
+
...(min !== 1 ? { min_values: min } : {}),
|
|
298
|
+
...(max !== loaded.options.length ? { max_values: max } : {}),
|
|
299
|
+
...(required ? {} : { required: false }),
|
|
300
|
+
};
|
|
301
|
+
break;
|
|
302
|
+
case 'checkbox': {
|
|
303
|
+
const checked = prefill !== undefined ? prefill === true : field.default === true;
|
|
304
|
+
component = {
|
|
305
|
+
type: ComponentType.Checkbox,
|
|
306
|
+
...base,
|
|
307
|
+
...(checked ? { default: true } : {}),
|
|
308
|
+
};
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const description = resolveText(`The description of ${what}`, field.description, context, { max: Limits.LabelDescription });
|
|
313
|
+
return {
|
|
314
|
+
type: ComponentType.Label,
|
|
315
|
+
label: resolveText(`The label of ${what}`, field.label, context, {
|
|
316
|
+
max: Limits.LabelText,
|
|
317
|
+
}),
|
|
318
|
+
...(description !== undefined ? { description } : {}),
|
|
319
|
+
component,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/** Checks what a form is opened with. */
|
|
323
|
+
function readPrefill(loaded, name, given) {
|
|
324
|
+
if (given === undefined)
|
|
325
|
+
return {};
|
|
326
|
+
if (!isRecord(given))
|
|
327
|
+
fail(`The options of ${name} are an object like { values: { ... } }.`);
|
|
328
|
+
const options = given;
|
|
329
|
+
for (const key of Object.keys(options)) {
|
|
330
|
+
if (key !== 'values')
|
|
331
|
+
fail(`"${key}" is not something ${name} takes when opened. It can take: values.`);
|
|
332
|
+
}
|
|
333
|
+
if (options.values === undefined)
|
|
334
|
+
return {};
|
|
335
|
+
if (!isRecord(options.values))
|
|
336
|
+
fail(`"values" of ${name} is an object whose keys are the names of its fields.`);
|
|
337
|
+
const values = options.values;
|
|
338
|
+
for (const [key, value] of Object.entries(values)) {
|
|
339
|
+
const field = loaded.fields.find(one => one.name === key);
|
|
340
|
+
if (!field)
|
|
341
|
+
fail(`"${key}" is not a field of ${name}. Its fields are: ${loaded.fields.map(one => one.name).join(', ')}.`);
|
|
342
|
+
const { type } = field.field;
|
|
343
|
+
const bad = (expected) => fail(`The value given for the field "${key}" of ${name} is ${expected}, got ${JSON.stringify(value) ?? typeof value}.`);
|
|
344
|
+
const known = (picked) => {
|
|
345
|
+
if (!field.options.some(option => option.value === picked)) {
|
|
346
|
+
fail(`${JSON.stringify(picked)} is not an option of the field "${key}" of ${name}. Its options are: ${field.options.map(option => option.value).join(', ')}.`);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
switch (type) {
|
|
350
|
+
case 'text':
|
|
351
|
+
if (typeof value !== 'string' || value.length > Limits.TextInputLength)
|
|
352
|
+
bad(`a text of ${Limits.TextInputLength} characters at most`);
|
|
353
|
+
break;
|
|
354
|
+
case 'select':
|
|
355
|
+
case 'checkboxes':
|
|
356
|
+
if (!Array.isArray(value))
|
|
357
|
+
bad('a list of its options');
|
|
358
|
+
value.forEach(known);
|
|
359
|
+
break;
|
|
360
|
+
case 'radio':
|
|
361
|
+
known(value);
|
|
362
|
+
break;
|
|
363
|
+
case 'checkbox':
|
|
364
|
+
if (typeof value !== 'boolean')
|
|
365
|
+
bad('true or false');
|
|
366
|
+
break;
|
|
367
|
+
default:
|
|
368
|
+
fail(`The field "${key}" of ${name} is a ${type}: it can't be given a value when the form opens.`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return values;
|
|
372
|
+
}
|
|
373
|
+
/** The form as Discord takes it, for the data and the values given. */
|
|
374
|
+
export function renderModal(loaded, args) {
|
|
375
|
+
const hasData = Object.keys(loaded.data).length > 0;
|
|
376
|
+
const { name } = loaded;
|
|
377
|
+
if (args.length > (hasData ? 2 : 1)) {
|
|
378
|
+
fail(`${name} takes ${hasData ? 'its data, then options' : 'options'} at most.`);
|
|
379
|
+
}
|
|
380
|
+
const values = checkData(name, loaded.data, hasData ? args[0] : undefined);
|
|
381
|
+
const prefill = readPrefill(loaded, name, hasData ? args[1] : args[0]);
|
|
382
|
+
const make = (context) => ({
|
|
383
|
+
custom_id: encodeCustomId(loaded.name, loaded.data, values),
|
|
384
|
+
title: resolveText(`The title of ${name}`, loaded.title, context, {
|
|
385
|
+
max: Limits.ModalTitle,
|
|
386
|
+
}),
|
|
387
|
+
components: loaded.fields.map(field => renderField(field, prefill[field.name], context)),
|
|
388
|
+
});
|
|
389
|
+
// Texts computed when the form opens wait for the language of the person.
|
|
390
|
+
const dynamic = isDynamic(loaded.title) ||
|
|
391
|
+
loaded.fields.some(({ field, options }) => Object.values(field).some(isDynamic) || hasDynamicOption(options));
|
|
392
|
+
return {
|
|
393
|
+
kind: 'modal',
|
|
394
|
+
raw: dynamic
|
|
395
|
+
? t => make({ t, data: values })
|
|
396
|
+
: make({ t: undefined, data: values }),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RawSelectOption } from '../discord/types/component.js';
|
|
2
|
+
import type { EmojiInput } from '../structures/message.js';
|
|
3
|
+
import type { DataShape } from './custom-id.js';
|
|
4
|
+
import { type DynamicText, type TextContext } from './component.js';
|
|
5
|
+
/** One option of a menu, with everything Discord shows for it. */
|
|
6
|
+
export interface RichOption<Data extends DataShape = DataShape> {
|
|
7
|
+
/** What the person sees (100 characters at most): a text, or a function of `t` and the data. */
|
|
8
|
+
label: DynamicText<Data>;
|
|
9
|
+
/** What your code receives (100 characters at most). */
|
|
10
|
+
value: string;
|
|
11
|
+
/** A second line under the label (100 characters at most): a text, or a function of `t` and the data. */
|
|
12
|
+
description?: DynamicText<Data>;
|
|
13
|
+
/** An emoji before the label. */
|
|
14
|
+
emoji?: EmojiInput;
|
|
15
|
+
/** Shows the option as picked when the menu appears. */
|
|
16
|
+
default?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The options of a menu: a list of texts (each is shown and received as
|
|
20
|
+
* is), an object whose keys are what is shown and values what your code
|
|
21
|
+
* receives, or a list of `RichOption` for descriptions and emojis.
|
|
22
|
+
*/
|
|
23
|
+
export type SelectOptions<Data extends DataShape = DataShape> = readonly string[] | Readonly<Record<string, string>> | readonly RichOption<Data>[];
|
|
24
|
+
/** An option as a file declared it, its texts maybe computed when sent. */
|
|
25
|
+
export type LoadedOption = Omit<RawSelectOption, 'label' | 'description'> & {
|
|
26
|
+
label: DynamicText;
|
|
27
|
+
description?: DynamicText;
|
|
28
|
+
};
|
|
29
|
+
/** The options as Discord takes them, their texts computed for who reads. */
|
|
30
|
+
export declare function resolveOptions(what: string, options: readonly LoadedOption[], context: TextContext): RawSelectOption[];
|
|
31
|
+
/** Whether a text of an option is computed when the message is sent. */
|
|
32
|
+
export declare const hasDynamicOption: (options: readonly LoadedOption[]) => boolean;
|
|
33
|
+
/** What `run` receives for one picked option, typed from the declaration. */
|
|
34
|
+
export type OptionValue<Options extends SelectOptions> = Options extends readonly (infer Item)[] ? Item extends string ? Item : Item extends {
|
|
35
|
+
value: infer Value extends string;
|
|
36
|
+
} ? Value : never : Options extends Readonly<Record<string, infer Value extends string>> ? Value : never;
|
|
37
|
+
/** Checks the options of a menu and returns them as Discord takes them. */
|
|
38
|
+
export declare function readOptions(what: string, options: unknown, { min, max }?: {
|
|
39
|
+
min?: number;
|
|
40
|
+
max?: number;
|
|
41
|
+
}): LoadedOption[];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The options of a select menu, a radio group or a checkbox group, as files
|
|
2
|
+
// declare them: a list of texts, an object from what is shown to what the
|
|
3
|
+
// code receives, or a list of objects with a description and an emoji.
|
|
4
|
+
// Written once for the three.
|
|
5
|
+
import { Limits } from '../discord/api.js';
|
|
6
|
+
import { emojiOf } from './instance.js';
|
|
7
|
+
import { isDynamic, resolveText, } from './component.js';
|
|
8
|
+
/** The options as Discord takes them, their texts computed for who reads. */
|
|
9
|
+
export function resolveOptions(what, options, context) {
|
|
10
|
+
return options.map((option, index) => {
|
|
11
|
+
const { label, description, ...rest } = option;
|
|
12
|
+
const resolved = resolveText(`The label of option ${index + 1} of ${what}`, label, context, { max: Limits.SelectOptionText });
|
|
13
|
+
const second = resolveText(`The description of option ${index + 1} of ${what}`, description, context, { max: Limits.SelectOptionText });
|
|
14
|
+
return {
|
|
15
|
+
...rest,
|
|
16
|
+
label: resolved,
|
|
17
|
+
...(second !== undefined ? { description: second } : {}),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Whether a text of an option is computed when the message is sent. */
|
|
22
|
+
export const hasDynamicOption = (options) => options.some(option => isDynamic(option.label) || isDynamic(option.description));
|
|
23
|
+
const fail = (message) => {
|
|
24
|
+
throw new TypeError(message);
|
|
25
|
+
};
|
|
26
|
+
/** Checks the options of a menu and returns them as Discord takes them. */
|
|
27
|
+
export function readOptions(what, options, { min = 1, max = Limits.SelectOptions } = {}) {
|
|
28
|
+
const example = `options: ['Pizza', 'Pasta'], options: { 'Shown text': 'value' } or options: [{ label: '...', value: '...', description: '...' }]`;
|
|
29
|
+
let list;
|
|
30
|
+
if (Array.isArray(options)) {
|
|
31
|
+
list = options.map((item, index) => {
|
|
32
|
+
if (typeof item === 'string')
|
|
33
|
+
return { label: item, value: item };
|
|
34
|
+
if (typeof item !== 'object' || item === null) {
|
|
35
|
+
return fail(`Option ${index + 1} of ${what} is a text or an object like { label: '...', value: '...' }, got ${item === null ? 'null' : typeof item}.`);
|
|
36
|
+
}
|
|
37
|
+
const rich = item;
|
|
38
|
+
for (const key of Object.keys(rich)) {
|
|
39
|
+
if (!['label', 'value', 'description', 'emoji', 'default'].includes(key)) {
|
|
40
|
+
fail(`"${key}" is not something an option of ${what} has. It can have: label, value, description, emoji, default.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if ((typeof rich.label !== 'string' && !isDynamic(rich.label)) ||
|
|
44
|
+
typeof rich.value !== 'string') {
|
|
45
|
+
fail(`Option ${index + 1} of ${what} needs a label and a value, both texts.`);
|
|
46
|
+
}
|
|
47
|
+
const option = {
|
|
48
|
+
label: rich.label,
|
|
49
|
+
value: rich.value,
|
|
50
|
+
};
|
|
51
|
+
if (rich.description !== undefined) {
|
|
52
|
+
if (typeof rich.description !== 'string' &&
|
|
53
|
+
!isDynamic(rich.description))
|
|
54
|
+
fail(`The description of option ${index + 1} of ${what} is a text.`);
|
|
55
|
+
option.description = rich.description;
|
|
56
|
+
}
|
|
57
|
+
if (rich.emoji !== undefined)
|
|
58
|
+
option.emoji = emojiOf(rich.emoji, `option ${index + 1} of ${what}`);
|
|
59
|
+
if (rich.default !== undefined) {
|
|
60
|
+
if (typeof rich.default !== 'boolean')
|
|
61
|
+
fail(`"default" of option ${index + 1} of ${what} is true or false.`);
|
|
62
|
+
if (rich.default)
|
|
63
|
+
option.default = true;
|
|
64
|
+
}
|
|
65
|
+
return option;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else if (typeof options === 'object' && options !== null) {
|
|
69
|
+
list = Object.entries(options).map(([label, value]) => {
|
|
70
|
+
if (typeof value !== 'string') {
|
|
71
|
+
return fail(`The option ${JSON.stringify(label)} of ${what} must be a text (what your code receives), got ${JSON.stringify(value) ?? typeof value}.`);
|
|
72
|
+
}
|
|
73
|
+
return { label, value };
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return fail(`${what} needs its options: ${example}.`);
|
|
78
|
+
}
|
|
79
|
+
if (list.length < min || list.length > max) {
|
|
80
|
+
fail(`${what} has ${list.length} options: Discord accepts between ${min} and ${max}.`);
|
|
81
|
+
}
|
|
82
|
+
const values = new Set();
|
|
83
|
+
for (const option of list) {
|
|
84
|
+
for (const [field, text] of [
|
|
85
|
+
['label', option.label],
|
|
86
|
+
['value', option.value],
|
|
87
|
+
['description', option.description],
|
|
88
|
+
]) {
|
|
89
|
+
// A function is checked when the message is sent.
|
|
90
|
+
if (text === undefined || isDynamic(text))
|
|
91
|
+
continue;
|
|
92
|
+
if (text.trim() === '' || text.length > Limits.SelectOptionText) {
|
|
93
|
+
fail(`The ${field} of an option of ${what} is ${text.length} characters long: Discord accepts between 1 and ${Limits.SelectOptionText}.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (values.has(option.value))
|
|
97
|
+
fail(`${what} has the value ${JSON.stringify(option.value)} twice.`);
|
|
98
|
+
values.add(option.value);
|
|
99
|
+
}
|
|
100
|
+
return list;
|
|
101
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RawComponent } from '../discord/types/component.js';
|
|
2
|
+
import type { Translator } from '../messages/messages.js';
|
|
3
|
+
export interface RenderedComponents {
|
|
4
|
+
raw: RawComponent[];
|
|
5
|
+
/** Whether the message needs the IS_COMPONENTS_V2 flag. */
|
|
6
|
+
v2: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The components of a message, as Discord takes them.
|
|
10
|
+
* @see https://docs.discord.com/developers/components/reference#component-object
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderComponents(components: readonly unknown[], t?: Translator): RenderedComponents;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Turns the `components` of a message into what Discord receives: rows made
|
|
2
|
+
// by themselves, the whole checked against the limits of a message, and
|
|
3
|
+
// whether the message is built with components only (Components V2).
|
|
4
|
+
import { Limits } from '../discord/api.js';
|
|
5
|
+
import { ComponentType, } from '../discord/types/component.js';
|
|
6
|
+
import { renderedOf, resolve } from './instance.js';
|
|
7
|
+
import { missingTranslator } from '../messages/translate.js';
|
|
8
|
+
import { autoRows } from './layout.js';
|
|
9
|
+
/** Every component of a tree, the nested ones included. */
|
|
10
|
+
function count(components) {
|
|
11
|
+
let total = 0;
|
|
12
|
+
for (const component of components) {
|
|
13
|
+
total += 1;
|
|
14
|
+
if ('components' in component && Array.isArray(component.components)) {
|
|
15
|
+
total += count(component.components);
|
|
16
|
+
}
|
|
17
|
+
if ('accessory' in component)
|
|
18
|
+
total += 1;
|
|
19
|
+
}
|
|
20
|
+
return total;
|
|
21
|
+
}
|
|
22
|
+
/** The characters of every text of a tree. */
|
|
23
|
+
function textLength(components) {
|
|
24
|
+
let total = 0;
|
|
25
|
+
for (const component of components) {
|
|
26
|
+
if (component.type === ComponentType.TextDisplay) {
|
|
27
|
+
total += component.content.length;
|
|
28
|
+
}
|
|
29
|
+
else if ('components' in component &&
|
|
30
|
+
Array.isArray(component.components)) {
|
|
31
|
+
total += textLength(component.components);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return total;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The components of a message, as Discord takes them.
|
|
38
|
+
* @see https://docs.discord.com/developers/components/reference#component-object
|
|
39
|
+
*/
|
|
40
|
+
export function renderComponents(components, t = missingTranslator(null)) {
|
|
41
|
+
if (!Array.isArray(components)) {
|
|
42
|
+
throw new TypeError(`The components of a message are a list, got ${typeof components}.`);
|
|
43
|
+
}
|
|
44
|
+
const pieces = components.map((component, index) => renderedOf(component, `Component ${index + 1} of the message`));
|
|
45
|
+
const rows = autoRows(pieces, 'a message');
|
|
46
|
+
const v2 = rows.some(one => one.kind !== 'row');
|
|
47
|
+
// The texts of a piece may be computed now, for who will read the message.
|
|
48
|
+
const raw = rows.map(one => resolve(one, t).raw);
|
|
49
|
+
const total = count(raw);
|
|
50
|
+
if (total > Limits.MessageComponents) {
|
|
51
|
+
throw new RangeError(`The message has ${total} components once its rows are made: Discord accepts ${Limits.MessageComponents} at most.`);
|
|
52
|
+
}
|
|
53
|
+
const characters = textLength(raw);
|
|
54
|
+
if (characters > Limits.TextDisplayTotal) {
|
|
55
|
+
throw new RangeError(`The texts of the components of the message add up to ${characters} characters: Discord accepts ${Limits.TextDisplayTotal} at most.`);
|
|
56
|
+
}
|
|
57
|
+
return { raw, v2 };
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RawInteraction } from '../discord/types/interaction.js';
|
|
2
|
+
import { type Reporter } from '../interactions/dispatch.js';
|
|
3
|
+
import type { Context } from '../structures/context.js';
|
|
4
|
+
import type { LoadedComponent } from './declaration.js';
|
|
5
|
+
/** A component, with the file and the export it comes from (to report its errors). */
|
|
6
|
+
export interface ComponentEntry {
|
|
7
|
+
file: string;
|
|
8
|
+
export: string;
|
|
9
|
+
component: LoadedComponent;
|
|
10
|
+
}
|
|
11
|
+
export type ComponentRouterOptions = Reporter;
|
|
12
|
+
export declare class ComponentRouter {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(options: ComponentRouterOptions);
|
|
15
|
+
/** Replaces every component (after a load or a reload). */
|
|
16
|
+
set(entries: Iterable<ComponentEntry>): void;
|
|
17
|
+
/** Handles an Interaction Create event, if it is a component or a form. */
|
|
18
|
+
dispatch(ctx: Context, raw: RawInteraction): void;
|
|
19
|
+
}
|