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
@@ -1,5 +1,5 @@
1
1
  import { fitsScope, commandWordsOf, afterStart } from './message-routes.js';
2
- import { isGuildType, primaryFirst, usageOf, paramTypeLabel } from './message-params.js';
2
+ import { effectiveScope, primaryFirst, usageOf, paramTypeLabel } from './message-params.js';
3
3
  import { splitWords } from './message-words.js';
4
4
  import { usageHeading } from '../common/errors.js';
5
5
  import { textRenderer } from '../common/meocord-text.js';
@@ -31,12 +31,13 @@ import { userWords } from '../util/user-text.util.js';
31
31
  const text = content.trim();
32
32
  const rest = afterStart(text, starts.prefixes.filter((prefix)=>prefix !== ''), starts.mention, caseSensitive);
33
33
  if (!rest) return undefined;
34
- const [first] = splitWords(rest);
34
+ const [first, second] = splitWords(rest);
35
35
  const key = (word)=>caseSensitive ? word : word.toLowerCase();
36
36
  if (!first || !words.some((word)=>key(word) === key(first.value))) return undefined;
37
+ // From where the next word starts, as the help word may have been written in quotes
37
38
  return {
38
39
  start: text.slice(0, text.length - rest.length),
39
- query: rest.slice(first.value.length).trim()
40
+ query: second ? rest.slice(second.start).trim() : ''
40
41
  };
41
42
  }
42
43
  /** The start a caller would type a route with here: its own prefix, a mention, or the start the help message used. */ function displayStart(route, start, starts) {
@@ -45,11 +46,6 @@ import { userWords } from '../util/user-text.util.js';
45
46
  if (route.prefix !== undefined) return route.prefix[0] ?? '';
46
47
  return start;
47
48
  }
48
- /** Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param or flag. */ function effectiveScope(route) {
49
- if (route.scope !== 'any') return route.scope;
50
- const guildOnly = route.tokens.some((token)=>'param' in token && token.type !== undefined && isGuildType(token.type)) || route.flags.some((flag)=>flag.type !== undefined && isGuildType(flag.type));
51
- return guildOnly ? 'guild' : 'any';
52
- }
53
49
  /** A route's params and flags, each with what it takes in words. */ function paramsOf(route, types, render) {
54
50
  const params = route.tokens.flatMap((token)=>{
55
51
  if (!('param' in token)) return [];
@@ -249,23 +245,49 @@ const sortedEntries = (entries)=>entries.sort((a, b)=>a.usage < b.usage ? -1 : a
249
245
  scope
250
246
  ].filter((line)=>line !== undefined).join('\n');
251
247
  }
252
- /** Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. */ function splitReply(text) {
248
+ // Built on first use, for a line longer than a message
249
+ let graphemes;
250
+ /**
251
+ * Where a line longer than one message is cut: after the last character a reader sees as one, such as an emoji, a flag
252
+ * or a letter with its accent, that still fits. A single such character longer than a message is cut between code points.
253
+ */ function fittingCut(piece) {
254
+ let cut = 0;
255
+ graphemes ?? (graphemes = new Intl.Segmenter(undefined, {
256
+ granularity: 'grapheme'
257
+ }));
258
+ for (const { index, segment } of graphemes.segment(piece)){
259
+ if (index + segment.length > MAX_REPLY_LENGTH) break;
260
+ cut = index + segment.length;
261
+ }
262
+ if (cut > 0) return cut;
263
+ const high = piece.charCodeAt(MAX_REPLY_LENGTH - 1);
264
+ return high >= 0xd800 && high <= 0xdbff ? MAX_REPLY_LENGTH - 1 : MAX_REPLY_LENGTH;
265
+ }
266
+ /**
267
+ * Splits a reply into Discord-sized messages at line breaks, a line longer than one message cut where it must be. A
268
+ * message of only whitespace, which Discord refuses as empty, is left out.
269
+ */ function splitReply(text) {
253
270
  const chunks = [];
271
+ const push = (chunk)=>{
272
+ if (chunk.trim() !== '') chunks.push(chunk);
273
+ };
254
274
  let current = '';
255
275
  for (const line of text.split('\n')){
256
- for(let piece = line;; piece = piece.slice(MAX_REPLY_LENGTH)){
257
- const part = piece.slice(0, MAX_REPLY_LENGTH);
276
+ for(let piece = line;;){
277
+ const cut = piece.length <= MAX_REPLY_LENGTH ? piece.length : fittingCut(piece);
278
+ const part = piece.slice(0, cut);
258
279
  const joined = current ? `${current}\n${part}` : part;
259
280
  if (joined.length <= MAX_REPLY_LENGTH) current = joined;
260
281
  else {
261
- chunks.push(current);
282
+ push(current);
262
283
  current = part;
263
284
  }
264
- if (piece.length <= MAX_REPLY_LENGTH) break;
285
+ if (cut === piece.length) break;
286
+ piece = piece.slice(cut);
265
287
  }
266
288
  }
267
- if (current) chunks.push(current);
289
+ push(current);
268
290
  return chunks;
269
291
  }
270
292
 
271
- export { computeMessageHelp, effectiveScope, helpInvocation, helpWords, isListable, matchHelpRequest, renderMessageHelp, splitReply };
293
+ export { computeMessageHelp, helpInvocation, helpWords, isListable, matchHelpRequest, renderMessageHelp, splitReply };
@@ -1,9 +1,10 @@
1
1
  import { MessageUsageError } from '../common/errors.js';
2
2
  import { usageIssue } from '../common/meocord-text.js';
3
3
  import { MessageEntityRef, resolveRefs } from './message-entities.js';
4
- import { splitWords, splitFlagWords } from './message-words.js';
4
+ import { caseKey, isLiteralFlag, splitWords, splitFlagWords, literalFlags } from './message-words.js';
5
5
  import { lookupTable, duration, bool, number, choicesOf } from './scalar-types.js';
6
6
  import { userWords } from '../util/user-text.util.js';
7
+ import { setOwn, ownValue, shadowInherited } from '../util/value.util.js';
7
8
 
8
9
  /** The ID a mention or a bare snowflake names, for the mention's kind. */ function idOf(word, mention) {
9
10
  return (mention.exec(word) ?? /^(\d{17,20})$/.exec(word))?.[1];
@@ -11,6 +12,10 @@ import { userWords } from '../util/user-text.util.js';
11
12
  const USER_MENTION = /^<@!?(\d{17,20})>$/;
12
13
  const ROLE_MENTION = /^<@&(\d{17,20})>$/;
13
14
  const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
15
+ /** The server's role a word names: its mention or ID, else its name in any case. */ function roleNamed(guild, word) {
16
+ const id = idOf(word, ROLE_MENTION);
17
+ return id ? guild.roles.cache.get(id) : guild.roles.cache.find((candidate)=>candidate.name.toLowerCase() === word.toLowerCase());
18
+ }
14
19
  /** The scalar types, each turning a word into its value or `undefined`, and the Discord ones, which resolve below. */ const BUILT_IN_TYPES = lookupTable({
15
20
  string: (word)=>word,
16
21
  int: (word)=>number(word, true),
@@ -41,7 +46,18 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
41
46
  'channel'
42
47
  ]);
43
48
  /** Whether a param type is found only in a server. */ const isGuildType = (type)=>GUILD_TYPES.has(type);
44
- /** Whether a route's pattern takes a param found only in a server, so a DM gets its usage instead. */ const needsGuild = (route)=>route.tokens.some((token)=>'param' in token && token.type !== undefined && GUILD_TYPES.has(token.type));
49
+ /** Whether a route's pattern takes a `member`, `role` or `channel` param, optional or not. */ const takesGuildParam = (route)=>route.tokens.some((token)=>'param' in token && token.type !== undefined && GUILD_TYPES.has(token.type));
50
+ /**
51
+ * Whether a route works only in a server, as dispatch decides it: a `member`, `role` or `channel` param, optional or
52
+ * not, or a required flag of one of those types. An optional flag of those types refuses a DM only when it is given.
53
+ */ const needsGuild = (route)=>takesGuildParam(route) || route.flags.some((flag)=>!flag.optional && flag.type !== undefined && GUILD_TYPES.has(flag.type));
54
+ /**
55
+ * Where a route works: its scope, narrowed to servers by a `member`, `role` or `channel` param, or required flag, as
56
+ * dispatch narrows it.
57
+ */ function effectiveScope(route) {
58
+ if (route.scope !== 'any') return route.scope;
59
+ return needsGuild(route) ? 'guild' : 'any';
60
+ }
45
61
  /** Whether a pattern's `{name:type}` names a type: built in, words to choose from, or one the app adds. */ function isKnownParamType(type, types) {
46
62
  return type in BUILT_IN_TYPES || Boolean(choicesOf(type)) || appType(types, type) !== undefined;
47
63
  }
@@ -121,14 +137,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
121
137
  */ function assertMessageScope(route, message, start) {
122
138
  const inGuild = message.guildId !== null && message.guildId !== undefined;
123
139
  if (route.scope === 'guild' && !inGuild) {
124
- throw new MessageUsageError(usageOf(route, start), [
125
- usageIssue({
126
- key: 'meocord.usage.serverOnly'
127
- })
128
- ], {
129
- serverOnly: true,
130
- quiet: start === ''
131
- });
140
+ throw serverOnly(usageOf(route, start), start === '');
132
141
  }
133
142
  if (route.scope === 'dm' && inGuild) {
134
143
  throw new MessageUsageError(usageOf(route, start), [
@@ -141,6 +150,15 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
141
150
  });
142
151
  }
143
152
  }
153
+ /** The usage error for a message sent outside a server to a command, or with a flag, that only a server has. */ const serverOnly = (usage, quiet)=>new MessageUsageError(usage, [
154
+ usageIssue({
155
+ key: 'meocord.usage.serverOnly'
156
+ })
157
+ ], {
158
+ serverOnly: true,
159
+ quiet
160
+ });
161
+ /** Whether a message gives one of a route's flags of a type only a server has, such as `--in=#general`. */ const givesGuildFlag = (route, flags)=>flags.some((given)=>route.flags.some((flag)=>flag.type !== undefined && GUILD_TYPES.has(flag.type) && caseKey(flag.flag, route.caseSensitive) === caseKey(given.name, route.caseSensitive)));
144
162
  /** Whether a value an app's `parse` returned is a ref to resolve after the guards. */ const isRef = (value)=>typeof value === 'object' && value !== null && 'cached' in value && typeof value.resolve === 'function';
145
163
  /**
146
164
  * The params a matched route's guards see, read from the words with no request to Discord: typed params and lists as
@@ -163,18 +181,20 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
163
181
  const issues = [];
164
182
  const items = [];
165
183
  for (const token of route.tokens){
166
- if (!('param' in token) || token.type === undefined || raw[token.param] === undefined) continue;
184
+ // Only a word the message gave is typed: an own one, never what Object.prototype has under the param's name
185
+ const word = 'param' in token ? ownValue(raw, token.param) : undefined;
186
+ if (!('param' in token) || token.type === undefined || word === undefined) continue;
167
187
  if (!token.rest) {
168
188
  items.push({
169
189
  key: token.param,
170
190
  label: token.param,
171
191
  type: token.type,
172
- word: raw[token.param]
192
+ word
173
193
  });
174
194
  continue;
175
195
  }
176
- const words = splitWords(raw[token.param]);
177
- params[token.param] = new Array(words.length);
196
+ const words = splitWords(word);
197
+ setOwn(params, token.param, new Array(words.length));
178
198
  words.forEach(({ value }, index)=>items.push({
179
199
  key: token.param,
180
200
  label: token.param,
@@ -184,19 +204,12 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
184
204
  }));
185
205
  }
186
206
  const flagIssues = route.flags.length > 0 ? readFlags(route, message, start, params, items) : [];
207
+ // readFlags takes out each flag's key, so one left out whose name Object.prototype has reads as left out again
208
+ if (route.inheritedNames.length > 0) shadowInherited(params, route.inheritedNames);
187
209
  const guild = message.guild;
188
- if (!guild && (needsGuild(route) || items.some((item)=>GUILD_TYPES.has(item.type)))) {
189
- throw new MessageUsageError(usage, [
190
- usageIssue({
191
- key: 'meocord.usage.serverOnly'
192
- })
193
- ], {
194
- serverOnly: true,
195
- quiet
196
- });
197
- }
210
+ if (!guild && (needsGuild(route) || items.some((item)=>GUILD_TYPES.has(item.type)))) throw serverOnly(usage, quiet);
198
211
  const put = (item, value)=>{
199
- if (item.index === undefined) params[item.key] = value;
212
+ if (item.index === undefined) setOwn(params, item.key, value);
200
213
  else params[item.key][item.index] = value;
201
214
  };
202
215
  const refs = [];
@@ -233,8 +246,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
233
246
  }
234
247
  } else if (type === 'role') {
235
248
  // Roles are cached with the Guilds intent, so a role is known here or not at all
236
- const id = idOf(word, ROLE_MENTION);
237
- const role = id ? guild.roles.cache.get(id) : guild.roles.cache.find((candidate)=>candidate.name.toLowerCase() === word.toLowerCase());
249
+ const role = roleNamed(guild, word);
238
250
  if (role) {
239
251
  refer(item, 'role', new MessageEntityRef('role', role.id, message.client, guild));
240
252
  continue;
@@ -274,9 +286,9 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
274
286
  const params = {
275
287
  ...parsed.params
276
288
  };
277
- for (const [key, value] of Object.entries(params))if (Array.isArray(value)) params[key] = [
289
+ for (const [key, value] of Object.entries(params))if (Array.isArray(value)) setOwn(params, key, [
278
290
  ...value
279
- ];
291
+ ]);
280
292
  const issues = [];
281
293
  for (const slot of refs){
282
294
  const value = slot.ref instanceof MessageEntityRef ? found.get(slot.ref) : await slot.ref.resolve();
@@ -284,7 +296,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
284
296
  issues.push(missingEntity(slot));
285
297
  continue;
286
298
  }
287
- if (slot.index === undefined) params[slot.key] = value;
299
+ if (slot.index === undefined) setOwn(params, slot.key, value);
288
300
  else params[slot.key][slot.index] = value;
289
301
  }
290
302
  if (issues.length > 0) throw new MessageUsageError(usage, issues, {
@@ -350,7 +362,11 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
350
362
  return {
351
363
  parseArgs: async (args)=>{
352
364
  assertMessageScope(route, message, start);
353
- if (given !== undefined) throw new MessageUsageError(usageOf(route, start), missingParams(route, given));
365
+ if (given !== undefined) {
366
+ // A DM to a command only a server has is told so before what its words lack, as one whose words fit is
367
+ if (!message.guild && (needsGuild(route) || givesGuildFlag(route, given.flags))) throw serverOnly(usageOf(route, start), start === '');
368
+ throw new MessageUsageError(usageOf(route, start), missingParams(route, given, types, message.guild));
369
+ }
354
370
  if (!hasTypedParams(route)) return args;
355
371
  parsed = await parseMessageParams(route, params, message, start, types);
356
372
  return [
@@ -401,7 +417,11 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
401
417
  const given = new Map();
402
418
  const unknown = new Set();
403
419
  const text = (message.content ?? '').trim();
404
- for (const flag of splitFlagWords(text.slice(start.length)).flags){
420
+ const split = splitFlagWords(text.slice(start.length));
421
+ // A flag the pattern's own `--` word took is that word, and neither one of its flags nor unknown
422
+ const literal = literalFlags(route.tokens, route.caseSensitive, split.words, split.flags);
423
+ for (const [index, flag] of split.flags.entries()){
424
+ if (literal.has(index)) continue;
405
425
  const declared = route.flags.find((candidate)=>key(candidate.flag) === key(flag.name));
406
426
  if (declared) given.set(declared.flag, flag);
407
427
  else if (!unknown.has(flag.name)) {
@@ -427,7 +447,7 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
427
447
  value: userWords(value)
428
448
  }
429
449
  }, flag.flag));
430
- else params[flag.flag] = on;
450
+ else setOwn(params, flag.flag, on);
431
451
  } else if (!given.has(flag.flag)) {
432
452
  if (!flag.optional) issues.push(usageIssue({
433
453
  key: 'meocord.usage.missing',
@@ -455,24 +475,84 @@ const CHANNEL_MENTION = /^<#(\d{17,20})>$/;
455
475
  return issues;
456
476
  }
457
477
  /**
458
- * What is wrong with a command whose words a message named but whose pattern it does not fit, given how many
459
- * words followed the command words: each required param it left out, or words beyond what the pattern takes.
460
- */ function missingParams(route, given) {
461
- const firstParam = route.tokens.findIndex((token)=>!('literal' in token));
462
- const after = firstParam === -1 ? [] : route.tokens.slice(firstParam);
463
- const missing = after.slice(given).filter((token)=>'param' in token && !token.optional).map((token)=>usageIssue({
464
- key: 'meocord.usage.missing',
465
- params: {
466
- param: token.param
478
+ * What is wrong with a command whose words a message named but whose pattern it does not fit, read word by word
479
+ * against the pattern: each word or required param it left out, a word given another in its place, else words
480
+ * beyond what the pattern takes, else the first word its param's built-in type does not take; and then each required
481
+ * flag it left out, as a message whose words fit is told. With none of those true, there are none, and the usage stands
482
+ * alone.
483
+ */ function missingParams(route, given, types, guild) {
484
+ const { words, flags } = given;
485
+ const missing = [];
486
+ // The flags given bare in place of the pattern's own `--` words, which are those words and set no flag
487
+ const literal = new Set();
488
+ let unfit;
489
+ let w = 0;
490
+ for (const token of route.tokens){
491
+ if ('literal' in token) {
492
+ if (w < words.length && caseKey(words[w].value, route.caseSensitive) === caseKey(token.literal, route.caseSensitive)) {
493
+ w++;
494
+ continue;
467
495
  }
468
- }, token.param));
469
- if (missing.length > 0) return missing;
470
- return [
496
+ // A pattern's own `--` word given bare in its place takes no word; another word in its place is not taken
497
+ const word = flags.find((flag)=>!literal.has(flag) && isLiteralFlag(flag, token.literal, w, route.caseSensitive));
498
+ if (word) literal.add(word);
499
+ else missing.push(usageIssue({
500
+ key: 'meocord.usage.missing',
501
+ params: {
502
+ param: token.literal
503
+ }
504
+ }));
505
+ } else if (w === words.length) {
506
+ if (!token.optional) missing.push(usageIssue({
507
+ key: 'meocord.usage.missing',
508
+ params: {
509
+ param: token.param
510
+ }
511
+ }, token.param));
512
+ } else if (token.rest) {
513
+ w = words.length;
514
+ } else {
515
+ // Words fill the params in order, as the usage shows them
516
+ const word = words[w++].value;
517
+ const type = token.type;
518
+ if (!unfit && type !== undefined && isUnfit(type, word, route.caseSensitive, guild)) {
519
+ unfit = {
520
+ key: token.param,
521
+ label: token.param,
522
+ type,
523
+ word
524
+ };
525
+ }
526
+ }
527
+ }
528
+ // With none of these, the reason may be one MeoCord has no text for, as a role's name the matcher takes only as a
529
+ // mention or an ID
530
+ const reasons = missing.length > 0 ? missing : w < words.length ? [
471
531
  usageIssue({
472
532
  key: 'meocord.usage.tooManyWords'
473
533
  })
534
+ ] : unfit ? [
535
+ wrongType(unfit, types)
536
+ ] : [];
537
+ // So one reply names all the message lacks, rather than the flags only once its words fit
538
+ const missingFlags = route.flags.filter((flag)=>flag.type !== undefined && !flag.optional && !flags.some((sent)=>!literal.has(sent) && caseKey(sent.name, route.caseSensitive) === caseKey(flag.flag, route.caseSensitive))).map((flag)=>usageIssue({
539
+ key: 'meocord.usage.missing',
540
+ params: {
541
+ param: `--${flag.flag}`
542
+ }
543
+ }, flag.flag));
544
+ return [
545
+ ...reasons,
546
+ ...missingFlags
474
547
  ];
475
548
  }
549
+ /**
550
+ * Whether a param of a type is known not to take a word: a built-in type or words to choose from that the word doesn't
551
+ * fit. A role is also found by its name, so only a word that names none of the server's roles is unfit.
552
+ */ function isUnfit(type, word, caseSensitive, guild) {
553
+ if (type === 'role') return guild !== null && roleNamed(guild, word) === undefined;
554
+ return (type in BUILT_IN_TYPES || Boolean(choicesOf(type))) && !fitsParamType(type, word, caseSensitive);
555
+ }
476
556
  /** Whether a route's params need resolving: it declares a type for one, or has flags. */ const hasTypedParams = (route)=>route.flags.length > 0 || route.tokens.some((token)=>'param' in token && token.type !== undefined);
477
557
 
478
- export { BUILT_IN_TYPES, assertMessageScope, fetchMessageParams, fitsParamType, hasTypedParams, isGuildType, isKnownParamType, messageCommandHooks, missingParams, needsGuild, paramTypeLabel, parseMessageParams, primaryFirst, subcommandUsageError, usageOf };
558
+ export { BUILT_IN_TYPES, assertMessageScope, effectiveScope, fetchMessageParams, fitsParamType, hasTypedParams, isGuildType, isKnownParamType, messageCommandHooks, missingParams, needsGuild, paramTypeLabel, parseMessageParams, primaryFirst, subcommandUsageError, takesGuildParam, usageOf };