meocord 4.2.2 → 4.2.3

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 (136) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/dist/cjs/_shared/{controller.decorator-U2NBWcoU.cjs → controller.decorator-hp5xdJU4.cjs} +512 -183
  3. package/dist/cjs/_shared/{startup-roots-lA5xkBFq.cjs → event-sources-DzqTODXg.cjs} +265 -80
  4. package/dist/cjs/_shared/{providers-B4MOFBrW.cjs → providers-BBvbnmLP.cjs} +780 -281
  5. package/dist/cjs/_shared/{sharded-cooldown-store-HTUb3Ii_.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-CbBHUu5P.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +432 -181
  9. package/dist/cjs/decorator/index.cjs +85 -120
  10. package/dist/cjs/testing/index.cjs +1664 -784
  11. package/dist/cli.json +1 -1
  12. package/dist/esm/bin/app-template/package.json.template +9 -9
  13. package/dist/esm/bin/app-template/src/main.ts.template +3 -2
  14. package/dist/esm/bin/generator.js +6 -4
  15. package/dist/esm/bin/helper/filter-generator.helper.js +6 -4
  16. package/dist/esm/bin/helper/guard-generator.helper.js +6 -4
  17. package/dist/esm/bin/helper/interceptor-generator.helper.js +6 -4
  18. package/dist/esm/bin/helper/observer-generator.helper.js +6 -4
  19. package/dist/esm/bin/helper/pipe-generator.helper.js +6 -4
  20. package/dist/esm/bin/helper/service-generator.helper.js +6 -4
  21. package/dist/esm/bin/meocord.js +106 -48
  22. package/dist/esm/build/native-addons.js +75 -19
  23. package/dist/esm/build/rsbuild-config.js +11 -3
  24. package/dist/esm/build/typed-externals.js +15 -2
  25. package/dist/esm/common/cooldown-store.js +38 -14
  26. package/dist/esm/common/deprecation.js +4 -1
  27. package/dist/esm/common/execution-context.js +3 -1
  28. package/dist/esm/common/explained-error.js +7 -3
  29. package/dist/esm/common/log-level.js +13 -9
  30. package/dist/esm/common/logger.js +4 -2
  31. package/dist/esm/common/meocord-text.js +11 -6
  32. package/dist/esm/common/redis-cooldown-store.js +3 -7
  33. package/dist/esm/common/response/flags.js +17 -1
  34. package/dist/esm/common/response/install-context.js +27 -16
  35. package/dist/esm/common/response/presenter.js +7 -3
  36. package/dist/esm/common/response/response-state.js +27 -5
  37. package/dist/esm/common/sharded-cooldown-store.js +11 -9
  38. package/dist/esm/common/theme.js +3 -1
  39. package/dist/esm/common/translator.js +17 -8
  40. package/dist/esm/core/autocomplete-routes.js +26 -0
  41. package/dist/esm/core/command-conflicts.js +15 -7
  42. package/dist/esm/core/command-registration.js +15 -1
  43. package/dist/esm/core/cooldown-runner.js +7 -9
  44. package/dist/esm/core/defer.js +2 -5
  45. package/dist/esm/core/dispatcher.js +83 -58
  46. package/dist/esm/core/event-requirements.js +67 -19
  47. package/dist/esm/core/event-sources.js +100 -0
  48. package/dist/esm/core/fallback.js +3 -1
  49. package/dist/esm/core/filter-runner.js +4 -4
  50. package/dist/esm/core/guard-runner.js +173 -52
  51. package/dist/esm/core/handler-input.js +24 -3
  52. package/dist/esm/core/handler-pipeline.js +44 -28
  53. package/dist/esm/core/handler-registry.js +51 -21
  54. package/dist/esm/core/input-runner.js +7 -8
  55. package/dist/esm/core/interceptor-runner.js +4 -5
  56. package/dist/esm/core/lifecycle-hooks.js +3 -8
  57. package/dist/esm/core/lifecycle-order.js +1 -15
  58. package/dist/esm/core/meocord-classes.js +14 -1
  59. package/dist/esm/core/meocord-factory.js +80 -53
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +5 -9
  62. package/dist/esm/core/message-params.js +126 -46
  63. package/dist/esm/core/message-routes.js +225 -45
  64. package/dist/esm/core/message-words.js +30 -2
  65. package/dist/esm/core/observer-runner.js +2 -1
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +28 -3
  68. package/dist/esm/core/scalar-types.js +57 -16
  69. package/dist/esm/core/shard-context.js +11 -10
  70. package/dist/esm/core/shard-exit.js +3 -3
  71. package/dist/esm/core/shard-manager.js +16 -6
  72. package/dist/esm/core/startup-roots.js +28 -8
  73. package/dist/esm/core/theme-resolvers.js +26 -11
  74. package/dist/esm/core/theme-runtime.js +23 -13
  75. package/dist/esm/core/theme-scope.js +27 -29
  76. package/dist/esm/core/theme-validation.js +1 -1
  77. package/dist/esm/decorator/app.decorator.js +6 -1
  78. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  79. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  80. package/dist/esm/decorator/controller.decorator.js +48 -19
  81. package/dist/esm/decorator/defer.decorator.js +2 -2
  82. package/dist/esm/decorator/event.decorator.js +4 -5
  83. package/dist/esm/decorator/filter.decorator.js +2 -2
  84. package/dist/esm/decorator/guard.decorator.js +4 -61
  85. package/dist/esm/decorator/inject.decorator.js +4 -1
  86. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  87. package/dist/esm/decorator/theme.decorator.js +4 -3
  88. package/dist/esm/decorator/validation.decorator.js +5 -5
  89. package/dist/esm/package.json.js +1 -1
  90. package/dist/esm/testing/catalog.js +29 -13
  91. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  92. package/dist/esm/testing/discord-defaults.js +449 -355
  93. package/dist/esm/testing/inspect-handler.js +91 -10
  94. package/dist/esm/testing/meocord-testing-module.js +195 -77
  95. package/dist/esm/testing/mock-fn.js +8 -2
  96. package/dist/esm/testing/mock-interaction.js +639 -113
  97. package/dist/esm/testing/response-log.js +8 -0
  98. package/dist/esm/testing/response.js +7 -5
  99. package/dist/esm/testing/routing.js +48 -6
  100. package/dist/esm/testing/strict-mocks.js +1 -0
  101. package/dist/esm/testing/theme.js +3 -2
  102. package/dist/esm/util/common.util.js +30 -9
  103. package/dist/esm/util/generator-cli.util.js +6 -4
  104. package/dist/esm/util/inherited-env.util.js +72 -13
  105. package/dist/esm/util/json.util.js +10 -3
  106. package/dist/esm/util/meocord-cli.util.js +3 -2
  107. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  108. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  109. package/dist/esm/util/meocord-source-config.util.js +13 -3
  110. package/dist/esm/util/platform.util.js +3 -2
  111. package/dist/esm/util/refusal.util.js +29 -11
  112. package/dist/esm/util/registration-mode.util.js +14 -1
  113. package/dist/esm/util/timer-limit.util.js +4 -1
  114. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  115. package/dist/esm/util/tsconfig.util.js +16 -58
  116. package/dist/esm/util/user-text.util.js +8 -4
  117. package/dist/esm/util/value.util.js +15 -4
  118. package/dist/types/common/index.d.cts +13 -9
  119. package/dist/types/common/index.d.ts +13 -9
  120. package/dist/types/core/index.d.cts +21 -11
  121. package/dist/types/core/index.d.ts +21 -11
  122. package/dist/types/decorator/index.d.cts +63 -63
  123. package/dist/types/decorator/index.d.ts +63 -63
  124. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  125. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  126. package/dist/types/interface/index.d.cts +1 -1
  127. package/dist/types/interface/index.d.ts +1 -1
  128. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  129. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  130. package/dist/types/testing/index.d.cts +137 -54
  131. package/dist/types/testing/index.d.ts +137 -54
  132. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  133. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  134. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  135. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  136. package/package.json +5 -2
@@ -91,9 +91,22 @@ let problem;
91
91
  * module, or imports a file that isn't there. Otherwise its code threw as it ran.
92
92
  */ function fixedInConfig(error) {
93
93
  const code = error?.code;
94
- if (error instanceof SyntaxError || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
94
+ if (unparsable(error) || code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;
95
95
  return typeof code === 'string' && code.startsWith('ERR_REQUIRE_');
96
96
  }
97
+ /** The name of Bun's error for a module it could not parse; several of them come together in an AggregateError. */ const BUN_PARSE_ERROR = 'BuildMessage';
98
+ /**
99
+ * Whether a module could not be parsed, so none of its code ran: on Node, a SyntaxError thrown from compiling it, whose
100
+ * first frame is Node's own; on Bun, its parse error. A SyntaxError the config's code throws as it runs, from
101
+ * `JSON.parse` or `new RegExp` on an environment value, is not one.
102
+ */ function unparsable(error) {
103
+ const bunParseError = (each)=>each?.name === BUN_PARSE_ERROR;
104
+ if (bunParseError(error)) return true;
105
+ if (error instanceof AggregateError) return error.errors.length > 0 && error.errors.every(bunParseError);
106
+ if (!(error instanceof SyntaxError)) return false;
107
+ const firstFrame = error.stack?.split('\n').find((line)=>line.trimStart().startsWith('at '));
108
+ return firstFrame === undefined || /[( ]node:/.test(firstFrame);
109
+ }
97
110
  /**
98
111
  * The package a module-not-found error names, when it is a package rather than a file: `dotenv` for `dotenv/config`,
99
112
  * `@scope/name` for `@scope/name/sub`, with the installed package that imports it, if one does. Undefined for any other
@@ -128,7 +141,7 @@ let problem;
128
141
  try {
129
142
  // Called through a variable so a bundler does not try to resolve the path while building the
130
143
  // application; the file is only there once the build is done.
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)));
144
+ 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-hp5xdJU4.cjs', document.baseURI).href)));
132
145
  const loaded = load(compiledPath);
133
146
  return loaded.default ?? loaded;
134
147
  } catch (error) {
@@ -142,6 +155,28 @@ let problem;
142
155
  }
143
156
  }
144
157
 
158
+ var version = "4.2.3";
159
+ var packageJson = {
160
+ version: version};
161
+
162
+ /**
163
+ * The name of a key the ES module and CommonJS builds of this meocord version share, and no other version has, for
164
+ * `Symbol.for`: a process that loads both builds of one install keeps one state under it, and two installed versions
165
+ * keep theirs apart. `Symbol.for(sharedName(...))` written out types as a `unique symbol`.
166
+ */ const sharedName = (name)=>`meocord@${packageJson.version}:${name}`;
167
+ /** The key named {@link sharedName}. */ function sharedKey(name) {
168
+ return Symbol.for(sharedName(name));
169
+ }
170
+ /** The state both builds of this version keep on `globalThis` under `name`, made by whichever asks first. */ function shared(name, make) {
171
+ const key = sharedKey(name);
172
+ const store = globalThis;
173
+ if (!Object.hasOwn(store, key)) Object.defineProperty(store, key, {
174
+ value: make(),
175
+ configurable: true
176
+ });
177
+ return store[key];
178
+ }
179
+
145
180
  /** The environment variable that sets the level for one run, over `logLevel` in meocord.config.ts. */ const LOG_LEVEL_ENV = 'MEOCORD_LOG_LEVEL';
146
181
  /** Each level's rank: a line prints when its rank is at least the level's. `silent` outranks every line. */ const LOG_LEVEL_RANK = {
147
182
  debug: 0,
@@ -151,32 +186,35 @@ let problem;
151
186
  silent: 4
152
187
  };
153
188
  const isLogLevel = (value)=>typeof value === 'string' && Object.hasOwn(LOG_LEVEL_RANK, value);
154
- let threshold;
155
- let rejectedEnv;
189
+ // One per process for this meocord version, so both builds print at one level and warn about a bad variable once
190
+ const level = shared('logLevel', ()=>({
191
+ threshold: undefined,
192
+ rejectedEnv: undefined
193
+ }));
156
194
  /**
157
195
  * The rank a line needs to print. Resolved on first use and kept, so logging reads neither the
158
196
  * environment nor the config again: `MEOCORD_LOG_LEVEL`, then `logLevel`, then `debug` in development
159
197
  * and `log` otherwise. `logLevel` applies to the built bot only; the CLI and tests would otherwise read
160
198
  * it from whatever `dist` a previous build left.
161
199
  */ function logThreshold() {
162
- if (threshold === undefined) {
200
+ if (level.threshold === undefined) {
163
201
  // The config first: loading it runs its dotenv call, which may set the variable
164
202
  const configured = isBuiltApplication() ? loadMeoCordConfig()?.logLevel : undefined;
165
203
  const raw = process.env[LOG_LEVEL_ENV];
166
204
  // An environment variable is often written in capitals, DEBUG for debug
167
205
  const fromEnv = raw?.toLowerCase();
168
- rejectedEnv = raw && !isLogLevel(fromEnv) ? raw : undefined;
169
- const level = isLogLevel(fromEnv) ? fromEnv : isLogLevel(configured) ? configured : process.env.NODE_ENV === 'development' ? 'debug' : 'log';
170
- threshold = LOG_LEVEL_RANK[level];
206
+ level.rejectedEnv = raw && !isLogLevel(fromEnv) ? raw : undefined;
207
+ const chosen = isLogLevel(fromEnv) ? fromEnv : isLogLevel(configured) ? configured : process.env.NODE_ENV === 'development' ? 'debug' : 'log';
208
+ level.threshold = LOG_LEVEL_RANK[chosen];
171
209
  }
172
- return threshold;
210
+ return level.threshold;
173
211
  }
174
212
  /**
175
213
  * An unknown `MEOCORD_LOG_LEVEL` the last resolution ignored, handed out once so the logger can warn
176
214
  * about it; undefined afterwards.
177
215
  */ function takeRejectedLogLevel() {
178
- const rejected = rejectedEnv;
179
- rejectedEnv = undefined;
216
+ const rejected = level.rejectedEnv;
217
+ level.rejectedEnv = undefined;
180
218
  return rejected;
181
219
  }
182
220
 
@@ -204,7 +242,8 @@ const tagColours = TAG_COLOURS;
204
242
  * The shortest value registered: a bot's credential is far longer, and a short value, such as a test's placeholder,
205
243
  * would replace ordinary words in every line.
206
244
  */ const MIN_HIDDEN_LENGTH = 16;
207
- const hiddenValues = new Set();
245
+ // One per process for this meocord version, so a value hidden through one build stays out of the other's lines
246
+ const hiddenValues = shared('hiddenValues', ()=>new Set());
208
247
  /**
209
248
  * Keeps `value`, such as the bot's credential, out of every line Logger prints from now on, whatever object, string
210
249
  * or error carries it: trimmed, and without the `Bot ` or `Bearer ` prefix discord.js strips.
@@ -345,7 +384,8 @@ const redact = (text)=>{
345
384
  }
346
385
  }
347
386
 
348
- const warned = new Set();
387
+ // One per process for this meocord version, so a warning both builds give prints once
388
+ const warned = shared('deprecationWarnings', ()=>new Set());
349
389
  /** Warns once per run that `old` goes in 5.0, naming what to use instead. */ function warnDeprecated(logger, old, replacement) {
350
390
  warnOnce(logger, `${old} is deprecated and will be removed in the next major version (5.0). Use ${replacement} instead.`);
351
391
  }
@@ -403,28 +443,6 @@ const warned = new Set();
403
443
  }
404
444
  });
405
445
 
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
446
  // One per process for this meocord version, so a call's theme reads the same from its ES module and CommonJS builds
429
447
  const scope = shared('themeScope', ()=>new node_async_hooks.AsyncLocalStorage());
430
448
  const isPlainObject$1 = (value)=>{
@@ -487,50 +505,50 @@ const merges = new WeakMap();
487
505
  if (!merged) byLayer.set(layer, merged = deepFreeze(mergeInto(theme, layer)));
488
506
  return merged;
489
507
  }
490
- // Written by the deprecated `Theme` statics: beneath every theme an app sets
491
- let legacyLayer;
492
- let legacyVersion = 0;
493
- let defaults = DEFAULT_THEME;
508
+ /**
509
+ * The theme state outside a call, one for both builds of this version so either reads what the other set: the defaults
510
+ * with what the deprecated `Theme` statics set beneath every theme, the app whose theme is read outside a call, and how
511
+ * an answer outside a call finds its theme.
512
+ */ const state = shared('themeState', ()=>({
513
+ defaults: DEFAULT_THEME,
514
+ legacyVersion: 0,
515
+ ambient: undefined,
516
+ ambientVersion: 0,
517
+ themeOutsideCalls: undefined
518
+ }));
494
519
  /** MeoCord's defaults, with what the deprecated `Theme` statics set over them. */ function defaultTheme() {
495
- return defaults;
520
+ return state.defaults;
496
521
  }
497
522
  /** The number of times the legacy layer has changed, so a theme built on an older one is built again. */ function themeLayersVersion() {
498
- return legacyVersion;
523
+ return state.legacyVersion;
499
524
  }
500
525
  /** Sets the layer the deprecated `Theme` statics write, merged over MeoCord's defaults. */ function setLegacyThemeLayer(layer) {
501
- legacyLayer = layer === undefined ? undefined : copyLayer(layer);
502
- defaults = mergeTheme(DEFAULT_THEME, legacyLayer);
503
- legacyVersion++;
526
+ state.defaults = mergeTheme(DEFAULT_THEME, layer === undefined ? undefined : copyLayer(layer));
527
+ state.legacyVersion++;
504
528
  }
505
- // The theme outside any call: the one of the app or testing module that claimed it, until it gives it up
506
- let ambient;
507
- let ambientVersion = 0;
508
529
  /** The number of times the app read outside a call has changed, so each app decides again whether its calls need a scope. */ function ambientThemeVersion() {
509
- return ambientVersion;
510
- }
511
- /** Whether some app's theme is the one read outside a call. */ function hasAmbientTheme() {
512
- return ambient !== undefined;
530
+ return state.ambientVersion;
513
531
  }
514
532
  /**
515
533
  * Makes an app's theme the one read outside a call, when no other app has: a bot runs one app, and code it runs
516
534
  * outside a handler, such as a scheduled job, then reads that app's theme. A second app in the same process keeps
517
535
  * its theme to its own calls.
518
536
  */ function claimAmbientTheme(owner, theme) {
519
- if (ambient && ambient.owner !== owner) return false;
520
- if (!ambient) ambientVersion++;
521
- ambient = {
537
+ if (state.ambient && state.ambient.owner !== owner) return false;
538
+ if (!state.ambient) state.ambientVersion++;
539
+ state.ambient = {
522
540
  owner,
523
541
  theme
524
542
  };
525
543
  return true;
526
544
  }
527
545
  /** Gives up the theme read outside a call, when `owner` has it: its start failed, or it has shut down. */ function releaseAmbientTheme(owner) {
528
- if (ambient?.owner !== owner) return;
529
- ambient = undefined;
530
- ambientVersion++;
546
+ if (state.ambient?.owner !== owner) return;
547
+ state.ambient = undefined;
548
+ state.ambientVersion++;
531
549
  }
532
550
  /** Whether `owner` is the app whose theme is read outside a call. */ function ownsAmbientTheme(owner) {
533
- return ambient?.owner === owner;
551
+ return state.ambient?.owner === owner;
534
552
  }
535
553
  /** Runs `fn` with `theme` as the theme of the call, for everything it runs, awaits or starts. */ function runWithTheme(theme, fn) {
536
554
  return scope.run({
@@ -546,10 +564,8 @@ let ambientVersion = 0;
546
564
  /** Runs `fn` in `themeScope`, whose theme may be replaced while it runs, as a per-server theme is found. */ function runInThemeScope(themeScope, fn) {
547
565
  return scope.run(themeScope, fn);
548
566
  }
549
- // Set by the runtime: the theme an app gives an interaction answered outside any call, as in a collector
550
- let themeOutsideCalls;
551
567
  /** Sets how an answer outside any call finds its theme: from the interaction's app, server and user. */ function setThemeOutsideCalls(resolve) {
552
- themeOutsideCalls = resolve;
568
+ state.themeOutsideCalls = resolve;
553
569
  }
554
570
  /**
555
571
  * The theme an answer to `interaction` uses: the running call's, or, outside any call, as in a collector's callback,
@@ -557,7 +573,7 @@ let themeOutsideCalls;
557
573
  */ function themeForInteraction(interaction) {
558
574
  const call = scope.getStore();
559
575
  if (call) return call.theme;
560
- return themeOutsideCalls?.(interaction) ?? useTheme();
576
+ return state.themeOutsideCalls?.(interaction) ?? useTheme();
561
577
  }
562
578
  /**
563
579
  * The theme of the running call, with every role present.
@@ -586,7 +602,7 @@ let themeOutsideCalls;
586
602
  *
587
603
  * @group Responses
588
604
  */ function useTheme() {
589
- return scope.getStore()?.theme ?? ambient?.theme() ?? defaults;
605
+ return scope.getStore()?.theme ?? state.ambient?.theme() ?? state.defaults;
590
606
  }
591
607
  /**
592
608
  * Makes a function run in the theme of the call that binds it, wherever it is called from later.
@@ -635,7 +651,10 @@ const EXPLAINED = Symbol.for('meocord.explained');
635
651
  *
636
652
  * Use it where a bot logs a startup failure, so an error MeoCord explained is not logged twice: an app
637
653
  * `MeoCordFactory.create()` refuses, such as two handlers for one command, or Discord refusing the bot token or a
638
- * privileged intent at login. The error is otherwise unchanged.
654
+ * privileged intent at login. A built bot has `process.exitCode` set for such an error already, and a start that a stop
655
+ * ended, which is explained too, leaves it as it was, so the process exits 0. Outside a build, as a script run by hand,
656
+ * `MeoCordFactory.create()` sets no code for an app it refuses, so a catch that returns early for an explained error,
657
+ * as below, sets `process.exitCode` itself there. The error is otherwise unchanged.
639
658
  *
640
659
  * @param error - An error `MeoCordFactory.create()` threw or `app.start()` rejected with.
641
660
  * @returns `true` when MeoCord explained the error.
@@ -651,9 +670,10 @@ const EXPLAINED = Symbol.for('meocord.explained');
651
670
  * await app.start()
652
671
  * }
653
672
  *
654
- * // An error MeoCord explained, such as an app it refuses or a token Discord refused, is already logged
673
+ * // An error MeoCord explained, such as an app it refuses or a token Discord refused, is already logged, with the exit code set
655
674
  * bootstrap().catch(error => {
656
- * if (!isExplainedError(error)) logger.error('Error during startup:', error)
675
+ * if (isExplainedError(error)) return
676
+ * logger.error('Error during startup:', error)
657
677
  * process.exitCode = 1
658
678
  * })
659
679
  * ```
@@ -682,10 +702,16 @@ const EXPLAINED = Symbol.for('meocord.explained');
682
702
  }
683
703
 
684
704
  const REFUSAL = Symbol.for('meocord.refusal');
685
- /** Whether this process reports refusals yet; see `reportRefusals`. */ let reporting = false;
686
- /** Refusals someone else reports, and ends the process for, which `reportRefusals` leaves to them. */ const handedOff = new WeakSet();
705
+ /**
706
+ * One per process for this meocord version, so the two builds report a refusal once: whether this process reports
707
+ * refusals yet (see `reportRefusals`), and the refusals someone else reports, and ends the process for, which it leaves
708
+ * to them.
709
+ */ const refusals = shared('refusalReporting', ()=>({
710
+ reporting: false,
711
+ handedOff: new WeakSet()
712
+ }));
687
713
  /** Leaves a refusal to whoever reports it instead, such as a shard's manager: going uncaught, it is neither logged nor exits. */ function handOffRefusal(error) {
688
- handedOff.add(error);
714
+ refusals.handedOff.add(error);
689
715
  }
690
716
  /**
691
717
  * Marks an error MeoCord raises for code it refuses as the application loads, such as a decorator given what it
@@ -696,18 +722,29 @@ const REFUSAL = Symbol.for('meocord.refusal');
696
722
  enumerable: false
697
723
  });
698
724
  // Before the error is thrown, so a built application reports it however the throw ends
699
- if (!reporting && isBuiltApplication()) reportRefusals();
725
+ if (!refusals.reporting && isBuiltApplication()) reportRefusals();
700
726
  return error;
701
727
  }
702
- /** What a decorator is applied to, as a refusal names it: `Class.method` on a method, `Class` on a class. */ function decoratedName(target, propertyKey) {
728
+ /** The name of the class a member decorator is applied in: a static member's target is the class itself. */ function ownerName(target) {
729
+ return (typeof target === 'function' ? target : target.constructor).name;
730
+ }
731
+ /**
732
+ * What a decorator is applied to, as a refusal names it: `Class.method` on a method, a static one included, `Class` on
733
+ * a class.
734
+ */ function decoratedName(target, propertyKey) {
703
735
  if (propertyKey === undefined) return target.name || 'a class';
704
- return `${target.constructor.name}.${String(propertyKey)}`;
736
+ return `${ownerName(target)}.${String(propertyKey)}`;
705
737
  }
706
738
  /**
707
739
  * Refuses a decorator that goes only on a method, or on a member, where it is applied to a class, directly or through
708
- * `applyDecorators`, rather than store what nothing reads or fail on the method it lacks.
740
+ * `applyDecorators`, rather than store what nothing reads or fail on the method it lacks. On a static member, where
741
+ * nothing reads it either, it warns; the next major version (5.0) refuses it there too.
709
742
  */ function refuseOnClass(decorator, target, propertyKey, place = 'a method') {
710
743
  if (propertyKey === undefined) throw refuse(new Error(`${decoratedName(target)}: ${decorator} goes on ${place}, not on a class.`));
744
+ if (typeof target === 'function') {
745
+ const where = place === 'a method' ? 'an instance method, not a static one; it never runs' : 'an instance member, not a static one, where nothing reads it';
746
+ warnOnce(new Logger(decorator.slice(1)), `${decoratedName(target, propertyKey)}: ${decorator} goes on ${where}. The next major version (5.0) refuses it.`);
747
+ }
711
748
  }
712
749
  /** Refuses a decorator that goes only on a class where it is applied to a method, directly or through `applyDecorators`. */ function refuseOnMethod(decorator, target, propertyKey) {
713
750
  if (propertyKey !== undefined) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${decorator} goes on a class, not on a method.`));
@@ -890,9 +927,9 @@ function keepOrThrow(error, target, propertyKey) {
890
927
  * report. Installed by the first refusal in a built application. Bun has no monitor event for a rejection, so there a
891
928
  * listener reports a refused rejection and rejects any other again without itself, unless the app listens itself.
892
929
  */ function reportRefusals(log = (text)=>new Logger('MeoCord').error(text), exit = (code)=>process.exit(code), { bun = process.versions.bun !== undefined, reject = (reason)=>void Promise.reject(reason) } = {}) {
893
- reporting = true;
930
+ refusals.reporting = true;
894
931
  const report = (error)=>{
895
- if (handedOff.has(error)) return;
932
+ if (refusals.handedOff.has(error)) return;
896
933
  if (!isExplainedError(error)) log(describeRefusal(error, process.cwd()));
897
934
  exit(1);
898
935
  };
@@ -911,8 +948,8 @@ function keepOrThrow(error, target, propertyKey) {
911
948
  process.on('unhandledRejection', onRejection);
912
949
  }
913
950
 
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;
951
+ /** 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\p{Bidi_Control}]/gu;
952
+ /** The same, with the quote and the backslash, for text shown between quotes. */ const UNSAFE_IN_QUOTES = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\p{Bidi_Control}"\\]/gu;
916
953
  const SHORT_ESCAPES = {
917
954
  '\n': '\\n',
918
955
  '\r': '\\r',
@@ -936,8 +973,12 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
936
973
  if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
937
974
  return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
938
975
  }
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(), {
976
+ /**
977
+ * Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed.
978
+ * Each run of whitespace reads as one space, and the spaces around the words are kept, since a word in quotes can
979
+ * carry them and be refused for them.
980
+ */ function userWords(text) {
981
+ return discord_js.escapeMarkdown(text.replace(/\s+/g, ' '), {
941
982
  maskedLink: true
942
983
  });
943
984
  }
@@ -992,7 +1033,7 @@ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an arr
992
1033
  const prototype = Object.getPrototypeOf(value);
993
1034
  return prototype === Object.prototype || prototype === null;
994
1035
  };
995
- const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(), since anything else would replace the whole";
1036
+ const PLAIN = "give a plain object: a database row's plain form, such as Mongoose's toObject() or Sequelize's get({ plain: true }), or an object literal of the values you set, since anything else would replace the whole";
996
1037
  /** What a non-plain object is: by its class, or as one without a plain prototype when it has none named. */ const describeInstance = (value)=>{
997
1038
  const name = value.constructor?.name;
998
1039
  return typeof name === 'string' && name ? `a ${name}` : 'an object without a plain prototype';
@@ -1088,6 +1129,107 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
1088
1129
  throw refuse(new Error(`${where}: the theme has ${problems.length} problem${problems.length === 1 ? '' : 's'}:\n${problems.map((problem)=>` ${problem}`).join('\n')}`));
1089
1130
  }
1090
1131
 
1132
+ /** The theme role each static stands for; `errorColor` is a fault in the bot, `danger`. */ const ROLES = {
1133
+ primaryColor: 'primary',
1134
+ successColor: 'success',
1135
+ infoColor: 'info',
1136
+ errorColor: 'danger',
1137
+ warningColor: 'warning'
1138
+ };
1139
+ const logger$6 = new Logger('Theme');
1140
+ // One per process for this meocord version, so a static set through one build keeps those set through the other
1141
+ const assigned = shared('legacyThemeColors', ()=>({}));
1142
+ /** Reads a role of the theme where it is read, warning once a run for each static read. */ function read(name) {
1143
+ warnDeprecated(logger$6, `Theme.${name}`, `useTheme().colors.${ROLES[name]}`);
1144
+ return useTheme().colors[ROLES[name]];
1145
+ }
1146
+ /** Sets a role beneath every theme an app sets; a value that is no colour is reported instead. */ function assign(name, value) {
1147
+ const role = ROLES[name];
1148
+ warnDeprecated(logger$6, `Assigning Theme.${name}`, `colors.${role} in @MeoCord({ theme })`);
1149
+ const [problem] = themeProblems({
1150
+ colors: {
1151
+ [role]: value
1152
+ }
1153
+ }, `Theme.${name}`);
1154
+ if (problem) {
1155
+ logger$6.warn(`${problem}. It was left unset.`);
1156
+ return;
1157
+ }
1158
+ assigned[role] = value;
1159
+ setLegacyThemeLayer({
1160
+ colors: {
1161
+ ...assigned
1162
+ }
1163
+ });
1164
+ }
1165
+ /**
1166
+ * Five of the theme's colours as static properties: primary, success, info, danger and warning.
1167
+ *
1168
+ * Each one reads the matching role of the theme where it is read, `useTheme().colors`, so code written against it
1169
+ * follows `@MeoCord({ theme })` and `@UseTheme` with no change. Assigning one recolours MeoCord's views, as a
1170
+ * role beneath every theme an app sets. Reading or assigning each one logs a warning once.
1171
+ *
1172
+ * @deprecated Since 4.1, and removed in the next major version (5.0). Use `useTheme().colors` instead. Themes
1173
+ * follow the app, a controller and a server; set the colours in `@MeoCord({ theme })`: `primaryColor` is
1174
+ * `colors.primary`, `successColor` `colors.success`, `infoColor` `colors.info`, `errorColor` `colors.danger` and
1175
+ * `warningColor` `colors.warning`.
1176
+ *
1177
+ * @example
1178
+ * ```ts
1179
+ * // Both colour the embed with the call's primary colour; write the second in new code
1180
+ * new EmbedBuilder().setColor(Theme.primaryColor)
1181
+ * new EmbedBuilder().setColor(useTheme().colors.primary)
1182
+ * ```
1183
+ *
1184
+ * @group Responses
1185
+ */ class Theme {
1186
+ /**
1187
+ * @deprecated Since 4.1, and removed in the next major version (5.0). Use `useTheme().colors.primary` instead. It
1188
+ * reads the theme where it is read; set `colors.primary` in `@MeoCord({ theme })`.
1189
+ */ static get primaryColor() {
1190
+ return read('primaryColor');
1191
+ }
1192
+ static set primaryColor(value) {
1193
+ assign('primaryColor', value);
1194
+ }
1195
+ /**
1196
+ * @deprecated Since 4.1, and removed in the next major version (5.0). Use `useTheme().colors.success` instead. It
1197
+ * reads the theme where it is read; set `colors.success` in `@MeoCord({ theme })`.
1198
+ */ static get successColor() {
1199
+ return read('successColor');
1200
+ }
1201
+ static set successColor(value) {
1202
+ assign('successColor', value);
1203
+ }
1204
+ /**
1205
+ * @deprecated Since 4.1, and removed in the next major version (5.0). Use `useTheme().colors.info` instead. It
1206
+ * reads the theme where it is read; set `colors.info` in `@MeoCord({ theme })`.
1207
+ */ static get infoColor() {
1208
+ return read('infoColor');
1209
+ }
1210
+ static set infoColor(value) {
1211
+ assign('infoColor', value);
1212
+ }
1213
+ /**
1214
+ * @deprecated Since 4.1, and removed in the next major version (5.0). Use `useTheme().colors.danger` instead. It
1215
+ * reads the theme where it is read; set `colors.danger` in `@MeoCord({ theme })`.
1216
+ */ static get errorColor() {
1217
+ return read('errorColor');
1218
+ }
1219
+ static set errorColor(value) {
1220
+ assign('errorColor', value);
1221
+ }
1222
+ /**
1223
+ * @deprecated Since 4.1, and removed in the next major version (5.0). Use `useTheme().colors.warning` instead. It
1224
+ * reads the theme where it is read; set `colors.warning` in `@MeoCord({ theme })`.
1225
+ */ static get warningColor() {
1226
+ return read('warningColor');
1227
+ }
1228
+ static set warningColor(value) {
1229
+ assign('warningColor', value);
1230
+ }
1231
+ }
1232
+
1091
1233
  /**
1092
1234
  * Composes several class or method decorators into one.
1093
1235
  *
@@ -1258,18 +1400,27 @@ const PLURAL_CATEGORIES = new Set([
1258
1400
  'other'
1259
1401
  ]);
1260
1402
  /** 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) {
1403
+ /** The value at a dotted key's path, or undefined where the path ends early. */ function valueAt(catalog, key) {
1265
1404
  let current = catalog;
1266
1405
  for (const part of key.split('.')){
1267
1406
  if (typeof current !== 'object' || current === null) return undefined;
1268
1407
  current = current[part];
1269
1408
  }
1409
+ return current;
1410
+ }
1411
+ /**
1412
+ * A message by its dotted key, or undefined when the catalog lacks it or the key names a group. `reference`, the
1413
+ * default catalog for a translation, decides where it has a group that an object is that group, so a translation that
1414
+ * keeps only a group's `other` message isn't read as a plural. Where it has a message, or lacks the key, the object's
1415
+ * own keys decide, so a translation may give a plural for a message its language needs one for. A plural without an
1416
+ * `other` form reads as missing, so a translation that left it out falls back to the next catalog.
1417
+ */ function lookup(catalog, key, reference = catalog) {
1418
+ const current = valueAt(catalog, key);
1270
1419
  if (typeof current === 'string') return current;
1271
- if (isPluralShape(current) && typeof current.other === 'string') return current;
1272
- return undefined;
1420
+ if (!isPluralShape(current) || typeof current.other !== 'string') return undefined;
1421
+ const shape = reference === catalog ? current : valueAt(reference, key);
1422
+ const group = typeof shape === 'object' && shape !== null && !isPluralShape(shape);
1423
+ return group ? undefined : current;
1273
1424
  }
1274
1425
  const plurals = new Map();
1275
1426
  /** The form of a found message to use: a plain message itself, or a plural's form for `count` in its locale's rules. */ function pluralForm({ message, locale }, count) {
@@ -1333,7 +1484,7 @@ class CatalogTranslator extends Translator {
1333
1484
  }
1334
1485
  translate(requested, key, params = {}) {
1335
1486
  for (const locale of this[LOCALE_CHAIN](requested)){
1336
- const message = lookup(this.catalogs[locale], key);
1487
+ const message = lookup(this.catalogs[locale], key, this.catalogs[this.defaultLocale]);
1337
1488
  if (message === undefined) continue;
1338
1489
  // An empty translation, as tools export an untranslated string, counts as missing: Discord refuses an empty reply
1339
1490
  const text = pluralForm({
@@ -1369,7 +1520,7 @@ class CatalogTranslator extends Translator {
1369
1520
  const localized = {};
1370
1521
  for (const locale of this.locales){
1371
1522
  if (locale === this.defaultLocale) continue;
1372
- const message = lookup(this.catalogs[locale], key);
1523
+ const message = lookup(this.catalogs[locale], key, this.catalogs[this.defaultLocale]);
1373
1524
  // A translation with a {param} is left out, as Discord would show it as written, and so is an empty one
1374
1525
  if (typeof message === 'string' && message !== '' && placeholderNames(message).length === 0) {
1375
1526
  localized[locale] = fillPlaceholders(message, (_name, whole)=>whole);
@@ -1543,8 +1694,9 @@ const ENGLISH = 'en-US';
1543
1694
  /**
1544
1695
  * The message a text is in for `locale`, and whether it is the app's: the app's catalogs down the locale's chain, with
1545
1696
  * MeoCord's English as the English catalog. An English request reads it after the app's English catalogs and before a
1546
- * default in another language; any other request reads it last.
1547
- */ function findText(translator, locale, key) {
1697
+ * default in another language; any other request reads it last. A message whose form for `count` is empty, as tools
1698
+ * export an untranslated string, counts as missing, as it does for the app's own texts.
1699
+ */ function findText(translator, locale, key, count) {
1548
1700
  const english = ()=>{
1549
1701
  const message = lookup(MEOCORD_MESSAGES, key);
1550
1702
  return message === undefined ? undefined : {
@@ -1559,7 +1711,10 @@ const ENGLISH = 'en-US';
1559
1711
  const beforeDefault = englishFirst && served === translator.defaultLocale ? english() : undefined;
1560
1712
  if (beforeDefault) return beforeDefault;
1561
1713
  const message = lookup(translator[CATALOGS][served], key);
1562
- if (message !== undefined) return {
1714
+ if (message !== undefined && pluralForm({
1715
+ message,
1716
+ locale: served
1717
+ }, count) !== '') return {
1563
1718
  message,
1564
1719
  locale: served,
1565
1720
  own: true
@@ -1580,9 +1735,9 @@ const lists = new Map();
1580
1735
  * that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
1581
1736
  * nothing in zh-CN and with a space in ja.
1582
1737
  */ function renderText(translator, locale, text) {
1583
- const found = findText(translator, locale, text.key);
1584
- if (!found) return text.fallback ?? text.key;
1585
1738
  const params = text.params ?? {};
1739
+ const found = findText(translator, locale, text.key, params.count);
1740
+ if (!found) return text.fallback ?? text.key;
1586
1741
  return fillPlaceholders(pluralForm(found, params.count), (name, whole)=>{
1587
1742
  if (!Object.hasOwn(params, name)) return whole;
1588
1743
  const value = params[name];
@@ -1608,7 +1763,8 @@ const translators = shared('clientTranslators', ()=>new WeakMap());
1608
1763
  /** A text for an interaction, in its user's language through its app's translator. */ function textFor(interaction, text) {
1609
1764
  return renderText(translatorOfClient(interaction.client), interactionLocale(interaction), text);
1610
1765
  }
1611
- const issueTexts = new WeakMap();
1766
+ // One per process for this meocord version, so an issue made by one build is translated by the other
1767
+ const issueTexts = shared('issueTexts', ()=>new WeakMap());
1612
1768
  /** An issue of a usage error, whose message is the text in English, keeping the text to translate it later. */ function usageIssue(text, param) {
1613
1769
  const issue = {
1614
1770
  ...param !== undefined && {
@@ -2017,15 +2173,26 @@ brand(CooldownStoreError, 'CooldownStoreError');
2017
2173
  }
2018
2174
  /** 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
2175
  /**
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.
2176
+ * Sets `key` on a plain object `target` as an own, enumerable data property: unlike `target[key] = value`, a key
2177
+ * named `__proto__` is a key like any other rather than the object's prototype, and a key such as `toString` is set
2178
+ * even where `Object.prototype` is frozen.
2022
2179
  */ function setOwn(target, key, value) {
2023
- Object.defineProperty(target, key, {
2180
+ // An assignment would call __proto__'s setter, and throw for a member of a frozen Object.prototype. Any other key
2181
+ // assigned is an own data property already; defining each would cost V8 its fast object shape
2182
+ if (key in Object.prototype) Object.defineProperty(target, key, {
2024
2183
  value,
2025
2184
  enumerable: true,
2026
2185
  writable: true,
2027
2186
  configurable: true
2028
2187
  });
2188
+ else target[key] = value;
2189
+ }
2190
+ /** `target`'s own value for `key`, never one it inherits, such as `Object.prototype.toString` for `toString`. */ const ownValue = (target, key)=>Object.hasOwn(target, key) ? target[key] : undefined;
2191
+ /**
2192
+ * Gives each of `keys`, names `Object.prototype` has such as `toString` or `constructor`, that `target` doesn't hold
2193
+ * an own `undefined`, so reading one left out gives `undefined` rather than the inherited member.
2194
+ */ function shadowInherited(target, keys) {
2195
+ for (const key of keys)if (!Object.hasOwn(target, key)) setOwn(target, key, undefined);
2029
2196
  }
2030
2197
 
2031
2198
  /**
@@ -2193,37 +2360,48 @@ brand(CooldownStoreError, 'CooldownStoreError');
2193
2360
  *
2194
2361
  * @group Utilities
2195
2362
  */ function getInstallContext(interaction) {
2196
- const owners = interaction.authorizingIntegrationOwners ?? {};
2197
- const guildInstalled = owners[discord_js.ApplicationIntegrationType.GuildInstall] !== undefined;
2198
- const userInstalled = owners[discord_js.ApplicationIntegrationType.UserInstall] !== undefined;
2363
+ const botInstalled = isBotInstalled(interaction);
2199
2364
  switch(interaction.context){
2200
2365
  case discord_js.InteractionContextType.BotDM:
2201
2366
  return {
2202
2367
  where: 'bot-dm',
2203
- botInstalled: true
2368
+ botInstalled
2204
2369
  };
2205
2370
  case discord_js.InteractionContextType.PrivateChannel:
2206
2371
  return {
2207
2372
  where: 'private-channel',
2208
- botInstalled: false
2373
+ botInstalled
2209
2374
  };
2210
2375
  case discord_js.InteractionContextType.Guild:
2211
- // With no owners reported, the interaction came through the bot, as every interaction did before user installs.
2212
2376
  return {
2213
2377
  where: 'guild',
2214
- botInstalled: guildInstalled || !userInstalled
2378
+ botInstalled
2215
2379
  };
2216
2380
  default:
2217
- // Discord omits the context for interactions it created before contexts existed, which reached only the bot.
2218
- return interaction.guildId ? {
2219
- where: 'guild',
2220
- botInstalled: true
2221
- } : {
2222
- where: 'bot-dm',
2223
- botInstalled: true
2381
+ return {
2382
+ where: interaction.guildId ? 'guild' : 'bot-dm',
2383
+ botInstalled
2224
2384
  };
2225
2385
  }
2226
2386
  }
2387
+ /**
2388
+ * Whether the bot is present where an interaction happened, as {@link getInstallContext} reports it, read from Discord's
2389
+ * `context` and the integrations that authorized it alone: no `guildId`, which only `where` needs.
2390
+ */ function isBotInstalled(interaction) {
2391
+ switch(interaction.context){
2392
+ case discord_js.InteractionContextType.PrivateChannel:
2393
+ return false;
2394
+ case discord_js.InteractionContextType.Guild:
2395
+ {
2396
+ const owners = interaction.authorizingIntegrationOwners ?? {};
2397
+ // With no owners reported, the interaction came through the bot, as every interaction did before user installs
2398
+ return owners[discord_js.ApplicationIntegrationType.GuildInstall] !== undefined || owners[discord_js.ApplicationIntegrationType.UserInstall] === undefined;
2399
+ }
2400
+ default:
2401
+ // A DM with the bot, or an interaction from before contexts existed, which reached only the bot
2402
+ return true;
2403
+ }
2404
+ }
2227
2405
 
2228
2406
  const { Ephemeral, SuppressEmbeds, SuppressNotifications, IsComponentsV2 } = discord_js.MessageFlags;
2229
2407
  /** The flags each call accepts; Discord rejects or ignores the rest. */ const ALLOWED = {
@@ -2256,6 +2434,22 @@ const { Ephemeral, SuppressEmbeds, SuppressNotifications, IsComponentsV2 } = dis
2256
2434
  /** The names of the flags set in a value, for warnings. */ function flagNames(flags) {
2257
2435
  return new discord_js.MessageFlagsBitField(flags).toArray();
2258
2436
  }
2437
+ /** Each call as a warning names it, its article included. */ const STEP_NAMES = {
2438
+ deferReply: 'a deferred reply',
2439
+ reply: 'a reply',
2440
+ update: 'an update',
2441
+ edit: 'an edit',
2442
+ followUp: 'a follow-up'
2443
+ };
2444
+ /** What an update or an edit does with `Ephemeral` instead, and how to answer only the member. */ const PRIVATE_HINT = {
2445
+ update: 'An update changes the message the member clicked, for everyone; to answer only them, use respond(interaction).followUp() with MessageFlags.Ephemeral.',
2446
+ edit: 'An edit keeps the message as public or private as it was; to answer only the member, use respond(interaction).followUp() with MessageFlags.Ephemeral.'
2447
+ };
2448
+ /** The development warning for the flags a call dropped, with how to answer privately when `Ephemeral` is one of them. */ function droppedFlagsWarning(step, dropped) {
2449
+ const text = `Dropped flags ${flagNames(dropped).join(', ')}, which ${STEP_NAMES[step]} cannot take.`;
2450
+ const hint = hasEphemeral(dropped) ? PRIVATE_HINT[step] : undefined;
2451
+ return hint ? `${text} ${hint}` : text;
2452
+ }
2259
2453
 
2260
2454
  const CDN_HOSTS = new Set([
2261
2455
  'cdn.discordapp.com',
@@ -2404,8 +2598,12 @@ function rewriteComponent(component, kept) {
2404
2598
  /** The id MeoCord gives the containers it renders, so a view left behind can be found again. */ const RENDERED_CONTAINER_ID = 0x4d43;
2405
2599
  // Shared by both builds of this version, so either answers with the app's presenter
2406
2600
  const presenters = shared('clientPresenters', ()=>new WeakMap());
2407
- /** Sets the presenter for the interactions a client receives. */ function setPresenter(client, presenter) {
2408
- presenters.set(client, presenter);
2601
+ /** Sets the presenter for the interactions a client receives; without one, they take the default. */ function setPresenter(client, presenter) {
2602
+ if (presenter) presenters.set(client, presenter);
2603
+ else presenters.delete(client);
2604
+ }
2605
+ /** The presenter set on a client, if any. */ function clientPresenter(client) {
2606
+ return presenters.get(client);
2409
2607
  }
2410
2608
  /** The presenter for an interaction's client, or the default one. */ function presenterFor(client) {
2411
2609
  return client && presenters.get(client) || defaultPresenter;
@@ -2800,15 +2998,25 @@ const logger$4 = new Logger('Theme');
2800
2998
  ]).finally(()=>clearTimeout(timer));
2801
2999
  }
2802
3000
  /**
2803
- * A result as a layer: copied, then checked; one with problems is left out, with a warning once per id when `report`.
2804
- * `null`, as a database gives for a missing row, is no theme, as `undefined` is.
3001
+ * A result as a layer: copied, then checked; one with problems, or that throws as it is read, is left out, with a
3002
+ * warning once per id when `report`. `null`, as a database gives for a missing row, is no theme, as `undefined` is.
2805
3003
  */ accept(id, result, report) {
2806
3004
  if (result === undefined || result === null) return undefined;
2807
- const layer = copyLayer(result);
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
- });
3005
+ const where = `themeFor.${this.kind} for ${this.kind} ${id}`;
3006
+ let layer;
3007
+ let problems;
3008
+ try {
3009
+ layer = copyLayer(result);
3010
+ // A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
3011
+ problems = themeProblems(layer, where, {
3012
+ forLog: true
3013
+ });
3014
+ } catch (error) {
3015
+ // A cycle overflows the copy, and a getter may throw: the result is left out as one with problems is
3016
+ problems = [
3017
+ `${where}: theme cannot be read: ${quoteForLog(String(error?.message ?? error))}`
3018
+ ];
3019
+ }
2812
3020
  if (problems.length === 0) {
2813
3021
  if (report) this.warned.delete(id);
2814
3022
  return layer;
@@ -2937,13 +3145,14 @@ const logger$4 = new Logger('Theme');
2937
3145
  const idOf = (value)=>typeof value === 'string' ? value : undefined;
2938
3146
  /**
2939
3147
  * The server and user a call comes from: an interaction's or a message's server and its user or author; a
2940
- * reaction's message's server and the user who reacted, which a reaction handler gets second; an event argument's
2941
- * server and user, when it has them.
3148
+ * reaction's message's server and the user who reacted, which a reaction handler gets second as `{ user }` and a
3149
+ * reaction event as the `User`; an event argument's server and user, when it has them.
2942
3150
  */ function themeTargets(args) {
2943
3151
  const [first, second] = args;
2944
3152
  if (!first || typeof first !== 'object') return {};
2945
3153
  const guildId = idOf(first.guildId) ?? idOf(first.guild?.id) ?? idOf(first.message?.guildId);
2946
- const userId = idOf(first.user?.id) ?? idOf(first.author?.id) ?? idOf(second?.user?.id);
3154
+ // Only a User's own id: an event of an old and a new channel, role or message would give the new one's otherwise
3155
+ const userId = idOf(first.user?.id) ?? idOf(first.author?.id) ?? idOf(second?.user?.id) ?? (second instanceof discord_js.User ? idOf(second.id) : undefined);
2947
3156
  return {
2948
3157
  guildId,
2949
3158
  userId
@@ -2970,7 +3179,8 @@ const idOf = (value)=>typeof value === 'string' ? value : undefined;
2970
3179
  user
2971
3180
  ];
2972
3181
  }
2973
- const stores = new WeakMap();
3182
+ // One per process for this meocord version, as the rest of an app's theme state is
3183
+ const stores = shared('themeCaches', ()=>new WeakMap());
2974
3184
  /**
2975
3185
  * An app's cache of the themes `@MeoCord({ themeFor })` looked up, by server and by user.
2976
3186
  *
@@ -3015,6 +3225,16 @@ const stores = new WeakMap();
3015
3225
  else stores.delete(themeCache);
3016
3226
  }
3017
3227
 
3228
+ function _class_private_field_loose_base(receiver, privateKey) {
3229
+ if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
3230
+ throw new TypeError("attempted to use private field on non-instance");
3231
+ }
3232
+ return receiver;
3233
+ }
3234
+ var id = 0;
3235
+ function _class_private_field_loose_key(name) {
3236
+ return "__private_" + id++ + "_" + name;
3237
+ }
3018
3238
  const ALREADY_ACKNOWLEDGED = 40060;
3019
3239
  /** Discord's code for an interaction it no longer knows, such as one past its three seconds unacknowledged. */ const UNKNOWN_INTERACTION = 10062;
3020
3240
  const TOKEN_EXPIRED = new Set([
@@ -3112,6 +3332,7 @@ const messageLocks = shared('messageLocks', ()=>new Map());
3112
3332
  function toJson(value) {
3113
3333
  return typeof value?.toJSON === 'function' ? value.toJSON() : value;
3114
3334
  }
3335
+ var _location = /*#__PURE__*/ _class_private_field_loose_key("_location");
3115
3336
  /** The response state behind `respond()`, with what `@Defer` and the testing helpers use besides. */ class InteractionResponse {
3116
3337
  get state() {
3117
3338
  this.sync();
@@ -3150,7 +3371,7 @@ function toJson(value) {
3150
3371
  flagsFor(step, requested, v2 = this.v2) {
3151
3372
  const { flags, dropped } = resolveFlags(step, requested, v2);
3152
3373
  if (dropped && development()) {
3153
- logger$3.warn(`Dropped flags ${flagNames(dropped).join(', ')}, which a ${step} cannot take.`);
3374
+ logger$3.warn(droppedFlagsWarning(step, dropped));
3154
3375
  }
3155
3376
  return flags;
3156
3377
  }
@@ -3674,7 +3895,7 @@ function toJson(value) {
3674
3895
  } catch (error) {
3675
3896
  const message = this.message;
3676
3897
  const expired = Date.now() - this.interaction.createdTimestamp >= TOKEN_EXPIRED_AFTER_MS;
3677
- if (!TOKEN_EXPIRED.has(errorCode(error)) || !expired || !this.location.botInstalled || !message) throw error;
3898
+ if (!TOKEN_EXPIRED.has(errorCode(error)) || !expired || !isBotInstalled(this.interaction) || !message) throw error;
3678
3899
  this.lastMessage = await this.call('message.edit', sent, ()=>message.edit(sent));
3679
3900
  }
3680
3901
  this.phase = 'replied';
@@ -3986,6 +4207,10 @@ function toJson(value) {
3986
4207
  } : body;
3987
4208
  }
3988
4209
  constructor(interaction){
4210
+ Object.defineProperty(this, _location, {
4211
+ writable: true,
4212
+ value: void 0
4213
+ });
3989
4214
  this.interaction = interaction;
3990
4215
  this.phase = 'unanswered';
3991
4216
  /** The answer steps asked for, run one after another; `pending` counts those not yet settled. */ this.queue = Promise.resolve();
@@ -4003,7 +4228,14 @@ function toJson(value) {
4003
4228
  this.suppressNotifications = false;
4004
4229
  // Set by @Defer's first step, so an error can name what acknowledged the interaction
4005
4230
  this.underDefer = false;
4006
- this.location = getInstallContext(interaction);
4231
+ Object.defineProperty(this, 'location', {
4232
+ get: ()=>{
4233
+ var _class_private_field_loose_base1, _location1;
4234
+ return (_class_private_field_loose_base1 = _class_private_field_loose_base(this, _location))[_location1 = _location] ?? (_class_private_field_loose_base1[_location1] = getInstallContext(interaction));
4235
+ },
4236
+ enumerable: true,
4237
+ configurable: true
4238
+ });
4007
4239
  const message = 'message' in interaction ? interaction.message : undefined;
4008
4240
  this.v2 = Boolean(message?.flags?.has(discord_js.MessageFlags.IsComponentsV2));
4009
4241
  }
@@ -4085,8 +4317,10 @@ const states = shared('responseStates', ()=>new WeakMap());
4085
4317
  /**
4086
4318
  * The response state of the interaction being handled, the same one `respond(interaction)` returns,
4087
4319
  * or `undefined` for anything that cannot be answered: a message, a reaction, an event or
4088
- * autocomplete.
4320
+ * autocomplete. An `@On('interactionCreate')` handler's call has none either, as MeoCord answers that interaction
4321
+ * through the handler it dispatches it to.
4089
4322
  */ get response() {
4323
+ if (this.getType() === 'event') return undefined;
4090
4324
  const interaction = this.getInteraction();
4091
4325
  return interaction?.isRepliable() ? respond(interaction) : undefined;
4092
4326
  }
@@ -4223,17 +4457,30 @@ const logger$2 = new Logger('ExecutionContext');
4223
4457
  }
4224
4458
  }
4225
4459
 
4226
- /**
4227
- * How many calls a cooldown allows, and in how long a window.
4228
- *
4229
- * @group Types
4230
- */ /** `verdict` with `release` added as a non-enumerable property, as the built-in stores give it. */ function withRelease(verdict, release) {
4460
+ /** `verdict` with `release` added as a non-enumerable property, as the built-in stores give it. */ function withRelease(verdict, release) {
4231
4461
  return Object.defineProperty(verdict, 'release', {
4232
4462
  value: release,
4233
4463
  enumerable: false,
4234
4464
  configurable: true
4235
4465
  });
4236
4466
  }
4467
+ /**
4468
+ * `verdict` with `retryTimestamp` added as a non-enumerable property, unless it holds the field already, so a refusal
4469
+ * that gains it compares and serialises as it did.
4470
+ */ function withRetryTimestamp(verdict, retryTimestamp) {
4471
+ if (retryTimestamp === undefined || Object.hasOwn(verdict, 'retryTimestamp')) return verdict;
4472
+ return Object.defineProperty(verdict, 'retryTimestamp', {
4473
+ value: retryTimestamp,
4474
+ enumerable: false,
4475
+ configurable: true
4476
+ });
4477
+ }
4478
+ /** A batch verdict for one key, as the built-in stores' `consume` gives it: `allowed`, `retryAfterMs`, and the wait's end. */ function singleVerdict({ allowed, retryAfterMs, retryTimestamp }) {
4479
+ return withRetryTimestamp({
4480
+ allowed,
4481
+ retryAfterMs
4482
+ }, retryTimestamp);
4483
+ }
4237
4484
  /**
4238
4485
  * Where `@Cooldown` counts calls.
4239
4486
  *
@@ -4284,10 +4531,11 @@ const logger$2 = new Logger('ExecutionContext');
4284
4531
  */ async consumeMany(entries) {
4285
4532
  for (const [index, { key, limit }] of entries.entries()){
4286
4533
  const verdict = await this.consume(key, limit);
4287
- if (!verdict.allowed) return {
4534
+ // The wait's end by name, as a built-in store's consume gives it non-enumerable, which a spread leaves out
4535
+ if (!verdict.allowed) return withRetryTimestamp({
4288
4536
  ...verdict,
4289
4537
  blocked: index
4290
- };
4538
+ }, verdict.retryTimestamp);
4291
4539
  }
4292
4540
  return {
4293
4541
  allowed: true,
@@ -4313,6 +4561,18 @@ const logger$2 = new Logger('ExecutionContext');
4313
4561
  });
4314
4562
  }
4315
4563
  }
4564
+ // CooldownStore's own consumeMany and peekMany carry a mark both builds share, so a check can tell a store's
4565
+ // override from either build's default, as a store that extends one build's class can be checked by the other's
4566
+ const DEFAULT_BATCH_METHOD = sharedKey('cooldown-store:default-batch-method');
4567
+ for (const method of [
4568
+ CooldownStore.prototype.consumeMany,
4569
+ CooldownStore.prototype.peekMany
4570
+ ]){
4571
+ Object.defineProperty(method, DEFAULT_BATCH_METHOD, {
4572
+ value: true
4573
+ });
4574
+ }
4575
+ /** Whether `method` is CooldownStore's own consumeMany or peekMany, from either build of this version. */ const isDefaultBatchMethod = (method)=>typeof method === 'function' && DEFAULT_BATCH_METHOD in method;
4316
4576
  /**
4317
4577
  * Of several refusals, the one a caller waits on: the call runs only once every entry allows it, so the
4318
4578
  * longest wait. Undefined when nothing refused.
@@ -4403,16 +4663,12 @@ const logger$2 = new Logger('ExecutionContext');
4403
4663
  * @see {@link CooldownStore}
4404
4664
  */ class MemoryCooldownStore extends CooldownStore {
4405
4665
  async consume(key, limit) {
4406
- const { allowed, retryAfterMs } = await this.consumeMany([
4666
+ return singleVerdict(await this.consumeMany([
4407
4667
  {
4408
4668
  key,
4409
4669
  limit
4410
4670
  }
4411
- ]);
4412
- return {
4413
- allowed,
4414
- retryAfterMs
4415
- };
4671
+ ]));
4416
4672
  }
4417
4673
  /**
4418
4674
  * Checks every entry, and records the call against all of them only if all allow it.
@@ -4635,16 +4891,12 @@ const DEFAULT_PREFIX = 'meocord:cooldown:';
4635
4891
  * @param limit - The calls allowed, and the window they are counted over.
4636
4892
  * @returns Whether this call was recorded, and if not, how long until one can be.
4637
4893
  */ async consume(key, limit) {
4638
- const { allowed, retryAfterMs } = await this.consumeMany([
4894
+ return singleVerdict(await this.consumeMany([
4639
4895
  {
4640
4896
  key,
4641
4897
  limit
4642
4898
  }
4643
- ]);
4644
- return {
4645
- allowed,
4646
- retryAfterMs
4647
- };
4899
+ ]));
4648
4900
  }
4649
4901
  /**
4650
4902
  * Records a call against every entry if all allow it, as one script: one round trip, however many
@@ -4833,34 +5085,70 @@ const UNIT_MS = lookupTable({
4833
5085
  w: 604800000
4834
5086
  });
4835
5087
  /** A number written in full, `whole` for an integer: `undefined` for anything else. */ function number(word, whole) {
4836
- if (!(whole ? /^[+-]?\d+$/ : /^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i).test(word)) return undefined;
5088
+ // Each run of digits splits only one way, so refusing a word takes time linear in its length
5089
+ if (!(whole ? /^[+-]?\d+$/ : /^[+-]?(\d+(?:\.\d*)?|\.\d+)(e[+-]?\d+)?$/i).test(word)) return undefined;
4837
5090
  const value = Number(word);
4838
5091
  return Number.isFinite(value) && (!whole || Number.isSafeInteger(value)) ? value : undefined;
4839
5092
  }
4840
5093
  /** `yes`, `no`, `true`, `false`, `on` or `off`, in any case, as a boolean. */ const bool = (word)=>BOOLEANS[word.toLowerCase()];
4841
- /** A length of time written as amounts and units, such as `90s`, `2h30m` or `1.5d`, in milliseconds. */ function duration(word) {
4842
- const parts = word.toLowerCase().match(/(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g);
5094
+ /**
5095
+ * A length of time written as amounts and units, such as `90s`, `2h30m` or `1.5d`, in milliseconds: `undefined` for one
5096
+ * too long to add up.
5097
+ */ function duration(word) {
5098
+ // Sticky: each part starts where the last ended, so the scan stops at the first that doesn't and reads each part once
5099
+ const parts = word.toLowerCase().match(/(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/gy);
4843
5100
  if (!parts || parts.join('') !== word.toLowerCase()) return undefined;
4844
- return parts.reduce((total, part)=>{
5101
+ const total = parts.reduce((sum, part)=>{
4845
5102
  const [, amount, unit] = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(part);
4846
- return total + Number(amount) * UNIT_MS[unit];
5103
+ return sum + Number(amount) * UNIT_MS[unit];
4847
5104
  }, 0);
5105
+ // Too long to add up, as number() refuses a value past the largest number
5106
+ return Number.isFinite(total) ? total : undefined;
4848
5107
  }
4849
5108
  /** The largest snowflake: Discord's IDs are unsigned 64-bit integers. */ const MAX_SNOWFLAKE = 2n ** 64n - 1n;
5109
+ /** The fewest and most digits a snowflake has, and the words of that many digits, with no leading zero. */ const SNOWFLAKE_DIGITS = {
5110
+ min: 17,
5111
+ max: 20
5112
+ };
5113
+ const SNOWFLAKE = new RegExp(`^[1-9]\\d{${SNOWFLAKE_DIGITS.min - 1},${SNOWFLAKE_DIGITS.max - 1}}$`);
4850
5114
  /**
4851
5115
  * A Discord ID, kept as its text: 17 to 20 digits, no leading zero, up to the largest 64-bit value. An ID's top 42 bits
4852
5116
  * are milliseconds since 2015-01-01, so every ID made from 2015-01-28 on has 17 digits or more, and none is a safe
4853
5117
  * integer.
4854
- */ const snowflake = (word)=>/^[1-9]\d{16,19}$/.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
4855
- /** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(word) ? word : undefined;
5118
+ */ const snowflake = (word)=>SNOWFLAKE.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
5119
+ /** A UUID's canonical 8-4-4-4-12 form, 36 characters with its dashes. */ const UUID = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
5120
+ const UUID_LENGTH = 36;
5121
+ /** A UUID in its canonical 8-4-4-4-12 form, in either case, kept as written. */ const uuid = (word)=>UUID.test(word) ? word : undefined;
4856
5122
  /** The words a type such as `on|off` chooses from, or `undefined` for any other type. */ const choicesOf = (type)=>type.includes('|') ? type.split('|') : undefined;
4857
- /** The types a component's customId segment can take: each parsed from the segment alone, with no request. */ const SEGMENT_TYPES = lookupTable({
4858
- string: (segment)=>segment,
4859
- int: (segment)=>number(segment, true),
4860
- number: (segment)=>number(segment, false),
4861
- bool,
4862
- snowflake,
4863
- uuid
5123
+ /** The shortest segment of any type: one character. */ const SHORTEST_SEGMENT = 1;
5124
+ /**
5125
+ * The types a component's customId segment can take: each parsed from the segment alone, with no request, and the
5126
+ * fewest characters a segment of it has, which the check for a pattern no customId can match counts.
5127
+ */ const SEGMENT_TYPES = lookupTable({
5128
+ string: {
5129
+ parse: (segment)=>segment,
5130
+ shortest: SHORTEST_SEGMENT
5131
+ },
5132
+ int: {
5133
+ parse: (segment)=>number(segment, true),
5134
+ shortest: SHORTEST_SEGMENT
5135
+ },
5136
+ number: {
5137
+ parse: (segment)=>number(segment, false),
5138
+ shortest: SHORTEST_SEGMENT
5139
+ },
5140
+ bool: {
5141
+ parse: bool,
5142
+ shortest: Math.min(...Object.keys(BOOLEANS).map((word)=>word.length))
5143
+ },
5144
+ snowflake: {
5145
+ parse: snowflake,
5146
+ shortest: SNOWFLAKE_DIGITS.min
5147
+ },
5148
+ uuid: {
5149
+ parse: uuid,
5150
+ shortest: UUID_LENGTH
5151
+ }
4864
5152
  });
4865
5153
  /**
4866
5154
  * Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
@@ -4869,7 +5157,12 @@ const UNIT_MS = lookupTable({
4869
5157
  /** A customId segment as its type's value, words to choose from compared as written: `undefined` when it is not one. */ function parseSegment(type, segment) {
4870
5158
  const choices = choicesOf(type);
4871
5159
  if (choices) return choices.includes(segment) ? segment : undefined;
4872
- return SEGMENT_TYPES[type](segment);
5160
+ return SEGMENT_TYPES[type].parse(segment);
5161
+ }
5162
+ /** The fewest characters a customId segment of `type` has: words to choose from count their shortest word. */ function shortestSegment(type) {
5163
+ const choices = choicesOf(type);
5164
+ if (choices) return Math.min(...choices.map((word)=>word.length));
5165
+ return SEGMENT_TYPES[type]?.shortest ?? SHORTEST_SEGMENT;
4873
5166
  }
4874
5167
 
4875
5168
  /**
@@ -4919,6 +5212,12 @@ const UNIT_MS = lookupTable({
4919
5212
  return `; it was given ${describeValue(value)}`;
4920
5213
  }
4921
5214
  const logger = new Logger('Command');
5215
+ /** The interaction types a method's `@Command` routes declare, each once. */ function routeTypesOf(prototype, method) {
5216
+ const metas = Object.values(getCommandMap(prototype) ?? {}).flat();
5217
+ return [
5218
+ ...new Set(metas.filter((meta)=>meta.methodName === method).map((meta)=>meta.type))
5219
+ ];
5220
+ }
4922
5221
  /**
4923
5222
  * The class's own handler list, started from a copy of the inherited one, so a subclass's
4924
5223
  * handlers never land in its base class's metadata.
@@ -4944,6 +5243,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4944
5243
  method,
4945
5244
  label: `autocomplete of ${optionName === undefined ? 'every option' : `"${optionName}"`} in "${commandPath}"`
4946
5245
  });
5246
+ /** An `@On` or `@Once` handler as a warning names its route; one event per method, as a subclass replaces it. */ const eventRoute = ({ method, event })=>({
5247
+ method,
5248
+ label: `event "${event}"`
5249
+ });
4947
5250
  /** Records a route a class's own decorator declares for a method, which the startup check reads. */ function declareRoute(target, declared) {
4948
5251
  const routes = Reflect.getOwnMetadata(META.declaredRoutes, target) ?? [];
4949
5252
  routes.push(declared);
@@ -4971,13 +5274,22 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4971
5274
  else handlers[index] = entry;
4972
5275
  Reflect.defineMetadata(key, handlers, target);
4973
5276
  }
5277
+ /** The `@On` and `@Once` entries a class's own decorators declare, without those it inherits from its base. */ function ownEventHandlers(prototype) {
5278
+ const inherited = Reflect.getMetadata(META.eventHandlers, Object.getPrototypeOf(prototype)) ?? [];
5279
+ return ownHandlerList(META.eventHandlers, prototype).filter((entry)=>!inherited.includes(entry));
5280
+ }
4974
5281
  /**
4975
5282
  * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
4976
5283
  * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
4977
5284
  */ function dropInheritedRoutes(prototype) {
4978
- const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
4979
- if (redeclared.size === 0) return;
4980
5285
  const base = Object.getPrototypeOf(prototype);
5286
+ // The class's own @On and @Once re-declare their methods too
5287
+ const ownEvents = ownEventHandlers(prototype);
5288
+ const redeclared = new Set([
5289
+ ...getDeclaredRoutes(prototype).map((route)=>route.method),
5290
+ ...ownEvents.map((entry)=>entry.method)
5291
+ ]);
5292
+ if (redeclared.size === 0) return;
4981
5293
  // An entry the class takes from its base, for a method it re-declares
4982
5294
  const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
4983
5295
  const commands = ownCommandMap(prototype);
@@ -5000,6 +5312,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
5000
5312
  [
5001
5313
  META.autocompleteHandlers,
5002
5314
  (entry)=>entry.methodName
5315
+ ],
5316
+ [
5317
+ META.eventHandlers,
5318
+ (entry)=>entry.method
5003
5319
  ]
5004
5320
  ];
5005
5321
  for (const [key, method] of lists){
@@ -5026,13 +5342,14 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
5026
5342
  function MessageHandler(pattern, options = {}) {
5027
5343
  return declaring(function(target, propertyKey) {
5028
5344
  refuseOnClass('@MessageHandler', target, propertyKey);
5029
- // An empty pattern means every message, as no pattern does
5030
- if (pattern === '') {
5031
- warnDeprecatedBehaviour(logger, `@MessageHandler('') on ${target.constructor.name}.${propertyKey}`, 'is refused', '@MessageHandler()');
5345
+ // An empty pattern, or one of spaces alone, means every message, as no pattern does
5346
+ const blank = pattern !== undefined && pattern.trim() === '';
5347
+ if (blank) {
5348
+ warnDeprecatedBehaviour(logger, `@MessageHandler('${pattern}') on ${decoratedName(target, propertyKey)}`, 'is refused', '@MessageHandler()');
5032
5349
  }
5033
5350
  const method = propertyKey.toString();
5034
5351
  const declared = {
5035
- pattern: pattern || undefined,
5352
+ pattern: blank ? undefined : pattern,
5036
5353
  method,
5037
5354
  options
5038
5355
  };
@@ -5154,13 +5471,14 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5154
5471
  params: params.length,
5155
5472
  typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
5156
5473
  });
5157
- // Each param takes at least one character
5474
+ // Each param takes at least the shortest value of its type
5475
+ const shortest = literalLength + params.reduce((sum, param)=>sum + shortestSegment(types[param] ?? 'string'), 0);
5158
5476
  return {
5159
5477
  regex,
5160
5478
  params,
5161
5479
  types: lookupTable(types),
5162
5480
  specificity,
5163
- shortest: literalLength + params.length
5481
+ shortest
5164
5482
  };
5165
5483
  }
5166
5484
  /** Why a `{name:type}` cannot type a customId segment, which holds text the bot wrote, with no message to read. */ function segmentTypeProblem(param, type) {
@@ -5233,13 +5551,17 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5233
5551
  refuseOnClass('@Command', target, propertyKey);
5234
5552
  const originalMethod = _descriptor.value;
5235
5553
  if (!originalMethod) {
5236
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: @Command is on something with no implementation.`));
5554
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: @Command is on something with no implementation.`));
5237
5555
  }
5238
5556
  // Wrap original method for interaction type validation
5239
5557
  const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
5240
5558
  const checked = function(interaction, params) {
5241
- if (!matchesCommandType(commandType, interaction)) {
5242
- throw new Error(`${target.constructor.name}.${propertyKey}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.`);
5559
+ // Every type the method's routes declare, in the class the call is made on: each @Command wraps it, so one alone
5560
+ // would refuse the others' interactions, and an override that adds a route type reaches this one through super
5561
+ const called = typeof this === 'object' && this !== null && Object.prototype.isPrototypeOf.call(target, this) ? Object.getPrototypeOf(this) : target;
5562
+ const types = routeTypesOf(called, propertyKey);
5563
+ if (!types.some((type)=>matchesCommandType(type, interaction))) {
5564
+ throw new Error(types.length <= 1 ? `${decoratedName(target, propertyKey)}: @Command('${commandName}', ${declaredAs}) takes ` + `${interactionClassName(commandType)}${givenInstead(interaction)}.` : `${decoratedName(target, propertyKey)}: its @Command routes take ${types.map(interactionClassName).join(' or ')}` + `${givenInstead(interaction)}.`);
5243
5565
  }
5244
5566
  return originalMethod.apply(this, [
5245
5567
  interaction,
@@ -5257,7 +5579,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5257
5579
  let guilds;
5258
5580
  // Determine command type and builder
5259
5581
  if (typeof builderOrType === 'function') {
5260
- const where = `${target.constructor.name}.${propertyKey}`;
5582
+ const where = decoratedName(target, propertyKey);
5261
5583
  // A subcommand is part of its command's builder, which belongs on the command's own name
5262
5584
  const subcommandPath = Reflect.getMetadata(META.commandType, builderOrType) === enum_index.CommandType.SLASH && commandName.includes(' ');
5263
5585
  const command = commandName.split(' ')[0];
@@ -5304,12 +5626,12 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5304
5626
  if (isCustomIdRouted(commandType)) {
5305
5627
  // A route warned as it was made
5306
5628
  const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
5307
- if (braces) logger.warn(`${target.constructor.name}.${propertyKey}: in the pattern "${commandName}", ${braces}`);
5629
+ if (braces) logger.warn(`${decoratedName(target, propertyKey)}: in the pattern "${commandName}", ${braces}`);
5308
5630
  let pattern;
5309
5631
  try {
5310
5632
  pattern = createRegexFromPattern(commandName);
5311
5633
  } catch (error) {
5312
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: ${error.message}`, {
5634
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${error.message}`, {
5313
5635
  cause: error
5314
5636
  }));
5315
5637
  }
@@ -5319,7 +5641,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5319
5641
  // Warned rather than refused, so a bot that carries such a handler still starts
5320
5642
  if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
5321
5643
  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.`);
5644
+ logger.warn(`${decoratedName(target, 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
5645
  }
5324
5646
  }
5325
5647
  const declared = {
@@ -5371,7 +5693,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5371
5693
  const registered = built?.type;
5372
5694
  if (!declared || registered === undefined || declared.kind === registered) return;
5373
5695
  const kind = registered === discord_js.ApplicationCommandType.User ? 'user' : 'message';
5374
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
5696
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: it takes a ${declared.name} context menu interaction, but ${builderName} ` + `registers "${commandName}" as a ${kind} context menu command. Declare the handler's interaction as the kind the ` + `builder's setType() names.`));
5375
5697
  }
5376
5698
  /**
5377
5699
  * Retrieves the command map for a given controller.
@@ -5532,11 +5854,13 @@ exports.ReactionHandler = ReactionHandler;
5532
5854
  exports.RedisCooldownStore = RedisCooldownStore;
5533
5855
  exports.SetMetadata = SetMetadata;
5534
5856
  exports.TEXT_LIMITS = TEXT_LIMITS;
5857
+ exports.Theme = Theme;
5535
5858
  exports.ThemeCache = ThemeCache;
5536
5859
  exports.Translator = Translator;
5537
5860
  exports.UnroutedExecutionContext = UnroutedExecutionContext;
5538
5861
  exports.UserError = UserError;
5539
5862
  exports.ValidationError = ValidationError;
5863
+ exports.addOwnHandler = addOwnHandler;
5540
5864
  exports.allDeclaredErrors = allDeclaredErrors;
5541
5865
  exports.ambientThemeVersion = ambientThemeVersion;
5542
5866
  exports.applyDecorators = applyDecorators;
@@ -5550,6 +5874,7 @@ exports.bundleEntry = bundleEntry;
5550
5874
  exports.callOrder = callOrder;
5551
5875
  exports.choicesOf = choicesOf;
5552
5876
  exports.claimAmbientTheme = claimAmbientTheme;
5877
+ exports.clientPresenter = clientPresenter;
5553
5878
  exports.collectStartupErrors = collectStartupErrors;
5554
5879
  exports.commandsNamed = commandsNamed;
5555
5880
  exports.compiledConfigMessage = compiledConfigMessage;
@@ -5571,6 +5896,7 @@ exports.describeValue = describeValue;
5571
5896
  exports.dropInheritedRoutes = dropInheritedRoutes;
5572
5897
  exports.duration = duration;
5573
5898
  exports.escapeForLog = escapeForLog;
5899
+ exports.eventRoute = eventRoute;
5574
5900
  exports.existingResponse = existingResponse;
5575
5901
  exports.fitText = fitText;
5576
5902
  exports.focusedOptionName = focusedOptionName;
@@ -5583,7 +5909,6 @@ exports.getInstallContext = getInstallContext;
5583
5909
  exports.getMessageHandlers = getMessageHandlers;
5584
5910
  exports.getReactionHandlers = getReactionHandlers;
5585
5911
  exports.handOffRefusal = handOffRefusal;
5586
- exports.hasAmbientTheme = hasAmbientTheme;
5587
5912
  exports.hasCustomId = hasCustomId;
5588
5913
  exports.hideInLogs = hideInLogs;
5589
5914
  exports.inferContextType = inferContextType;
@@ -5592,6 +5917,7 @@ exports.isBuiltApplication = isBuiltApplication;
5592
5917
  exports.isConstructible = isConstructible;
5593
5918
  exports.isConstructor = isConstructor;
5594
5919
  exports.isCustomIdRouted = isCustomIdRouted;
5920
+ exports.isDefaultBatchMethod = isDefaultBatchMethod;
5595
5921
  exports.isExplainedError = isExplainedError;
5596
5922
  exports.isPluralShape = isPluralShape;
5597
5923
  exports.isRefusal = isRefusal;
@@ -5613,7 +5939,9 @@ exports.mergeTheme = mergeTheme;
5613
5939
  exports.messageLocale = messageLocale;
5614
5940
  exports.missingTranslatorError = missingTranslatorError;
5615
5941
  exports.number = number;
5616
- exports.ownHandlerList = ownHandlerList;
5942
+ exports.ownEventHandlers = ownEventHandlers;
5943
+ exports.ownValue = ownValue;
5944
+ exports.ownerName = ownerName;
5617
5945
  exports.ownsAmbientTheme = ownsAmbientTheme;
5618
5946
  exports.paramNarrowness = paramNarrowness;
5619
5947
  exports.parseSegment = parseSegment;
@@ -5637,33 +5965,34 @@ exports.responseOf = responseOf;
5637
5965
  exports.routeSpecificity = routeSpecificity;
5638
5966
  exports.runInThemeScope = runInThemeScope;
5639
5967
  exports.runWithTheme = runWithTheme;
5640
- exports.setLegacyThemeLayer = setLegacyThemeLayer;
5641
5968
  exports.setOwn = setOwn;
5642
5969
  exports.setPresenter = setPresenter;
5643
5970
  exports.setStartupErrorsMode = setStartupErrorsMode;
5644
5971
  exports.setThemeOutsideCalls = setThemeOutsideCalls;
5972
+ exports.shadowInherited = shadowInherited;
5645
5973
  exports.shared = shared;
5646
5974
  exports.sharedCustomId = sharedCustomId;
5647
5975
  exports.sharedName = sharedName;
5976
+ exports.singleVerdict = singleVerdict;
5648
5977
  exports.stampCall = stampCall;
5649
5978
  exports.startupError = startupError;
5979
+ exports.startupErrorsOf = startupErrorsOf;
5650
5980
  exports.stopOnStartupErrors = stopOnStartupErrors;
5651
5981
  exports.textOfIssue = textOfIssue;
5652
5982
  exports.textRenderer = textRenderer;
5653
5983
  exports.themeForInteraction = themeForInteraction;
5654
5984
  exports.themeForProblem = themeForProblem;
5655
5985
  exports.themeLayersVersion = themeLayersVersion;
5656
- exports.themeProblems = themeProblems;
5657
5986
  exports.themeResolverClass = themeResolverClass;
5658
5987
  exports.translatorOfClient = translatorOfClient;
5659
5988
  exports.usageHeading = usageHeading;
5660
5989
  exports.usageIssue = usageIssue;
5661
5990
  exports.useTheme = useTheme;
5662
5991
  exports.userWords = userWords;
5663
- exports.warnDeprecated = warnDeprecated;
5664
5992
  exports.warnDeprecatedBehaviour = warnDeprecatedBehaviour;
5665
5993
  exports.warnOnce = warnOnce;
5666
5994
  exports.warnPlaceholder = warnPlaceholder;
5995
+ exports.withArticle = withArticle;
5667
5996
  exports.withRelease = withRelease;
5668
5997
  exports.withSendableFiles = withSendableFiles;
5669
5998
  exports.withoutFiles = withoutFiles;