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,249 @@
|
|
|
1
|
+
// The runtime of `t`: which language to take, and a text with its
|
|
2
|
+
// placeholders filled in. No dependency on the loader or on Discord.
|
|
3
|
+
import { Locale } from '../discord/types/common.js';
|
|
4
|
+
/** The forms a plural can have, in the order of the docs. */
|
|
5
|
+
export const PLURAL_FORMS = [
|
|
6
|
+
'zero',
|
|
7
|
+
'one',
|
|
8
|
+
'two',
|
|
9
|
+
'few',
|
|
10
|
+
'many',
|
|
11
|
+
'other',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Cuts a text around its placeholders, read from left to right: `{name}`
|
|
15
|
+
* is a placeholder, `{{` and `}}` are braces written as is, and a `{`
|
|
16
|
+
* with another `{` before its `}` is a brace too.
|
|
17
|
+
*/
|
|
18
|
+
export function compile(text) {
|
|
19
|
+
const parts = [];
|
|
20
|
+
const params = [];
|
|
21
|
+
let literal = '';
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < text.length) {
|
|
24
|
+
const char = text[i];
|
|
25
|
+
if (char === '{' && text[i + 1] === '{') {
|
|
26
|
+
literal += '{';
|
|
27
|
+
i += 2;
|
|
28
|
+
}
|
|
29
|
+
else if (char === '}' && text[i + 1] === '}') {
|
|
30
|
+
literal += '}';
|
|
31
|
+
i += 2;
|
|
32
|
+
}
|
|
33
|
+
else if (char === '{') {
|
|
34
|
+
const close = text.indexOf('}', i + 1);
|
|
35
|
+
const open = text.indexOf('{', i + 1);
|
|
36
|
+
if (close === -1 || (open !== -1 && open < close)) {
|
|
37
|
+
literal += '{';
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const name = text.slice(i + 1, close);
|
|
42
|
+
parts.push(literal, name);
|
|
43
|
+
if (!params.includes(name))
|
|
44
|
+
params.push(name);
|
|
45
|
+
literal = '';
|
|
46
|
+
i = close + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
literal += char;
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
parts.push(literal);
|
|
55
|
+
return { text, parts, params };
|
|
56
|
+
}
|
|
57
|
+
/** Puts the forms of a plural together: `count` picks one when filled. */
|
|
58
|
+
export function compilePlural(forms) {
|
|
59
|
+
const params = ['count'];
|
|
60
|
+
for (const form of forms.values()) {
|
|
61
|
+
for (const name of form.params) {
|
|
62
|
+
if (!params.includes(name))
|
|
63
|
+
params.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { forms, params };
|
|
67
|
+
}
|
|
68
|
+
/** A text of a language file, trusted, compiled. */
|
|
69
|
+
export function templateOf(text) {
|
|
70
|
+
if (typeof text === 'string')
|
|
71
|
+
return compile(text);
|
|
72
|
+
return compilePlural(new Map(Object.entries(text).map(([form, one]) => [
|
|
73
|
+
form,
|
|
74
|
+
compile(one),
|
|
75
|
+
])));
|
|
76
|
+
}
|
|
77
|
+
/** Whether a template is a plural. */
|
|
78
|
+
export const isPlural = (template) => 'forms' in template;
|
|
79
|
+
/** Every locale Discord knows. */
|
|
80
|
+
export const LOCALES = new Set(Object.values(Locale));
|
|
81
|
+
/**
|
|
82
|
+
* The language of the file to use for one asked: the same one, else one
|
|
83
|
+
* of the same language in another region (`en-GB` → `en-US`, `es-419` →
|
|
84
|
+
* `es-ES`, `zh-TW` → `zh-CN`), else the default.
|
|
85
|
+
*/
|
|
86
|
+
export function pickLocale(messages, locale) {
|
|
87
|
+
if (locale === null)
|
|
88
|
+
return messages.default;
|
|
89
|
+
if (messages.locales.has(locale))
|
|
90
|
+
return locale;
|
|
91
|
+
const language = locale.split('-')[0];
|
|
92
|
+
for (const known of messages.locales.keys()) {
|
|
93
|
+
if (known.split('-')[0] === language)
|
|
94
|
+
return known;
|
|
95
|
+
}
|
|
96
|
+
return messages.default;
|
|
97
|
+
}
|
|
98
|
+
const numberFormats = new Map();
|
|
99
|
+
const pluralRules = new Map();
|
|
100
|
+
/** How a language writes a number: `1,234` in English, `1 234` in French. */
|
|
101
|
+
function numberFormat(locale) {
|
|
102
|
+
let format = numberFormats.get(locale);
|
|
103
|
+
if (!format) {
|
|
104
|
+
try {
|
|
105
|
+
format = new Intl.NumberFormat(locale);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
format = new Intl.NumberFormat('en-US');
|
|
109
|
+
}
|
|
110
|
+
numberFormats.set(locale, format);
|
|
111
|
+
}
|
|
112
|
+
return format;
|
|
113
|
+
}
|
|
114
|
+
/** Which form of a plural a language uses for a number. */
|
|
115
|
+
function pluralRule(locale) {
|
|
116
|
+
let rules = pluralRules.get(locale);
|
|
117
|
+
if (!rules) {
|
|
118
|
+
try {
|
|
119
|
+
rules = new Intl.PluralRules(locale);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
rules = new Intl.PluralRules('en-US');
|
|
123
|
+
}
|
|
124
|
+
pluralRules.set(locale, rules);
|
|
125
|
+
}
|
|
126
|
+
return rules;
|
|
127
|
+
}
|
|
128
|
+
/** A value where a placeholder is: a number the way the language writes it. */
|
|
129
|
+
const show = (value, locale) => typeof value === 'number'
|
|
130
|
+
? numberFormat(locale).format(value)
|
|
131
|
+
: String(value);
|
|
132
|
+
/**
|
|
133
|
+
* Fills the placeholders of a template, with an error naming a missing
|
|
134
|
+
* one. Numbers are written the way `locale` writes them, and a plural
|
|
135
|
+
* takes the form that language uses for `count`.
|
|
136
|
+
*/
|
|
137
|
+
export function fill(key, template, params, locale = 'en-US') {
|
|
138
|
+
if (template.params.length === 0)
|
|
139
|
+
return template.text;
|
|
140
|
+
const missing = template.params.filter(name => params?.[name] === undefined);
|
|
141
|
+
if (missing.length > 0) {
|
|
142
|
+
throw new TypeError(`The message "${key}" needs ${missing.map(name => `{${name}}`).join(', ')}: t('${key}', { ${template.params.join(', ')} })`);
|
|
143
|
+
}
|
|
144
|
+
if (isPlural(template)) {
|
|
145
|
+
const count = params.count;
|
|
146
|
+
if (typeof count !== 'number') {
|
|
147
|
+
throw new TypeError(`The message "${key}" is a plural: its {count} is a number, got ${JSON.stringify(count)}.`);
|
|
148
|
+
}
|
|
149
|
+
const form = pluralRule(locale).select(count);
|
|
150
|
+
template = template.forms.get(form) ?? template.forms.get('other');
|
|
151
|
+
}
|
|
152
|
+
let text = '';
|
|
153
|
+
template.parts.forEach((part, index) => {
|
|
154
|
+
text += index % 2 === 0 ? part : show(params[part], locale);
|
|
155
|
+
});
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
/** The `t` of each language asked, made once per set of messages. */
|
|
159
|
+
const translators = new WeakMap();
|
|
160
|
+
/** `t` for one language of the file, the same object every time. */
|
|
161
|
+
export function translatorFor(messages, locale) {
|
|
162
|
+
let made = translators.get(messages);
|
|
163
|
+
if (!made)
|
|
164
|
+
translators.set(messages, (made = new Map()));
|
|
165
|
+
const asked = locale ?? '';
|
|
166
|
+
const known = made.get(asked);
|
|
167
|
+
if (known)
|
|
168
|
+
return known;
|
|
169
|
+
const picked = pickLocale(messages, locale);
|
|
170
|
+
const texts = messages.locales.get(picked);
|
|
171
|
+
// A text the language does not have is the default language's.
|
|
172
|
+
const base = messages.locales.get(messages.default);
|
|
173
|
+
const t = ((key, params) => {
|
|
174
|
+
const own = texts.get(key);
|
|
175
|
+
if (own)
|
|
176
|
+
return fill(key, own, params, picked);
|
|
177
|
+
const template = base.get(key);
|
|
178
|
+
if (!template) {
|
|
179
|
+
throw new TypeError(`There is no message "${key}" in your languages. They have: ${[...base.keys()].join(', ')}.`);
|
|
180
|
+
}
|
|
181
|
+
return fill(key, template, params, messages.default);
|
|
182
|
+
});
|
|
183
|
+
Object.defineProperties(t, {
|
|
184
|
+
locale: { value: picked, enumerable: true },
|
|
185
|
+
in: {
|
|
186
|
+
// `t.in('fr')` is a `t`, `t.in('fr', 'welcome', { name })` a text.
|
|
187
|
+
value: (other, key, params) => key === undefined
|
|
188
|
+
? translatorFor(messages, other)
|
|
189
|
+
: translatorFor(messages, other)(key, params),
|
|
190
|
+
enumerable: false,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
Object.freeze(t);
|
|
194
|
+
made.set(asked, t);
|
|
195
|
+
return t;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The language of those who will read what is sent: the person when they
|
|
199
|
+
* alone see it (an ephemeral answer, a private message), else the language
|
|
200
|
+
* of the server when Discord knows it (it only does for Community servers:
|
|
201
|
+
* elsewhere `preferred_locale` is always `en-US`), else none, which means
|
|
202
|
+
* the default language of the file.
|
|
203
|
+
* @see https://docs.discord.com/developers/resources/guild#guild-object-guild-structure
|
|
204
|
+
*/
|
|
205
|
+
export function audienceLocale({ person, guild, ephemeral, }) {
|
|
206
|
+
if (ephemeral || !guild)
|
|
207
|
+
return person ?? null;
|
|
208
|
+
return guild.features.includes('COMMUNITY') ? guild.preferredLocale : null;
|
|
209
|
+
}
|
|
210
|
+
const missing = new Map();
|
|
211
|
+
/** A `t` for a project without a language: it says so when called. */
|
|
212
|
+
export function missingTranslator(locale) {
|
|
213
|
+
const shown = locale ?? 'en-US';
|
|
214
|
+
const known = missing.get(shown);
|
|
215
|
+
if (known)
|
|
216
|
+
return known;
|
|
217
|
+
const t = (() => {
|
|
218
|
+
throw new TypeError("This project has no language: declare one with language({ locale: 'en-US', texts: { ... } }) to use t().");
|
|
219
|
+
});
|
|
220
|
+
Object.defineProperties(t, {
|
|
221
|
+
locale: { value: shown, enumerable: true },
|
|
222
|
+
in: {
|
|
223
|
+
value: (_other, key) => key === undefined ? t : t(),
|
|
224
|
+
enumerable: false,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
Object.freeze(t);
|
|
228
|
+
missing.set(shown, t);
|
|
229
|
+
return t;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* `t` for what the bot sends, in the language of who will read it. Without
|
|
233
|
+
* a language, a `t` that explains it is missing when called: the
|
|
234
|
+
* texts of a component may still be functions of it.
|
|
235
|
+
*/
|
|
236
|
+
export function translatorOf(ctx, audience) {
|
|
237
|
+
const locale = audienceLocale(audience);
|
|
238
|
+
return ctx.messages
|
|
239
|
+
? translatorFor(ctx.messages, locale)
|
|
240
|
+
: missingTranslator(locale);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* What a handler receives about translations: `t` in the language asked
|
|
244
|
+
* when the project has a language, nothing otherwise. Spread it
|
|
245
|
+
* into the context of any handler.
|
|
246
|
+
*/
|
|
247
|
+
export function translation(ctx, locale) {
|
|
248
|
+
return ctx.messages ? { t: translatorFor(ctx.messages, locale) } : {};
|
|
249
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Declaration } from '../loader/loader.js';
|
|
2
|
+
import { type RawGatewayPresenceUpdate } from '../discord/types/gateway-events.js';
|
|
3
|
+
/** The presence, checked: what the gateway sends. */
|
|
4
|
+
export interface LoadedPresence {
|
|
5
|
+
/** The Update Presence payload (also sent with Identify). */
|
|
6
|
+
readonly raw: RawGatewayPresenceUpdate;
|
|
7
|
+
/** What tells one presence from another: the payload, as text. */
|
|
8
|
+
readonly key: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What a bot without a presence shows: online, doing nothing. Sent when
|
|
12
|
+
* the declaration is removed while the bot runs.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_PRESENCE: RawGatewayPresenceUpdate;
|
|
15
|
+
/**
|
|
16
|
+
* The presence of the bot, declared once anywhere in `src/` with
|
|
17
|
+
* `presence()` and exported.
|
|
18
|
+
*/
|
|
19
|
+
export declare const presenceDeclaration: Declaration<LoadedPresence>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// What a file declares with `presence()`: what the bot shows under its
|
|
2
|
+
// name. It is checked when it loads and turned into what the gateway
|
|
3
|
+
// sends, so a wrong status or type is explained before the bot connects.
|
|
4
|
+
import { ActivityType, } from '../discord/types/gateway-events.js';
|
|
5
|
+
import { isPresenceDeclaration } from './presence.js';
|
|
6
|
+
const fail = message => {
|
|
7
|
+
throw new TypeError(message);
|
|
8
|
+
};
|
|
9
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
const KEYS = ['status', 'activity'];
|
|
11
|
+
const ACTIVITY_KEYS = ['type', 'name', 'url'];
|
|
12
|
+
/**
|
|
13
|
+
* The statuses a file may give.
|
|
14
|
+
* @see https://docs.discord.com/developers/events/gateway-events#update-presence-status-types
|
|
15
|
+
*/
|
|
16
|
+
const STATUSES = ['online', 'idle', 'dnd', 'invisible'];
|
|
17
|
+
/**
|
|
18
|
+
* The activity types, by the word a file writes.
|
|
19
|
+
* @see https://docs.discord.com/developers/events/gateway-events#activity-object-activity-types
|
|
20
|
+
*/
|
|
21
|
+
const TYPES = {
|
|
22
|
+
playing: ActivityType.Playing,
|
|
23
|
+
streaming: ActivityType.Streaming,
|
|
24
|
+
listening: ActivityType.Listening,
|
|
25
|
+
watching: ActivityType.Watching,
|
|
26
|
+
custom: ActivityType.Custom,
|
|
27
|
+
competing: ActivityType.Competing,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* "The streaming type currently only supports Twitch and YouTube. Only
|
|
31
|
+
* https://twitch.tv/ and https://youtube.com/ urls will work."
|
|
32
|
+
* @see https://docs.discord.com/developers/events/gateway-events#activity-object-activity-types
|
|
33
|
+
*/
|
|
34
|
+
const STREAM_URL = /^https:\/\/(?:www\.)?(?:twitch\.tv|youtube\.com)\//;
|
|
35
|
+
const list = (words) => words.map(word => `'${word}'`).join(', ');
|
|
36
|
+
const EXAMPLE = `presence({ status: 'online', activity: { type: 'watching', name: 'over the server' } })`;
|
|
37
|
+
/**
|
|
38
|
+
* What a bot without a presence shows: online, doing nothing. Sent when
|
|
39
|
+
* the declaration is removed while the bot runs.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_PRESENCE = Object.freeze({
|
|
42
|
+
since: null,
|
|
43
|
+
activities: [],
|
|
44
|
+
status: 'online',
|
|
45
|
+
afk: false,
|
|
46
|
+
});
|
|
47
|
+
/** Turns a checked config into what the gateway sends. */
|
|
48
|
+
function toRaw(config) {
|
|
49
|
+
const activities = [];
|
|
50
|
+
if (config.activity) {
|
|
51
|
+
const { type, name, url } = config.activity;
|
|
52
|
+
activities.push(type === 'custom'
|
|
53
|
+
? // A custom status shows its `state`; `name` is not shown but required.
|
|
54
|
+
{ name: 'Custom Status', type: TYPES.custom, state: name }
|
|
55
|
+
: { name, type: TYPES[type], ...(url ? { url } : {}) });
|
|
56
|
+
}
|
|
57
|
+
// `since` and `afk` are about a person's client going idle: a bot is
|
|
58
|
+
// never away from its keyboard.
|
|
59
|
+
return {
|
|
60
|
+
since: null,
|
|
61
|
+
activities,
|
|
62
|
+
status: config.status ?? 'online',
|
|
63
|
+
afk: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The presence of the bot, declared once anywhere in `src/` with
|
|
68
|
+
* `presence()` and exported.
|
|
69
|
+
*/
|
|
70
|
+
export const presenceDeclaration = {
|
|
71
|
+
one: 'presence',
|
|
72
|
+
many: 'presences',
|
|
73
|
+
is: isPresenceDeclaration,
|
|
74
|
+
list: false,
|
|
75
|
+
read(value) {
|
|
76
|
+
const config = value.config;
|
|
77
|
+
if (!isRecord(config)) {
|
|
78
|
+
return fail(`presence() needs an object: ${EXAMPLE}`);
|
|
79
|
+
}
|
|
80
|
+
for (const key of Object.keys(config)) {
|
|
81
|
+
if (!KEYS.includes(key)) {
|
|
82
|
+
fail(`"${key}" is not something a presence has. It can have: ${KEYS.join(', ')}.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const { status, activity } = config;
|
|
86
|
+
if (status !== undefined &&
|
|
87
|
+
!STATUSES.includes(status)) {
|
|
88
|
+
fail(`"status" is ${JSON.stringify(status)}: it can be ${list(STATUSES)}.`);
|
|
89
|
+
}
|
|
90
|
+
if (activity !== undefined) {
|
|
91
|
+
if (!isRecord(activity)) {
|
|
92
|
+
fail(`"activity" is what the bot is doing, an object: activity: { type: 'playing', name: '/help' }`);
|
|
93
|
+
}
|
|
94
|
+
for (const key of Object.keys(activity)) {
|
|
95
|
+
if (!ACTIVITY_KEYS.includes(key)) {
|
|
96
|
+
fail(`"${key}" is not something an activity has. It can have: ${ACTIVITY_KEYS.join(', ')}.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { type, name, url } = activity;
|
|
100
|
+
if (typeof type !== 'string' || !(type in TYPES)) {
|
|
101
|
+
fail(`The "type" of the activity is ${JSON.stringify(type)}: it can be ${list(Object.keys(TYPES))}.`);
|
|
102
|
+
}
|
|
103
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
104
|
+
fail(`The "name" of the activity is the text shown under the name of the bot: it can't be empty.`);
|
|
105
|
+
}
|
|
106
|
+
if (type === 'streaming') {
|
|
107
|
+
if (typeof url !== 'string' || !STREAM_URL.test(url)) {
|
|
108
|
+
fail(`A "streaming" activity needs the "url" of the stream, and Discord only accepts a https://twitch.tv/ or a https://youtube.com/ link${typeof url === 'string' ? ` (got ${JSON.stringify(url)})` : ''}.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (url !== undefined) {
|
|
112
|
+
fail(`"url" only goes with a "streaming" activity: a ${JSON.stringify(type)} activity has no link.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const raw = toRaw(config);
|
|
116
|
+
return { raw, key: JSON.stringify(raw) };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status of the bot: `'online'` (green), `'idle'` (orange), `'dnd'`
|
|
3
|
+
* (red, "do not disturb") or `'invisible'` (shown as offline).
|
|
4
|
+
*/
|
|
5
|
+
export type PresenceStatusName = 'online' | 'idle' | 'dnd' | 'invisible';
|
|
6
|
+
/**
|
|
7
|
+
* What the bot is doing, as Discord shows it: "Playing {name}",
|
|
8
|
+
* "Listening to {name}", "Watching {name}", "Competing in {name}", or
|
|
9
|
+
* the text itself with `'custom'`.
|
|
10
|
+
*/
|
|
11
|
+
export type ActivityKind = 'playing' | 'listening' | 'watching' | 'competing' | 'custom';
|
|
12
|
+
/** An activity that is not a stream. */
|
|
13
|
+
export interface PlainActivity {
|
|
14
|
+
type: ActivityKind;
|
|
15
|
+
/** What is shown after the type (or alone with `'custom'`). */
|
|
16
|
+
name: string;
|
|
17
|
+
url?: never;
|
|
18
|
+
}
|
|
19
|
+
/** A stream: "Streaming {name}", with a link to it. */
|
|
20
|
+
export interface StreamingActivity {
|
|
21
|
+
type: 'streaming';
|
|
22
|
+
/** What is shown after "Streaming". */
|
|
23
|
+
name: string;
|
|
24
|
+
/** The stream: Discord only accepts a `https://twitch.tv/` or a `https://youtube.com/` link. */
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
/** What the bot is doing. */
|
|
28
|
+
export type Activity = PlainActivity | StreamingActivity;
|
|
29
|
+
/** What a file gives to `presence()`. */
|
|
30
|
+
export interface PresenceConfig {
|
|
31
|
+
/** The status of the bot. `'online'` by default. */
|
|
32
|
+
status?: PresenceStatusName;
|
|
33
|
+
/** What the bot is doing. Nothing by default. */
|
|
34
|
+
activity?: Activity;
|
|
35
|
+
}
|
|
36
|
+
/** What `presence()` returns: a declaration the framework finds in the exports of a file. */
|
|
37
|
+
export interface PresenceDeclaration {
|
|
38
|
+
/** What the file gave to `presence()`, not checked yet. */
|
|
39
|
+
readonly config: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Declares the presence of the bot: its status and what it is doing, shown
|
|
43
|
+
* under its name. Export the result from any file of `src/`, once; the bot
|
|
44
|
+
* has it as soon as it is connected.
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { presence } from 'chapterjs';
|
|
48
|
+
*
|
|
49
|
+
* export default presence({
|
|
50
|
+
* status: 'online',
|
|
51
|
+
* activity: { type: 'watching', name: 'over the server' },
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function presence(config: PresenceConfig): PresenceDeclaration;
|
|
56
|
+
/** Whether a value was made by `presence()`. */
|
|
57
|
+
export declare function isPresenceDeclaration(value: unknown): value is PresenceDeclaration;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// `presence()`, as user files import it from 'chapterjs': what the bot
|
|
2
|
+
// shows under its name, its status and what it is doing.
|
|
3
|
+
// https://docs.discord.com/developers/events/gateway-events#update-presence
|
|
4
|
+
const BRAND = Symbol.for('chapterjs.presence');
|
|
5
|
+
/**
|
|
6
|
+
* Declares the presence of the bot: its status and what it is doing, shown
|
|
7
|
+
* under its name. Export the result from any file of `src/`, once; the bot
|
|
8
|
+
* has it as soon as it is connected.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { presence } from 'chapterjs';
|
|
12
|
+
*
|
|
13
|
+
* export default presence({
|
|
14
|
+
* status: 'online',
|
|
15
|
+
* activity: { type: 'watching', name: 'over the server' },
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function presence(config) {
|
|
20
|
+
return Object.freeze({ [BRAND]: true, config });
|
|
21
|
+
}
|
|
22
|
+
/** Whether a value was made by `presence()`. */
|
|
23
|
+
export function isPresenceDeclaration(value) {
|
|
24
|
+
return (typeof value === 'object' &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
value[BRAND] === true);
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { HttpMethod } from '../discord/endpoint.js';
|
|
2
|
+
/** One problem Discord found in a field of the request. */
|
|
3
|
+
export interface DiscordFieldError {
|
|
4
|
+
/** Where the problem is: `embeds[0].title`, or `` for the whole request. */
|
|
5
|
+
path: string;
|
|
6
|
+
/** The kind of problem, as Discord names it (`BASE_TYPE_MAX_LENGTH`). */
|
|
7
|
+
code: string;
|
|
8
|
+
/** What is wrong, in English. */
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Turns the nested `errors` object of an error response into a flat list.
|
|
13
|
+
* A problem is an `_errors` array placed under the key of the field, at any
|
|
14
|
+
* depth; array items are keyed by their index.
|
|
15
|
+
* @see https://docs.discord.com/developers/reference#error-messages
|
|
16
|
+
*/
|
|
17
|
+
export declare function flattenFieldErrors(errors: unknown, path?: string): DiscordFieldError[];
|
|
18
|
+
/** Discord answered a request with an error. */
|
|
19
|
+
export declare class DiscordApiError extends Error {
|
|
20
|
+
readonly name = "DiscordApiError";
|
|
21
|
+
/** The HTTP status of the response (403, 404...). */
|
|
22
|
+
readonly status: number;
|
|
23
|
+
/**
|
|
24
|
+
* The JSON error code of Discord (50013 for "Missing Permissions"...), or
|
|
25
|
+
* 0 when the response had none.
|
|
26
|
+
* @see https://docs.discord.com/developers/topics/opcodes-and-status-codes#json-json-error-codes
|
|
27
|
+
*/
|
|
28
|
+
readonly code: number;
|
|
29
|
+
/** The fields of the request Discord refused, when it says which. */
|
|
30
|
+
readonly fieldErrors: readonly DiscordFieldError[];
|
|
31
|
+
/** The HTTP method of the request that failed. */
|
|
32
|
+
readonly method: HttpMethod;
|
|
33
|
+
/** The path of the endpoint as written in the documentation. */
|
|
34
|
+
readonly route: string;
|
|
35
|
+
constructor(input: {
|
|
36
|
+
status: number;
|
|
37
|
+
method: HttpMethod;
|
|
38
|
+
route: string;
|
|
39
|
+
body: unknown;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** A request could not reach Discord, or Discord kept failing. */
|
|
43
|
+
export declare class DiscordUnavailableError extends Error {
|
|
44
|
+
readonly name = "DiscordUnavailableError";
|
|
45
|
+
/** The HTTP method of the request that failed. */
|
|
46
|
+
readonly method: HttpMethod;
|
|
47
|
+
/** The path of the endpoint as written in the documentation. */
|
|
48
|
+
readonly route: string;
|
|
49
|
+
constructor(method: HttpMethod, route: string, cause: unknown);
|
|
50
|
+
}
|
|
51
|
+
/** The bot token was refused: no request can succeed until it is fixed. */
|
|
52
|
+
export declare class InvalidTokenError extends Error {
|
|
53
|
+
readonly name = "InvalidTokenError";
|
|
54
|
+
constructor();
|
|
55
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { JSON_ERROR_CODES } from '../discord/codes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Turns the nested `errors` object of an error response into a flat list.
|
|
4
|
+
* A problem is an `_errors` array placed under the key of the field, at any
|
|
5
|
+
* depth; array items are keyed by their index.
|
|
6
|
+
* @see https://docs.discord.com/developers/reference#error-messages
|
|
7
|
+
*/
|
|
8
|
+
export function flattenFieldErrors(errors, path = '') {
|
|
9
|
+
if (typeof errors !== 'object' || errors === null)
|
|
10
|
+
return [];
|
|
11
|
+
const found = [];
|
|
12
|
+
for (const [key, value] of Object.entries(errors)) {
|
|
13
|
+
if (key === '_errors') {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
continue;
|
|
16
|
+
for (const error of value) {
|
|
17
|
+
found.push({
|
|
18
|
+
path,
|
|
19
|
+
code: String(error?.code ?? ''),
|
|
20
|
+
message: String(error?.message ?? ''),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const child = /^\d+$/.test(key)
|
|
26
|
+
? `${path}[${key}]`
|
|
27
|
+
: path === ''
|
|
28
|
+
? key
|
|
29
|
+
: `${path}.${key}`;
|
|
30
|
+
found.push(...flattenFieldErrors(value, child));
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
/** Discord answered a request with an error. */
|
|
35
|
+
export class DiscordApiError extends Error {
|
|
36
|
+
name = 'DiscordApiError';
|
|
37
|
+
/** The HTTP status of the response (403, 404...). */
|
|
38
|
+
status;
|
|
39
|
+
/**
|
|
40
|
+
* The JSON error code of Discord (50013 for "Missing Permissions"...), or
|
|
41
|
+
* 0 when the response had none.
|
|
42
|
+
* @see https://docs.discord.com/developers/topics/opcodes-and-status-codes#json-json-error-codes
|
|
43
|
+
*/
|
|
44
|
+
code;
|
|
45
|
+
/** The fields of the request Discord refused, when it says which. */
|
|
46
|
+
fieldErrors;
|
|
47
|
+
/** The HTTP method of the request that failed. */
|
|
48
|
+
method;
|
|
49
|
+
/** The path of the endpoint as written in the documentation. */
|
|
50
|
+
route;
|
|
51
|
+
constructor(input) {
|
|
52
|
+
const body = typeof input.body === 'object' && input.body !== null
|
|
53
|
+
? input.body
|
|
54
|
+
: {};
|
|
55
|
+
const code = typeof body.code === 'number' ? body.code : 0;
|
|
56
|
+
const fieldErrors = flattenFieldErrors(body.errors);
|
|
57
|
+
const reason = typeof body.message === 'string' && body.message !== ''
|
|
58
|
+
? body.message
|
|
59
|
+
: ((typeof body.code === 'number'
|
|
60
|
+
? JSON_ERROR_CODES[code]
|
|
61
|
+
: undefined) ?? `HTTP ${input.status}`);
|
|
62
|
+
const details = fieldErrors
|
|
63
|
+
.map(error => `\n ${error.path || 'request'}: ${error.message}`)
|
|
64
|
+
.join('');
|
|
65
|
+
super(`Discord refused ${input.method} ${input.route}: ${reason} (${code || input.status})${details}`);
|
|
66
|
+
this.status = input.status;
|
|
67
|
+
this.code = code;
|
|
68
|
+
this.fieldErrors = fieldErrors;
|
|
69
|
+
this.method = input.method;
|
|
70
|
+
this.route = input.route;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** A request could not reach Discord, or Discord kept failing. */
|
|
74
|
+
export class DiscordUnavailableError extends Error {
|
|
75
|
+
name = 'DiscordUnavailableError';
|
|
76
|
+
/** The HTTP method of the request that failed. */
|
|
77
|
+
method;
|
|
78
|
+
/** The path of the endpoint as written in the documentation. */
|
|
79
|
+
route;
|
|
80
|
+
constructor(method, route, cause) {
|
|
81
|
+
super(`Discord could not be reached for ${method} ${route}. Check your internet connection and https://discordstatus.com, then try again.`, { cause });
|
|
82
|
+
this.method = method;
|
|
83
|
+
this.route = route;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The bot token was refused: no request can succeed until it is fixed. */
|
|
87
|
+
export class InvalidTokenError extends Error {
|
|
88
|
+
name = 'InvalidTokenError';
|
|
89
|
+
constructor() {
|
|
90
|
+
super('Discord refused the bot token. Copy it again from the Developer Portal (your application → Bot → Reset Token) into BOT_TOKEN in your .env file.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** What the headers of a response say about the rate limit of its route. */
|
|
2
|
+
export interface RateLimitHeaders {
|
|
3
|
+
/** `X-RateLimit-Bucket`: routes sharing a limit share this value. */
|
|
4
|
+
bucket: string | null;
|
|
5
|
+
/** `X-RateLimit-Remaining` */
|
|
6
|
+
remaining: number | null;
|
|
7
|
+
/** `X-RateLimit-Reset-After`, in seconds. */
|
|
8
|
+
resetAfter: number | null;
|
|
9
|
+
}
|
|
10
|
+
/** A 429 response. */
|
|
11
|
+
export interface RateLimitHit {
|
|
12
|
+
/** `Retry-After` / `retry_after`, in seconds. */
|
|
13
|
+
retryAfter: number;
|
|
14
|
+
/** `X-RateLimit-Scope`: `user`, `global` or `shared`. */
|
|
15
|
+
scope: string | null;
|
|
16
|
+
/** `X-RateLimit-Global` */
|
|
17
|
+
global: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function readRateLimitHeaders(headers: Headers): RateLimitHeaders;
|
|
20
|
+
export interface RateLimiterOptions {
|
|
21
|
+
/** Requests per second for the whole bot. Default: 50. */
|
|
22
|
+
globalLimit?: number;
|
|
23
|
+
/** Called every time a request has to wait. */
|
|
24
|
+
onWait?: (info: {
|
|
25
|
+
route: string;
|
|
26
|
+
ms: number;
|
|
27
|
+
global: boolean;
|
|
28
|
+
}) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare class RateLimiter {
|
|
31
|
+
#private;
|
|
32
|
+
constructor(options?: RateLimiterOptions);
|
|
33
|
+
/**
|
|
34
|
+
* Runs `send` when the limits allow it. Requests of the same bucket run
|
|
35
|
+
* one after the other; different buckets run in parallel. `send` reports
|
|
36
|
+
* what the response said through `update` and returns its result, or a
|
|
37
|
+
* `RateLimitHit` to be run again after the delay Discord asked for.
|
|
38
|
+
*/
|
|
39
|
+
run<T>(input: {
|
|
40
|
+
route: string;
|
|
41
|
+
major: string;
|
|
42
|
+
/** Interaction endpoints are not bound to the global rate limit. */
|
|
43
|
+
countsForGlobal: boolean;
|
|
44
|
+
}, send: (update: (headers: RateLimitHeaders) => void) => Promise<T | RateLimitHit>): Promise<T>;
|
|
45
|
+
}
|
|
46
|
+
export declare function rateLimitHit(hit: RateLimitHit): RateLimitHit;
|