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.
Files changed (103) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -13
  3. package/dist/cjs/_shared/{controller.decorator-CRn9kWob.cjs → controller.decorator-U2NBWcoU.cjs} +416 -108
  4. package/dist/cjs/_shared/{providers-ChvMqnLd.cjs → providers-B4MOFBrW.cjs} +202 -50
  5. package/dist/cjs/_shared/{sharded-cooldown-store-BdAUTdYs.cjs → sharded-cooldown-store-HTUb3Ii_.cjs} +10 -7
  6. package/dist/cjs/_shared/{startup-roots-CQo8mSP8.cjs → startup-roots-lA5xkBFq.cjs} +228 -71
  7. package/dist/cjs/_shared/{translate-error-DhDVbT5M.cjs → translate-error-CbBHUu5P.cjs} +10 -5
  8. package/dist/cjs/common/index.cjs +3 -3
  9. package/dist/cjs/core/index.cjs +188 -169
  10. package/dist/cjs/decorator/index.cjs +52 -7
  11. package/dist/cjs/testing/index.cjs +1854 -311
  12. package/dist/cli.json +1 -1
  13. package/dist/esm/bin/generator.js +4 -5
  14. package/dist/esm/bin/helper/filter-generator.helper.js +4 -5
  15. package/dist/esm/bin/helper/guard-generator.helper.js +4 -5
  16. package/dist/esm/bin/helper/interceptor-generator.helper.js +4 -5
  17. package/dist/esm/bin/helper/observer-generator.helper.js +4 -5
  18. package/dist/esm/bin/helper/pipe-generator.helper.js +4 -5
  19. package/dist/esm/bin/helper/service-generator.helper.js +4 -5
  20. package/dist/esm/bin/meocord.js +84 -25
  21. package/dist/esm/build/load-config.pre-entry.js +13 -3
  22. package/dist/esm/build/rsbuild-config.js +16 -12
  23. package/dist/esm/build/typed-externals.js +36 -0
  24. package/dist/esm/common/cooldown-store.js +10 -2
  25. package/dist/esm/common/errors.js +25 -0
  26. package/dist/esm/common/meocord-text.js +11 -8
  27. package/dist/esm/common/response/call-order.js +9 -4
  28. package/dist/esm/common/response/presenter.js +33 -6
  29. package/dist/esm/common/response/response-state.js +89 -15
  30. package/dist/esm/common/route.js +11 -4
  31. package/dist/esm/common/sharded-cooldown-store.js +1 -1
  32. package/dist/esm/common/translator.js +52 -11
  33. package/dist/esm/core/command-registration.js +93 -24
  34. package/dist/esm/core/command-routes.js +2 -2
  35. package/dist/esm/core/cooldown-runner.js +29 -8
  36. package/dist/esm/core/dispatcher.js +9 -8
  37. package/dist/esm/core/fallback.js +20 -11
  38. package/dist/esm/core/filter-runner.js +2 -2
  39. package/dist/esm/core/guard-runner.js +16 -3
  40. package/dist/esm/core/handler-input.js +4 -3
  41. package/dist/esm/core/handler-pipeline.js +10 -4
  42. package/dist/esm/core/lifecycle-hooks.js +134 -47
  43. package/dist/esm/core/meocord-factory.js +59 -34
  44. package/dist/esm/core/meocord.app.js +116 -75
  45. package/dist/esm/core/message-help.js +32 -6
  46. package/dist/esm/core/observer-runner.js +2 -1
  47. package/dist/esm/core/providers.js +19 -4
  48. package/dist/esm/core/running-calls.js +43 -0
  49. package/dist/esm/core/shard-context.js +5 -6
  50. package/dist/esm/core/shard-manager.js +16 -10
  51. package/dist/esm/core/stage-scope.js +2 -1
  52. package/dist/esm/core/startup-roots.js +21 -1
  53. package/dist/esm/core/theme-resolvers.js +4 -1
  54. package/dist/esm/core/theme-scope.js +7 -2
  55. package/dist/esm/core/theme-validation.js +17 -10
  56. package/dist/esm/decorator/app.decorator.js +31 -0
  57. package/dist/esm/decorator/controller.decorator.js +56 -20
  58. package/dist/esm/decorator/cooldown.decorator.js +17 -0
  59. package/dist/esm/decorator/filter.decorator.js +2 -2
  60. package/dist/esm/decorator/guard.decorator.js +3 -2
  61. package/dist/esm/package.json.js +1 -1
  62. package/dist/esm/testing/catalog.js +20 -13
  63. package/dist/esm/testing/cooldown-store-suite.js +67 -5
  64. package/dist/esm/testing/discord-defaults.js +225 -30
  65. package/dist/esm/testing/meocord-testing-module.js +158 -44
  66. package/dist/esm/testing/mock-fn.js +107 -26
  67. package/dist/esm/testing/mock-interaction.js +1250 -182
  68. package/dist/esm/testing/snowflake.js +9 -2
  69. package/dist/esm/testing/strict-mocks.js +40 -17
  70. package/dist/esm/util/common.util.js +47 -19
  71. package/dist/esm/util/dev-runner.util.js +8 -8
  72. package/dist/esm/util/generator-cli.util.js +4 -5
  73. package/dist/esm/util/interaction.util.js +2 -2
  74. package/dist/esm/util/meocord-config-loader.util.js +10 -2
  75. package/dist/esm/util/meocord-config-validation.util.js +3 -5
  76. package/dist/esm/util/meocord-source-config.util.js +65 -20
  77. package/dist/esm/util/method-wrapper.util.js +16 -0
  78. package/dist/esm/util/platform.util.js +10 -10
  79. package/dist/esm/util/shard-process.util.js +4 -1
  80. package/dist/esm/util/sharding-mode.util.js +5 -7
  81. package/dist/esm/util/shared-state.util.js +21 -0
  82. package/dist/esm/util/tsconfig-paths.util.js +29 -0
  83. package/dist/esm/util/tsconfig.util.js +10 -10
  84. package/dist/esm/util/value.util.js +30 -2
  85. package/dist/types/common/index.d.cts +10 -7
  86. package/dist/types/common/index.d.ts +10 -7
  87. package/dist/types/core/index.d.cts +3 -2
  88. package/dist/types/core/index.d.ts +3 -2
  89. package/dist/types/decorator/index.d.cts +1 -0
  90. package/dist/types/decorator/index.d.ts +1 -0
  91. package/dist/types/{index-CdXsFgjE.d.ts → index-DKJclIi-.d.ts} +45 -21
  92. package/dist/types/{index-nn5p7OoX.d.cts → index-Dlf0ZZtS.d.cts} +45 -21
  93. package/dist/types/interface/index.d.cts +1 -1
  94. package/dist/types/interface/index.d.ts +1 -1
  95. package/dist/types/{route-BZgFoNeB.d.cts → route-Bh1U13yy.d.cts} +3 -2
  96. package/dist/types/{route-BZgFoNeB.d.ts → route-Bh1U13yy.d.ts} +3 -2
  97. package/dist/types/testing/index.d.cts +198 -62
  98. package/dist/types/testing/index.d.ts +198 -62
  99. package/dist/types/{theme-resolvers-UKr3-6Ys.d.cts → theme-resolvers-CyEz9pnv.d.cts} +1 -1
  100. package/dist/types/{theme-resolvers-BcOJVzR9.d.ts → theme-resolvers-DA204O-q.d.ts} +1 -1
  101. package/dist/types/{translator-DHMkkCpg.d.cts → translator-Bg7hwxDF.d.cts} +21 -8
  102. package/dist/types/{translator-BViWKtLJ.d.ts → translator-CeP4bCzp.d.ts} +21 -8
  103. package/package.json +2 -2
@@ -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` : 'Fix meocord.config.ts';
84
- return `MeoCord config at ${problem.path} failed to load: ${reason}. ${fix}, then run \`meocord build\`.`;
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-CRn9kWob.cjs', document.baseURI).href)));
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
- const scope = new node_async_hooks.AsyncLocalStorage();
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 handler's theme, @UseTheme included
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 keycap, or a pictograph with an optional presentation
905
- * selector or skin tone, joined to others by zero-width joiners into one sequence.
906
- */ const UNICODE_EMOJI = /^(?:\p{Regional_Indicator}{2}|[#*0-9]️?⃣|\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?(?:‍\p{Extended_Pictographic}(?:️|\p{Emoji_Modifier})?)*)$/u;
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 describe = (value)=>typeof value === 'string' ? `'${value}'` : Array.isArray(value) ? `[${value.map((item)=>typeof item === 'string' ? `'${item}'` : String(item)).join(', ')}]` : value === null ? 'null' : typeof value === 'object' ? 'an object' : String(value);
922
- const describeGroup = (value)=>Array.isArray(value) ? 'an array' : describe(value);
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
- */ function themeProblems(theme, where) {
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
- /** A message by its dotted key, or undefined when the catalog lacks it or the key names a group. */ function lookup(catalog, key) {
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 (typeof current === 'object' && current !== null && typeof current.other === 'string') {
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 !== undefined) return interpolate(pluralForm({
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), params);
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: Discord would show it as written
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
- /** A list in a locale's words: "a, b, or c" in en-US, or, for a unit, "a, b, c". */ function listIn(locale, style, items) {
1460
- let format = lists.get(`${locale} ${style}`);
1461
- if (!format) lists.set(`${locale} ${style}`, format = new Intl.ListFormat(locale, {
1462
- type: style === 'or' ? 'disjunction' : 'unit'
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. A list in MeoCord's English is joined with its `joiner`, a comma
1468
- * unless given; in an app's catalog, in the words of the locale that has it.
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 ? listIn(found.locale, value.style, value.list) : value.list.join(value.joiner ?? ', ');
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
- const translators = new WeakMap();
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
- /** 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);
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
- * 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
- });
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[option.name] = resolveOptionValue(option);
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
- const presenters = new WeakMap();
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
- function textOf(view) {
2279
- return view.emoji ? `${view.emoji} ${view.text}` : view.text;
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(textOf(view));
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 text = new discord_js.TextDisplayBuilder().setContent(view.title ? `### ${view.title}\n${textOf(view)}` : textOf(view));
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
- let last = 0;
2507
- const order = new WeakMap();
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
- const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`);
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
- const messageLocks = new Map();
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
- * Undoes `@Defer`'s acknowledgement after a guard denied the call silently: a command's deferred
3247
- * reply is deleted while nothing was sent into it; a component's invisible acknowledgement needs
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
- // Denied before the 'auto' timer: acknowledged here, since Discord tells the user an unanswered call failed
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
- if (this.phase === 'deferred' && answersWithOwnMessage(this.interaction)) {
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 first.
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
- await this.call('deleteReply', undefined, ()=>this.interaction.deleteReply());
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
- /** Edits the answer through the interaction, and through the channel only once its token has expired. */ async editMessage(body, { restoring = false } = {}) {
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', sent, ()=>this.interaction.editReply(sent));
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
- const states = new WeakMap();
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
- /** Drops the keys whose calls have all left their window, once a minute from the store's first call. */ startSweeping() {
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(()=>sweep(this.calls, Date.now()), SWEEP_INTERVAL_MS);
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[name] ?? [], (entry)=>entry.methodName)(meta));
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
- /** The class's own command map, started from a copy of the inherited one, for the same reason. */ function ownCommandMap(target) {
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
- return Object.fromEntries(Object.entries(inherited).map(([name, metas])=>[
4753
- name,
4754
- [
4755
- ...metas
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 and its 4.1 specificity. A `{name}` takes a
4826
- * whole `/`-separated segment, so a uuid is captured whole and `profile/{uuid}` never overlaps
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
- _descriptor.value = function(interaction, params) {
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 ` + `localizations, which Discord limits to 32 and 100 characters.`, {
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
- const metas = (_commands = commands)[_commandName = commandName] ?? (_commands[_commandName] = []);
5045
- const inherited = getCommandMap(Object.getPrototypeOf(target))?.[commandName] ?? [];
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;