meocord 4.2.1 → 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 (137) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cjs/_shared/{controller.decorator-XKMlcUZz.cjs → controller.decorator-hp5xdJU4.cjs} +550 -189
  3. package/dist/cjs/_shared/{startup-roots-CgfPQrNl.cjs → event-sources-DzqTODXg.cjs} +274 -88
  4. package/dist/cjs/_shared/{providers-BZgJyfZS.cjs → providers-BBvbnmLP.cjs} +837 -295
  5. package/dist/cjs/_shared/{sharded-cooldown-store-CsJJyCZd.cjs → sharded-cooldown-store-B62g5O88.cjs} +11 -9
  6. package/dist/cjs/_shared/{translate-error-BUAR1WYG.cjs → translate-error-wsgfsUET.cjs} +1 -1
  7. package/dist/cjs/common/index.cjs +4 -104
  8. package/dist/cjs/core/index.cjs +435 -182
  9. package/dist/cjs/decorator/index.cjs +116 -121
  10. package/dist/cjs/testing/index.cjs +1923 -808
  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 +32 -10
  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 +27 -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 +92 -66
  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 +6 -6
  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 +54 -32
  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 +83 -54
  60. package/dist/esm/core/meocord.app.js +174 -89
  61. package/dist/esm/core/message-help.js +37 -15
  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 +4 -2
  66. package/dist/esm/core/process-state.js +19 -0
  67. package/dist/esm/core/providers.js +39 -4
  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/stage-scope.js +2 -1
  73. package/dist/esm/core/startup-roots.js +28 -8
  74. package/dist/esm/core/theme-resolvers.js +26 -11
  75. package/dist/esm/core/theme-runtime.js +23 -13
  76. package/dist/esm/core/theme-scope.js +31 -30
  77. package/dist/esm/core/theme-validation.js +1 -1
  78. package/dist/esm/decorator/app.decorator.js +37 -1
  79. package/dist/esm/decorator/command-builder.decorator.js +14 -0
  80. package/dist/esm/decorator/controller-class.decorator.js +1 -1
  81. package/dist/esm/decorator/controller.decorator.js +48 -19
  82. package/dist/esm/decorator/defer.decorator.js +2 -2
  83. package/dist/esm/decorator/event.decorator.js +4 -5
  84. package/dist/esm/decorator/filter.decorator.js +4 -4
  85. package/dist/esm/decorator/guard.decorator.js +4 -61
  86. package/dist/esm/decorator/inject.decorator.js +4 -1
  87. package/dist/esm/decorator/interceptor.decorator.js +2 -2
  88. package/dist/esm/decorator/theme.decorator.js +4 -3
  89. package/dist/esm/decorator/validation.decorator.js +5 -5
  90. package/dist/esm/package.json.js +1 -1
  91. package/dist/esm/testing/catalog.js +32 -25
  92. package/dist/esm/testing/cooldown-store-suite.js +3 -3
  93. package/dist/esm/testing/discord-defaults.js +449 -335
  94. package/dist/esm/testing/inspect-handler.js +91 -10
  95. package/dist/esm/testing/meocord-testing-module.js +217 -91
  96. package/dist/esm/testing/mock-fn.js +8 -2
  97. package/dist/esm/testing/mock-interaction.js +864 -124
  98. package/dist/esm/testing/response-log.js +8 -0
  99. package/dist/esm/testing/response.js +7 -5
  100. package/dist/esm/testing/routing.js +48 -6
  101. package/dist/esm/testing/strict-mocks.js +8 -4
  102. package/dist/esm/testing/theme.js +3 -2
  103. package/dist/esm/util/common.util.js +30 -9
  104. package/dist/esm/util/generator-cli.util.js +6 -4
  105. package/dist/esm/util/inherited-env.util.js +72 -13
  106. package/dist/esm/util/json.util.js +10 -3
  107. package/dist/esm/util/meocord-cli.util.js +3 -2
  108. package/dist/esm/util/meocord-config-loader.util.js +14 -1
  109. package/dist/esm/util/meocord-config-validation.util.js +13 -3
  110. package/dist/esm/util/meocord-source-config.util.js +13 -3
  111. package/dist/esm/util/platform.util.js +3 -2
  112. package/dist/esm/util/refusal.util.js +29 -11
  113. package/dist/esm/util/registration-mode.util.js +14 -1
  114. package/dist/esm/util/timer-limit.util.js +4 -1
  115. package/dist/esm/util/tsconfig-paths.util.js +38 -7
  116. package/dist/esm/util/tsconfig.util.js +16 -58
  117. package/dist/esm/util/user-text.util.js +8 -4
  118. package/dist/esm/util/value.util.js +33 -5
  119. package/dist/types/common/index.d.cts +17 -10
  120. package/dist/types/common/index.d.ts +17 -10
  121. package/dist/types/core/index.d.cts +21 -11
  122. package/dist/types/core/index.d.ts +21 -11
  123. package/dist/types/decorator/index.d.cts +63 -63
  124. package/dist/types/decorator/index.d.ts +63 -63
  125. package/dist/types/{index-DKJclIi-.d.ts → index-Brkd6QGL.d.ts} +81 -35
  126. package/dist/types/{index-Dlf0ZZtS.d.cts → index-CqXImwF_.d.cts} +81 -35
  127. package/dist/types/interface/index.d.cts +1 -1
  128. package/dist/types/interface/index.d.ts +1 -1
  129. package/dist/types/{route-Bh1U13yy.d.ts → route-IQrK-MN2.d.cts} +11 -3
  130. package/dist/types/{route-Bh1U13yy.d.cts → route-bI-okq7g.d.ts} +11 -3
  131. package/dist/types/testing/index.d.cts +158 -61
  132. package/dist/types/testing/index.d.ts +158 -61
  133. package/dist/types/{theme-resolvers-CyEz9pnv.d.cts → theme-resolvers-AtgeaYqF.d.cts} +1 -1
  134. package/dist/types/{theme-resolvers-DA204O-q.d.ts → theme-resolvers-BgqysV8d.d.ts} +1 -1
  135. package/dist/types/{translator-Bg7hwxDF.d.cts → translator-D8aydlCE.d.ts} +12 -11
  136. package/dist/types/{translator-CeP4bCzp.d.ts → translator-iJhDlgwL.d.cts} +12 -11
  137. 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-XKMlcUZz.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.1";
407
- var packageJson = {
408
- version: version};
409
-
410
- /**
411
- * The name of a key the ES module and CommonJS builds of this meocord version share, and no other version has, for
412
- * `Symbol.for`: a process that loads both builds of one install keeps one state under it, and two installed versions
413
- * keep theirs apart. `Symbol.for(sharedName(...))` written out types as a `unique symbol`.
414
- */ const sharedName = (name)=>`meocord@${packageJson.version}:${name}`;
415
- /** The key named {@link sharedName}. */ function sharedKey(name) {
416
- return Symbol.for(sharedName(name));
417
- }
418
- /** The state both builds of this version keep on `globalThis` under `name`, made by whichever asks first. */ function shared(name, make) {
419
- const key = sharedKey(name);
420
- const store = globalThis;
421
- if (!Object.hasOwn(store, key)) Object.defineProperty(store, key, {
422
- value: make(),
423
- configurable: true
424
- });
425
- return store[key];
426
- }
427
-
428
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.
@@ -595,6 +611,9 @@ let themeOutsideCalls;
595
611
  * Such a listener runs in its emitter's context, so without it a `useTheme()` inside reads the theme outside any call.
596
612
  * Timers and promises the handler starts keep its theme without it.
597
613
  *
614
+ * The theme is the binding call's whole theme, its server's and user's themes included, so every later call, whoever
615
+ * it is for, is answered in the starting member's theme.
616
+ *
598
617
  * @param fn - The function to run in the call's theme; `this` and its arguments are passed through.
599
618
  * @returns A function that runs `fn` in the theme `useTheme()` returns where `bindTheme` is called.
600
619
  *
@@ -604,7 +623,7 @@ let themeOutsideCalls;
604
623
  * async poll(message: Message) {
605
624
  * const collector = message.createMessageComponentCollector({ componentType: ComponentType.Button, time: 60_000 })
606
625
  * collector.on('collect', bindTheme(async (click: ButtonInteraction) => {
607
- * const { emojis } = useTheme() // the handler's theme, @UseTheme included
626
+ * const { emojis } = useTheme() // the starting member's theme, @UseTheme included
608
627
  * await click.reply(`${emojis.success} Picked`)
609
628
  * }))
610
629
  * }
@@ -632,7 +651,10 @@ const EXPLAINED = Symbol.for('meocord.explained');
632
651
  *
633
652
  * Use it where a bot logs a startup failure, so an error MeoCord explained is not logged twice: an app
634
653
  * `MeoCordFactory.create()` refuses, such as two handlers for one command, or Discord refusing the bot token or a
635
- * 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.
636
658
  *
637
659
  * @param error - An error `MeoCordFactory.create()` threw or `app.start()` rejected with.
638
660
  * @returns `true` when MeoCord explained the error.
@@ -648,9 +670,10 @@ const EXPLAINED = Symbol.for('meocord.explained');
648
670
  * await app.start()
649
671
  * }
650
672
  *
651
- * // 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
652
674
  * bootstrap().catch(error => {
653
- * if (!isExplainedError(error)) logger.error('Error during startup:', error)
675
+ * if (isExplainedError(error)) return
676
+ * logger.error('Error during startup:', error)
654
677
  * process.exitCode = 1
655
678
  * })
656
679
  * ```
@@ -679,10 +702,16 @@ const EXPLAINED = Symbol.for('meocord.explained');
679
702
  }
680
703
 
681
704
  const REFUSAL = Symbol.for('meocord.refusal');
682
- /** Whether this process reports refusals yet; see `reportRefusals`. */ let reporting = false;
683
- /** 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
+ }));
684
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) {
685
- handedOff.add(error);
714
+ refusals.handedOff.add(error);
686
715
  }
687
716
  /**
688
717
  * Marks an error MeoCord raises for code it refuses as the application loads, such as a decorator given what it
@@ -693,18 +722,29 @@ const REFUSAL = Symbol.for('meocord.refusal');
693
722
  enumerable: false
694
723
  });
695
724
  // Before the error is thrown, so a built application reports it however the throw ends
696
- if (!reporting && isBuiltApplication()) reportRefusals();
725
+ if (!refusals.reporting && isBuiltApplication()) reportRefusals();
697
726
  return error;
698
727
  }
699
- /** 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) {
700
735
  if (propertyKey === undefined) return target.name || 'a class';
701
- return `${target.constructor.name}.${String(propertyKey)}`;
736
+ return `${ownerName(target)}.${String(propertyKey)}`;
702
737
  }
703
738
  /**
704
739
  * Refuses a decorator that goes only on a method, or on a member, where it is applied to a class, directly or through
705
- * `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.
706
742
  */ function refuseOnClass(decorator, target, propertyKey, place = 'a method') {
707
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
+ }
708
748
  }
709
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) {
710
750
  if (propertyKey !== undefined) throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${decorator} goes on a class, not on a method.`));
@@ -887,9 +927,9 @@ function keepOrThrow(error, target, propertyKey) {
887
927
  * report. Installed by the first refusal in a built application. Bun has no monitor event for a rejection, so there a
888
928
  * listener reports a refused rejection and rejects any other again without itself, unless the app listens itself.
889
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) } = {}) {
890
- reporting = true;
930
+ refusals.reporting = true;
891
931
  const report = (error)=>{
892
- if (handedOff.has(error)) return;
932
+ if (refusals.handedOff.has(error)) return;
893
933
  if (!isExplainedError(error)) log(describeRefusal(error, process.cwd()));
894
934
  exit(1);
895
935
  };
@@ -908,8 +948,8 @@ function keepOrThrow(error, target, propertyKey) {
908
948
  process.on('unhandledRejection', onRejection);
909
949
  }
910
950
 
911
- /** Characters a log line shows escaped: C0 and C1 controls, DEL, line and paragraph separators, and bidi controls. */ const UNSAFE_IN_LOG = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
912
- /** The same, with the quote and the backslash, for text shown between quotes. */ const UNSAFE_IN_QUOTES = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069"\\]/g;
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;
913
953
  const SHORT_ESCAPES = {
914
954
  '\n': '\\n',
915
955
  '\r': '\\r',
@@ -933,8 +973,12 @@ const escapeChar = (char)=>SHORT_ESCAPES[char] ?? `\\u${char.charCodeAt(0).toStr
933
973
  if (characters.length <= LOGGED_CHARACTERS) return quoted(text);
934
974
  return `${quoted(`${characters.slice(0, LOGGED_CHARACTERS).join('')}…`)} (${characters.length} characters)`;
935
975
  }
936
- /** Words a user wrote, as a reply quotes them: on one line, with their markdown, masked links included, shown as typed. */ function userWords(text) {
937
- return discord_js.escapeMarkdown(text.replace(/\s+/g, ' ').trim(), {
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, ' '), {
938
982
  maskedLink: true
939
983
  });
940
984
  }
@@ -989,7 +1033,7 @@ const describeGroup = (value, quote = asWritten)=>Array.isArray(value) ? 'an arr
989
1033
  const prototype = Object.getPrototypeOf(value);
990
1034
  return prototype === Object.prototype || prototype === null;
991
1035
  };
992
- 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";
993
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)=>{
994
1038
  const name = value.constructor?.name;
995
1039
  return typeof name === 'string' && name ? `a ${name}` : 'an object without a plain prototype';
@@ -1085,6 +1129,107 @@ const PLAIN = "give a plain object, such as { ...value }, or a row's toObject(),
1085
1129
  throw refuse(new Error(`${where}: the theme has ${problems.length} problem${problems.length === 1 ? '' : 's'}:\n${problems.map((problem)=>` ${problem}`).join('\n')}`));
1086
1130
  }
1087
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
+
1088
1233
  /**
1089
1234
  * Composes several class or method decorators into one.
1090
1235
  *
@@ -1246,17 +1391,36 @@ const logger$5 = new Logger('SetMetadata');
1246
1391
  }
1247
1392
  const DISCORD_LOCALES = new Set(Object.values(discord_js.Locale));
1248
1393
  const languageOf = (locale)=>locale.split('-')[0];
1249
- /** A message by its dotted key, or undefined when the catalog lacks it or the key names a group. */ function lookup(catalog, key) {
1394
+ const PLURAL_CATEGORIES = new Set([
1395
+ 'zero',
1396
+ 'one',
1397
+ 'two',
1398
+ 'few',
1399
+ 'many',
1400
+ 'other'
1401
+ ]);
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));
1403
+ /** The value at a dotted key's path, or undefined where the path ends early. */ function valueAt(catalog, key) {
1250
1404
  let current = catalog;
1251
1405
  for (const part of key.split('.')){
1252
1406
  if (typeof current !== 'object' || current === null) return undefined;
1253
1407
  current = current[part];
1254
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);
1255
1419
  if (typeof current === 'string') return current;
1256
- if (typeof current === 'object' && current !== null && typeof current.other === 'string') {
1257
- return current;
1258
- }
1259
- 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;
1260
1424
  }
1261
1425
  const plurals = new Map();
1262
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) {
@@ -1320,7 +1484,7 @@ class CatalogTranslator extends Translator {
1320
1484
  }
1321
1485
  translate(requested, key, params = {}) {
1322
1486
  for (const locale of this[LOCALE_CHAIN](requested)){
1323
- const message = lookup(this.catalogs[locale], key);
1487
+ const message = lookup(this.catalogs[locale], key, this.catalogs[this.defaultLocale]);
1324
1488
  if (message === undefined) continue;
1325
1489
  // An empty translation, as tools export an untranslated string, counts as missing: Discord refuses an empty reply
1326
1490
  const text = pluralForm({
@@ -1356,7 +1520,7 @@ class CatalogTranslator extends Translator {
1356
1520
  const localized = {};
1357
1521
  for (const locale of this.locales){
1358
1522
  if (locale === this.defaultLocale) continue;
1359
- const message = lookup(this.catalogs[locale], key);
1523
+ const message = lookup(this.catalogs[locale], key, this.catalogs[this.defaultLocale]);
1360
1524
  // A translation with a {param} is left out, as Discord would show it as written, and so is an empty one
1361
1525
  if (typeof message === 'string' && message !== '' && placeholderNames(message).length === 0) {
1362
1526
  localized[locale] = fillPlaceholders(message, (_name, whole)=>whole);
@@ -1530,8 +1694,9 @@ const ENGLISH = 'en-US';
1530
1694
  /**
1531
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
1532
1696
  * MeoCord's English as the English catalog. An English request reads it after the app's English catalogs and before a
1533
- * default in another language; any other request reads it last.
1534
- */ 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) {
1535
1700
  const english = ()=>{
1536
1701
  const message = lookup(MEOCORD_MESSAGES, key);
1537
1702
  return message === undefined ? undefined : {
@@ -1546,7 +1711,10 @@ const ENGLISH = 'en-US';
1546
1711
  const beforeDefault = englishFirst && served === translator.defaultLocale ? english() : undefined;
1547
1712
  if (beforeDefault) return beforeDefault;
1548
1713
  const message = lookup(translator[CATALOGS][served], key);
1549
- if (message !== undefined) return {
1714
+ if (message !== undefined && pluralForm({
1715
+ message,
1716
+ locale: served
1717
+ }, count) !== '') return {
1550
1718
  message,
1551
1719
  locale: served,
1552
1720
  own: true
@@ -1567,9 +1735,9 @@ const lists = new Map();
1567
1735
  * that has it; any other list is joined with its `joiner`, a comma unless given, as Intl's `unit` lists join with
1568
1736
  * nothing in zh-CN and with a space in ja.
1569
1737
  */ function renderText(translator, locale, text) {
1570
- const found = findText(translator, locale, text.key);
1571
- if (!found) return text.fallback ?? text.key;
1572
1738
  const params = text.params ?? {};
1739
+ const found = findText(translator, locale, text.key, params.count);
1740
+ if (!found) return text.fallback ?? text.key;
1573
1741
  return fillPlaceholders(pluralForm(found, params.count), (name, whole)=>{
1574
1742
  if (!Object.hasOwn(params, name)) return whole;
1575
1743
  const value = params[name];
@@ -1595,7 +1763,8 @@ const translators = shared('clientTranslators', ()=>new WeakMap());
1595
1763
  /** A text for an interaction, in its user's language through its app's translator. */ function textFor(interaction, text) {
1596
1764
  return renderText(translatorOfClient(interaction.client), interactionLocale(interaction), text);
1597
1765
  }
1598
- 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());
1599
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) {
1600
1769
  const issue = {
1601
1770
  ...param !== undefined && {
@@ -1978,7 +2147,24 @@ brand(CooldownStoreError, 'CooldownStoreError');
1978
2147
  else logger.error(`Could not ${what}:`, error);
1979
2148
  }
1980
2149
 
1981
- /** Whether a value is a class, which a stage entry or a `@Catch` type must be: what `new` and `instanceof` take. */ const isConstructor = (value)=>typeof value === 'function';
2150
+ /** Whether `new` takes a value, as `instanceof` needs of its right side, such as a `@Catch` type. */ function isConstructible(value) {
2151
+ if (typeof value !== 'function') return false;
2152
+ try {
2153
+ // Fails unless value can be new.target, which only a constructor can; none of value's constructor code runs
2154
+ Reflect.construct(String, [], value);
2155
+ return true;
2156
+ } catch {
2157
+ return false;
2158
+ }
2159
+ }
2160
+ /**
2161
+ * Whether a value is a class, which a stage entry or an app option must be: constructible, with an object prototype for
2162
+ * the metadata MeoCord reads. An arrow function, a method, an async or a generator function is none.
2163
+ */ function isConstructor(value) {
2164
+ if (!isConstructible(value)) return false;
2165
+ const prototype = value.prototype;
2166
+ return typeof prototype === 'object' && prototype !== null;
2167
+ }
1982
2168
  /** A value as a refusal or a warning names it: a string quoted, `null` and `undefined` as written, else its kind. */ function describeValue(value) {
1983
2169
  if (value === null || value === undefined) return String(value);
1984
2170
  if (typeof value === 'string') return quoteForLog(value);
@@ -1987,15 +2173,26 @@ brand(CooldownStoreError, 'CooldownStoreError');
1987
2173
  }
1988
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}`;
1989
2175
  /**
1990
- * Sets `key` on `target` as an own, enumerable data property: unlike `target[key] = value`, a key named `__proto__`
1991
- * is a key like any other rather than the object's prototype.
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.
1992
2179
  */ function setOwn(target, key, value) {
1993
- 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, {
1994
2183
  value,
1995
2184
  enumerable: true,
1996
2185
  writable: true,
1997
2186
  configurable: true
1998
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);
1999
2196
  }
2000
2197
 
2001
2198
  /**
@@ -2163,37 +2360,48 @@ brand(CooldownStoreError, 'CooldownStoreError');
2163
2360
  *
2164
2361
  * @group Utilities
2165
2362
  */ function getInstallContext(interaction) {
2166
- const owners = interaction.authorizingIntegrationOwners ?? {};
2167
- const guildInstalled = owners[discord_js.ApplicationIntegrationType.GuildInstall] !== undefined;
2168
- const userInstalled = owners[discord_js.ApplicationIntegrationType.UserInstall] !== undefined;
2363
+ const botInstalled = isBotInstalled(interaction);
2169
2364
  switch(interaction.context){
2170
2365
  case discord_js.InteractionContextType.BotDM:
2171
2366
  return {
2172
2367
  where: 'bot-dm',
2173
- botInstalled: true
2368
+ botInstalled
2174
2369
  };
2175
2370
  case discord_js.InteractionContextType.PrivateChannel:
2176
2371
  return {
2177
2372
  where: 'private-channel',
2178
- botInstalled: false
2373
+ botInstalled
2179
2374
  };
2180
2375
  case discord_js.InteractionContextType.Guild:
2181
- // With no owners reported, the interaction came through the bot, as every interaction did before user installs.
2182
2376
  return {
2183
2377
  where: 'guild',
2184
- botInstalled: guildInstalled || !userInstalled
2378
+ botInstalled
2185
2379
  };
2186
2380
  default:
2187
- // Discord omits the context for interactions it created before contexts existed, which reached only the bot.
2188
- return interaction.guildId ? {
2189
- where: 'guild',
2190
- botInstalled: true
2191
- } : {
2192
- where: 'bot-dm',
2193
- botInstalled: true
2381
+ return {
2382
+ where: interaction.guildId ? 'guild' : 'bot-dm',
2383
+ botInstalled
2194
2384
  };
2195
2385
  }
2196
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
+ }
2197
2405
 
2198
2406
  const { Ephemeral, SuppressEmbeds, SuppressNotifications, IsComponentsV2 } = discord_js.MessageFlags;
2199
2407
  /** The flags each call accepts; Discord rejects or ignores the rest. */ const ALLOWED = {
@@ -2226,6 +2434,22 @@ const { Ephemeral, SuppressEmbeds, SuppressNotifications, IsComponentsV2 } = dis
2226
2434
  /** The names of the flags set in a value, for warnings. */ function flagNames(flags) {
2227
2435
  return new discord_js.MessageFlagsBitField(flags).toArray();
2228
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
+ }
2229
2453
 
2230
2454
  const CDN_HOSTS = new Set([
2231
2455
  'cdn.discordapp.com',
@@ -2374,8 +2598,12 @@ function rewriteComponent(component, kept) {
2374
2598
  /** The id MeoCord gives the containers it renders, so a view left behind can be found again. */ const RENDERED_CONTAINER_ID = 0x4d43;
2375
2599
  // Shared by both builds of this version, so either answers with the app's presenter
2376
2600
  const presenters = shared('clientPresenters', ()=>new WeakMap());
2377
- /** Sets the presenter for the interactions a client receives. */ function setPresenter(client, presenter) {
2378
- 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);
2379
2607
  }
2380
2608
  /** The presenter for an interaction's client, or the default one. */ function presenterFor(client) {
2381
2609
  return client && presenters.get(client) || defaultPresenter;
@@ -2770,15 +2998,25 @@ const logger$4 = new Logger('Theme');
2770
2998
  ]).finally(()=>clearTimeout(timer));
2771
2999
  }
2772
3000
  /**
2773
- * A result as a layer: copied, then checked; one with problems is left out, with a warning once per id when `report`.
2774
- * `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.
2775
3003
  */ accept(id, result, report) {
2776
3004
  if (result === undefined || result === null) return undefined;
2777
- const layer = copyLayer(result);
2778
- // A value from the app's data, such as a user's saved choice, shown as a log line shows outside text
2779
- const problems = themeProblems(layer, `themeFor.${this.kind} for ${this.kind} ${id}`, {
2780
- forLog: true
2781
- });
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
+ }
2782
3020
  if (problems.length === 0) {
2783
3021
  if (report) this.warned.delete(id);
2784
3022
  return layer;
@@ -2907,13 +3145,14 @@ const logger$4 = new Logger('Theme');
2907
3145
  const idOf = (value)=>typeof value === 'string' ? value : undefined;
2908
3146
  /**
2909
3147
  * The server and user a call comes from: an interaction's or a message's server and its user or author; a
2910
- * reaction's message's server and the user who reacted, which a reaction handler gets second; an event argument's
2911
- * 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.
2912
3150
  */ function themeTargets(args) {
2913
3151
  const [first, second] = args;
2914
3152
  if (!first || typeof first !== 'object') return {};
2915
3153
  const guildId = idOf(first.guildId) ?? idOf(first.guild?.id) ?? idOf(first.message?.guildId);
2916
- 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);
2917
3156
  return {
2918
3157
  guildId,
2919
3158
  userId
@@ -2940,7 +3179,8 @@ const idOf = (value)=>typeof value === 'string' ? value : undefined;
2940
3179
  user
2941
3180
  ];
2942
3181
  }
2943
- 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());
2944
3184
  /**
2945
3185
  * An app's cache of the themes `@MeoCord({ themeFor })` looked up, by server and by user.
2946
3186
  *
@@ -2985,6 +3225,16 @@ const stores = new WeakMap();
2985
3225
  else stores.delete(themeCache);
2986
3226
  }
2987
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
+ }
2988
3238
  const ALREADY_ACKNOWLEDGED = 40060;
2989
3239
  /** Discord's code for an interaction it no longer knows, such as one past its three seconds unacknowledged. */ const UNKNOWN_INTERACTION = 10062;
2990
3240
  const TOKEN_EXPIRED = new Set([
@@ -3082,6 +3332,7 @@ const messageLocks = shared('messageLocks', ()=>new Map());
3082
3332
  function toJson(value) {
3083
3333
  return typeof value?.toJSON === 'function' ? value.toJSON() : value;
3084
3334
  }
3335
+ var _location = /*#__PURE__*/ _class_private_field_loose_key("_location");
3085
3336
  /** The response state behind `respond()`, with what `@Defer` and the testing helpers use besides. */ class InteractionResponse {
3086
3337
  get state() {
3087
3338
  this.sync();
@@ -3120,7 +3371,7 @@ function toJson(value) {
3120
3371
  flagsFor(step, requested, v2 = this.v2) {
3121
3372
  const { flags, dropped } = resolveFlags(step, requested, v2);
3122
3373
  if (dropped && development()) {
3123
- logger$3.warn(`Dropped flags ${flagNames(dropped).join(', ')}, which a ${step} cannot take.`);
3374
+ logger$3.warn(droppedFlagsWarning(step, dropped));
3124
3375
  }
3125
3376
  return flags;
3126
3377
  }
@@ -3383,15 +3634,15 @@ function toJson(value) {
3383
3634
  });
3384
3635
  }
3385
3636
  /**
3386
- * Undoes `@Defer`'s acknowledgement after a guard denied the call silently: a command's deferred
3387
- * reply is deleted while nothing was sent into it; a component's invisible acknowledgement needs
3388
- * nothing. Never throws.
3637
+ * Ends a call a guard denied silently. A component still unanswered is acknowledged invisibly, with or without
3638
+ * `@Defer`, since Discord tells the user an unanswered call failed; so is a command `@Defer`'s 'auto' timer was
3639
+ * waiting on, whose deferred reply is then deleted, as `@Defer`'s is while nothing was sent into it. Never throws.
3389
3640
  */ abandon() {
3390
3641
  const waiting = this.timer !== undefined;
3391
3642
  this.cancelScheduled();
3392
3643
  return this.queued(async ()=>{
3393
- // Denied before the 'auto' timer: acknowledged here, since Discord tells the user an unanswered call failed
3394
- if (waiting) await this.acknowledgeUnanswered({
3644
+ // A command's acknowledgement shows "thinking…", so one is made only for the timer that would have made it
3645
+ if (waiting || !answersWithOwnMessage(this.interaction)) await this.acknowledgeUnanswered({
3395
3646
  ephemeral: true
3396
3647
  });
3397
3648
  this.sync();
@@ -3644,7 +3895,7 @@ function toJson(value) {
3644
3895
  } catch (error) {
3645
3896
  const message = this.message;
3646
3897
  const expired = Date.now() - this.interaction.createdTimestamp >= TOKEN_EXPIRED_AFTER_MS;
3647
- 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;
3648
3899
  this.lastMessage = await this.call('message.edit', sent, ()=>message.edit(sent));
3649
3900
  }
3650
3901
  this.phase = 'replied';
@@ -3956,6 +4207,10 @@ function toJson(value) {
3956
4207
  } : body;
3957
4208
  }
3958
4209
  constructor(interaction){
4210
+ Object.defineProperty(this, _location, {
4211
+ writable: true,
4212
+ value: void 0
4213
+ });
3959
4214
  this.interaction = interaction;
3960
4215
  this.phase = 'unanswered';
3961
4216
  /** The answer steps asked for, run one after another; `pending` counts those not yet settled. */ this.queue = Promise.resolve();
@@ -3973,7 +4228,14 @@ function toJson(value) {
3973
4228
  this.suppressNotifications = false;
3974
4229
  // Set by @Defer's first step, so an error can name what acknowledged the interaction
3975
4230
  this.underDefer = false;
3976
- 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
+ });
3977
4239
  const message = 'message' in interaction ? interaction.message : undefined;
3978
4240
  this.v2 = Boolean(message?.flags?.has(discord_js.MessageFlags.IsComponentsV2));
3979
4241
  }
@@ -4055,8 +4317,10 @@ const states = shared('responseStates', ()=>new WeakMap());
4055
4317
  /**
4056
4318
  * The response state of the interaction being handled, the same one `respond(interaction)` returns,
4057
4319
  * or `undefined` for anything that cannot be answered: a message, a reaction, an event or
4058
- * 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.
4059
4322
  */ get response() {
4323
+ if (this.getType() === 'event') return undefined;
4060
4324
  const interaction = this.getInteraction();
4061
4325
  return interaction?.isRepliable() ? respond(interaction) : undefined;
4062
4326
  }
@@ -4193,17 +4457,30 @@ const logger$2 = new Logger('ExecutionContext');
4193
4457
  }
4194
4458
  }
4195
4459
 
4196
- /**
4197
- * How many calls a cooldown allows, and in how long a window.
4198
- *
4199
- * @group Types
4200
- */ /** `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) {
4201
4461
  return Object.defineProperty(verdict, 'release', {
4202
4462
  value: release,
4203
4463
  enumerable: false,
4204
4464
  configurable: true
4205
4465
  });
4206
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
+ }
4207
4484
  /**
4208
4485
  * Where `@Cooldown` counts calls.
4209
4486
  *
@@ -4254,10 +4531,11 @@ const logger$2 = new Logger('ExecutionContext');
4254
4531
  */ async consumeMany(entries) {
4255
4532
  for (const [index, { key, limit }] of entries.entries()){
4256
4533
  const verdict = await this.consume(key, limit);
4257
- 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({
4258
4536
  ...verdict,
4259
4537
  blocked: index
4260
- };
4538
+ }, verdict.retryTimestamp);
4261
4539
  }
4262
4540
  return {
4263
4541
  allowed: true,
@@ -4283,6 +4561,18 @@ const logger$2 = new Logger('ExecutionContext');
4283
4561
  });
4284
4562
  }
4285
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;
4286
4576
  /**
4287
4577
  * Of several refusals, the one a caller waits on: the call runs only once every entry allows it, so the
4288
4578
  * longest wait. Undefined when nothing refused.
@@ -4373,16 +4663,12 @@ const logger$2 = new Logger('ExecutionContext');
4373
4663
  * @see {@link CooldownStore}
4374
4664
  */ class MemoryCooldownStore extends CooldownStore {
4375
4665
  async consume(key, limit) {
4376
- const { allowed, retryAfterMs } = await this.consumeMany([
4666
+ return singleVerdict(await this.consumeMany([
4377
4667
  {
4378
4668
  key,
4379
4669
  limit
4380
4670
  }
4381
- ]);
4382
- return {
4383
- allowed,
4384
- retryAfterMs
4385
- };
4671
+ ]));
4386
4672
  }
4387
4673
  /**
4388
4674
  * Checks every entry, and records the call against all of them only if all allow it.
@@ -4605,16 +4891,12 @@ const DEFAULT_PREFIX = 'meocord:cooldown:';
4605
4891
  * @param limit - The calls allowed, and the window they are counted over.
4606
4892
  * @returns Whether this call was recorded, and if not, how long until one can be.
4607
4893
  */ async consume(key, limit) {
4608
- const { allowed, retryAfterMs } = await this.consumeMany([
4894
+ return singleVerdict(await this.consumeMany([
4609
4895
  {
4610
4896
  key,
4611
4897
  limit
4612
4898
  }
4613
- ]);
4614
- return {
4615
- allowed,
4616
- retryAfterMs
4617
- };
4899
+ ]));
4618
4900
  }
4619
4901
  /**
4620
4902
  * Records a call against every entry if all allow it, as one script: one round trip, however many
@@ -4803,34 +5085,70 @@ const UNIT_MS = lookupTable({
4803
5085
  w: 604800000
4804
5086
  });
4805
5087
  /** A number written in full, `whole` for an integer: `undefined` for anything else. */ function number(word, whole) {
4806
- 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;
4807
5090
  const value = Number(word);
4808
5091
  return Number.isFinite(value) && (!whole || Number.isSafeInteger(value)) ? value : undefined;
4809
5092
  }
4810
5093
  /** `yes`, `no`, `true`, `false`, `on` or `off`, in any case, as a boolean. */ const bool = (word)=>BOOLEANS[word.toLowerCase()];
4811
- /** A length of time written as amounts and units, such as `90s`, `2h30m` or `1.5d`, in milliseconds. */ function duration(word) {
4812
- 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);
4813
5100
  if (!parts || parts.join('') !== word.toLowerCase()) return undefined;
4814
- return parts.reduce((total, part)=>{
5101
+ const total = parts.reduce((sum, part)=>{
4815
5102
  const [, amount, unit] = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(part);
4816
- return total + Number(amount) * UNIT_MS[unit];
5103
+ return sum + Number(amount) * UNIT_MS[unit];
4817
5104
  }, 0);
5105
+ // Too long to add up, as number() refuses a value past the largest number
5106
+ return Number.isFinite(total) ? total : undefined;
4818
5107
  }
4819
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}}$`);
4820
5114
  /**
4821
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
4822
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
4823
5117
  * integer.
4824
- */ const snowflake = (word)=>/^[1-9]\d{16,19}$/.test(word) && BigInt(word) <= MAX_SNOWFLAKE ? word : undefined;
4825
- /** 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;
4826
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;
4827
- /** The types a component's customId segment can take: each parsed from the segment alone, with no request. */ const SEGMENT_TYPES = lookupTable({
4828
- string: (segment)=>segment,
4829
- int: (segment)=>number(segment, true),
4830
- number: (segment)=>number(segment, false),
4831
- bool,
4832
- snowflake,
4833
- 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
+ }
4834
5152
  });
4835
5153
  /**
4836
5154
  * Whether `type` can type a customId segment: `string`, `int`, `number`, `bool`, `snowflake`, `uuid`, or words to
@@ -4839,7 +5157,12 @@ const UNIT_MS = lookupTable({
4839
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) {
4840
5158
  const choices = choicesOf(type);
4841
5159
  if (choices) return choices.includes(segment) ? segment : undefined;
4842
- 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;
4843
5166
  }
4844
5167
 
4845
5168
  /**
@@ -4889,6 +5212,12 @@ const UNIT_MS = lookupTable({
4889
5212
  return `; it was given ${describeValue(value)}`;
4890
5213
  }
4891
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
+ }
4892
5221
  /**
4893
5222
  * The class's own handler list, started from a copy of the inherited one, so a subclass's
4894
5223
  * handlers never land in its base class's metadata.
@@ -4914,6 +5243,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4914
5243
  method,
4915
5244
  label: `autocomplete of ${optionName === undefined ? 'every option' : `"${optionName}"`} in "${commandPath}"`
4916
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
+ });
4917
5250
  /** Records a route a class's own decorator declares for a method, which the startup check reads. */ function declareRoute(target, declared) {
4918
5251
  const routes = Reflect.getOwnMetadata(META.declaredRoutes, target) ?? [];
4919
5252
  routes.push(declared);
@@ -4941,13 +5274,22 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4941
5274
  else handlers[index] = entry;
4942
5275
  Reflect.defineMetadata(key, handlers, target);
4943
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
+ }
4944
5281
  /**
4945
5282
  * Leaves each method the class's own decorators declare only those routes, of every kind, dropping the ones it inherits
4946
5283
  * for it: what `@Controller({ inheritedRoutes: 'replace' })` does once the class's method decorators have run.
4947
5284
  */ function dropInheritedRoutes(prototype) {
4948
- const redeclared = new Set(getDeclaredRoutes(prototype).map((route)=>route.method));
4949
- if (redeclared.size === 0) return;
4950
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;
4951
5293
  // An entry the class takes from its base, for a method it re-declares
4952
5294
  const inheritedFor = (inherited, method)=>(entry)=>inherited.includes(entry) && redeclared.has(method(entry));
4953
5295
  const commands = ownCommandMap(prototype);
@@ -4970,6 +5312,10 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4970
5312
  [
4971
5313
  META.autocompleteHandlers,
4972
5314
  (entry)=>entry.methodName
5315
+ ],
5316
+ [
5317
+ META.eventHandlers,
5318
+ (entry)=>entry.method
4973
5319
  ]
4974
5320
  ];
4975
5321
  for (const [key, method] of lists){
@@ -4996,13 +5342,14 @@ const autocompleteRoute = (method, commandPath, optionName)=>({
4996
5342
  function MessageHandler(pattern, options = {}) {
4997
5343
  return declaring(function(target, propertyKey) {
4998
5344
  refuseOnClass('@MessageHandler', target, propertyKey);
4999
- // An empty pattern means every message, as no pattern does
5000
- if (pattern === '') {
5001
- 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()');
5002
5349
  }
5003
5350
  const method = propertyKey.toString();
5004
5351
  const declared = {
5005
- pattern: pattern || undefined,
5352
+ pattern: blank ? undefined : pattern,
5006
5353
  method,
5007
5354
  options
5008
5355
  };
@@ -5124,13 +5471,14 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5124
5471
  params: params.length,
5125
5472
  typed: Object.values(types).reduce((sum, type)=>sum + (choicesOf(type) ? 4 : type === 'bool' ? 3 : type === 'int' ? 2 : 1), 0)
5126
5473
  });
5127
- // 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);
5128
5476
  return {
5129
5477
  regex,
5130
5478
  params,
5131
5479
  types: lookupTable(types),
5132
5480
  specificity,
5133
- shortest: literalLength + params.length
5481
+ shortest
5134
5482
  };
5135
5483
  }
5136
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) {
@@ -5203,13 +5551,17 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5203
5551
  refuseOnClass('@Command', target, propertyKey);
5204
5552
  const originalMethod = _descriptor.value;
5205
5553
  if (!originalMethod) {
5206
- 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.`));
5207
5555
  }
5208
5556
  // Wrap original method for interaction type validation
5209
5557
  const declaredAs = typeof builderOrType === 'function' ? builderOrType.name : `CommandType.${builderOrType}`;
5210
5558
  const checked = function(interaction, params) {
5211
- if (!matchesCommandType(commandType, interaction)) {
5212
- 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)}.`);
5213
5565
  }
5214
5566
  return originalMethod.apply(this, [
5215
5567
  interaction,
@@ -5227,7 +5579,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5227
5579
  let guilds;
5228
5580
  // Determine command type and builder
5229
5581
  if (typeof builderOrType === 'function') {
5230
- const where = `${target.constructor.name}.${propertyKey}`;
5582
+ const where = decoratedName(target, propertyKey);
5231
5583
  // A subcommand is part of its command's builder, which belongs on the command's own name
5232
5584
  const subcommandPath = Reflect.getMetadata(META.commandType, builderOrType) === enum_index.CommandType.SLASH && commandName.includes(' ');
5233
5585
  const command = commandName.split(' ')[0];
@@ -5274,12 +5626,12 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5274
5626
  if (isCustomIdRouted(commandType)) {
5275
5627
  // A route warned as it was made
5276
5628
  const braces = typeof name === 'string' ? literalBraces(commandName) : undefined;
5277
- 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}`);
5278
5630
  let pattern;
5279
5631
  try {
5280
5632
  pattern = createRegexFromPattern(commandName);
5281
5633
  } catch (error) {
5282
- throw refuse(new Error(`${target.constructor.name}.${propertyKey}: ${error.message}`, {
5634
+ throw refuse(new Error(`${decoratedName(target, propertyKey)}: ${error.message}`, {
5283
5635
  cause: error
5284
5636
  }));
5285
5637
  }
@@ -5289,7 +5641,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5289
5641
  // Warned rather than refused, so a bot that carries such a handler still starts
5290
5642
  if (pattern.shortest === 0 || pattern.shortest > MAX_CUSTOM_ID_LENGTH) {
5291
5643
  const length = pattern.shortest === 0 ? 'the pattern is empty' : `its shortest is ${pattern.shortest} characters`;
5292
- logger.warn(`${target.constructor.name}.${propertyKey}: the pattern "${commandName}" matches no customId: Discord's customIds ` + `are 1 to ${MAX_CUSTOM_ID_LENGTH} characters, and ${length}, so the handler never runs.`);
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.`);
5293
5645
  }
5294
5646
  }
5295
5647
  const declared = {
@@ -5341,7 +5693,7 @@ const WHOLE_PLACEHOLDER = new RegExp(`^${PLACEHOLDER_PATTERN.source}$`);
5341
5693
  const registered = built?.type;
5342
5694
  if (!declared || registered === undefined || declared.kind === registered) return;
5343
5695
  const kind = registered === discord_js.ApplicationCommandType.User ? 'user' : 'message';
5344
- 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.`));
5345
5697
  }
5346
5698
  /**
5347
5699
  * Retrieves the command map for a given controller.
@@ -5502,11 +5854,13 @@ exports.ReactionHandler = ReactionHandler;
5502
5854
  exports.RedisCooldownStore = RedisCooldownStore;
5503
5855
  exports.SetMetadata = SetMetadata;
5504
5856
  exports.TEXT_LIMITS = TEXT_LIMITS;
5857
+ exports.Theme = Theme;
5505
5858
  exports.ThemeCache = ThemeCache;
5506
5859
  exports.Translator = Translator;
5507
5860
  exports.UnroutedExecutionContext = UnroutedExecutionContext;
5508
5861
  exports.UserError = UserError;
5509
5862
  exports.ValidationError = ValidationError;
5863
+ exports.addOwnHandler = addOwnHandler;
5510
5864
  exports.allDeclaredErrors = allDeclaredErrors;
5511
5865
  exports.ambientThemeVersion = ambientThemeVersion;
5512
5866
  exports.applyDecorators = applyDecorators;
@@ -5520,6 +5874,7 @@ exports.bundleEntry = bundleEntry;
5520
5874
  exports.callOrder = callOrder;
5521
5875
  exports.choicesOf = choicesOf;
5522
5876
  exports.claimAmbientTheme = claimAmbientTheme;
5877
+ exports.clientPresenter = clientPresenter;
5523
5878
  exports.collectStartupErrors = collectStartupErrors;
5524
5879
  exports.commandsNamed = commandsNamed;
5525
5880
  exports.compiledConfigMessage = compiledConfigMessage;
@@ -5541,6 +5896,7 @@ exports.describeValue = describeValue;
5541
5896
  exports.dropInheritedRoutes = dropInheritedRoutes;
5542
5897
  exports.duration = duration;
5543
5898
  exports.escapeForLog = escapeForLog;
5899
+ exports.eventRoute = eventRoute;
5544
5900
  exports.existingResponse = existingResponse;
5545
5901
  exports.fitText = fitText;
5546
5902
  exports.focusedOptionName = focusedOptionName;
@@ -5553,15 +5909,17 @@ exports.getInstallContext = getInstallContext;
5553
5909
  exports.getMessageHandlers = getMessageHandlers;
5554
5910
  exports.getReactionHandlers = getReactionHandlers;
5555
5911
  exports.handOffRefusal = handOffRefusal;
5556
- exports.hasAmbientTheme = hasAmbientTheme;
5557
5912
  exports.hasCustomId = hasCustomId;
5558
5913
  exports.hideInLogs = hideInLogs;
5559
5914
  exports.inferContextType = inferContextType;
5560
5915
  exports.interactionLocale = interactionLocale;
5561
5916
  exports.isBuiltApplication = isBuiltApplication;
5917
+ exports.isConstructible = isConstructible;
5562
5918
  exports.isConstructor = isConstructor;
5563
5919
  exports.isCustomIdRouted = isCustomIdRouted;
5920
+ exports.isDefaultBatchMethod = isDefaultBatchMethod;
5564
5921
  exports.isExplainedError = isExplainedError;
5922
+ exports.isPluralShape = isPluralShape;
5565
5923
  exports.isRefusal = isRefusal;
5566
5924
  exports.isTooLarge = isTooLarge;
5567
5925
  exports.isUserOutcome = isUserOutcome;
@@ -5581,7 +5939,9 @@ exports.mergeTheme = mergeTheme;
5581
5939
  exports.messageLocale = messageLocale;
5582
5940
  exports.missingTranslatorError = missingTranslatorError;
5583
5941
  exports.number = number;
5584
- exports.ownHandlerList = ownHandlerList;
5942
+ exports.ownEventHandlers = ownEventHandlers;
5943
+ exports.ownValue = ownValue;
5944
+ exports.ownerName = ownerName;
5585
5945
  exports.ownsAmbientTheme = ownsAmbientTheme;
5586
5946
  exports.paramNarrowness = paramNarrowness;
5587
5947
  exports.parseSegment = parseSegment;
@@ -5605,33 +5965,34 @@ exports.responseOf = responseOf;
5605
5965
  exports.routeSpecificity = routeSpecificity;
5606
5966
  exports.runInThemeScope = runInThemeScope;
5607
5967
  exports.runWithTheme = runWithTheme;
5608
- exports.setLegacyThemeLayer = setLegacyThemeLayer;
5609
5968
  exports.setOwn = setOwn;
5610
5969
  exports.setPresenter = setPresenter;
5611
5970
  exports.setStartupErrorsMode = setStartupErrorsMode;
5612
5971
  exports.setThemeOutsideCalls = setThemeOutsideCalls;
5972
+ exports.shadowInherited = shadowInherited;
5613
5973
  exports.shared = shared;
5614
5974
  exports.sharedCustomId = sharedCustomId;
5615
5975
  exports.sharedName = sharedName;
5976
+ exports.singleVerdict = singleVerdict;
5616
5977
  exports.stampCall = stampCall;
5617
5978
  exports.startupError = startupError;
5979
+ exports.startupErrorsOf = startupErrorsOf;
5618
5980
  exports.stopOnStartupErrors = stopOnStartupErrors;
5619
5981
  exports.textOfIssue = textOfIssue;
5620
5982
  exports.textRenderer = textRenderer;
5621
5983
  exports.themeForInteraction = themeForInteraction;
5622
5984
  exports.themeForProblem = themeForProblem;
5623
5985
  exports.themeLayersVersion = themeLayersVersion;
5624
- exports.themeProblems = themeProblems;
5625
5986
  exports.themeResolverClass = themeResolverClass;
5626
5987
  exports.translatorOfClient = translatorOfClient;
5627
5988
  exports.usageHeading = usageHeading;
5628
5989
  exports.usageIssue = usageIssue;
5629
5990
  exports.useTheme = useTheme;
5630
5991
  exports.userWords = userWords;
5631
- exports.warnDeprecated = warnDeprecated;
5632
5992
  exports.warnDeprecatedBehaviour = warnDeprecatedBehaviour;
5633
5993
  exports.warnOnce = warnOnce;
5634
5994
  exports.warnPlaceholder = warnPlaceholder;
5995
+ exports.withArticle = withArticle;
5635
5996
  exports.withRelease = withRelease;
5636
5997
  exports.withSendableFiles = withSendableFiles;
5637
5998
  exports.withoutFiles = withoutFiles;