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
@@ -5,8 +5,8 @@ import { UserError } from '../errors.js';
5
5
  import { textFor } from '../meocord-text.js';
6
6
  import { logFailedSend } from './send-failure.js';
7
7
  import { describeInteraction } from '../../util/interaction.util.js';
8
- import { getInstallContext } from './install-context.js';
9
- import { resolveFlags, flagNames, hasEphemeral, hasComponentsV2 } from './flags.js';
8
+ import { isBotInstalled, getInstallContext } from './install-context.js';
9
+ import { resolveFlags, droppedFlagsWarning, hasEphemeral, hasComponentsV2 } from './flags.js';
10
10
  import { rewriteAttachmentUrls, keptAttachmentNames } from './attachments.js';
11
11
  import { renderEmbed, isTooLarge, REFUSED_AS_TOO_LARGE, withoutFiles, renderContainer, attachmentsOf, withSendableFiles, presenterFor, presentedMessage, TEXT_LIMITS, defaultPresenter, DEFAULT_ATTACHMENT_SIZE_LIMIT } from './presenter.js';
12
12
  import { sameEmbed, withoutRenderedViews, lockComponents, countComponents, V2_COMPONENT_LIMIT, EMBED_LIMIT } from './components.js';
@@ -16,6 +16,16 @@ import { themeForInteraction } from '../../core/theme-scope.js';
16
16
  import { LOADING_DRAW_TIMEOUT_MS } from '../../core/theme-resolvers.js';
17
17
  import { shared } from '../../util/shared-state.util.js';
18
18
 
19
+ function _class_private_field_loose_base(receiver, privateKey) {
20
+ if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) {
21
+ throw new TypeError("attempted to use private field on non-instance");
22
+ }
23
+ return receiver;
24
+ }
25
+ var id = 0;
26
+ function _class_private_field_loose_key(name) {
27
+ return "__private_" + id++ + "_" + name;
28
+ }
19
29
  const ALREADY_ACKNOWLEDGED = 40060;
20
30
  /** Discord's code for an interaction it no longer knows, such as one past its three seconds unacknowledged. */ const UNKNOWN_INTERACTION = 10062;
21
31
  const TOKEN_EXPIRED = new Set([
@@ -113,6 +123,7 @@ const messageLocks = shared('messageLocks', ()=>new Map());
113
123
  function toJson(value) {
114
124
  return typeof value?.toJSON === 'function' ? value.toJSON() : value;
115
125
  }
126
+ var _location = /*#__PURE__*/ _class_private_field_loose_key("_location");
116
127
  /** The response state behind `respond()`, with what `@Defer` and the testing helpers use besides. */ class InteractionResponse {
117
128
  get state() {
118
129
  this.sync();
@@ -151,7 +162,7 @@ function toJson(value) {
151
162
  flagsFor(step, requested, v2 = this.v2) {
152
163
  const { flags, dropped } = resolveFlags(step, requested, v2);
153
164
  if (dropped && development()) {
154
- logger.warn(`Dropped flags ${flagNames(dropped).join(', ')}, which a ${step} cannot take.`);
165
+ logger.warn(droppedFlagsWarning(step, dropped));
155
166
  }
156
167
  return flags;
157
168
  }
@@ -414,15 +425,15 @@ function toJson(value) {
414
425
  });
415
426
  }
416
427
  /**
417
- * Undoes `@Defer`'s acknowledgement after a guard denied the call silently: a command's deferred
418
- * reply is deleted while nothing was sent into it; a component's invisible acknowledgement needs
419
- * nothing. Never throws.
428
+ * Ends a call a guard denied silently. A component still unanswered is acknowledged invisibly, with or without
429
+ * `@Defer`, since Discord tells the user an unanswered call failed; so is a command `@Defer`'s 'auto' timer was
430
+ * waiting on, whose deferred reply is then deleted, as `@Defer`'s is while nothing was sent into it. Never throws.
420
431
  */ abandon() {
421
432
  const waiting = this.timer !== undefined;
422
433
  this.cancelScheduled();
423
434
  return this.queued(async ()=>{
424
- // Denied before the 'auto' timer: acknowledged here, since Discord tells the user an unanswered call failed
425
- if (waiting) await this.acknowledgeUnanswered({
435
+ // A command's acknowledgement shows "thinking…", so one is made only for the timer that would have made it
436
+ if (waiting || !answersWithOwnMessage(this.interaction)) await this.acknowledgeUnanswered({
426
437
  ephemeral: true
427
438
  });
428
439
  this.sync();
@@ -675,7 +686,7 @@ function toJson(value) {
675
686
  } catch (error) {
676
687
  const message = this.message;
677
688
  const expired = Date.now() - this.interaction.createdTimestamp >= TOKEN_EXPIRED_AFTER_MS;
678
- if (!TOKEN_EXPIRED.has(errorCode(error)) || !expired || !this.location.botInstalled || !message) throw error;
689
+ if (!TOKEN_EXPIRED.has(errorCode(error)) || !expired || !isBotInstalled(this.interaction) || !message) throw error;
679
690
  this.lastMessage = await this.call('message.edit', sent, ()=>message.edit(sent));
680
691
  }
681
692
  this.phase = 'replied';
@@ -987,6 +998,10 @@ function toJson(value) {
987
998
  } : body;
988
999
  }
989
1000
  constructor(interaction){
1001
+ Object.defineProperty(this, _location, {
1002
+ writable: true,
1003
+ value: void 0
1004
+ });
990
1005
  this.interaction = interaction;
991
1006
  this.phase = 'unanswered';
992
1007
  /** The answer steps asked for, run one after another; `pending` counts those not yet settled. */ this.queue = Promise.resolve();
@@ -1004,7 +1019,14 @@ function toJson(value) {
1004
1019
  this.suppressNotifications = false;
1005
1020
  // Set by @Defer's first step, so an error can name what acknowledged the interaction
1006
1021
  this.underDefer = false;
1007
- this.location = getInstallContext(interaction);
1022
+ Object.defineProperty(this, 'location', {
1023
+ get: ()=>{
1024
+ var _class_private_field_loose_base1, _location1;
1025
+ return (_class_private_field_loose_base1 = _class_private_field_loose_base(this, _location))[_location1 = _location] ?? (_class_private_field_loose_base1[_location1] = getInstallContext(interaction));
1026
+ },
1027
+ enumerable: true,
1028
+ configurable: true
1029
+ });
1008
1030
  const message = 'message' in interaction ? interaction.message : undefined;
1009
1031
  this.v2 = Boolean(message?.flags?.has(MessageFlags.IsComponentsV2));
1010
1032
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { CooldownStore, withRelease, MemoryCooldownStore, forgetRecorded, recordedOf } from './cooldown-store.js';
2
+ import { CooldownStore, singleVerdict, withRelease, MemoryCooldownStore, forgetRecorded, recordedOf } from './cooldown-store.js';
3
3
  import { isShardMessage } from '../core/shard-messages.js';
4
4
  import { managerGone } from '../util/sharding-mode.util.js';
5
5
  import { isShardProcess } from '../util/shard-process.util.js';
@@ -60,16 +60,12 @@ import { isShardProcess } from '../util/shard-process.util.js';
60
60
  * @param limit - The calls allowed, and the window they are counted over.
61
61
  * @returns Whether this call was recorded, and if not, how long until one can be.
62
62
  */ async consume(key, limit) {
63
- const { allowed, retryAfterMs } = await this.consumeMany([
63
+ return singleVerdict(await this.consumeMany([
64
64
  {
65
65
  key,
66
66
  limit
67
67
  }
68
- ]);
69
- return {
70
- allowed,
71
- retryAfterMs
72
- };
68
+ ]));
73
69
  }
74
70
  /**
75
71
  * Records a call against every entry if all allow it, in one message to the manager.
@@ -108,11 +104,17 @@ import { isShardProcess } from '../util/shard-process.util.js';
108
104
  this.pending.set(id, {
109
105
  resolve: (verdict, recorded)=>{
110
106
  settle();
111
- // The manager's undo is the calls it recorded, sent back to it
112
- const release = async ()=>channel.send({
107
+ // The manager's undo is the calls it recorded, sent back once: it finds a call by key and time, so a second
108
+ // undo would take another call recorded in the same millisecond
109
+ let released = false;
110
+ const release = async ()=>{
111
+ if (released) return;
112
+ released = true;
113
+ channel.send({
113
114
  meocord: 'cooldown-release',
114
115
  recorded: recorded ?? []
115
116
  });
117
+ };
116
118
  resolve(!peek && verdict.allowed && recorded ? withRelease(verdict, release) : verdict);
117
119
  },
118
120
  reject: (error)=>(settle(), reject(error))
@@ -2,6 +2,7 @@ import { warnDeprecated } from './deprecation.js';
2
2
  import { Logger } from './logger.js';
3
3
  import { useTheme, setLegacyThemeLayer } from '../core/theme-scope.js';
4
4
  import { themeProblems } from '../core/theme-validation.js';
5
+ import { shared } from '../util/shared-state.util.js';
5
6
 
6
7
  /** The theme role each static stands for; `errorColor` is a fault in the bot, `danger`. */ const ROLES = {
7
8
  primaryColor: 'primary',
@@ -11,7 +12,8 @@ import { themeProblems } from '../core/theme-validation.js';
11
12
  warningColor: 'warning'
12
13
  };
13
14
  const logger = new Logger('Theme');
14
- const assigned = {};
15
+ // One per process for this meocord version, so a static set through one build keeps those set through the other
16
+ const assigned = shared('legacyThemeColors', ()=>({}));
15
17
  /** Reads a role of the theme where it is read, warning once a run for each static read. */ function read(name) {
16
18
  warnDeprecated(logger, `Theme.${name}`, `useTheme().colors.${ROLES[name]}`);
17
19
  return useTheme().colors[ROLES[name]];
@@ -69,17 +69,36 @@ import { sharedName } from '../util/shared-state.util.js';
69
69
  }
70
70
  const DISCORD_LOCALES = new Set(Object.values(Locale));
71
71
  const languageOf = (locale)=>locale.split('-')[0];
72
- /** A message by its dotted key, or undefined when the catalog lacks it or the key names a group. */ function lookup(catalog, key) {
72
+ const PLURAL_CATEGORIES = new Set([
73
+ 'zero',
74
+ 'one',
75
+ 'two',
76
+ 'few',
77
+ 'many',
78
+ 'other'
79
+ ]);
80
+ /** 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));
81
+ /** The value at a dotted key's path, or undefined where the path ends early. */ function valueAt(catalog, key) {
73
82
  let current = catalog;
74
83
  for (const part of key.split('.')){
75
84
  if (typeof current !== 'object' || current === null) return undefined;
76
85
  current = current[part];
77
86
  }
87
+ return current;
88
+ }
89
+ /**
90
+ * A message by its dotted key, or undefined when the catalog lacks it or the key names a group. `reference`, the
91
+ * default catalog for a translation, decides where it has a group that an object is that group, so a translation that
92
+ * 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
93
+ * own keys decide, so a translation may give a plural for a message its language needs one for. A plural without an
94
+ * `other` form reads as missing, so a translation that left it out falls back to the next catalog.
95
+ */ function lookup(catalog, key, reference = catalog) {
96
+ const current = valueAt(catalog, key);
78
97
  if (typeof current === 'string') return current;
79
- if (typeof current === 'object' && current !== null && typeof current.other === 'string') {
80
- return current;
81
- }
82
- return undefined;
98
+ if (!isPluralShape(current) || typeof current.other !== 'string') return undefined;
99
+ const shape = reference === catalog ? current : valueAt(reference, key);
100
+ const group = typeof shape === 'object' && shape !== null && !isPluralShape(shape);
101
+ return group ? undefined : current;
83
102
  }
84
103
  const plurals = new Map();
85
104
  /** 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) {
@@ -143,7 +162,7 @@ class CatalogTranslator extends Translator {
143
162
  }
144
163
  translate(requested, key, params = {}) {
145
164
  for (const locale of this[LOCALE_CHAIN](requested)){
146
- const message = lookup(this.catalogs[locale], key);
165
+ const message = lookup(this.catalogs[locale], key, this.catalogs[this.defaultLocale]);
147
166
  if (message === undefined) continue;
148
167
  // An empty translation, as tools export an untranslated string, counts as missing: Discord refuses an empty reply
149
168
  const text = pluralForm({
@@ -179,7 +198,7 @@ class CatalogTranslator extends Translator {
179
198
  const localized = {};
180
199
  for (const locale of this.locales){
181
200
  if (locale === this.defaultLocale) continue;
182
- const message = lookup(this.catalogs[locale], key);
201
+ const message = lookup(this.catalogs[locale], key, this.catalogs[this.defaultLocale]);
183
202
  // A translation with a {param} is left out, as Discord would show it as written, and so is an empty one
184
203
  if (typeof message === 'string' && message !== '' && placeholderNames(message).length === 0) {
185
204
  localized[locale] = fillPlaceholders(message, (_name, whole)=>whole);
@@ -278,4 +297,4 @@ class CatalogTranslator extends Translator {
278
297
  return new CatalogTranslator(defaultLocale, locales);
279
298
  }
280
299
 
281
- export { CATALOGS, LOCALE_CHAIN, Translator, createTranslator, defineCatalog, fillPlaceholders, languageOf, lookup, missingTranslatorError, placeholderNames, pluralForm };
300
+ export { CATALOGS, LOCALE_CHAIN, Translator, createTranslator, defineCatalog, fillPlaceholders, isPluralShape, languageOf, lookup, missingTranslatorError, placeholderNames, pluralForm };
@@ -0,0 +1,26 @@
1
+ import { getAutocompleteHandlers } from '../decorator/controller.decorator.js';
2
+ import { focusedOptionName, resolveCommandPaths } from '../util/interaction.util.js';
3
+
4
+ /**
5
+ * Every `@Autocomplete` handler of the given controllers, read from their metadata, with the handlers of one option
6
+ * before those of a whole command, so an option's handler wins over its command's whatever order they are declared in.
7
+ */ function buildAutocompleteRoutes(controllers) {
8
+ const routes = controllers.flatMap((controllerClass)=>getAutocompleteHandlers(controllerClass.prototype).map((meta)=>({
9
+ controllerClass,
10
+ meta
11
+ })));
12
+ return routes.sort((a, b)=>Number(Boolean(b.meta.optionName)) - Number(Boolean(a.meta.optionName)));
13
+ }
14
+ /**
15
+ * The handler an autocomplete reaches, as dispatch routes it: its full command path, then its command's name, and at
16
+ * each the first handler whose option is the focused one, or that names none.
17
+ */ function matchAutocompleteRoute(routes, interaction) {
18
+ const focused = focusedOptionName(interaction);
19
+ for (const path of resolveCommandPaths(interaction)){
20
+ const route = routes.find(({ meta })=>meta.commandPath === path && (meta.optionName === undefined || meta.optionName === focused));
21
+ if (route) return route;
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ export { buildAutocompleteRoutes, matchAutocompleteRoute };
@@ -1,6 +1,7 @@
1
1
  import { ApplicationCommandType, ApplicationCommandOptionType } from 'discord.js';
2
2
  import { Logger } from '../common/logger.js';
3
- import { getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, getHandlerRoutes, getMessageHandlers, getReactionHandlers } from '../decorator/controller.decorator.js';
3
+ import { getAutocompleteHandlers, getCommandMap, getDeclaredRoutes, ownEventHandlers, eventRoute, getHandlerRoutes, getMessageHandlers, getReactionHandlers } from '../decorator/controller.decorator.js';
4
+ import { getEventHandlers } from '../decorator/event.decorator.js';
4
5
  import { serialise, registrationKey, SENT_AS, commandNameOf } from './command-registration.js';
5
6
  import { buildComponentRoutes, findComponentRouteConflicts } from './component-routes.js';
6
7
  import { CommandType } from '../enum/controller.enum.js';
@@ -246,9 +247,9 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
246
247
  }
247
248
  /**
248
249
  * Warns about a handler a subclass re-declares on other routes while it still answers the ones it inherits, for
249
- * `@Command`, `@MessageHandler`, `@ReactionHandler` and `@Autocomplete` alike. The next major version (5.0) drops
250
- * the inherited routes. Every class a listed controller extends is checked, a base that is not listed included, for
251
- * the methods the controller still inherits through it.
250
+ * `@Command`, `@MessageHandler`, `@ReactionHandler`, `@Autocomplete`, `@On` and `@Once` alike. The next major version
251
+ * (5.0) drops the inherited routes. Every class a listed controller extends is checked, a base that is not listed
252
+ * included, for the methods the controller still inherits through it.
252
253
  */ function warnInheritedRoutes(controllerClasses) {
253
254
  // A set, so a class two listed controllers extend is named once, for the routes either of them still answers
254
255
  const problems = new Set();
@@ -257,11 +258,18 @@ const quoted = (names)=>names.length < 2 ? names.map((name)=>`"${name}"`).join('
257
258
  // nothing of them from above it, so a class above is not named for them
258
259
  const cut = new Set();
259
260
  for(let prototype = controllerClass.prototype; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)){
260
- const declared = getDeclaredRoutes(prototype);
261
261
  const base = Object.getPrototypeOf(prototype);
262
- if (declared.length === 0 || !base) continue;
262
+ if (!base) continue;
263
+ const declared = [
264
+ ...getDeclaredRoutes(prototype),
265
+ ...ownEventHandlers(prototype).map(eventRoute)
266
+ ];
267
+ if (declared.length === 0) continue;
263
268
  if (Reflect.getOwnMetadata(META.replacesInheritedRoutes, prototype)) for (const route of declared)cut.add(route.method);
264
- const inherited = getHandlerRoutes(base).filter((route)=>!cut.has(route.method));
269
+ const inherited = [
270
+ ...getHandlerRoutes(base),
271
+ ...getEventHandlers(base).map(eventRoute)
272
+ ].filter((route)=>!cut.has(route.method));
265
273
  for (const method of new Set(declared.map((route)=>route.method))){
266
274
  const own = [
267
275
  ...new Set(declared.filter((route)=>route.method === method).map((route)=>route.label))
@@ -61,6 +61,7 @@ import { CommandType } from '../enum/controller.enum.js';
61
61
  if (existing === undefined) {
62
62
  const name = typeof body.name === 'string' ? body.name : commandName;
63
63
  unlocalizable.push(...localizationProblems(name, body));
64
+ const builderName = builderClass?.name;
64
65
  byKey.set(key, {
65
66
  builderClass: builderClass ?? builder,
66
67
  command: {
@@ -68,6 +69,9 @@ import { CommandType } from '../enum/controller.enum.js';
68
69
  body,
69
70
  ...guilds && {
70
71
  guilds
72
+ },
73
+ ...typeof builderName === 'string' && builderName && {
74
+ builder: builderName
71
75
  }
72
76
  }
73
77
  });
@@ -105,6 +109,14 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
105
109
  }
106
110
  /** Whether a command is an activity's primary entry point, which Discord takes only globally. */ const isEntryPoint = (command)=>command.body.type === ApplicationCommandType.PrimaryEntryPoint;
107
111
  /** What a blank guild to register every command to says, as an unset variable gives one. */ const BLANK_GUILD = 'No commands were registered: the guild to register to is blank. Give a guild id, or leave it out to register where commands.* says.';
112
+ /**
113
+ * Names every command whose builder lists guilds but no guild id, each with its builder, and says the run registers
114
+ * none of them yet still succeeds.
115
+ */ function blankGuildsWarning(commands) {
116
+ const one = commands.length === 1;
117
+ const lines = commands.map((command)=>` ${command.builder ? `${command.builder}: ` : ''}"${command.name}"`);
118
+ return `${commands.length} ${one ? 'command is' : 'commands are'} not registered: ${one ? 'its builder lists' : 'their builders list'} ` + 'no guild ids, as when the variable holding them is unset.\n' + `${lines.join('\n')}\n` + 'Set the guild ids; a builder without guilds registers where commands.* says. `meocord register` still exits 0 for ' + 'these; the next major version (5.0) exits 1.';
119
+ }
108
120
  /**
109
121
  * Where each command is sent: one bulk update per scope. A scope's update replaces everything the application has
110
122
  * there, so the default scope is included even when it ends up empty, unless `commands.guilds` lists no guild id.
@@ -148,6 +160,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
148
160
  guild
149
161
  });
150
162
  const unregistered = [];
163
+ const blankGuilds = [];
151
164
  for (const command of commands){
152
165
  if (isEntryPoint(command)) {
153
166
  if (command.guilds) {
@@ -157,7 +170,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
157
170
  } else if (command.guilds) {
158
171
  const own = ids(command.guilds);
159
172
  // Falling back to the default scope could publish a staff-only command everywhere.
160
- if (own.length === 0) logger.warn(`Command "${command.name}" lists no guild ids in its builder, so it is not registered.`);
173
+ if (own.length === 0) blankGuilds.push(command);
161
174
  for (const guild of own)add({
162
175
  guild
163
176
  }, command);
@@ -171,6 +184,7 @@ const scopeKey = (scope)=>scope === 'global' ? 'global' : `guild-${scope.guild}`
171
184
  }, command);
172
185
  }
173
186
  }
187
+ if (blankGuilds.length > 0) logger.warn(blankGuildsWarning(blankGuilds));
174
188
  if (nowhere) {
175
189
  const outcome = unregistered.length === 0 ? targets.has('global') ? 'only the entry point command is registered globally' : 'nothing is registered globally' : `${unregistered.join(', ')} ${unregistered.length === 1 ? 'is' : 'are'} not registered, rather than registered globally`;
176
190
  logger.warn(`commands.guilds lists no guild id, as an unset environment variable leaves it, so ${outcome}. ` + `Set the guild ids, or remove commands.guilds to register globally.`);
@@ -1,18 +1,17 @@
1
1
  import 'reflect-metadata';
2
2
  import { BaseInteraction, Message } from 'discord.js';
3
- import { CooldownStoreError, CooldownError } from '../common/errors.js';
3
+ import { CooldownError, CooldownStoreError } from '../common/errors.js';
4
4
  import { CooldownStore, MemoryCooldownStore } from '../common/cooldown-store.js';
5
5
  import { Logger } from '../common/logger.js';
6
- import { perHandler, sourcePrototype, stageClasses } from './guard-runner.js';
6
+ import { perHandler, methodStages, sourcePrototype, stageClasses } from './guard-runner.js';
7
7
  import { META } from '../util/metadata-keys.js';
8
8
 
9
- /** The cooldowns on a handler: each class's, base first, then the method's. */ const handlerCooldowns = perHandler((prototype, methodName)=>{
10
- const source = sourcePrototype(prototype, methodName);
11
- if (!source) return [];
9
+ /** The cooldowns on a handler: each class's, base first, then the method's and its overrides', base first. */ const handlerCooldowns = perHandler((prototype, methodName)=>{
10
+ if (!sourcePrototype(prototype, methodName)) return [];
12
11
  return [
13
12
  // Base first, as guards run
14
13
  ...stageClasses(prototype, methodName).flatMap((cls)=>Reflect.getOwnMetadata(META.classCooldowns, cls) ?? []),
15
- ...Reflect.getOwnMetadata(META.methodCooldowns, source, methodName) ?? []
14
+ ...methodCooldowns(prototype, methodName)
16
15
  ];
17
16
  });
18
17
  /** The counted cooldowns of each list {@link handlerCooldowns} gives, which is the same for a handler. */ const countedCooldowns = new WeakMap();
@@ -53,9 +52,8 @@ import { META } from '../util/metadata-keys.js';
53
52
  }
54
53
  return counted;
55
54
  }
56
- /** The cooldowns declared on a method itself. */ function methodCooldowns(prototype, methodName) {
57
- const source = sourcePrototype(prototype, methodName);
58
- return source ? Reflect.getOwnMetadata(META.methodCooldowns, source, methodName) ?? [] : [];
55
+ /** The cooldowns declared on a method itself and on the methods it overrides, base first. */ function methodCooldowns(prototype, methodName) {
56
+ return methodStages(prototype, methodName, META.methodCooldowns);
59
57
  }
60
58
  /** Who and where a call came from: an interaction's or a message's user, server and channel. */ function caller(first) {
61
59
  if (first instanceof BaseInteraction) return {
@@ -1,15 +1,12 @@
1
1
  import 'reflect-metadata';
2
- import { perHandler, sourcePrototype } from './guard-runner.js';
2
+ import { perHandler, nearestMethodStage } from './guard-runner.js';
3
3
  import { getMessageHandlers, getReactionHandlers, getAutocompleteHandlers } from '../decorator/controller.decorator.js';
4
4
  import { getEventHandlers } from '../decorator/event.decorator.js';
5
5
  import { META } from '../util/metadata-keys.js';
6
6
 
7
7
  /** How long `@Defer({ mode: 'auto' })` waits for an answer by default. */ const AUTO_DEFER_AFTER_MS = 1500;
8
8
  /** The latest `'auto'` acknowledges, counted from the interaction's creation: Discord allows three seconds. */ const AUTO_DEFER_LIMIT_MS = 2500;
9
- /** The `@Defer` options of a handler, read where the handler is declared. */ const handlerDefer = perHandler((prototype, methodName)=>{
10
- const source = sourcePrototype(prototype, methodName);
11
- return source ? Reflect.getOwnMetadata(META.deferOptions, source, methodName) : undefined;
12
- });
9
+ /** The `@Defer` options of a handler: its own, or else those of the nearest method it overrides that has them. */ const handlerDefer = perHandler((prototype, methodName)=>nearestMethodStage(prototype, methodName, META.deferOptions));
13
10
  /** The kind of non-interaction handler a method is, if any: `@Defer` cannot apply to one. */ function nonInteractionHandler(prototype, methodName) {
14
11
  if (getMessageHandlers(prototype).some((handler)=>handler.method === methodName)) return 'message';
15
12
  if (getReactionHandlers(prototype).some((handler)=>handler.method === methodName)) return 'reaction';