meocord 4.2.0 → 4.2.2
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/CHANGELOG.md +322 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +188 -169
- package/dist/cjs/decorator/index.cjs +52 -7
- package/dist/cjs/testing/index.cjs +1854 -311
- package/dist/cli.json +1 -1
- package/dist/esm/bin/generator.js +4 -5
- package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
- package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
- package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
- package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
- package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
- package/dist/esm/bin/helper/service-generator.helper.js +4 -5
- package/dist/esm/bin/meocord.js +84 -25
- package/dist/esm/build/load-config.pre-entry.js +13 -3
- package/dist/esm/build/rsbuild-config.js +16 -12
- package/dist/esm/build/typed-externals.js +36 -0
- package/dist/esm/common/cooldown-store.js +10 -2
- package/dist/esm/common/errors.js +25 -0
- package/dist/esm/common/meocord-text.js +11 -8
- package/dist/esm/common/response/call-order.js +9 -4
- package/dist/esm/common/response/presenter.js +33 -6
- package/dist/esm/common/response/response-state.js +89 -15
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +52 -11
- package/dist/esm/core/command-registration.js +93 -24
- package/dist/esm/core/command-routes.js +2 -2
- package/dist/esm/core/cooldown-runner.js +29 -8
- package/dist/esm/core/dispatcher.js +9 -8
- package/dist/esm/core/fallback.js +20 -11
- package/dist/esm/core/filter-runner.js +2 -2
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/handler-pipeline.js +10 -4
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +59 -34
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/message-help.js +32 -6
- package/dist/esm/core/observer-runner.js +2 -1
- package/dist/esm/core/providers.js +19 -4
- package/dist/esm/core/running-calls.js +43 -0
- package/dist/esm/core/shard-context.js +5 -6
- package/dist/esm/core/shard-manager.js +16 -10
- package/dist/esm/core/stage-scope.js +2 -1
- package/dist/esm/core/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +7 -2
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/app.decorator.js +31 -0
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/filter.decorator.js +2 -2
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +20 -13
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +225 -30
- package/dist/esm/testing/meocord-testing-module.js +158 -44
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1250 -182
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +40 -17
- package/dist/esm/util/common.util.js +47 -19
- package/dist/esm/util/dev-runner.util.js +8 -8
- package/dist/esm/util/generator-cli.util.js +4 -5
- package/dist/esm/util/interaction.util.js +2 -2
- package/dist/esm/util/meocord-config-loader.util.js +10 -2
- package/dist/esm/util/meocord-config-validation.util.js +3 -5
- package/dist/esm/util/meocord-source-config.util.js +65 -20
- package/dist/esm/util/method-wrapper.util.js +16 -0
- package/dist/esm/util/platform.util.js +10 -10
- package/dist/esm/util/shard-process.util.js +4 -1
- package/dist/esm/util/sharding-mode.util.js +5 -7
- package/dist/esm/util/shared-state.util.js +21 -0
- package/dist/esm/util/tsconfig-paths.util.js +29 -0
- package/dist/esm/util/tsconfig.util.js +10 -10
- package/dist/esm/util/value.util.js +30 -2
- package/dist/types/common/index.d.cts +10 -7
- package/dist/types/common/index.d.ts +10 -7
- package/dist/types/core/index.d.cts +3 -2
- package/dist/types/core/index.d.ts +3 -2
- package/dist/types/decorator/index.d.cts +1 -0
- package/dist/types/decorator/index.d.ts +1 -0
- package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
- package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
- package/dist/types/interface/index.d.cts +1 -1
- package/dist/types/interface/index.d.ts +1 -1
- package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
- package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
- package/dist/types/testing/index.d.cts +198 -62
- package/dist/types/testing/index.d.ts +198 -62
- package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
- package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
- package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
- package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
- package/package.json +2 -2
package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs}
RENAMED
|
@@ -80,12 +80,20 @@ let problem;
|
|
|
80
80
|
const reason = (problem.error instanceof Error ? problem.error.message : String(problem.error)).replace(/\.$/, '');
|
|
81
81
|
const missing = missingPackage(problem.error);
|
|
82
82
|
const neededBy = missing?.importer ? `, which ${missing.importer} imports,` : '';
|
|
83
|
-
const fix = missing ? `Install ${missing.name}${neededBy} in the project
|
|
84
|
-
return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}
|
|
83
|
+
const fix = missing ? `Install ${missing.name}${neededBy} in the project, then run \`meocord build\`.` : fixedInConfig(problem.error) ? 'Fix meocord.config.ts, then run `meocord build`.' : 'It threw as it loaded: check the environment it reads, such as its .env files.';
|
|
84
|
+
return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}`;
|
|
85
85
|
}
|
|
86
86
|
const where = problem?.path ?? 'meocord.config.mjs';
|
|
87
87
|
return `MeoCord config not found at ${where} (working directory ${process.cwd()}). Run \`meocord build\`, and start the bot from the dist it writes.`;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether the config's file or imports are at fault, which a rebuild after fixing it mends: it could not be read as a
|
|
91
|
+
* module, or imports a file that isn't there. Otherwise its code threw as it ran.
|
|
92
|
+
*/ function fixedInConfig(error) {
|
|
93
|
+
const code = error?.code;
|
|
94
|
+
if (error instanceof SyntaxError || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
|
|
95
|
+
return typeof code === 'string' && code.startsWith('ERR_REQUIRE_');
|
|
96
|
+
}
|
|
89
97
|
/**
|
|
90
98
|
* The package a module-not-found error names, when it is a package rather than a file: `dotenv` for `dotenv/config`,
|
|
91
99
|
* `@scope/name` for `@scope/name/sub`, with the installed package that imports it, if one does. Undefined for any other
|
|
@@ -120,7 +128,7 @@ let problem;
|
|
|
120
128
|
try {
|
|
121
129
|
// Called through a variable so a bundler does not try to resolve the path while building the
|
|
122
130
|
// application; the file is only there once the build is done.
|
|
123
|
-
const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-
|
|
131
|
+
const load = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('_shared/controller.decorator-U2NBWcoU.cjs', document.baseURI).href)));
|
|
124
132
|
const loaded = load(compiledPath);
|
|
125
133
|
return loaded.default ?? loaded;
|
|
126
134
|
} catch (error) {
|
|
@@ -395,7 +403,30 @@ const warned = new Set();
|
|
|
395
403
|
}
|
|
396
404
|
});
|
|
397
405
|
|
|
398
|
-
|
|
406
|
+
var version = "4.2.2";
|
|
407
|
+
var packageJson = {
|
|
408
|
+
version: version};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The name of a key the ES module and CommonJS builds of this meocord version share, and no other version has, for
|
|
412
|
+
* `Symbol.for`: a process that loads both builds of one install keeps one state under it, and two installed versions
|
|
413
|
+
* keep theirs apart. `Symbol.for(sharedName(...))` written out types as a `unique symbol`.
|
|
414
|
+
*/ const sharedName = (name)=>`meocord@${packageJson.version}:${name}`;
|
|
415
|
+
/** The key named {@link sharedName}. */ function sharedKey(name) {
|
|
416
|
+
return Symbol.for(sharedName(name));
|
|
417
|
+
}
|
|
418
|
+
/** The state both builds of this version keep on `globalThis` under `name`, made by whichever asks first. */ function shared(name, make) {
|
|
419
|
+
const key = sharedKey(name);
|
|
420
|
+
const store = globalThis;
|
|
421
|
+
if (!Object.hasOwn(store, key)) Object.defineProperty(store, key, {
|
|
422
|
+
value: make(),
|
|
423
|
+
configurable: true
|
|
424
|
+
});
|
|
425
|
+
return store[key];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// One per process for this meocord version, so a call's theme reads the same from its ES module and CommonJS builds
|
|
429
|
+
const scope = shared('themeScope', ()=>new node_async_hooks.AsyncLocalStorage());
|
|
399
430
|
const isPlainObject$1 = (value)=>{
|
|
400
431
|
if (value === null || typeof value !== 'object') return false;
|
|
401
432
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -564,6 +595,9 @@ let themeOutsideCalls;
|
|
|
564
595
|
* Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
|
|
565
596
|
* Timers and promises the handler starts keep its theme without it.
|
|
566
597
|
*
|
|
598
|
+
* The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
|
|
599
|
+
* it is for, is answered in the starting member's theme.
|
|
600
|
+
*
|
|
567
601
|
* @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
|
|
568
602
|
* @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
|
|
569
603
|
*
|
|
@@ -573,7 +607,7 @@ let themeOutsideCalls;
|
|
|
573
607
|
* async poll(message: Message) {
|
|
574
608
|
* const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
|
|
575
609
|
* collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
|
|
576
|
-
* const { emojis } = useTheme() // the
|
|
610
|
+
* const { emojis } = useTheme() // the starting member's theme, @UseTheme included
|
|
577
611
|
* await click.reply(`${emojis.success} Picked`)
|
|
578
612
|
* }))
|
|
579
613
|
* }
|
|
@@ -877,6 +911,38 @@ function keepOrThrow(error, target, propertyKey) {
|
|
|
877
911
|
process.on('unhandledRejection', onRejection);
|
|
878
912
|
}
|
|
879
913
|
|
|
914
|
+
/** Characters a log line shows escaped: C0 and C1 controls, DEL, line and paragraph separators, and bidi controls. */ const UNSAFE_IN_LOG = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
915
|
+
/** The same, with the quote and the backslash, for text shown between quotes. */ const UNSAFE_IN_QUOTES = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069"\\]/g;
|
|
916
|
+
const SHORT_ESCAPES = {
|
|
917
|
+
'\n': '\\n',
|
|
918
|
+
'\r': '\\r',
|
|
919
|
+
'\t': '\\t',
|
|
920
|
+
'"': '\\"',
|
|
921
|
+
'\\': '\\\\'
|
|
922
|
+
};
|
|
923
|
+
const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
924
|
+
/** Text for a log line, on one line: its control characters escaped, so it can't start a line of its own. */ function escapeForLog(text) {
|
|
925
|
+
return text.replace(UNSAFE_IN_LOG, escapeChar);
|
|
926
|
+
}
|
|
927
|
+
/** How many characters of a user's text a log line quotes before cutting it short. */ const LOGGED_CHARACTERS = 200;
|
|
928
|
+
/**
|
|
929
|
+
* A user's text as a log line quotes it: escaped as {@link escapeForLog} does, with its quotes and backslashes too,
|
|
930
|
+
* and cut short after 200 characters, with its length.
|
|
931
|
+
*/ function quoteForLog(text) {
|
|
932
|
+
const characters = [
|
|
933
|
+
...text
|
|
934
|
+
];
|
|
935
|
+
const quoted = (shown)=>`"${shown.replace(UNSAFE_IN_QUOTES, escapeChar)}"`;
|
|
936
|
+
if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
|
|
937
|
+
return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
|
|
938
|
+
}
|
|
939
|
+
/** Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed. */ function userWords(text) {
|
|
940
|
+
return discord_js.escapeMarkdown(text.replace(/\s+/g, ' ').trim(), {
|
|
941
|
+
maskedLink: true
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
/** Items as a sentence lists them: `a`, `a and b`, `a, b and c`. */ const listed = (items)=>items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
945
|
+
|
|
880
946
|
/**
|
|
881
947
|
* The role names MeoCord keeps for roles it may add to `colors`, `emojis` and `buttons`: the runtime copy of
|
|
882
948
|
* `ReservedThemeRole`, which refuses them in TypeScript at the root theme, so that a JavaScript app is told too. A type
|
|
@@ -901,9 +967,10 @@ const EMOJI = 'a unicode emoji, or a custom one written <:name:id> or <a:name:id
|
|
|
901
967
|
const BUTTON = 'ButtonStyle.Primary, Secondary, Success or Danger';
|
|
902
968
|
/** A custom emoji as Discord writes one in text: its name, 2 to 32 word characters, and its id. */ const CUSTOM_EMOJI$1 = /^<a?:\w{2,32}:\d{17,20}>$/;
|
|
903
969
|
/**
|
|
904
|
-
* One unicode emoji: a flag of two regional indicators, a
|
|
905
|
-
* selector or skin tone, joined to others by
|
|
906
|
-
|
|
970
|
+
* One unicode emoji: a flag of two regional indicators, a subdivision flag (a black flag, tag characters and a cancel
|
|
971
|
+
* tag, as England's), a keycap, or a pictograph with an optional presentation selector or skin tone, joined to others by
|
|
972
|
+
* zero-width joiners into one sequence.
|
|
973
|
+
*/ const UNICODE_EMOJI = /^(?:\p{Regional_Indicator}{2}|\u{1F3F4}[\u{E0020}-\u{E007E}]+\u{E007F}|[#*0-9]️?⃣|\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?(?:\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?)*)$/u;
|
|
907
974
|
const byte = (value)=>Number.isInteger(value) && value >= 0 && value <= 255;
|
|
908
975
|
/** Whether discord.js resolves the value to a colour Discord takes. Stricter on tuples, whose channels it only sums. */ function isColour(value) {
|
|
909
976
|
if (typeof value === 'string') return /^#?[\da-f]{6}$/i.test(value) || value === 'Random' || value === 'Default' || Object.hasOwn(discord_js.Colors, value);
|
|
@@ -918,8 +985,9 @@ const THEMED_BUTTON_STYLES = [
|
|
|
918
985
|
discord_js.ButtonStyle.Danger
|
|
919
986
|
];
|
|
920
987
|
const isButtonStyle = (value)=>THEMED_BUTTON_STYLES.includes(value);
|
|
921
|
-
const
|
|
922
|
-
const
|
|
988
|
+
const asWritten = (text)=>`'${text}'`;
|
|
989
|
+
const describe = (value, quote = asWritten)=>typeof value === 'string' ? quote(value) : Array.isArray(value) ? `[${value.map((item)=>typeof item === 'string' ? quote(item) : String(item)).join(', ')}]` : value === null ? 'null' : typeof value === 'object' ? 'an object' : String(value);
|
|
990
|
+
const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an array' : describe(value, quote);
|
|
923
991
|
/** Whether a value is a plain object, as an object literal or `JSON.parse` makes: one a theme merges key by key. */ const isPlainObject = (value)=>{
|
|
924
992
|
const prototype = Object.getPrototypeOf(value);
|
|
925
993
|
return prototype === Object.prototype || prototype === null;
|
|
@@ -951,11 +1019,15 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
|
|
|
951
1019
|
* roles an app added, a colour discord.js cannot resolve, an emoji Discord would refuse, a button style other than
|
|
952
1020
|
* Discord's four coloured ones, or a reserved role name. A group of the app's own is its to check.
|
|
953
1021
|
* @param where - Where it was set, such as `themeFor.guild for guild 123`, to begin each line with.
|
|
954
|
-
|
|
1022
|
+
* @param options.forLog - Shows values and role names as a log line shows text from outside the code, for a theme a
|
|
1023
|
+
* `themeFor` resolver returned: escaped, and a value quoted and cut short.
|
|
1024
|
+
*/ function themeProblems(theme, where, { forLog = false } = {}) {
|
|
955
1025
|
const at = where ? `${where}: ` : '';
|
|
1026
|
+
const quote = forLog ? quoteForLog : asWritten;
|
|
1027
|
+
const name = forLog ? escapeForLog : (role)=>role;
|
|
956
1028
|
if (typeof theme !== 'object' || theme === null || Array.isArray(theme)) {
|
|
957
1029
|
return [
|
|
958
|
-
`${at}theme must be an object of groups (got ${describeGroup(theme)})`
|
|
1030
|
+
`${at}theme must be an object of groups (got ${describeGroup(theme, quote)})`
|
|
959
1031
|
];
|
|
960
1032
|
}
|
|
961
1033
|
if (!isPlainObject(theme)) return [
|
|
@@ -967,7 +1039,7 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
|
|
|
967
1039
|
const roles = theme[group];
|
|
968
1040
|
if (roles === undefined) continue;
|
|
969
1041
|
if (typeof roles !== 'object' || roles === null || Array.isArray(roles)) {
|
|
970
|
-
problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles)})`);
|
|
1042
|
+
problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles, quote)})`);
|
|
971
1043
|
continue;
|
|
972
1044
|
}
|
|
973
1045
|
if (!isPlainObject(roles)) {
|
|
@@ -975,8 +1047,8 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
|
|
|
975
1047
|
continue;
|
|
976
1048
|
}
|
|
977
1049
|
for (const [role, value] of Object.entries(roles)){
|
|
978
|
-
if (reserved.has(role)) problems.push(`${at}theme.${group}.${role}: MeoCord reserves the role name ${role} for a role it may add; rename yours`);
|
|
979
|
-
else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${role}: ${describe(value)} is not ${what}: give ${instead}`);
|
|
1050
|
+
if (reserved.has(role)) problems.push(`${at}theme.${group}.${name(role)}: MeoCord reserves the role name ${name(role)} for a role it may add; rename yours`);
|
|
1051
|
+
else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${name(role)}: ${describe(value, quote)} is not ${what}: give ${instead}`);
|
|
980
1052
|
}
|
|
981
1053
|
}
|
|
982
1054
|
return problems;
|
|
@@ -1170,23 +1242,33 @@ const logger$5 = new Logger('SetMetadata');
|
|
|
1170
1242
|
/**
|
|
1171
1243
|
* Where a translator made by `createTranslator` keeps its catalogs, for MeoCord's texts, `@MeoCord` and
|
|
1172
1244
|
* `expectCompleteCatalog`.
|
|
1173
|
-
*/ const CATALOGS = Symbol('catalogs');
|
|
1174
|
-
/** How MeoCord's own texts read a translator made by `createTranslator`: the catalogs that serve a locale, in order. */ const LOCALE_CHAIN = Symbol('localeChain');
|
|
1245
|
+
*/ const CATALOGS = Symbol.for(sharedName('translator:catalogs'));
|
|
1246
|
+
/** How MeoCord's own texts read a translator made by `createTranslator`: the catalogs that serve a locale, in order. */ const LOCALE_CHAIN = Symbol.for(sharedName('translator:localeChain'));
|
|
1175
1247
|
/** For a class that injects `Translator` in an app that configured none. */ function missingTranslatorError(cls) {
|
|
1176
1248
|
return new Error(`${cls.name}: it injects Translator, but @MeoCord has no i18n. Pass @MeoCord({ i18n: t }), where t comes from createTranslator.`);
|
|
1177
1249
|
}
|
|
1178
1250
|
const DISCORD_LOCALES = new Set(Object.values(discord_js.Locale));
|
|
1179
1251
|
const languageOf = (locale)=>locale.split('-')[0];
|
|
1180
|
-
|
|
1252
|
+
const PLURAL_CATEGORIES = new Set([
|
|
1253
|
+
'zero',
|
|
1254
|
+
'one',
|
|
1255
|
+
'two',
|
|
1256
|
+
'few',
|
|
1257
|
+
'many',
|
|
1258
|
+
'other'
|
|
1259
|
+
]);
|
|
1260
|
+
/** A plural's shape: a non-empty object whose every key is a plural category, as the types read one; any other is a group. */ const isPluralShape = (value)=>typeof value === 'object' && value !== null && Object.keys(value).length > 0 && Object.keys(value).every((key)=>PLURAL_CATEGORIES.has(key));
|
|
1261
|
+
/**
|
|
1262
|
+
* A message by its dotted key, or undefined when the catalog lacks it or the key names a group. A plural without an
|
|
1263
|
+
* `other` form reads as missing, so a translation that left it out falls back to the next catalog.
|
|
1264
|
+
*/ function lookup(catalog, key) {
|
|
1181
1265
|
let current = catalog;
|
|
1182
1266
|
for (const part of key.split('.')){
|
|
1183
1267
|
if (typeof current !== 'object' || current === null) return undefined;
|
|
1184
1268
|
current = current[part];
|
|
1185
1269
|
}
|
|
1186
1270
|
if (typeof current === 'string') return current;
|
|
1187
|
-
if (
|
|
1188
|
-
return current;
|
|
1189
|
-
}
|
|
1271
|
+
if (isPluralShape(current) && typeof current.other === 'string') return current;
|
|
1190
1272
|
return undefined;
|
|
1191
1273
|
}
|
|
1192
1274
|
const plurals = new Map();
|
|
@@ -1218,6 +1300,22 @@ function interpolate(message, params) {
|
|
|
1218
1300
|
}
|
|
1219
1301
|
return typeof current === 'object' && current !== null;
|
|
1220
1302
|
}
|
|
1303
|
+
/** The locale Discord shows a locale's users when that locale has no localization of its own, by its locale fallbacks. */ const FALLBACK_PARTNERS = {
|
|
1304
|
+
[discord_js.Locale.EnglishUS]: discord_js.Locale.EnglishGB,
|
|
1305
|
+
[discord_js.Locale.EnglishGB]: discord_js.Locale.EnglishUS,
|
|
1306
|
+
[discord_js.Locale.SpanishLATAM]: discord_js.Locale.SpanishES
|
|
1307
|
+
};
|
|
1308
|
+
/** Each key with a `.` in it, which `lookup` splits as a path and so never reaches, with the group it sits in. */ function dottedKeys(catalog, group = '') {
|
|
1309
|
+
return Object.entries(catalog).flatMap(([name, value])=>[
|
|
1310
|
+
...name.includes('.') ? [
|
|
1311
|
+
{
|
|
1312
|
+
name,
|
|
1313
|
+
group
|
|
1314
|
+
}
|
|
1315
|
+
] : [],
|
|
1316
|
+
...typeof value === 'object' && value !== null ? dottedKeys(value, group ? `${group}.${name}` : name) : []
|
|
1317
|
+
]);
|
|
1318
|
+
}
|
|
1221
1319
|
class CatalogTranslator extends Translator {
|
|
1222
1320
|
/** The catalogs that serve a locale, most specific first, ending with the default. */ [LOCALE_CHAIN](requested) {
|
|
1223
1321
|
const chain = [];
|
|
@@ -1236,10 +1334,13 @@ class CatalogTranslator extends Translator {
|
|
|
1236
1334
|
translate(requested, key, params = {}) {
|
|
1237
1335
|
for (const locale of this[LOCALE_CHAIN](requested)){
|
|
1238
1336
|
const message = lookup(this.catalogs[locale], key);
|
|
1239
|
-
if (message
|
|
1337
|
+
if (message === undefined) continue;
|
|
1338
|
+
// An empty translation, as tools export an untranslated string, counts as missing: Discord refuses an empty reply
|
|
1339
|
+
const text = pluralForm({
|
|
1240
1340
|
message,
|
|
1241
1341
|
locale
|
|
1242
|
-
}, params.count)
|
|
1342
|
+
}, params.count);
|
|
1343
|
+
if (text !== '') return interpolate(text, params);
|
|
1243
1344
|
}
|
|
1244
1345
|
this.warnUnknown(key);
|
|
1245
1346
|
return key;
|
|
@@ -1269,11 +1370,16 @@ class CatalogTranslator extends Translator {
|
|
|
1269
1370
|
for (const locale of this.locales){
|
|
1270
1371
|
if (locale === this.defaultLocale) continue;
|
|
1271
1372
|
const message = lookup(this.catalogs[locale], key);
|
|
1272
|
-
// A translation with a {param} is left out
|
|
1273
|
-
if (typeof message === 'string' && placeholderNames(message).length === 0) {
|
|
1373
|
+
// A translation with a {param} is left out, as Discord would show it as written, and so is an empty one
|
|
1374
|
+
if (typeof message === 'string' && message !== '' && placeholderNames(message).length === 0) {
|
|
1274
1375
|
localized[locale] = fillPlaceholders(message, (_name, whole)=>whole);
|
|
1275
1376
|
}
|
|
1276
1377
|
}
|
|
1378
|
+
// Discord gives a locale it falls back from its partner's value, so the default keeps its own wording beside one
|
|
1379
|
+
const partner = FALLBACK_PARTNERS[this.defaultLocale];
|
|
1380
|
+
if (partner && localized[partner] !== undefined && typeof original === 'string' && original !== '') {
|
|
1381
|
+
localized[this.defaultLocale] = fillPlaceholders(original, (_name, whole)=>whole);
|
|
1382
|
+
}
|
|
1277
1383
|
return localized;
|
|
1278
1384
|
}
|
|
1279
1385
|
for(interaction, options = {}) {
|
|
@@ -1352,6 +1458,12 @@ class CatalogTranslator extends Translator {
|
|
|
1352
1458
|
}
|
|
1353
1459
|
}
|
|
1354
1460
|
if (!locales[defaultLocale]) throw refuse(new Error(`The default locale "${defaultLocale}" has no catalog in locales.`));
|
|
1461
|
+
const logger = new Logger('Translator');
|
|
1462
|
+
for (const [locale, catalog] of Object.entries(locales)){
|
|
1463
|
+
for (const { name, group } of dottedKeys(catalog)){
|
|
1464
|
+
logger.warn(`The ${locale} catalog's key "${name}"${group ? ` in ${group}` : ''} has a ".", which a lookup reads as a path, so its ` + 'message is never found. Nest it as a group instead.');
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1355
1467
|
// Typed by the default catalog; at runtime every catalog is read the same way.
|
|
1356
1468
|
return new CatalogTranslator(defaultLocale, locales);
|
|
1357
1469
|
}
|
|
@@ -1456,16 +1568,17 @@ const ENGLISH = 'en-US';
|
|
|
1456
1568
|
return english();
|
|
1457
1569
|
}
|
|
1458
1570
|
const lists = new Map();
|
|
1459
|
-
/**
|
|
1460
|
-
let format = lists.get(
|
|
1461
|
-
if (!format) lists.set(
|
|
1462
|
-
type:
|
|
1571
|
+
/** An `or` list in a locale's words, such as "a, b, or c" in en-US. */ function orListIn(locale, items) {
|
|
1572
|
+
let format = lists.get(locale);
|
|
1573
|
+
if (!format) lists.set(locale, format = new Intl.ListFormat(locale, {
|
|
1574
|
+
type: 'disjunction'
|
|
1463
1575
|
}));
|
|
1464
1576
|
return format.format(items);
|
|
1465
1577
|
}
|
|
1466
1578
|
/**
|
|
1467
|
-
* A text in `locale`, as {@link findText} finds it.
|
|
1468
|
-
*
|
|
1579
|
+
* A text in `locale`, as {@link findText} finds it. An `or` list in an app's catalog reads in the words of the locale
|
|
1580
|
+
* that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
|
|
1581
|
+
* nothing in zh-CN and with a space in ja.
|
|
1469
1582
|
*/ function renderText(translator, locale, text) {
|
|
1470
1583
|
const found = findText(translator, locale, text.key);
|
|
1471
1584
|
if (!found) return text.fallback ?? text.key;
|
|
@@ -1474,14 +1587,15 @@ const lists = new Map();
|
|
|
1474
1587
|
if (!Object.hasOwn(params, name)) return whole;
|
|
1475
1588
|
const value = params[name];
|
|
1476
1589
|
if (typeof value !== 'object') return String(value);
|
|
1477
|
-
if ('list' in value) return found.own ?
|
|
1590
|
+
if ('list' in value) return found.own && value.style === 'or' ? orListIn(found.locale, value.list) : value.list.join(value.joiner ?? ', ');
|
|
1478
1591
|
return renderText(translator, locale, value);
|
|
1479
1592
|
});
|
|
1480
1593
|
}
|
|
1481
1594
|
/** Renders texts in one locale through one translator: a word or number as it is, a text in the locale's words. */ function textRenderer(translator, locale) {
|
|
1482
1595
|
return (text)=>typeof text !== 'object' ? String(text) : 'list' in text ? text.list.join(text.joiner ?? ', ') : renderText(translator, locale, text);
|
|
1483
1596
|
}
|
|
1484
|
-
|
|
1597
|
+
// Shared by both builds of this version, so either answers in the app's language
|
|
1598
|
+
const translators = shared('clientTranslators', ()=>new WeakMap());
|
|
1485
1599
|
/** Records the translator of the app a client's interactions and messages come to. */ function registerClientTranslator(client, translator) {
|
|
1486
1600
|
if (translator) translators.set(client, translator);
|
|
1487
1601
|
else translators.delete(client);
|
|
@@ -1837,6 +1951,30 @@ const issueTexts = new WeakMap();
|
|
|
1837
1951
|
this.name = 'CooldownStoreError';
|
|
1838
1952
|
}
|
|
1839
1953
|
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Makes `instanceof` hold for an error from either build of this meocord version, ES module or CommonJS: the class's
|
|
1956
|
+
* prototype carries a brand both builds share, which `instanceof` on the class itself checks. A subclass, such as an
|
|
1957
|
+
* app's own `UserError`, keeps the usual check, so it still matches only its own instances.
|
|
1958
|
+
*/ function brand(error, name) {
|
|
1959
|
+
const key = sharedKey(`error:${name}`);
|
|
1960
|
+
Object.defineProperty(error.prototype, key, {
|
|
1961
|
+
value: true
|
|
1962
|
+
});
|
|
1963
|
+
Object.defineProperty(error, Symbol.hasInstance, {
|
|
1964
|
+
value (value) {
|
|
1965
|
+
if (this !== error) return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
1966
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null && key in value;
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
// Named as written, since a minifier may rename the classes themselves
|
|
1971
|
+
brand(GuardDeniedError, 'GuardDeniedError');
|
|
1972
|
+
brand(UserError, 'UserError');
|
|
1973
|
+
brand(CommandNotFoundError, 'CommandNotFoundError');
|
|
1974
|
+
brand(ValidationError, 'ValidationError');
|
|
1975
|
+
brand(MessageUsageError, 'MessageUsageError');
|
|
1976
|
+
brand(CooldownError, 'CooldownError');
|
|
1977
|
+
brand(CooldownStoreError, 'CooldownStoreError');
|
|
1840
1978
|
|
|
1841
1979
|
/** Discord's codes for a body it could not read: invalid form body or JSON, or an empty message. */ const MALFORMED_BODY = new Set([
|
|
1842
1980
|
discord_js.RESTJSONErrorCodes.InvalidFormBodyOrContentType,
|
|
@@ -1853,39 +1991,24 @@ const issueTexts = new WeakMap();
|
|
|
1853
1991
|
else logger.error(`Could not ${what}:`, error);
|
|
1854
1992
|
}
|
|
1855
1993
|
|
|
1856
|
-
/**
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
}
|
|
1865
|
-
const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
1866
|
-
/** Text for a log line, on one line: its control characters escaped, so it can't start a line of its own. */ function escapeForLog(text) {
|
|
1867
|
-
return text.replace(UNSAFE_IN_LOG, escapeChar);
|
|
1994
|
+
/** Whether `new` takes a value, as `instanceof` needs of its right side, such as a `@Catch` type. */ function isConstructible(value) {
|
|
1995
|
+
if (typeof value !== 'function') return false;
|
|
1996
|
+
try {
|
|
1997
|
+
// Fails unless value can be new.target, which only a constructor can; none of value's constructor code runs
|
|
1998
|
+
Reflect.construct(String, [], value);
|
|
1999
|
+
return true;
|
|
2000
|
+
} catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
1868
2003
|
}
|
|
1869
|
-
/** How many characters of a user's text a log line quotes before cutting it short. */ const LOGGED_CHARACTERS = 200;
|
|
1870
2004
|
/**
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
*/ function
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const quoted = (shown)=>`"${shown.replace(UNSAFE_IN_QUOTES, escapeChar)}"`;
|
|
1878
|
-
if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
|
|
1879
|
-
return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
|
|
1880
|
-
}
|
|
1881
|
-
/** Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed. */ function userWords(text) {
|
|
1882
|
-
return discord_js.escapeMarkdown(text.replace(/\s+/g, ' ').trim(), {
|
|
1883
|
-
maskedLink: true
|
|
1884
|
-
});
|
|
2005
|
+
* Whether a value is a class, which a stage entry or an app option must be: constructible, with an object prototype for
|
|
2006
|
+
* the metadata MeoCord reads. An arrow function, a method, an async or a generator function is none.
|
|
2007
|
+
*/ function isConstructor(value) {
|
|
2008
|
+
if (!isConstructible(value)) return false;
|
|
2009
|
+
const prototype = value.prototype;
|
|
2010
|
+
return typeof prototype === 'object' && prototype !== null;
|
|
1885
2011
|
}
|
|
1886
|
-
/** Items as a sentence lists them: `a`, `a and b`, `a, b and c`. */ const listed = (items)=>items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
1887
|
-
|
|
1888
|
-
/** Whether a value is a class, which a stage entry or a `@Catch` type must be: what `new` and `instanceof` take. */ const isConstructor = (value)=>typeof value === 'function';
|
|
1889
2012
|
/** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
|
|
1890
2013
|
if (value === null || value === undefined) return String(value);
|
|
1891
2014
|
if (typeof value === 'string') return quoteForLog(value);
|
|
@@ -1893,6 +2016,17 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
|
|
|
1893
2016
|
return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
|
|
1894
2017
|
}
|
|
1895
2018
|
/** A class's name after its article, such as `an AutocompleteInteraction`; a `U` name reads as "you", so takes `a`. */ const withArticle = (name)=>`${/^[AEIO]/i.test(name) ? 'an' : 'a'} ${name}`;
|
|
2019
|
+
/**
|
|
2020
|
+
* Sets `key` on `target` as an own, enumerable data property: unlike `target[key] = value`, a key named `__proto__`
|
|
2021
|
+
* is a key like any other rather than the object's prototype.
|
|
2022
|
+
*/ function setOwn(target, key, value) {
|
|
2023
|
+
Object.defineProperty(target, key, {
|
|
2024
|
+
value,
|
|
2025
|
+
enumerable: true,
|
|
2026
|
+
writable: true,
|
|
2027
|
+
configurable: true
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
1896
2030
|
|
|
1897
2031
|
/**
|
|
1898
2032
|
* The discord.js interaction class each command type handles, shared by `@Command`, the dispatcher and the testing
|
|
@@ -1990,7 +2124,7 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
|
|
|
1990
2124
|
walk(option.options ?? []);
|
|
1991
2125
|
continue;
|
|
1992
2126
|
}
|
|
1993
|
-
params
|
|
2127
|
+
setOwn(params, option.name, resolveOptionValue(option));
|
|
1994
2128
|
}
|
|
1995
2129
|
};
|
|
1996
2130
|
walk(data);
|
|
@@ -2268,16 +2402,41 @@ function rewriteComponent(component, kept) {
|
|
|
2268
2402
|
})
|
|
2269
2403
|
};
|
|
2270
2404
|
/** The id MeoCord gives the containers it renders, so a view left behind can be found again. */ const RENDERED_CONTAINER_ID = 0x4d43;
|
|
2271
|
-
|
|
2405
|
+
// Shared by both builds of this version, so either answers with the app's presenter
|
|
2406
|
+
const presenters = shared('clientPresenters', ()=>new WeakMap());
|
|
2272
2407
|
/** Sets the presenter for the interactions a client receives. */ function setPresenter(client, presenter) {
|
|
2273
2408
|
presenters.set(client, presenter);
|
|
2274
2409
|
}
|
|
2275
2410
|
/** The presenter for an interaction's client, or the default one. */ function presenterFor(client) {
|
|
2276
2411
|
return client && presenters.get(client) || defaultPresenter;
|
|
2277
2412
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2413
|
+
/** Discord's limits on a message's text, counted by `String.length` as discord.js's builders count them. */ const TEXT_LIMITS = {
|
|
2414
|
+
embed: 4096,
|
|
2415
|
+
v2: 4000,
|
|
2416
|
+
content: 2000
|
|
2417
|
+
};
|
|
2418
|
+
/** `text` cut to `limit` characters ending in `…`, on a code point boundary so no surrogate pair is split. */ function fitText(text, limit) {
|
|
2419
|
+
if (text.length <= limit) return text;
|
|
2420
|
+
let cut = text.slice(0, Math.max(0, limit - 1));
|
|
2421
|
+
const last = cut.charCodeAt(cut.length - 1);
|
|
2422
|
+
if (last >= 0xd800 && last <= 0xdbff) cut = cut.slice(0, -1);
|
|
2423
|
+
return `${cut}…`;
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* An error's message as a presenter is given it: fitted to `limit`, and, when empty or only whitespace, which Discord
|
|
2427
|
+
* refuses as a text, MeoCord's generic error text, so a presenter that writes it as its text always renders.
|
|
2428
|
+
*/ function presentedMessage(message, limit, generic) {
|
|
2429
|
+
return fitText(message.trim() === '' ? generic() : message, limit);
|
|
2280
2430
|
}
|
|
2431
|
+
/**
|
|
2432
|
+
* The view's text after what MeoCord puts before it, its emoji and a Text Display's title line. When only those take
|
|
2433
|
+
* the two past `limit`, the text is cut so they fit; a text past `limit` by itself is left, a view MeoCord can't render.
|
|
2434
|
+
*/ function textAfter(prefix, view, limit) {
|
|
2435
|
+
const total = prefix.length + view.text.length;
|
|
2436
|
+
if (total <= limit || view.text.length > limit || prefix.length >= limit) return prefix + view.text;
|
|
2437
|
+
return prefix + fitText(view.text, limit - prefix.length);
|
|
2438
|
+
}
|
|
2439
|
+
const emojiOf = (view)=>view.emoji ? `${view.emoji} ` : '';
|
|
2281
2440
|
/** Discord's limit of attachments on one message. */ const ATTACHMENT_LIMIT = 10;
|
|
2282
2441
|
/** The most items Discord shows in one media gallery. */ const MEDIA_GALLERY_LIMIT = 10;
|
|
2283
2442
|
/**
|
|
@@ -2361,7 +2520,7 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
|
|
|
2361
2520
|
return (view.files ?? []).filter((file)=>!shown.has(`attachment://${nameOf(file)}`));
|
|
2362
2521
|
}
|
|
2363
2522
|
/** A view as an embed: its image, or else its first image file not its thumbnail, as the embed's image. */ function renderEmbed(view) {
|
|
2364
|
-
const embed = new discord_js.EmbedBuilder().setDescription(
|
|
2523
|
+
const embed = new discord_js.EmbedBuilder().setDescription(textAfter(emojiOf(view), view, TEXT_LIMITS.embed));
|
|
2365
2524
|
if (view.title) embed.setTitle(view.title);
|
|
2366
2525
|
if (view.color !== undefined) embed.setColor(view.color);
|
|
2367
2526
|
const image = view.image ?? unshownFiles(view).map(nameOf).find((name)=>IMAGE.test(name));
|
|
@@ -2372,7 +2531,8 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
|
|
|
2372
2531
|
/** A view as a Components V2 container, carrying MeoCord's id. */ function renderContainer(view) {
|
|
2373
2532
|
const container = new discord_js.ContainerBuilder().setId(RENDERED_CONTAINER_ID);
|
|
2374
2533
|
if (view.color !== undefined) container.setAccentColor(discord_js.resolveColor(view.color));
|
|
2375
|
-
const
|
|
2534
|
+
const titleLine = view.title ? `### ${view.title}\n` : '';
|
|
2535
|
+
const text = new discord_js.TextDisplayBuilder().setContent(textAfter(titleLine + emojiOf(view), view, TEXT_LIMITS.v2));
|
|
2376
2536
|
if (view.thumbnail) {
|
|
2377
2537
|
container.addSectionComponents(new discord_js.SectionBuilder().addTextDisplayComponents(text).setThumbnailAccessory(new discord_js.ThumbnailBuilder().setURL(urlOf(view, view.thumbnail))));
|
|
2378
2538
|
} else container.addTextDisplayComponents(text);
|
|
@@ -2503,13 +2663,16 @@ const EMBED_CONTENT = [
|
|
|
2503
2663
|
return sameJson(content(a), content(b));
|
|
2504
2664
|
}
|
|
2505
2665
|
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2666
|
+
// One count for both builds of this version, so calls either makes merge in the order made
|
|
2667
|
+
const calls = shared('callOrder', ()=>({
|
|
2668
|
+
last: 0,
|
|
2669
|
+
order: new WeakMap()
|
|
2670
|
+
}));
|
|
2508
2671
|
/** Marks a Discord call as made now, so calls recorded in different places can be merged in the order made. */ function stampCall(call) {
|
|
2509
|
-
order.set(call, ++last);
|
|
2672
|
+
calls.order.set(call, ++calls.last);
|
|
2510
2673
|
}
|
|
2511
2674
|
/** When a call stamped by {@link stampCall} was made, relative to the others. */ function callOrder(call) {
|
|
2512
|
-
return order.get(call) ?? 0;
|
|
2675
|
+
return calls.order.get(call) ?? 0;
|
|
2513
2676
|
}
|
|
2514
2677
|
|
|
2515
2678
|
/**
|
|
@@ -2642,7 +2805,10 @@ const logger$4 = new Logger('Theme');
|
|
|
2642
2805
|
*/ accept(id, result, report) {
|
|
2643
2806
|
if (result === undefined || result === null) return undefined;
|
|
2644
2807
|
const layer = copyLayer(result);
|
|
2645
|
-
|
|
2808
|
+
// A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
|
|
2809
|
+
const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`, {
|
|
2810
|
+
forLog: true
|
|
2811
|
+
});
|
|
2646
2812
|
if (problems.length === 0) {
|
|
2647
2813
|
if (report) this.warned.delete(id);
|
|
2648
2814
|
return layer;
|
|
@@ -2889,7 +3055,8 @@ function toBody(payload) {
|
|
|
2889
3055
|
*/ function editedSince(stamp, ours) {
|
|
2890
3056
|
return typeof stamp === 'number' && ours !== undefined && stamp > ours;
|
|
2891
3057
|
}
|
|
2892
|
-
|
|
3058
|
+
// Both builds of this meocord version keep one, so a message one locks, the other sees locked
|
|
3059
|
+
const messageLocks = shared('messageLocks', ()=>new Map());
|
|
2893
3060
|
/** How long a locked message is remembered once no call holds it. */ const LOCK_MEMORY_MS = 60000;
|
|
2894
3061
|
/**
|
|
2895
3062
|
* A body with the theme's primary colour where the app left one unset: an embed with no `color`, and a
|
|
@@ -2951,9 +3118,12 @@ function toJson(value) {
|
|
|
2951
3118
|
return this.phase;
|
|
2952
3119
|
}
|
|
2953
3120
|
get message() {
|
|
3121
|
+
// A deleted answer, or a follow-up in its place, leaves the component's own message behind
|
|
3122
|
+
if (this.answer.kind !== 'original') return this.lastMessage;
|
|
2954
3123
|
return this.lastMessage ?? ('message' in this.interaction ? this.interaction.message ?? undefined : undefined);
|
|
2955
3124
|
}
|
|
2956
3125
|
get original() {
|
|
3126
|
+
// Each component is a top-level component's API JSON, as `toJSON()` gave it; the lock reads them as plain JSON
|
|
2957
3127
|
return this.snapshot;
|
|
2958
3128
|
}
|
|
2959
3129
|
/** The Discord calls made through this state, for tests. */ get history() {
|
|
@@ -3243,15 +3413,15 @@ function toJson(value) {
|
|
|
3243
3413
|
});
|
|
3244
3414
|
}
|
|
3245
3415
|
/**
|
|
3246
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
* nothing. Never throws.
|
|
3416
|
+
* Ends a call a guard denied silently. A component still unanswered is acknowledged invisibly, with or without
|
|
3417
|
+
* `@Defer`, since Discord tells the user an unanswered call failed; so is a command `@Defer`'s 'auto' timer was
|
|
3418
|
+
* waiting on, whose deferred reply is then deleted, as `@Defer`'s is while nothing was sent into it. Never throws.
|
|
3249
3419
|
*/ abandon() {
|
|
3250
3420
|
const waiting = this.timer !== undefined;
|
|
3251
3421
|
this.cancelScheduled();
|
|
3252
3422
|
return this.queued(async ()=>{
|
|
3253
|
-
//
|
|
3254
|
-
if (waiting) await this.acknowledgeUnanswered({
|
|
3423
|
+
// A command's acknowledgement shows "thinking…", so one is made only for the timer that would have made it
|
|
3424
|
+
if (waiting || !answersWithOwnMessage(this.interaction)) await this.acknowledgeUnanswered({
|
|
3255
3425
|
ephemeral: true
|
|
3256
3426
|
});
|
|
3257
3427
|
this.sync();
|
|
@@ -3285,9 +3455,11 @@ function toJson(value) {
|
|
|
3285
3455
|
this.sync();
|
|
3286
3456
|
const body = await this.themed(payload, options);
|
|
3287
3457
|
if (this.phase === 'unanswered') return this.reply(body);
|
|
3288
|
-
|
|
3458
|
+
// Once the answer is deleted, the next message sent is the answer
|
|
3459
|
+
if (this.answer.kind === 'deleted') return this.answerWith(body);
|
|
3460
|
+
if (this.phase === 'deferred' && answersWithOwnMessage(this.interaction) && this.answer.kind === 'original') {
|
|
3289
3461
|
// Discord makes a follow-up to a deferred, unsent reply that reply, ignoring its flags: a private one
|
|
3290
|
-
// would be shown to everyone on a public deferral, so that deferral is deleted
|
|
3462
|
+
// would be shown to everyone on a public deferral, so that deferral is deleted, and the follow-up is the answer.
|
|
3291
3463
|
const requested = Number(discord_js.MessageFlagsBitField.resolve(body.flags ?? 0));
|
|
3292
3464
|
if (!hasEphemeral(requested)) return this.editMessage(body);
|
|
3293
3465
|
// A private deferral is already what the flag asks for, which an edit cannot take
|
|
@@ -3297,7 +3469,11 @@ function toJson(value) {
|
|
|
3297
3469
|
});
|
|
3298
3470
|
await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
|
|
3299
3471
|
this.phase = 'replied';
|
|
3472
|
+
return this.answerWith(body);
|
|
3300
3473
|
}
|
|
3474
|
+
return (await this.sendFollowUp(body)).message;
|
|
3475
|
+
}
|
|
3476
|
+
/** Sends a follow-up, with the flags it was sent with. */ async sendFollowUp(body) {
|
|
3301
3477
|
const flags = this.withSuppression(this.flagsFor('followUp', body.flags, false));
|
|
3302
3478
|
const sent = forMode(body, hasComponentsV2(flags));
|
|
3303
3479
|
const message = await this.call('followUp', {
|
|
@@ -3307,6 +3483,20 @@ function toJson(value) {
|
|
|
3307
3483
|
...sent,
|
|
3308
3484
|
flags
|
|
3309
3485
|
}));
|
|
3486
|
+
return {
|
|
3487
|
+
message: message,
|
|
3488
|
+
flags
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
/** Sends a follow-up that becomes the answer, which later edits and `delete()` act on by its id. */ async answerWith(body) {
|
|
3492
|
+
const { message, flags } = await this.sendFollowUp(body);
|
|
3493
|
+
// Its privacy as sent, not as read back from the message
|
|
3494
|
+
this.answer = {
|
|
3495
|
+
kind: 'followUp',
|
|
3496
|
+
id: message.id,
|
|
3497
|
+
ephemeral: hasEphemeral(flags)
|
|
3498
|
+
};
|
|
3499
|
+
this.lastMessage = message;
|
|
3310
3500
|
return message;
|
|
3311
3501
|
}
|
|
3312
3502
|
delete() {
|
|
@@ -3314,7 +3504,22 @@ function toJson(value) {
|
|
|
3314
3504
|
this.sync();
|
|
3315
3505
|
if (this.phase === 'unanswered') throw new Error('There is no answer to delete: the interaction has not been answered.');
|
|
3316
3506
|
this.refuseAfterModal();
|
|
3317
|
-
|
|
3507
|
+
const { answer } = this;
|
|
3508
|
+
if (answer.kind === 'deleted') throw new Error('There is no answer to delete: it was deleted.');
|
|
3509
|
+
// A follow-up is as private as it was sent, a command's own reply as the interaction's answer, a component's
|
|
3510
|
+
// message as its flags
|
|
3511
|
+
const shown = this.message?.flags?.has(discord_js.MessageFlags.Ephemeral) ?? false;
|
|
3512
|
+
const ephemeral = answer.kind === 'followUp' ? answer.ephemeral : answersWithOwnMessage(this.interaction) ? Boolean(this.interaction.ephemeral) || shown : shown;
|
|
3513
|
+
if (answer.kind === 'followUp') await this.call('deleteReply', answer.id, ()=>this.interaction.deleteReply(answer.id));
|
|
3514
|
+
else await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
|
|
3515
|
+
this.answer = {
|
|
3516
|
+
kind: 'deleted',
|
|
3517
|
+
ephemeral
|
|
3518
|
+
};
|
|
3519
|
+
this.lastMessage = undefined;
|
|
3520
|
+
// The message is gone: nothing is left to put back as it was before a lock
|
|
3521
|
+
this.settled = true;
|
|
3522
|
+
this.leave();
|
|
3318
3523
|
});
|
|
3319
3524
|
}
|
|
3320
3525
|
/** A modal is the interaction's whole answer: what the user enters arrives as a submit interaction of its own. */ refuseAfterModal() {
|
|
@@ -3424,7 +3629,20 @@ function toJson(value) {
|
|
|
3424
3629
|
withAttachments(body) {
|
|
3425
3630
|
return rewriteAttachmentUrls(body, keptAttachmentNames(body, this.message?.attachments?.values() ?? []));
|
|
3426
3631
|
}
|
|
3427
|
-
/**
|
|
3632
|
+
/**
|
|
3633
|
+
* Edits the answer through the interaction, and through the channel only once its token has expired. Once the answer
|
|
3634
|
+
* is deleted, the edit is sent as a follow-up that becomes the answer, as private as the deleted one unless it sets
|
|
3635
|
+
* flags of its own.
|
|
3636
|
+
*/ async editMessage(body, { restoring = false } = {}) {
|
|
3637
|
+
const { answer } = this;
|
|
3638
|
+
if (answer.kind === 'deleted') {
|
|
3639
|
+
if (restoring) return undefined;
|
|
3640
|
+
const { attachments: _attachments, ...rest } = body;
|
|
3641
|
+
return this.answerWith({
|
|
3642
|
+
...rest,
|
|
3643
|
+
flags: body.flags ?? (answer.ephemeral ? discord_js.MessageFlags.Ephemeral : 0)
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3428
3646
|
if (!restoring) {
|
|
3429
3647
|
this.settled = true;
|
|
3430
3648
|
this.leave();
|
|
@@ -3446,8 +3664,13 @@ function toJson(value) {
|
|
|
3446
3664
|
...this.withAttachments(forMode(body, this.v2)),
|
|
3447
3665
|
flags
|
|
3448
3666
|
};
|
|
3667
|
+
// A follow-up that is the answer is edited by its id
|
|
3668
|
+
const edit = answer.kind === 'followUp' ? {
|
|
3669
|
+
...sent,
|
|
3670
|
+
message: answer.id
|
|
3671
|
+
} : sent;
|
|
3449
3672
|
try {
|
|
3450
|
-
this.lastMessage = await this.call('editReply',
|
|
3673
|
+
this.lastMessage = await this.call('editReply', edit, ()=>this.interaction.editReply(edit));
|
|
3451
3674
|
} catch (error) {
|
|
3452
3675
|
const message = this.message;
|
|
3453
3676
|
const expired = Date.now() - this.interaction.createdTimestamp >= TOKEN_EXPIRED_AFTER_MS;
|
|
@@ -3566,12 +3789,16 @@ function toJson(value) {
|
|
|
3566
3789
|
* place, and the failure is returned for the caller to report once the user is answered.
|
|
3567
3790
|
*/ async drawnView(error, message, theme) {
|
|
3568
3791
|
const tone = isUserOutcome(error, this.interaction) ? 'warning' : 'danger';
|
|
3792
|
+
const context = this.presenterContext(this.v2, theme);
|
|
3793
|
+
// Fitted to the mode's limit first, so a presenter that writes the message as its text, MeoCord's own included, renders
|
|
3794
|
+
const generic = ()=>textFor(this.interaction, {
|
|
3795
|
+
key: 'meocord.fallback.error'
|
|
3796
|
+
});
|
|
3569
3797
|
const presented = {
|
|
3570
|
-
message,
|
|
3798
|
+
message: presentedMessage(message, TEXT_LIMITS[context.mode], generic),
|
|
3571
3799
|
error,
|
|
3572
3800
|
tone
|
|
3573
3801
|
};
|
|
3574
|
-
const context = this.presenterContext(this.v2, theme);
|
|
3575
3802
|
const ready = (view)=>{
|
|
3576
3803
|
const sendable = this.sendable(themedView(view, theme), this.keptBeside());
|
|
3577
3804
|
// Rendered once here, so a view MeoCord cannot render fails before anything is sent
|
|
@@ -3714,6 +3941,11 @@ function toJson(value) {
|
|
|
3714
3941
|
await this.followUpNow(this.privateError(view));
|
|
3715
3942
|
return;
|
|
3716
3943
|
}
|
|
3944
|
+
// A deleted message has nothing to add the error to or put back: the error follows up, and is the answer
|
|
3945
|
+
if (this.answer.kind === 'deleted') {
|
|
3946
|
+
await this.followUpNow(this.privateError(view));
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3717
3949
|
// The message the component is on: whether it is private does not change with edits.
|
|
3718
3950
|
const current = 'message' in this.interaction ? this.interaction.message ?? undefined : this.message;
|
|
3719
3951
|
if (current?.flags?.has(discord_js.MessageFlags.Ephemeral)) {
|
|
@@ -3759,6 +3991,12 @@ function toJson(value) {
|
|
|
3759
3991
|
/** The answer steps asked for, run one after another; `pending` counts those not yet settled. */ this.queue = Promise.resolve();
|
|
3760
3992
|
this.pending = 0;
|
|
3761
3993
|
/** Whether the answer was a modal, which has no message to edit or delete. */ this.modalShown = false;
|
|
3994
|
+
/**
|
|
3995
|
+
* Which message is the answer: the interaction's own reply, a follow-up that took its place, or none since `delete()`,
|
|
3996
|
+
* with whether the deleted one was private. `sync()` leaves it alone, so every edit follows it.
|
|
3997
|
+
*/ this.answer = {
|
|
3998
|
+
kind: 'original'
|
|
3999
|
+
};
|
|
3762
4000
|
this.calls = [];
|
|
3763
4001
|
/** Whether the interaction was acknowledged with a private deferred reply for a drawn error view, which that view replaces. */ this.privateDeferral = false;
|
|
3764
4002
|
/** Whether the locked message was answered, so nothing restores it again. */ this.settled = false;
|
|
@@ -3770,7 +4008,8 @@ function toJson(value) {
|
|
|
3770
4008
|
this.v2 = Boolean(message?.flags?.has(discord_js.MessageFlags.IsComponentsV2));
|
|
3771
4009
|
}
|
|
3772
4010
|
}
|
|
3773
|
-
|
|
4011
|
+
// One state per interaction for both builds of this version, so either sees where the other left its answer
|
|
4012
|
+
const states = shared('responseStates', ()=>new WeakMap());
|
|
3774
4013
|
/** The response state of a repliable interaction, created on first use. */ function responseOf(interaction) {
|
|
3775
4014
|
let state = states.get(interaction);
|
|
3776
4015
|
if (!state) states.set(interaction, state = new InteractionResponse(interaction));
|
|
@@ -4240,9 +4479,17 @@ const logger$2 = new Logger('ExecutionContext');
|
|
|
4240
4479
|
};
|
|
4241
4480
|
});
|
|
4242
4481
|
}
|
|
4243
|
-
/**
|
|
4482
|
+
/**
|
|
4483
|
+
* Drops the keys whose calls have all left their window, once a minute from a call, until a sweep leaves none: an
|
|
4484
|
+
* idle store then holds no timer, so one nothing references is collected, and its next call starts the sweep again.
|
|
4485
|
+
*/ startSweeping() {
|
|
4244
4486
|
if (this.sweeper) return;
|
|
4245
|
-
this.sweeper = setInterval(()=>
|
|
4487
|
+
this.sweeper = setInterval(()=>{
|
|
4488
|
+
sweep(this.calls, Date.now());
|
|
4489
|
+
if (this.calls.size > 0) return;
|
|
4490
|
+
clearInterval(this.sweeper);
|
|
4491
|
+
this.sweeper = undefined;
|
|
4492
|
+
}, SWEEP_INTERVAL_MS);
|
|
4246
4493
|
// A cooldown must never be what keeps a stopping bot alive.
|
|
4247
4494
|
this.sweeper.unref?.();
|
|
4248
4495
|
}
|
|
@@ -4539,6 +4786,21 @@ const messageOf = (error)=>String(error?.message ?? error);
|
|
|
4539
4786
|
};
|
|
4540
4787
|
}
|
|
4541
4788
|
|
|
4789
|
+
/**
|
|
4790
|
+
* Gives a decorator's `wrapper` the `length` and `name` of the method it replaces, so whatever reads the handler, such
|
|
4791
|
+
* as `invoke` counting the arguments it takes, sees the method as it was declared.
|
|
4792
|
+
*/ function wrapsMethod(wrapper, original) {
|
|
4793
|
+
Object.defineProperty(wrapper, 'length', {
|
|
4794
|
+
value: original.length,
|
|
4795
|
+
configurable: true
|
|
4796
|
+
});
|
|
4797
|
+
Object.defineProperty(wrapper, 'name', {
|
|
4798
|
+
value: original.name,
|
|
4799
|
+
configurable: true
|
|
4800
|
+
});
|
|
4801
|
+
return wrapper;
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4542
4804
|
/**
|
|
4543
4805
|
* How specific a route pattern is, for ranking the message patterns that can match one message: more literal
|
|
4544
4806
|
* words first, then a fixed length before one taking the rest, then no optional param before one, then
|
|
@@ -4719,9 +4981,9 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
4719
4981
|
// An entry the class takes from its base, for a method it re-declares
|
|
4720
4982
|
const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
|
|
4721
4983
|
const commands = ownCommandMap(prototype);
|
|
4722
|
-
const baseCommands = Reflect.getMetadata(META.commands, base) ??
|
|
4984
|
+
const baseCommands = Reflect.getMetadata(META.commands, base) ?? Object.create(null);
|
|
4723
4985
|
for (const [name, metas] of Object.entries(commands)){
|
|
4724
|
-
const kept = metas.filter((meta)=>!inheritedFor(baseCommands
|
|
4986
|
+
const kept = metas.filter((meta)=>!inheritedFor(commandsNamed(baseCommands, name), (entry)=>entry.methodName)(meta));
|
|
4725
4987
|
if (kept.length > 0) commands[name] = kept;
|
|
4726
4988
|
else delete commands[name];
|
|
4727
4989
|
}
|
|
@@ -4745,16 +5007,21 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
4745
5007
|
Reflect.defineMetadata(key, ownHandlerList(key, prototype).filter((entry)=>!inheritedFor(inherited, method)(entry)), prototype);
|
|
4746
5008
|
}
|
|
4747
5009
|
}
|
|
4748
|
-
/**
|
|
5010
|
+
/**
|
|
5011
|
+
* The class's own command map, started from a copy of the inherited one, for the same reason. Keyed by command name
|
|
5012
|
+
* without a prototype, so a command named like an inherited key, such as `constructor`, is a key like any other.
|
|
5013
|
+
*/ function ownCommandMap(target) {
|
|
4749
5014
|
const own = Reflect.getOwnMetadata(META.commands, target);
|
|
4750
5015
|
if (own) return own;
|
|
4751
5016
|
const inherited = Reflect.getMetadata(META.commands, target) ?? {};
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
5017
|
+
const copy = Object.create(null);
|
|
5018
|
+
for (const [name, metas] of Object.entries(inherited))copy[name] = [
|
|
5019
|
+
...metas
|
|
5020
|
+
];
|
|
5021
|
+
return copy;
|
|
5022
|
+
}
|
|
5023
|
+
/** The handlers a command map holds for `name`: its own entry only, never one `Object.prototype` holds. */ function commandsNamed(map, name) {
|
|
5024
|
+
return map && Object.hasOwn(map, name) ? map[name] : [];
|
|
4758
5025
|
}
|
|
4759
5026
|
function MessageHandler(pattern, options = {}) {
|
|
4760
5027
|
return declaring(function(target, propertyKey) {
|
|
@@ -4819,12 +5086,24 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
|
|
|
4819
5086
|
}
|
|
4820
5087
|
// `{name}`, or `{name:type}` with the type read as far as the brace, so a type no segment can hold is named
|
|
4821
5088
|
const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
5089
|
+
// The whole of a brace pair that is a param, as PLACEHOLDER_PATTERN reads one
|
|
5090
|
+
const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
5091
|
+
/**
|
|
5092
|
+
* Why a pattern's brace pairs that aren't params are matched as literal text, or undefined when it has none: a
|
|
5093
|
+
* param's name is ASCII letters, digits and _, so `{café}` is text.
|
|
5094
|
+
*/ function literalBraces(pattern) {
|
|
5095
|
+
const braces = (pattern.match(/\{[^{}]*}/g) ?? []).filter((brace)=>!WHOLE_PLACEHOLDER.test(brace));
|
|
5096
|
+
if (braces.length === 0) return undefined;
|
|
5097
|
+
const one = braces.length === 1;
|
|
5098
|
+
return `${braces.join(', ')} ${one ? "isn't a param" : "aren't params"}, since a param's name is ASCII letters, digits and _; ` + `${one ? 'it is' : 'they are'} matched as literal text.`;
|
|
5099
|
+
}
|
|
4822
5100
|
/** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
|
|
5101
|
+
/** The longest customId Discord accepts; the shortest is one character. */ const MAX_CUSTOM_ID_LENGTH = 100;
|
|
4823
5102
|
/** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
4824
5103
|
/**
|
|
4825
|
-
* Compiles a pattern into a regex, its parameter names, their types
|
|
4826
|
-
* whole `/`-separated segment, so a uuid is captured whole and
|
|
4827
|
-
* `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
|
|
5104
|
+
* Compiles a pattern into a regex, its parameter names, their types, its 4.1 specificity and the length of the
|
|
5105
|
+
* shortest customId it matches. A `{name}` takes a whole `/`-separated segment, so a uuid is captured whole and
|
|
5106
|
+
* `profile/{uuid}` never overlaps `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
|
|
4828
5107
|
*/ function createRegexFromPattern(pattern) {
|
|
4829
5108
|
const params = [];
|
|
4830
5109
|
// By the param's own name, so one named like an inherited key, such as `__proto__`, keeps its type
|
|
@@ -4845,6 +5124,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4845
5124
|
if (literal !== '' && !literal.endsWith(PARAM_SEPARATOR) || after !== undefined && after !== PARAM_SEPARATOR) {
|
|
4846
5125
|
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} must occupy a whole segment, so it has to be ` + `preceded and followed by "${PARAM_SEPARATOR}" or by the ends of the pattern. ` + `Write "a${PARAM_SEPARATOR}{${param}}" rather than "a-{${param}}".`));
|
|
4847
5126
|
}
|
|
5127
|
+
// A regex group's name, which the engine would refuse with the compiled regex in place of the pattern
|
|
5128
|
+
if (params.includes(param)) {
|
|
5129
|
+
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} appears twice; give each param its own name.`));
|
|
5130
|
+
}
|
|
5131
|
+
if (/^\d/.test(param)) {
|
|
5132
|
+
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} starts with a digit; start a param's name with a letter or _.`));
|
|
5133
|
+
}
|
|
4848
5134
|
if (type !== undefined) {
|
|
4849
5135
|
if (!isSegmentType(type)) throw refuse(new Error(`Invalid pattern "${pattern}": ${segmentTypeProblem(param, type)}`));
|
|
4850
5136
|
if (choicesOf(type)?.includes('')) {
|
|
@@ -4868,11 +5154,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4868
5154
|
params: params.length,
|
|
4869
5155
|
typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
|
|
4870
5156
|
});
|
|
5157
|
+
// Each param takes at least one character
|
|
4871
5158
|
return {
|
|
4872
5159
|
regex,
|
|
4873
5160
|
params,
|
|
4874
5161
|
types: lookupTable(types),
|
|
4875
|
-
specificity
|
|
5162
|
+
specificity,
|
|
5163
|
+
shortest: literalLength + params.length
|
|
4876
5164
|
};
|
|
4877
5165
|
}
|
|
4878
5166
|
/** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
|
|
@@ -4942,7 +5230,6 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4942
5230
|
*/ function Command(name, builderOrType) {
|
|
4943
5231
|
const commandName = typeof name === 'string' ? name : name.pattern;
|
|
4944
5232
|
return declaring(function(target, propertyKey, _descriptor) {
|
|
4945
|
-
var _commands, _commandName;
|
|
4946
5233
|
refuseOnClass('@Command', target, propertyKey);
|
|
4947
5234
|
const originalMethod = _descriptor.value;
|
|
4948
5235
|
if (!originalMethod) {
|
|
@@ -4950,7 +5237,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4950
5237
|
}
|
|
4951
5238
|
// Wrap original method for interaction type validation
|
|
4952
5239
|
const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
|
|
4953
|
-
|
|
5240
|
+
const checked = function(interaction, params) {
|
|
4954
5241
|
if (!matchesCommandType(commandType, interaction)) {
|
|
4955
5242
|
throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
|
|
4956
5243
|
}
|
|
@@ -4959,6 +5246,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4959
5246
|
params
|
|
4960
5247
|
]);
|
|
4961
5248
|
};
|
|
5249
|
+
_descriptor.value = wrapsMethod(checked, originalMethod);
|
|
4962
5250
|
// This class's own map, inherited routes included
|
|
4963
5251
|
const commands = ownCommandMap(target);
|
|
4964
5252
|
let builderInstance;
|
|
@@ -4994,7 +5282,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4994
5282
|
}));
|
|
4995
5283
|
}
|
|
4996
5284
|
// discord.js builders validate as they are set, and their errors name neither the command nor the field.
|
|
4997
|
-
throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` +
|
|
5285
|
+
throw refuse(new Error(`${where}: ${builderOrType.name} could not build "${commandName}": ${detail}. Check its names, descriptions and ` + 'localizations, which Discord limits to 32 characters for a name and 100 for a description. A name is letters, ' + 'numbers, `-` and `_`, lowercase where the script has case, and without spaces, in every locale.', {
|
|
4998
5286
|
cause: error
|
|
4999
5287
|
}));
|
|
5000
5288
|
}
|
|
@@ -5014,6 +5302,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
5014
5302
|
commandType = builderOrType;
|
|
5015
5303
|
}
|
|
5016
5304
|
if (isCustomIdRouted(commandType)) {
|
|
5305
|
+
// A route warned as it was made
|
|
5306
|
+
const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
|
|
5307
|
+
if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
|
|
5017
5308
|
let pattern;
|
|
5018
5309
|
try {
|
|
5019
5310
|
pattern = createRegexFromPattern(commandName);
|
|
@@ -5025,6 +5316,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
5025
5316
|
regex = pattern.regex;
|
|
5026
5317
|
dynamicParams = pattern.params;
|
|
5027
5318
|
specificity = pattern.specificity;
|
|
5319
|
+
// Warned rather than refused, so a bot that carries such a handler still starts
|
|
5320
|
+
if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
|
|
5321
|
+
const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
|
|
5322
|
+
logger.warn(`${target.constructor.name}.${propertyKey}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
|
|
5323
|
+
}
|
|
5028
5324
|
}
|
|
5029
5325
|
const declared = {
|
|
5030
5326
|
methodName: propertyKey,
|
|
@@ -5041,8 +5337,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
5041
5337
|
}
|
|
5042
5338
|
};
|
|
5043
5339
|
// One the class inherits for this method and route is replaced where it stands, so this one's options apply
|
|
5044
|
-
|
|
5045
|
-
const
|
|
5340
|
+
if (!Object.hasOwn(commands, commandName)) commands[commandName] = [];
|
|
5341
|
+
const metas = commands[commandName];
|
|
5342
|
+
const inherited = commandsNamed(getCommandMap(Object.getPrototypeOf(target)), commandName);
|
|
5046
5343
|
const replaced = metas.findIndex((meta)=>inherited.includes(meta) && meta.methodName === propertyKey && meta.type === commandType);
|
|
5047
5344
|
if (replaced === -1) metas.push(declared);
|
|
5048
5345
|
else metas[replaced] = declared;
|
|
@@ -5224,6 +5521,7 @@ exports.ExecutionContext = ExecutionContext;
|
|
|
5224
5521
|
exports.GuardDeniedError = GuardDeniedError;
|
|
5225
5522
|
exports.HandlerExecutionContext = HandlerExecutionContext;
|
|
5226
5523
|
exports.Logger = Logger;
|
|
5524
|
+
exports.MAX_CUSTOM_ID_LENGTH = MAX_CUSTOM_ID_LENGTH;
|
|
5227
5525
|
exports.MEOCORD_MESSAGES = MEOCORD_MESSAGES;
|
|
5228
5526
|
exports.META = META;
|
|
5229
5527
|
exports.MemoryCooldownStore = MemoryCooldownStore;
|
|
@@ -5233,6 +5531,7 @@ exports.REFUSED_AS_TOO_LARGE = REFUSED_AS_TOO_LARGE;
|
|
|
5233
5531
|
exports.ReactionHandler = ReactionHandler;
|
|
5234
5532
|
exports.RedisCooldownStore = RedisCooldownStore;
|
|
5235
5533
|
exports.SetMetadata = SetMetadata;
|
|
5534
|
+
exports.TEXT_LIMITS = TEXT_LIMITS;
|
|
5236
5535
|
exports.ThemeCache = ThemeCache;
|
|
5237
5536
|
exports.Translator = Translator;
|
|
5238
5537
|
exports.UnroutedExecutionContext = UnroutedExecutionContext;
|
|
@@ -5252,6 +5551,7 @@ exports.callOrder = callOrder;
|
|
|
5252
5551
|
exports.choicesOf = choicesOf;
|
|
5253
5552
|
exports.claimAmbientTheme = claimAmbientTheme;
|
|
5254
5553
|
exports.collectStartupErrors = collectStartupErrors;
|
|
5554
|
+
exports.commandsNamed = commandsNamed;
|
|
5255
5555
|
exports.compiledConfigMessage = compiledConfigMessage;
|
|
5256
5556
|
exports.cooldownMessage = cooldownMessage;
|
|
5257
5557
|
exports.cooldownStoreMessage = cooldownStoreMessage;
|
|
@@ -5272,6 +5572,7 @@ exports.dropInheritedRoutes = dropInheritedRoutes;
|
|
|
5272
5572
|
exports.duration = duration;
|
|
5273
5573
|
exports.escapeForLog = escapeForLog;
|
|
5274
5574
|
exports.existingResponse = existingResponse;
|
|
5575
|
+
exports.fitText = fitText;
|
|
5275
5576
|
exports.focusedOptionName = focusedOptionName;
|
|
5276
5577
|
exports.forgetRecorded = forgetRecorded;
|
|
5277
5578
|
exports.getAutocompleteHandlers = getAutocompleteHandlers;
|
|
@@ -5288,13 +5589,16 @@ exports.hideInLogs = hideInLogs;
|
|
|
5288
5589
|
exports.inferContextType = inferContextType;
|
|
5289
5590
|
exports.interactionLocale = interactionLocale;
|
|
5290
5591
|
exports.isBuiltApplication = isBuiltApplication;
|
|
5592
|
+
exports.isConstructible = isConstructible;
|
|
5291
5593
|
exports.isConstructor = isConstructor;
|
|
5292
5594
|
exports.isCustomIdRouted = isCustomIdRouted;
|
|
5293
5595
|
exports.isExplainedError = isExplainedError;
|
|
5596
|
+
exports.isPluralShape = isPluralShape;
|
|
5294
5597
|
exports.isRefusal = isRefusal;
|
|
5295
5598
|
exports.isTooLarge = isTooLarge;
|
|
5296
5599
|
exports.isUserOutcome = isUserOutcome;
|
|
5297
5600
|
exports.listed = listed;
|
|
5601
|
+
exports.literalBraces = literalBraces;
|
|
5298
5602
|
exports.loadMeoCordConfig = loadMeoCordConfig;
|
|
5299
5603
|
exports.logFailedSend = logFailedSend;
|
|
5300
5604
|
exports.logStartupErrors = logStartupErrors;
|
|
@@ -5334,10 +5638,13 @@ exports.routeSpecificity = routeSpecificity;
|
|
|
5334
5638
|
exports.runInThemeScope = runInThemeScope;
|
|
5335
5639
|
exports.runWithTheme = runWithTheme;
|
|
5336
5640
|
exports.setLegacyThemeLayer = setLegacyThemeLayer;
|
|
5641
|
+
exports.setOwn = setOwn;
|
|
5337
5642
|
exports.setPresenter = setPresenter;
|
|
5338
5643
|
exports.setStartupErrorsMode = setStartupErrorsMode;
|
|
5339
5644
|
exports.setThemeOutsideCalls = setThemeOutsideCalls;
|
|
5645
|
+
exports.shared = shared;
|
|
5340
5646
|
exports.sharedCustomId = sharedCustomId;
|
|
5647
|
+
exports.sharedName = sharedName;
|
|
5341
5648
|
exports.stampCall = stampCall;
|
|
5342
5649
|
exports.startupError = startupError;
|
|
5343
5650
|
exports.stopOnStartupErrors = stopOnStartupErrors;
|
|
@@ -5360,3 +5667,4 @@ exports.warnPlaceholder = warnPlaceholder;
|
|
|
5360
5667
|
exports.withRelease = withRelease;
|
|
5361
5668
|
exports.withSendableFiles = withSendableFiles;
|
|
5362
5669
|
exports.withoutFiles = withoutFiles;
|
|
5670
|
+
exports.wrapsMethod = wrapsMethod;
|