meocord 4.2.0 → 4.2.1
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 +264 -0
- package/THIRD_PARTY_NOTICES.md +14 -13
- package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-XKMlcUZz.cjs} +376 -100
- package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-BZgJyfZS.cjs} +145 -36
- package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-CsJJyCZd.cjs} +10 -7
- package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-CgfPQrNl.cjs} +219 -63
- package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-BUAR1WYG.cjs} +10 -5
- package/dist/cjs/common/index.cjs +3 -3
- package/dist/cjs/core/index.cjs +185 -168
- package/dist/cjs/decorator/index.cjs +21 -6
- package/dist/cjs/testing/index.cjs +1594 -286
- 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 +84 -10
- package/dist/esm/common/route.js +11 -4
- package/dist/esm/common/sharded-cooldown-store.js +1 -1
- package/dist/esm/common/translator.js +37 -6
- 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/fallback.js +20 -11
- package/dist/esm/core/guard-runner.js +16 -3
- package/dist/esm/core/handler-input.js +4 -3
- package/dist/esm/core/lifecycle-hooks.js +134 -47
- package/dist/esm/core/meocord-factory.js +55 -32
- package/dist/esm/core/meocord.app.js +116 -75
- package/dist/esm/core/providers.js +7 -2
- 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/startup-roots.js +21 -1
- package/dist/esm/core/theme-resolvers.js +4 -1
- package/dist/esm/core/theme-scope.js +3 -1
- package/dist/esm/core/theme-validation.js +17 -10
- package/dist/esm/decorator/controller.decorator.js +56 -20
- package/dist/esm/decorator/cooldown.decorator.js +17 -0
- package/dist/esm/decorator/guard.decorator.js +3 -2
- package/dist/esm/package.json.js +1 -1
- package/dist/esm/testing/catalog.js +26 -10
- package/dist/esm/testing/cooldown-store-suite.js +67 -5
- package/dist/esm/testing/discord-defaults.js +205 -30
- package/dist/esm/testing/meocord-testing-module.js +134 -28
- package/dist/esm/testing/mock-fn.js +107 -26
- package/dist/esm/testing/mock-interaction.js +1026 -172
- package/dist/esm/testing/snowflake.js +9 -2
- package/dist/esm/testing/strict-mocks.js +37 -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 +12 -1
- package/dist/types/common/index.d.cts +6 -6
- package/dist/types/common/index.d.ts +6 -6
- 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 +175 -53
- package/dist/types/testing/index.d.ts +175 -53
- 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-XKMlcUZz.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-XKMlcUZz.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.1";
|
|
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);
|
|
@@ -877,6 +908,38 @@ function keepOrThrow(error, target, propertyKey) {
|
|
|
877
908
|
process.on('unhandledRejection', onRejection);
|
|
878
909
|
}
|
|
879
910
|
|
|
911
|
+
/** 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;
|
|
912
|
+
/** 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;
|
|
913
|
+
const SHORT_ESCAPES = {
|
|
914
|
+
'\n': '\\n',
|
|
915
|
+
'\r': '\\r',
|
|
916
|
+
'\t': '\\t',
|
|
917
|
+
'"': '\\"',
|
|
918
|
+
'\\': '\\\\'
|
|
919
|
+
};
|
|
920
|
+
const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
921
|
+
/** 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) {
|
|
922
|
+
return text.replace(UNSAFE_IN_LOG, escapeChar);
|
|
923
|
+
}
|
|
924
|
+
/** How many characters of a user's text a log line quotes before cutting it short. */ const LOGGED_CHARACTERS = 200;
|
|
925
|
+
/**
|
|
926
|
+
* A user's text as a log line quotes it: escaped as {@link escapeForLog} does, with its quotes and backslashes too,
|
|
927
|
+
* and cut short after 200 characters, with its length.
|
|
928
|
+
*/ function quoteForLog(text) {
|
|
929
|
+
const characters = [
|
|
930
|
+
...text
|
|
931
|
+
];
|
|
932
|
+
const quoted = (shown)=>`"${shown.replace(UNSAFE_IN_QUOTES, escapeChar)}"`;
|
|
933
|
+
if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
|
|
934
|
+
return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
|
|
935
|
+
}
|
|
936
|
+
/** Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed. */ function userWords(text) {
|
|
937
|
+
return discord_js.escapeMarkdown(text.replace(/\s+/g, ' ').trim(), {
|
|
938
|
+
maskedLink: true
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/** 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]}`;
|
|
942
|
+
|
|
880
943
|
/**
|
|
881
944
|
* The role names MeoCord keeps for roles it may add to `colors`, `emojis` and `buttons`: the runtime copy of
|
|
882
945
|
* `ReservedThemeRole`, which refuses them in TypeScript at the root theme, so that a JavaScript app is told too. A type
|
|
@@ -901,9 +964,10 @@ const EMOJI = 'a unicode emoji, or a custom one written <:name:id> or <a:name:id
|
|
|
901
964
|
const BUTTON = 'ButtonStyle.Primary, Secondary, Success or Danger';
|
|
902
965
|
/** 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
966
|
/**
|
|
904
|
-
* One unicode emoji: a flag of two regional indicators, a
|
|
905
|
-
* selector or skin tone, joined to others by
|
|
906
|
-
|
|
967
|
+
* One unicode emoji: a flag of two regional indicators, a subdivision flag (a black flag, tag characters and a cancel
|
|
968
|
+
* tag, as England's), a keycap, or a pictograph with an optional presentation selector or skin tone, joined to others by
|
|
969
|
+
* zero-width joiners into one sequence.
|
|
970
|
+
*/ 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
971
|
const byte = (value)=>Number.isInteger(value) && value >= 0 && value <= 255;
|
|
908
972
|
/** Whether discord.js resolves the value to a colour Discord takes. Stricter on tuples, whose channels it only sums. */ function isColour(value) {
|
|
909
973
|
if (typeof value === 'string') return /^#?[\da-f]{6}$/i.test(value) || value === 'Random' || value === 'Default' || Object.hasOwn(discord_js.Colors, value);
|
|
@@ -918,8 +982,9 @@ const THEMED_BUTTON_STYLES = [
|
|
|
918
982
|
discord_js.ButtonStyle.Danger
|
|
919
983
|
];
|
|
920
984
|
const isButtonStyle = (value)=>THEMED_BUTTON_STYLES.includes(value);
|
|
921
|
-
const
|
|
922
|
-
const
|
|
985
|
+
const asWritten = (text)=>`'${text}'`;
|
|
986
|
+
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);
|
|
987
|
+
const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an array' : describe(value, quote);
|
|
923
988
|
/** 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
989
|
const prototype = Object.getPrototypeOf(value);
|
|
925
990
|
return prototype === Object.prototype || prototype === null;
|
|
@@ -951,11 +1016,15 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
|
|
|
951
1016
|
* roles an app added, a colour discord.js cannot resolve, an emoji Discord would refuse, a button style other than
|
|
952
1017
|
* Discord's four coloured ones, or a reserved role name. A group of the app's own is its to check.
|
|
953
1018
|
* @param where - Where it was set, such as `themeFor.guild for guild 123`, to begin each line with.
|
|
954
|
-
|
|
1019
|
+
* @param options.forLog - Shows values and role names as a log line shows text from outside the code, for a theme a
|
|
1020
|
+
* `themeFor` resolver returned: escaped, and a value quoted and cut short.
|
|
1021
|
+
*/ function themeProblems(theme, where, { forLog = false } = {}) {
|
|
955
1022
|
const at = where ? `${where}: ` : '';
|
|
1023
|
+
const quote = forLog ? quoteForLog : asWritten;
|
|
1024
|
+
const name = forLog ? escapeForLog : (role)=>role;
|
|
956
1025
|
if (typeof theme !== 'object' || theme === null || Array.isArray(theme)) {
|
|
957
1026
|
return [
|
|
958
|
-
`${at}theme must be an object of groups (got ${describeGroup(theme)})`
|
|
1027
|
+
`${at}theme must be an object of groups (got ${describeGroup(theme, quote)})`
|
|
959
1028
|
];
|
|
960
1029
|
}
|
|
961
1030
|
if (!isPlainObject(theme)) return [
|
|
@@ -967,7 +1036,7 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
|
|
|
967
1036
|
const roles = theme[group];
|
|
968
1037
|
if (roles === undefined) continue;
|
|
969
1038
|
if (typeof roles !== 'object' || roles === null || Array.isArray(roles)) {
|
|
970
|
-
problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles)})`);
|
|
1039
|
+
problems.push(`${at}theme.${group} must be an object of roles (got ${describeGroup(roles, quote)})`);
|
|
971
1040
|
continue;
|
|
972
1041
|
}
|
|
973
1042
|
if (!isPlainObject(roles)) {
|
|
@@ -975,8 +1044,8 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
|
|
|
975
1044
|
continue;
|
|
976
1045
|
}
|
|
977
1046
|
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}`);
|
|
1047
|
+
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`);
|
|
1048
|
+
else if (value !== undefined && !check(value)) problems.push(`${at}theme.${group}.${name(role)}: ${describe(value, quote)} is not ${what}: give ${instead}`);
|
|
980
1049
|
}
|
|
981
1050
|
}
|
|
982
1051
|
return problems;
|
|
@@ -1170,8 +1239,8 @@ const logger$5 = new Logger('SetMetadata');
|
|
|
1170
1239
|
/**
|
|
1171
1240
|
* Where a translator made by `createTranslator` keeps its catalogs, for MeoCord's texts, `@MeoCord` and
|
|
1172
1241
|
* `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');
|
|
1242
|
+
*/ const CATALOGS = Symbol.for(sharedName('translator:catalogs'));
|
|
1243
|
+
/** 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
1244
|
/** For a class that injects `Translator` in an app that configured none. */ function missingTranslatorError(cls) {
|
|
1176
1245
|
return new Error(`${cls.name}: it injects Translator, but @MeoCord has no i18n. Pass @MeoCord({ i18n: t }), where t comes from createTranslator.`);
|
|
1177
1246
|
}
|
|
@@ -1218,6 +1287,22 @@ function interpolate(message, params) {
|
|
|
1218
1287
|
}
|
|
1219
1288
|
return typeof current === 'object' && current !== null;
|
|
1220
1289
|
}
|
|
1290
|
+
/** The locale Discord shows a locale's users when that locale has no localization of its own, by its locale fallbacks. */ const FALLBACK_PARTNERS = {
|
|
1291
|
+
[discord_js.Locale.EnglishUS]: discord_js.Locale.EnglishGB,
|
|
1292
|
+
[discord_js.Locale.EnglishGB]: discord_js.Locale.EnglishUS,
|
|
1293
|
+
[discord_js.Locale.SpanishLATAM]: discord_js.Locale.SpanishES
|
|
1294
|
+
};
|
|
1295
|
+
/** 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 = '') {
|
|
1296
|
+
return Object.entries(catalog).flatMap(([name, value])=>[
|
|
1297
|
+
...name.includes('.') ? [
|
|
1298
|
+
{
|
|
1299
|
+
name,
|
|
1300
|
+
group
|
|
1301
|
+
}
|
|
1302
|
+
] : [],
|
|
1303
|
+
...typeof value === 'object' && value !== null ? dottedKeys(value, group ? `${group}.${name}` : name) : []
|
|
1304
|
+
]);
|
|
1305
|
+
}
|
|
1221
1306
|
class CatalogTranslator extends Translator {
|
|
1222
1307
|
/** The catalogs that serve a locale, most specific first, ending with the default. */ [LOCALE_CHAIN](requested) {
|
|
1223
1308
|
const chain = [];
|
|
@@ -1236,10 +1321,13 @@ class CatalogTranslator extends Translator {
|
|
|
1236
1321
|
translate(requested, key, params = {}) {
|
|
1237
1322
|
for (const locale of this[LOCALE_CHAIN](requested)){
|
|
1238
1323
|
const message = lookup(this.catalogs[locale], key);
|
|
1239
|
-
if (message
|
|
1324
|
+
if (message === undefined) continue;
|
|
1325
|
+
// An empty translation, as tools export an untranslated string, counts as missing: Discord refuses an empty reply
|
|
1326
|
+
const text = pluralForm({
|
|
1240
1327
|
message,
|
|
1241
1328
|
locale
|
|
1242
|
-
}, params.count)
|
|
1329
|
+
}, params.count);
|
|
1330
|
+
if (text !== '') return interpolate(text, params);
|
|
1243
1331
|
}
|
|
1244
1332
|
this.warnUnknown(key);
|
|
1245
1333
|
return key;
|
|
@@ -1269,11 +1357,16 @@ class CatalogTranslator extends Translator {
|
|
|
1269
1357
|
for (const locale of this.locales){
|
|
1270
1358
|
if (locale === this.defaultLocale) continue;
|
|
1271
1359
|
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) {
|
|
1360
|
+
// A translation with a {param} is left out, as Discord would show it as written, and so is an empty one
|
|
1361
|
+
if (typeof message === 'string' && message !== '' && placeholderNames(message).length === 0) {
|
|
1274
1362
|
localized[locale] = fillPlaceholders(message, (_name, whole)=>whole);
|
|
1275
1363
|
}
|
|
1276
1364
|
}
|
|
1365
|
+
// Discord gives a locale it falls back from its partner's value, so the default keeps its own wording beside one
|
|
1366
|
+
const partner = FALLBACK_PARTNERS[this.defaultLocale];
|
|
1367
|
+
if (partner && localized[partner] !== undefined && typeof original === 'string' && original !== '') {
|
|
1368
|
+
localized[this.defaultLocale] = fillPlaceholders(original, (_name, whole)=>whole);
|
|
1369
|
+
}
|
|
1277
1370
|
return localized;
|
|
1278
1371
|
}
|
|
1279
1372
|
for(interaction, options = {}) {
|
|
@@ -1352,6 +1445,12 @@ class CatalogTranslator extends Translator {
|
|
|
1352
1445
|
}
|
|
1353
1446
|
}
|
|
1354
1447
|
if (!locales[defaultLocale]) throw refuse(new Error(`The default locale "${defaultLocale}" has no catalog in locales.`));
|
|
1448
|
+
const logger = new Logger('Translator');
|
|
1449
|
+
for (const [locale, catalog] of Object.entries(locales)){
|
|
1450
|
+
for (const { name, group } of dottedKeys(catalog)){
|
|
1451
|
+
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.');
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1355
1454
|
// Typed by the default catalog; at runtime every catalog is read the same way.
|
|
1356
1455
|
return new CatalogTranslator(defaultLocale, locales);
|
|
1357
1456
|
}
|
|
@@ -1456,16 +1555,17 @@ const ENGLISH = 'en-US';
|
|
|
1456
1555
|
return english();
|
|
1457
1556
|
}
|
|
1458
1557
|
const lists = new Map();
|
|
1459
|
-
/**
|
|
1460
|
-
let format = lists.get(
|
|
1461
|
-
if (!format) lists.set(
|
|
1462
|
-
type:
|
|
1558
|
+
/** An `or` list in a locale's words, such as "a, b, or c" in en-US. */ function orListIn(locale, items) {
|
|
1559
|
+
let format = lists.get(locale);
|
|
1560
|
+
if (!format) lists.set(locale, format = new Intl.ListFormat(locale, {
|
|
1561
|
+
type: 'disjunction'
|
|
1463
1562
|
}));
|
|
1464
1563
|
return format.format(items);
|
|
1465
1564
|
}
|
|
1466
1565
|
/**
|
|
1467
|
-
* A text in `locale`, as {@link findText} finds it.
|
|
1468
|
-
*
|
|
1566
|
+
* A text in `locale`, as {@link findText} finds it. An `or` list in an app's catalog reads in the words of the locale
|
|
1567
|
+
* that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
|
|
1568
|
+
* nothing in zh-CN and with a space in ja.
|
|
1469
1569
|
*/ function renderText(translator, locale, text) {
|
|
1470
1570
|
const found = findText(translator, locale, text.key);
|
|
1471
1571
|
if (!found) return text.fallback ?? text.key;
|
|
@@ -1474,14 +1574,15 @@ const lists = new Map();
|
|
|
1474
1574
|
if (!Object.hasOwn(params, name)) return whole;
|
|
1475
1575
|
const value = params[name];
|
|
1476
1576
|
if (typeof value !== 'object') return String(value);
|
|
1477
|
-
if ('list' in value) return found.own ?
|
|
1577
|
+
if ('list' in value) return found.own && value.style === 'or' ? orListIn(found.locale, value.list) : value.list.join(value.joiner ?? ', ');
|
|
1478
1578
|
return renderText(translator, locale, value);
|
|
1479
1579
|
});
|
|
1480
1580
|
}
|
|
1481
1581
|
/** 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
1582
|
return (text)=>typeof text !== 'object' ? String(text) : 'list' in text ? text.list.join(text.joiner ?? ', ') : renderText(translator, locale, text);
|
|
1483
1583
|
}
|
|
1484
|
-
|
|
1584
|
+
// Shared by both builds of this version, so either answers in the app's language
|
|
1585
|
+
const translators = shared('clientTranslators', ()=>new WeakMap());
|
|
1485
1586
|
/** Records the translator of the app a client's interactions and messages come to. */ function registerClientTranslator(client, translator) {
|
|
1486
1587
|
if (translator) translators.set(client, translator);
|
|
1487
1588
|
else translators.delete(client);
|
|
@@ -1837,6 +1938,30 @@ const issueTexts = new WeakMap();
|
|
|
1837
1938
|
this.name = 'CooldownStoreError';
|
|
1838
1939
|
}
|
|
1839
1940
|
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Makes `instanceof` hold for an error from either build of this meocord version, ES module or CommonJS: the class's
|
|
1943
|
+
* prototype carries a brand both builds share, which `instanceof` on the class itself checks. A subclass, such as an
|
|
1944
|
+
* app's own `UserError`, keeps the usual check, so it still matches only its own instances.
|
|
1945
|
+
*/ function brand(error, name) {
|
|
1946
|
+
const key = sharedKey(`error:${name}`);
|
|
1947
|
+
Object.defineProperty(error.prototype, key, {
|
|
1948
|
+
value: true
|
|
1949
|
+
});
|
|
1950
|
+
Object.defineProperty(error, Symbol.hasInstance, {
|
|
1951
|
+
value (value) {
|
|
1952
|
+
if (this !== error) return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
1953
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null && key in value;
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
// Named as written, since a minifier may rename the classes themselves
|
|
1958
|
+
brand(GuardDeniedError, 'GuardDeniedError');
|
|
1959
|
+
brand(UserError, 'UserError');
|
|
1960
|
+
brand(CommandNotFoundError, 'CommandNotFoundError');
|
|
1961
|
+
brand(ValidationError, 'ValidationError');
|
|
1962
|
+
brand(MessageUsageError, 'MessageUsageError');
|
|
1963
|
+
brand(CooldownError, 'CooldownError');
|
|
1964
|
+
brand(CooldownStoreError, 'CooldownStoreError');
|
|
1840
1965
|
|
|
1841
1966
|
/** 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
1967
|
discord_js.RESTJSONErrorCodes.InvalidFormBodyOrContentType,
|
|
@@ -1853,38 +1978,6 @@ const issueTexts = new WeakMap();
|
|
|
1853
1978
|
else logger.error(`Could not ${what}:`, error);
|
|
1854
1979
|
}
|
|
1855
1980
|
|
|
1856
|
-
/** 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;
|
|
1857
|
-
/** 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;
|
|
1858
|
-
const SHORT_ESCAPES = {
|
|
1859
|
-
'\n': '\\n',
|
|
1860
|
-
'\r': '\\r',
|
|
1861
|
-
'\t': '\\t',
|
|
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);
|
|
1868
|
-
}
|
|
1869
|
-
/** How many characters of a user's text a log line quotes before cutting it short. */ const LOGGED_CHARACTERS = 200;
|
|
1870
|
-
/**
|
|
1871
|
-
* A user's text as a log line quotes it: escaped as {@link escapeForLog} does, with its quotes and backslashes too,
|
|
1872
|
-
* and cut short after 200 characters, with its length.
|
|
1873
|
-
*/ function quoteForLog(text) {
|
|
1874
|
-
const characters = [
|
|
1875
|
-
...text
|
|
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
|
-
});
|
|
1885
|
-
}
|
|
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
1981
|
/** 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
1982
|
/** 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
1983
|
if (value === null || value === undefined) return String(value);
|
|
@@ -1893,6 +1986,17 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
|
|
|
1893
1986
|
return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
|
|
1894
1987
|
}
|
|
1895
1988
|
/** 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}`;
|
|
1989
|
+
/**
|
|
1990
|
+
* Sets `key` on `target` as an own, enumerable data property: unlike `target[key] = value`, a key named `__proto__`
|
|
1991
|
+
* is a key like any other rather than the object's prototype.
|
|
1992
|
+
*/ function setOwn(target, key, value) {
|
|
1993
|
+
Object.defineProperty(target, key, {
|
|
1994
|
+
value,
|
|
1995
|
+
enumerable: true,
|
|
1996
|
+
writable: true,
|
|
1997
|
+
configurable: true
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
1896
2000
|
|
|
1897
2001
|
/**
|
|
1898
2002
|
* The discord.js interaction class each command type handles, shared by `@Command`, the dispatcher and the testing
|
|
@@ -1990,7 +2094,7 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
|
|
|
1990
2094
|
walk(option.options ?? []);
|
|
1991
2095
|
continue;
|
|
1992
2096
|
}
|
|
1993
|
-
params
|
|
2097
|
+
setOwn(params, option.name, resolveOptionValue(option));
|
|
1994
2098
|
}
|
|
1995
2099
|
};
|
|
1996
2100
|
walk(data);
|
|
@@ -2268,16 +2372,41 @@ function rewriteComponent(component, kept) {
|
|
|
2268
2372
|
})
|
|
2269
2373
|
};
|
|
2270
2374
|
/** The id MeoCord gives the containers it renders, so a view left behind can be found again. */ const RENDERED_CONTAINER_ID = 0x4d43;
|
|
2271
|
-
|
|
2375
|
+
// Shared by both builds of this version, so either answers with the app's presenter
|
|
2376
|
+
const presenters = shared('clientPresenters', ()=>new WeakMap());
|
|
2272
2377
|
/** Sets the presenter for the interactions a client receives. */ function setPresenter(client, presenter) {
|
|
2273
2378
|
presenters.set(client, presenter);
|
|
2274
2379
|
}
|
|
2275
2380
|
/** The presenter for an interaction's client, or the default one. */ function presenterFor(client) {
|
|
2276
2381
|
return client && presenters.get(client) || defaultPresenter;
|
|
2277
2382
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2383
|
+
/** Discord's limits on a message's text, counted by `String.length` as discord.js's builders count them. */ const TEXT_LIMITS = {
|
|
2384
|
+
embed: 4096,
|
|
2385
|
+
v2: 4000,
|
|
2386
|
+
content: 2000
|
|
2387
|
+
};
|
|
2388
|
+
/** `text` cut to `limit` characters ending in `…`, on a code point boundary so no surrogate pair is split. */ function fitText(text, limit) {
|
|
2389
|
+
if (text.length <= limit) return text;
|
|
2390
|
+
let cut = text.slice(0, Math.max(0, limit - 1));
|
|
2391
|
+
const last = cut.charCodeAt(cut.length - 1);
|
|
2392
|
+
if (last >= 0xd800 && last <= 0xdbff) cut = cut.slice(0, -1);
|
|
2393
|
+
return `${cut}…`;
|
|
2280
2394
|
}
|
|
2395
|
+
/**
|
|
2396
|
+
* An error's message as a presenter is given it: fitted to `limit`, and, when empty or only whitespace, which Discord
|
|
2397
|
+
* refuses as a text, MeoCord's generic error text, so a presenter that writes it as its text always renders.
|
|
2398
|
+
*/ function presentedMessage(message, limit, generic) {
|
|
2399
|
+
return fitText(message.trim() === '' ? generic() : message, limit);
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* The view's text after what MeoCord puts before it, its emoji and a Text Display's title line. When only those take
|
|
2403
|
+
* 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.
|
|
2404
|
+
*/ function textAfter(prefix, view, limit) {
|
|
2405
|
+
const total = prefix.length + view.text.length;
|
|
2406
|
+
if (total <= limit || view.text.length > limit || prefix.length >= limit) return prefix + view.text;
|
|
2407
|
+
return prefix + fitText(view.text, limit - prefix.length);
|
|
2408
|
+
}
|
|
2409
|
+
const emojiOf = (view)=>view.emoji ? `${view.emoji} ` : '';
|
|
2281
2410
|
/** Discord's limit of attachments on one message. */ const ATTACHMENT_LIMIT = 10;
|
|
2282
2411
|
/** The most items Discord shows in one media gallery. */ const MEDIA_GALLERY_LIMIT = 10;
|
|
2283
2412
|
/**
|
|
@@ -2361,7 +2490,7 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
|
|
|
2361
2490
|
return (view.files ?? []).filter((file)=>!shown.has(`attachment://${nameOf(file)}`));
|
|
2362
2491
|
}
|
|
2363
2492
|
/** 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(
|
|
2493
|
+
const embed = new discord_js.EmbedBuilder().setDescription(textAfter(emojiOf(view), view, TEXT_LIMITS.embed));
|
|
2365
2494
|
if (view.title) embed.setTitle(view.title);
|
|
2366
2495
|
if (view.color !== undefined) embed.setColor(view.color);
|
|
2367
2496
|
const image = view.image ?? unshownFiles(view).map(nameOf).find((name)=>IMAGE.test(name));
|
|
@@ -2372,7 +2501,8 @@ const IMAGE = /\.(png|jpe?g|gif|webp)$/i;
|
|
|
2372
2501
|
/** A view as a Components V2 container, carrying MeoCord's id. */ function renderContainer(view) {
|
|
2373
2502
|
const container = new discord_js.ContainerBuilder().setId(RENDERED_CONTAINER_ID);
|
|
2374
2503
|
if (view.color !== undefined) container.setAccentColor(discord_js.resolveColor(view.color));
|
|
2375
|
-
const
|
|
2504
|
+
const titleLine = view.title ? `### ${view.title}\n` : '';
|
|
2505
|
+
const text = new discord_js.TextDisplayBuilder().setContent(textAfter(titleLine + emojiOf(view), view, TEXT_LIMITS.v2));
|
|
2376
2506
|
if (view.thumbnail) {
|
|
2377
2507
|
container.addSectionComponents(new discord_js.SectionBuilder().addTextDisplayComponents(text).setThumbnailAccessory(new discord_js.ThumbnailBuilder().setURL(urlOf(view, view.thumbnail))));
|
|
2378
2508
|
} else container.addTextDisplayComponents(text);
|
|
@@ -2503,13 +2633,16 @@ const EMBED_CONTENT = [
|
|
|
2503
2633
|
return sameJson(content(a), content(b));
|
|
2504
2634
|
}
|
|
2505
2635
|
|
|
2506
|
-
|
|
2507
|
-
const
|
|
2636
|
+
// One count for both builds of this version, so calls either makes merge in the order made
|
|
2637
|
+
const calls = shared('callOrder', ()=>({
|
|
2638
|
+
last: 0,
|
|
2639
|
+
order: new WeakMap()
|
|
2640
|
+
}));
|
|
2508
2641
|
/** 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);
|
|
2642
|
+
calls.order.set(call, ++calls.last);
|
|
2510
2643
|
}
|
|
2511
2644
|
/** When a call stamped by {@link stampCall} was made, relative to the others. */ function callOrder(call) {
|
|
2512
|
-
return order.get(call) ?? 0;
|
|
2645
|
+
return calls.order.get(call) ?? 0;
|
|
2513
2646
|
}
|
|
2514
2647
|
|
|
2515
2648
|
/**
|
|
@@ -2642,7 +2775,10 @@ const logger$4 = new Logger('Theme');
|
|
|
2642
2775
|
*/ accept(id, result, report) {
|
|
2643
2776
|
if (result === undefined || result === null) return undefined;
|
|
2644
2777
|
const layer = copyLayer(result);
|
|
2645
|
-
|
|
2778
|
+
// A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
|
|
2779
|
+
const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`, {
|
|
2780
|
+
forLog: true
|
|
2781
|
+
});
|
|
2646
2782
|
if (problems.length === 0) {
|
|
2647
2783
|
if (report) this.warned.delete(id);
|
|
2648
2784
|
return layer;
|
|
@@ -2889,7 +3025,8 @@ function toBody(payload) {
|
|
|
2889
3025
|
*/ function editedSince(stamp, ours) {
|
|
2890
3026
|
return typeof stamp === 'number' && ours !== undefined && stamp > ours;
|
|
2891
3027
|
}
|
|
2892
|
-
|
|
3028
|
+
// Both builds of this meocord version keep one, so a message one locks, the other sees locked
|
|
3029
|
+
const messageLocks = shared('messageLocks', ()=>new Map());
|
|
2893
3030
|
/** How long a locked message is remembered once no call holds it. */ const LOCK_MEMORY_MS = 60000;
|
|
2894
3031
|
/**
|
|
2895
3032
|
* A body with the theme's primary colour where the app left one unset: an embed with no `color`, and a
|
|
@@ -2951,9 +3088,12 @@ function toJson(value) {
|
|
|
2951
3088
|
return this.phase;
|
|
2952
3089
|
}
|
|
2953
3090
|
get message() {
|
|
3091
|
+
// A deleted answer, or a follow-up in its place, leaves the component's own message behind
|
|
3092
|
+
if (this.answer.kind !== 'original') return this.lastMessage;
|
|
2954
3093
|
return this.lastMessage ?? ('message' in this.interaction ? this.interaction.message ?? undefined : undefined);
|
|
2955
3094
|
}
|
|
2956
3095
|
get original() {
|
|
3096
|
+
// Each component is a top-level component's API JSON, as `toJSON()` gave it; the lock reads them as plain JSON
|
|
2957
3097
|
return this.snapshot;
|
|
2958
3098
|
}
|
|
2959
3099
|
/** The Discord calls made through this state, for tests. */ get history() {
|
|
@@ -3285,9 +3425,11 @@ function toJson(value) {
|
|
|
3285
3425
|
this.sync();
|
|
3286
3426
|
const body = await this.themed(payload, options);
|
|
3287
3427
|
if (this.phase === 'unanswered') return this.reply(body);
|
|
3288
|
-
|
|
3428
|
+
// Once the answer is deleted, the next message sent is the answer
|
|
3429
|
+
if (this.answer.kind === 'deleted') return this.answerWith(body);
|
|
3430
|
+
if (this.phase === 'deferred' && answersWithOwnMessage(this.interaction) && this.answer.kind === 'original') {
|
|
3289
3431
|
// 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
|
|
3432
|
+
// would be shown to everyone on a public deferral, so that deferral is deleted, and the follow-up is the answer.
|
|
3291
3433
|
const requested = Number(discord_js.MessageFlagsBitField.resolve(body.flags ?? 0));
|
|
3292
3434
|
if (!hasEphemeral(requested)) return this.editMessage(body);
|
|
3293
3435
|
// A private deferral is already what the flag asks for, which an edit cannot take
|
|
@@ -3297,7 +3439,11 @@ function toJson(value) {
|
|
|
3297
3439
|
});
|
|
3298
3440
|
await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
|
|
3299
3441
|
this.phase = 'replied';
|
|
3442
|
+
return this.answerWith(body);
|
|
3300
3443
|
}
|
|
3444
|
+
return (await this.sendFollowUp(body)).message;
|
|
3445
|
+
}
|
|
3446
|
+
/** Sends a follow-up, with the flags it was sent with. */ async sendFollowUp(body) {
|
|
3301
3447
|
const flags = this.withSuppression(this.flagsFor('followUp', body.flags, false));
|
|
3302
3448
|
const sent = forMode(body, hasComponentsV2(flags));
|
|
3303
3449
|
const message = await this.call('followUp', {
|
|
@@ -3307,6 +3453,20 @@ function toJson(value) {
|
|
|
3307
3453
|
...sent,
|
|
3308
3454
|
flags
|
|
3309
3455
|
}));
|
|
3456
|
+
return {
|
|
3457
|
+
message: message,
|
|
3458
|
+
flags
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
/** Sends a follow-up that becomes the answer, which later edits and `delete()` act on by its id. */ async answerWith(body) {
|
|
3462
|
+
const { message, flags } = await this.sendFollowUp(body);
|
|
3463
|
+
// Its privacy as sent, not as read back from the message
|
|
3464
|
+
this.answer = {
|
|
3465
|
+
kind: 'followUp',
|
|
3466
|
+
id: message.id,
|
|
3467
|
+
ephemeral: hasEphemeral(flags)
|
|
3468
|
+
};
|
|
3469
|
+
this.lastMessage = message;
|
|
3310
3470
|
return message;
|
|
3311
3471
|
}
|
|
3312
3472
|
delete() {
|
|
@@ -3314,7 +3474,22 @@ function toJson(value) {
|
|
|
3314
3474
|
this.sync();
|
|
3315
3475
|
if (this.phase === 'unanswered') throw new Error('There is no answer to delete: the interaction has not been answered.');
|
|
3316
3476
|
this.refuseAfterModal();
|
|
3317
|
-
|
|
3477
|
+
const { answer } = this;
|
|
3478
|
+
if (answer.kind === 'deleted') throw new Error('There is no answer to delete: it was deleted.');
|
|
3479
|
+
// A follow-up is as private as it was sent, a command's own reply as the interaction's answer, a component's
|
|
3480
|
+
// message as its flags
|
|
3481
|
+
const shown = this.message?.flags?.has(discord_js.MessageFlags.Ephemeral) ?? false;
|
|
3482
|
+
const ephemeral = answer.kind === 'followUp' ? answer.ephemeral : answersWithOwnMessage(this.interaction) ? Boolean(this.interaction.ephemeral) || shown : shown;
|
|
3483
|
+
if (answer.kind === 'followUp') await this.call('deleteReply', answer.id, ()=>this.interaction.deleteReply(answer.id));
|
|
3484
|
+
else await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
|
|
3485
|
+
this.answer = {
|
|
3486
|
+
kind: 'deleted',
|
|
3487
|
+
ephemeral
|
|
3488
|
+
};
|
|
3489
|
+
this.lastMessage = undefined;
|
|
3490
|
+
// The message is gone: nothing is left to put back as it was before a lock
|
|
3491
|
+
this.settled = true;
|
|
3492
|
+
this.leave();
|
|
3318
3493
|
});
|
|
3319
3494
|
}
|
|
3320
3495
|
/** A modal is the interaction's whole answer: what the user enters arrives as a submit interaction of its own. */ refuseAfterModal() {
|
|
@@ -3424,7 +3599,20 @@ function toJson(value) {
|
|
|
3424
3599
|
withAttachments(body) {
|
|
3425
3600
|
return rewriteAttachmentUrls(body, keptAttachmentNames(body, this.message?.attachments?.values() ?? []));
|
|
3426
3601
|
}
|
|
3427
|
-
/**
|
|
3602
|
+
/**
|
|
3603
|
+
* Edits the answer through the interaction, and through the channel only once its token has expired. Once the answer
|
|
3604
|
+
* is deleted, the edit is sent as a follow-up that becomes the answer, as private as the deleted one unless it sets
|
|
3605
|
+
* flags of its own.
|
|
3606
|
+
*/ async editMessage(body, { restoring = false } = {}) {
|
|
3607
|
+
const { answer } = this;
|
|
3608
|
+
if (answer.kind === 'deleted') {
|
|
3609
|
+
if (restoring) return undefined;
|
|
3610
|
+
const { attachments: _attachments, ...rest } = body;
|
|
3611
|
+
return this.answerWith({
|
|
3612
|
+
...rest,
|
|
3613
|
+
flags: body.flags ?? (answer.ephemeral ? discord_js.MessageFlags.Ephemeral : 0)
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3428
3616
|
if (!restoring) {
|
|
3429
3617
|
this.settled = true;
|
|
3430
3618
|
this.leave();
|
|
@@ -3446,8 +3634,13 @@ function toJson(value) {
|
|
|
3446
3634
|
...this.withAttachments(forMode(body, this.v2)),
|
|
3447
3635
|
flags
|
|
3448
3636
|
};
|
|
3637
|
+
// A follow-up that is the answer is edited by its id
|
|
3638
|
+
const edit = answer.kind === 'followUp' ? {
|
|
3639
|
+
...sent,
|
|
3640
|
+
message: answer.id
|
|
3641
|
+
} : sent;
|
|
3449
3642
|
try {
|
|
3450
|
-
this.lastMessage = await this.call('editReply',
|
|
3643
|
+
this.lastMessage = await this.call('editReply', edit, ()=>this.interaction.editReply(edit));
|
|
3451
3644
|
} catch (error) {
|
|
3452
3645
|
const message = this.message;
|
|
3453
3646
|
const expired = Date.now() - this.interaction.createdTimestamp >= TOKEN_EXPIRED_AFTER_MS;
|
|
@@ -3566,12 +3759,16 @@ function toJson(value) {
|
|
|
3566
3759
|
* place, and the failure is returned for the caller to report once the user is answered.
|
|
3567
3760
|
*/ async drawnView(error, message, theme) {
|
|
3568
3761
|
const tone = isUserOutcome(error, this.interaction) ? 'warning' : 'danger';
|
|
3762
|
+
const context = this.presenterContext(this.v2, theme);
|
|
3763
|
+
// Fitted to the mode's limit first, so a presenter that writes the message as its text, MeoCord's own included, renders
|
|
3764
|
+
const generic = ()=>textFor(this.interaction, {
|
|
3765
|
+
key: 'meocord.fallback.error'
|
|
3766
|
+
});
|
|
3569
3767
|
const presented = {
|
|
3570
|
-
message,
|
|
3768
|
+
message: presentedMessage(message, TEXT_LIMITS[context.mode], generic),
|
|
3571
3769
|
error,
|
|
3572
3770
|
tone
|
|
3573
3771
|
};
|
|
3574
|
-
const context = this.presenterContext(this.v2, theme);
|
|
3575
3772
|
const ready = (view)=>{
|
|
3576
3773
|
const sendable = this.sendable(themedView(view, theme), this.keptBeside());
|
|
3577
3774
|
// Rendered once here, so a view MeoCord cannot render fails before anything is sent
|
|
@@ -3714,6 +3911,11 @@ function toJson(value) {
|
|
|
3714
3911
|
await this.followUpNow(this.privateError(view));
|
|
3715
3912
|
return;
|
|
3716
3913
|
}
|
|
3914
|
+
// A deleted message has nothing to add the error to or put back: the error follows up, and is the answer
|
|
3915
|
+
if (this.answer.kind === 'deleted') {
|
|
3916
|
+
await this.followUpNow(this.privateError(view));
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3717
3919
|
// The message the component is on: whether it is private does not change with edits.
|
|
3718
3920
|
const current = 'message' in this.interaction ? this.interaction.message ?? undefined : this.message;
|
|
3719
3921
|
if (current?.flags?.has(discord_js.MessageFlags.Ephemeral)) {
|
|
@@ -3759,6 +3961,12 @@ function toJson(value) {
|
|
|
3759
3961
|
/** The answer steps asked for, run one after another; `pending` counts those not yet settled. */ this.queue = Promise.resolve();
|
|
3760
3962
|
this.pending = 0;
|
|
3761
3963
|
/** Whether the answer was a modal, which has no message to edit or delete. */ this.modalShown = false;
|
|
3964
|
+
/**
|
|
3965
|
+
* Which message is the answer: the interaction's own reply, a follow-up that took its place, or none since `delete()`,
|
|
3966
|
+
* with whether the deleted one was private. `sync()` leaves it alone, so every edit follows it.
|
|
3967
|
+
*/ this.answer = {
|
|
3968
|
+
kind: 'original'
|
|
3969
|
+
};
|
|
3762
3970
|
this.calls = [];
|
|
3763
3971
|
/** Whether the interaction was acknowledged with a private deferred reply for a drawn error view, which that view replaces. */ this.privateDeferral = false;
|
|
3764
3972
|
/** Whether the locked message was answered, so nothing restores it again. */ this.settled = false;
|
|
@@ -3770,7 +3978,8 @@ function toJson(value) {
|
|
|
3770
3978
|
this.v2 = Boolean(message?.flags?.has(discord_js.MessageFlags.IsComponentsV2));
|
|
3771
3979
|
}
|
|
3772
3980
|
}
|
|
3773
|
-
|
|
3981
|
+
// One state per interaction for both builds of this version, so either sees where the other left its answer
|
|
3982
|
+
const states = shared('responseStates', ()=>new WeakMap());
|
|
3774
3983
|
/** The response state of a repliable interaction, created on first use. */ function responseOf(interaction) {
|
|
3775
3984
|
let state = states.get(interaction);
|
|
3776
3985
|
if (!state) states.set(interaction, state = new InteractionResponse(interaction));
|
|
@@ -4240,9 +4449,17 @@ const logger$2 = new Logger('ExecutionContext');
|
|
|
4240
4449
|
};
|
|
4241
4450
|
});
|
|
4242
4451
|
}
|
|
4243
|
-
/**
|
|
4452
|
+
/**
|
|
4453
|
+
* Drops the keys whose calls have all left their window, once a minute from a call, until a sweep leaves none: an
|
|
4454
|
+
* idle store then holds no timer, so one nothing references is collected, and its next call starts the sweep again.
|
|
4455
|
+
*/ startSweeping() {
|
|
4244
4456
|
if (this.sweeper) return;
|
|
4245
|
-
this.sweeper = setInterval(()=>
|
|
4457
|
+
this.sweeper = setInterval(()=>{
|
|
4458
|
+
sweep(this.calls, Date.now());
|
|
4459
|
+
if (this.calls.size > 0) return;
|
|
4460
|
+
clearInterval(this.sweeper);
|
|
4461
|
+
this.sweeper = undefined;
|
|
4462
|
+
}, SWEEP_INTERVAL_MS);
|
|
4246
4463
|
// A cooldown must never be what keeps a stopping bot alive.
|
|
4247
4464
|
this.sweeper.unref?.();
|
|
4248
4465
|
}
|
|
@@ -4539,6 +4756,21 @@ const messageOf = (error)=>String(error?.message ?? error);
|
|
|
4539
4756
|
};
|
|
4540
4757
|
}
|
|
4541
4758
|
|
|
4759
|
+
/**
|
|
4760
|
+
* Gives a decorator's `wrapper` the `length` and `name` of the method it replaces, so whatever reads the handler, such
|
|
4761
|
+
* as `invoke` counting the arguments it takes, sees the method as it was declared.
|
|
4762
|
+
*/ function wrapsMethod(wrapper, original) {
|
|
4763
|
+
Object.defineProperty(wrapper, 'length', {
|
|
4764
|
+
value: original.length,
|
|
4765
|
+
configurable: true
|
|
4766
|
+
});
|
|
4767
|
+
Object.defineProperty(wrapper, 'name', {
|
|
4768
|
+
value: original.name,
|
|
4769
|
+
configurable: true
|
|
4770
|
+
});
|
|
4771
|
+
return wrapper;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4542
4774
|
/**
|
|
4543
4775
|
* How specific a route pattern is, for ranking the message patterns that can match one message: more literal
|
|
4544
4776
|
* words first, then a fixed length before one taking the rest, then no optional param before one, then
|
|
@@ -4719,9 +4951,9 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
4719
4951
|
// An entry the class takes from its base, for a method it re-declares
|
|
4720
4952
|
const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
|
|
4721
4953
|
const commands = ownCommandMap(prototype);
|
|
4722
|
-
const baseCommands = Reflect.getMetadata(META.commands, base) ??
|
|
4954
|
+
const baseCommands = Reflect.getMetadata(META.commands, base) ?? Object.create(null);
|
|
4723
4955
|
for (const [name, metas] of Object.entries(commands)){
|
|
4724
|
-
const kept = metas.filter((meta)=>!inheritedFor(baseCommands
|
|
4956
|
+
const kept = metas.filter((meta)=>!inheritedFor(commandsNamed(baseCommands, name), (entry)=>entry.methodName)(meta));
|
|
4725
4957
|
if (kept.length > 0) commands[name] = kept;
|
|
4726
4958
|
else delete commands[name];
|
|
4727
4959
|
}
|
|
@@ -4745,16 +4977,21 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
|
|
|
4745
4977
|
Reflect.defineMetadata(key, ownHandlerList(key, prototype).filter((entry)=>!inheritedFor(inherited, method)(entry)), prototype);
|
|
4746
4978
|
}
|
|
4747
4979
|
}
|
|
4748
|
-
/**
|
|
4980
|
+
/**
|
|
4981
|
+
* The class's own command map, started from a copy of the inherited one, for the same reason. Keyed by command name
|
|
4982
|
+
* without a prototype, so a command named like an inherited key, such as `constructor`, is a key like any other.
|
|
4983
|
+
*/ function ownCommandMap(target) {
|
|
4749
4984
|
const own = Reflect.getOwnMetadata(META.commands, target);
|
|
4750
4985
|
if (own) return own;
|
|
4751
4986
|
const inherited = Reflect.getMetadata(META.commands, target) ?? {};
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4987
|
+
const copy = Object.create(null);
|
|
4988
|
+
for (const [name, metas] of Object.entries(inherited))copy[name] = [
|
|
4989
|
+
...metas
|
|
4990
|
+
];
|
|
4991
|
+
return copy;
|
|
4992
|
+
}
|
|
4993
|
+
/** The handlers a command map holds for `name`: its own entry only, never one `Object.prototype` holds. */ function commandsNamed(map, name) {
|
|
4994
|
+
return map && Object.hasOwn(map, name) ? map[name] : [];
|
|
4758
4995
|
}
|
|
4759
4996
|
function MessageHandler(pattern, options = {}) {
|
|
4760
4997
|
return declaring(function(target, propertyKey) {
|
|
@@ -4819,12 +5056,24 @@ function ReactionHandler(emojiOrSettings, settings = {}) {
|
|
|
4819
5056
|
}
|
|
4820
5057
|
// `{name}`, or `{name:type}` with the type read as far as the brace, so a type no segment can hold is named
|
|
4821
5058
|
const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
5059
|
+
// The whole of a brace pair that is a param, as PLACEHOLDER_PATTERN reads one
|
|
5060
|
+
const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
|
|
5061
|
+
/**
|
|
5062
|
+
* Why a pattern's brace pairs that aren't params are matched as literal text, or undefined when it has none: a
|
|
5063
|
+
* param's name is ASCII letters, digits and _, so `{café}` is text.
|
|
5064
|
+
*/ function literalBraces(pattern) {
|
|
5065
|
+
const braces = (pattern.match(/\{[^{}]*}/g) ?? []).filter((brace)=>!WHOLE_PLACEHOLDER.test(brace));
|
|
5066
|
+
if (braces.length === 0) return undefined;
|
|
5067
|
+
const one = braces.length === 1;
|
|
5068
|
+
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.`;
|
|
5069
|
+
}
|
|
4822
5070
|
/** The character a parameter will not cross, so one pattern segment maps to one value. */ const PARAM_SEPARATOR = '/';
|
|
5071
|
+
/** The longest customId Discord accepts; the shortest is one character. */ const MAX_CUSTOM_ID_LENGTH = 100;
|
|
4823
5072
|
/** Escapes a literal stretch of a pattern so only placeholders stay meaningful. */ const escapeLiteral = (literal)=>literal.replace(/[/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
4824
5073
|
/**
|
|
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.
|
|
5074
|
+
* Compiles a pattern into a regex, its parameter names, their types, its 4.1 specificity and the length of the
|
|
5075
|
+
* shortest customId it matches. A `{name}` takes a whole `/`-separated segment, so a uuid is captured whole and
|
|
5076
|
+
* `profile/{uuid}` never overlaps `profile/{uuid}/{id}`; a param that shares a segment with literal text is refused.
|
|
4828
5077
|
*/ function createRegexFromPattern(pattern) {
|
|
4829
5078
|
const params = [];
|
|
4830
5079
|
// By the param's own name, so one named like an inherited key, such as `__proto__`, keeps its type
|
|
@@ -4845,6 +5094,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4845
5094
|
if (literal !== '' && !literal.endsWith(PARAM_SEPARATOR) || after !== undefined && after !== PARAM_SEPARATOR) {
|
|
4846
5095
|
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
5096
|
}
|
|
5097
|
+
// A regex group's name, which the engine would refuse with the compiled regex in place of the pattern
|
|
5098
|
+
if (params.includes(param)) {
|
|
5099
|
+
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} appears twice; give each param its own name.`));
|
|
5100
|
+
}
|
|
5101
|
+
if (/^\d/.test(param)) {
|
|
5102
|
+
throw refuse(new Error(`Invalid pattern "${pattern}": {${param}} starts with a digit; start a param's name with a letter or _.`));
|
|
5103
|
+
}
|
|
4848
5104
|
if (type !== undefined) {
|
|
4849
5105
|
if (!isSegmentType(type)) throw refuse(new Error(`Invalid pattern "${pattern}": ${segmentTypeProblem(param, type)}`));
|
|
4850
5106
|
if (choicesOf(type)?.includes('')) {
|
|
@@ -4868,11 +5124,13 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4868
5124
|
params: params.length,
|
|
4869
5125
|
typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
|
|
4870
5126
|
});
|
|
5127
|
+
// Each param takes at least one character
|
|
4871
5128
|
return {
|
|
4872
5129
|
regex,
|
|
4873
5130
|
params,
|
|
4874
5131
|
types: lookupTable(types),
|
|
4875
|
-
specificity
|
|
5132
|
+
specificity,
|
|
5133
|
+
shortest: literalLength + params.length
|
|
4876
5134
|
};
|
|
4877
5135
|
}
|
|
4878
5136
|
/** 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 +5200,6 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4942
5200
|
*/ function Command(name, builderOrType) {
|
|
4943
5201
|
const commandName = typeof name === 'string' ? name : name.pattern;
|
|
4944
5202
|
return declaring(function(target, propertyKey, _descriptor) {
|
|
4945
|
-
var _commands, _commandName;
|
|
4946
5203
|
refuseOnClass('@Command', target, propertyKey);
|
|
4947
5204
|
const originalMethod = _descriptor.value;
|
|
4948
5205
|
if (!originalMethod) {
|
|
@@ -4950,7 +5207,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4950
5207
|
}
|
|
4951
5208
|
// Wrap original method for interaction type validation
|
|
4952
5209
|
const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
|
|
4953
|
-
|
|
5210
|
+
const checked = function(interaction, params) {
|
|
4954
5211
|
if (!matchesCommandType(commandType, interaction)) {
|
|
4955
5212
|
throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
|
|
4956
5213
|
}
|
|
@@ -4959,6 +5216,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4959
5216
|
params
|
|
4960
5217
|
]);
|
|
4961
5218
|
};
|
|
5219
|
+
_descriptor.value = wrapsMethod(checked, originalMethod);
|
|
4962
5220
|
// This class's own map, inherited routes included
|
|
4963
5221
|
const commands = ownCommandMap(target);
|
|
4964
5222
|
let builderInstance;
|
|
@@ -4994,7 +5252,7 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
4994
5252
|
}));
|
|
4995
5253
|
}
|
|
4996
5254
|
// 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 ` +
|
|
5255
|
+
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
5256
|
cause: error
|
|
4999
5257
|
}));
|
|
5000
5258
|
}
|
|
@@ -5014,6 +5272,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
5014
5272
|
commandType = builderOrType;
|
|
5015
5273
|
}
|
|
5016
5274
|
if (isCustomIdRouted(commandType)) {
|
|
5275
|
+
// A route warned as it was made
|
|
5276
|
+
const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
|
|
5277
|
+
if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
|
|
5017
5278
|
let pattern;
|
|
5018
5279
|
try {
|
|
5019
5280
|
pattern = createRegexFromPattern(commandName);
|
|
@@ -5025,6 +5286,11 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
5025
5286
|
regex = pattern.regex;
|
|
5026
5287
|
dynamicParams = pattern.params;
|
|
5027
5288
|
specificity = pattern.specificity;
|
|
5289
|
+
// Warned rather than refused, so a bot that carries such a handler still starts
|
|
5290
|
+
if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
|
|
5291
|
+
const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
|
|
5292
|
+
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.`);
|
|
5293
|
+
}
|
|
5028
5294
|
}
|
|
5029
5295
|
const declared = {
|
|
5030
5296
|
methodName: propertyKey,
|
|
@@ -5041,8 +5307,9 @@ const PLACEHOLDER_PATTERN = /\{(\w+)(?::([^}/]*))?}/g;
|
|
|
5041
5307
|
}
|
|
5042
5308
|
};
|
|
5043
5309
|
// One the class inherits for this method and route is replaced where it stands, so this one's options apply
|
|
5044
|
-
|
|
5045
|
-
const
|
|
5310
|
+
if (!Object.hasOwn(commands, commandName)) commands[commandName] = [];
|
|
5311
|
+
const metas = commands[commandName];
|
|
5312
|
+
const inherited = commandsNamed(getCommandMap(Object.getPrototypeOf(target)), commandName);
|
|
5046
5313
|
const replaced = metas.findIndex((meta)=>inherited.includes(meta) && meta.methodName === propertyKey && meta.type === commandType);
|
|
5047
5314
|
if (replaced === -1) metas.push(declared);
|
|
5048
5315
|
else metas[replaced] = declared;
|
|
@@ -5224,6 +5491,7 @@ exports.ExecutionContext = ExecutionContext;
|
|
|
5224
5491
|
exports.GuardDeniedError = GuardDeniedError;
|
|
5225
5492
|
exports.HandlerExecutionContext = HandlerExecutionContext;
|
|
5226
5493
|
exports.Logger = Logger;
|
|
5494
|
+
exports.MAX_CUSTOM_ID_LENGTH = MAX_CUSTOM_ID_LENGTH;
|
|
5227
5495
|
exports.MEOCORD_MESSAGES = MEOCORD_MESSAGES;
|
|
5228
5496
|
exports.META = META;
|
|
5229
5497
|
exports.MemoryCooldownStore = MemoryCooldownStore;
|
|
@@ -5233,6 +5501,7 @@ exports.REFUSED_AS_TOO_LARGE = REFUSED_AS_TOO_LARGE;
|
|
|
5233
5501
|
exports.ReactionHandler = ReactionHandler;
|
|
5234
5502
|
exports.RedisCooldownStore = RedisCooldownStore;
|
|
5235
5503
|
exports.SetMetadata = SetMetadata;
|
|
5504
|
+
exports.TEXT_LIMITS = TEXT_LIMITS;
|
|
5236
5505
|
exports.ThemeCache = ThemeCache;
|
|
5237
5506
|
exports.Translator = Translator;
|
|
5238
5507
|
exports.UnroutedExecutionContext = UnroutedExecutionContext;
|
|
@@ -5252,6 +5521,7 @@ exports.callOrder = callOrder;
|
|
|
5252
5521
|
exports.choicesOf = choicesOf;
|
|
5253
5522
|
exports.claimAmbientTheme = claimAmbientTheme;
|
|
5254
5523
|
exports.collectStartupErrors = collectStartupErrors;
|
|
5524
|
+
exports.commandsNamed = commandsNamed;
|
|
5255
5525
|
exports.compiledConfigMessage = compiledConfigMessage;
|
|
5256
5526
|
exports.cooldownMessage = cooldownMessage;
|
|
5257
5527
|
exports.cooldownStoreMessage = cooldownStoreMessage;
|
|
@@ -5272,6 +5542,7 @@ exports.dropInheritedRoutes = dropInheritedRoutes;
|
|
|
5272
5542
|
exports.duration = duration;
|
|
5273
5543
|
exports.escapeForLog = escapeForLog;
|
|
5274
5544
|
exports.existingResponse = existingResponse;
|
|
5545
|
+
exports.fitText = fitText;
|
|
5275
5546
|
exports.focusedOptionName = focusedOptionName;
|
|
5276
5547
|
exports.forgetRecorded = forgetRecorded;
|
|
5277
5548
|
exports.getAutocompleteHandlers = getAutocompleteHandlers;
|
|
@@ -5295,6 +5566,7 @@ exports.isRefusal = isRefusal;
|
|
|
5295
5566
|
exports.isTooLarge = isTooLarge;
|
|
5296
5567
|
exports.isUserOutcome = isUserOutcome;
|
|
5297
5568
|
exports.listed = listed;
|
|
5569
|
+
exports.literalBraces = literalBraces;
|
|
5298
5570
|
exports.loadMeoCordConfig = loadMeoCordConfig;
|
|
5299
5571
|
exports.logFailedSend = logFailedSend;
|
|
5300
5572
|
exports.logStartupErrors = logStartupErrors;
|
|
@@ -5334,10 +5606,13 @@ exports.routeSpecificity = routeSpecificity;
|
|
|
5334
5606
|
exports.runInThemeScope = runInThemeScope;
|
|
5335
5607
|
exports.runWithTheme = runWithTheme;
|
|
5336
5608
|
exports.setLegacyThemeLayer = setLegacyThemeLayer;
|
|
5609
|
+
exports.setOwn = setOwn;
|
|
5337
5610
|
exports.setPresenter = setPresenter;
|
|
5338
5611
|
exports.setStartupErrorsMode = setStartupErrorsMode;
|
|
5339
5612
|
exports.setThemeOutsideCalls = setThemeOutsideCalls;
|
|
5613
|
+
exports.shared = shared;
|
|
5340
5614
|
exports.sharedCustomId = sharedCustomId;
|
|
5615
|
+
exports.sharedName = sharedName;
|
|
5341
5616
|
exports.stampCall = stampCall;
|
|
5342
5617
|
exports.startupError = startupError;
|
|
5343
5618
|
exports.stopOnStartupErrors = stopOnStartupErrors;
|
|
@@ -5360,3 +5635,4 @@ exports.warnPlaceholder = warnPlaceholder;
|
|
|
5360
5635
|
exports.withRelease = withRelease;
|
|
5361
5636
|
exports.withSendableFiles = withSendableFiles;
|
|
5362
5637
|
exports.withoutFiles = withoutFiles;
|
|
5638
|
+
exports.wrapsMethod = wrapsMethod;
|