meocord 3.2.0 → 3.2.1

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.
@@ -26,23 +26,44 @@ const createErrorEmbed = (description)=>{
26
26
  return embed;
27
27
  };
28
28
 
29
+ /** What Discord assumes a command body is when it carries no type of its own. */ const DEFAULT_APPLICATION_COMMAND_TYPE = discord_js.ApplicationCommandType.ChatInput;
29
30
  /**
30
- * The name a builder registers under, which is what Discord deduplicates on.
31
+ * The body a builder registers, which is what Discord sees.
31
32
  *
32
- * Read from the built payload rather than from the `@Command` argument: a builder is
33
- * free to name the command something other than the string it was handed, and it is
34
- * the payload Discord sees.
35
- */ function commandNameOf(builder) {
33
+ * Read from the built payload rather than from the `@Command` arguments: a builder is
34
+ * free to describe the command differently from the strings it was handed.
35
+ */ function payloadOf(builder) {
36
36
  try {
37
- const json = typeof builder.toJSON === 'function' ? builder.toJSON() : builder;
38
- return typeof json?.name === 'string' ? json.name : undefined;
37
+ return typeof builder.toJSON === 'function' ? builder.toJSON() : builder;
39
38
  } catch {
40
39
  // A builder missing a required field throws from toJSON. Surfacing that is the
41
40
  // registration call's job, where it is reported against the command Discord
42
41
  // rejected -- deduplication should not be what turns it into a startup crash.
43
- return undefined;
42
+ return {};
44
43
  }
45
44
  }
45
+ /** The name a builder registers under. */ function commandNameOf(builder) {
46
+ const { name } = payloadOf(builder);
47
+ return typeof name === 'string' ? name : undefined;
48
+ }
49
+ /**
50
+ * The application command type a builder registers as.
51
+ *
52
+ * A slash builder may leave the field out, so an absent type is read as the chat input
53
+ * one Discord would infer — which keeps untyped slash builders colliding with each
54
+ * other rather than each claiming a key of its own.
55
+ */ function commandTypeOf(builder) {
56
+ const { type } = payloadOf(builder);
57
+ return typeof type === 'number' ? type : DEFAULT_APPLICATION_COMMAND_TYPE;
58
+ }
59
+ /**
60
+ * The identity Discord gives a command.
61
+ *
62
+ * The numeric type leads, so the two halves can never be read apart wrongly: everything
63
+ * before the first separator is the type, everything after it is the name.
64
+ */ function registrationKey(builder, fallbackName) {
65
+ return `${commandTypeOf(builder)}:${commandNameOf(builder) ?? fallbackName}`;
66
+ }
46
67
  class MeoCordApp {
47
68
  /**
48
69
  * Runs an event handler so a failure inside it cannot take the process down.
@@ -109,11 +130,13 @@ class MeoCordApp {
109
130
  }
110
131
  }
111
132
  async registerCommands() {
112
- // Keyed by registered name: a command whose subcommands live in separate methods
113
- // declares the same name more than once, and sending its builder twice makes
114
- // Discord reject the whole payload. The first builder wins, and a second one that
115
- // is not the same object is reported rather than silently dropped.
116
- const buildersByName = new Map();
133
+ // Keyed by type and name together, because that pair is what Discord treats as one
134
+ // command: a user context menu and a message context menu are free to share a name,
135
+ // and keying on the name alone would drop one of them from the payload. Within a
136
+ // single type the name is unique, so a command whose subcommands live in separate
137
+ // methods still contributes its builder once — sending it twice makes Discord
138
+ // reject the whole payload.
139
+ const buildersByCommand = new Map();
117
140
  for (const controllerClass of this.controllerClasses){
118
141
  const instance = this.getInstance(controllerClass);
119
142
  const commandMap = controller_decorator.getCommandMap(instance);
@@ -122,18 +145,18 @@ class MeoCordApp {
122
145
  if (!Array.isArray(commandMetadataArray)) continue;
123
146
  for (const { builder, type } of commandMetadataArray){
124
147
  if (!(type in enum_index.CommandType) || !builder) continue;
125
- const registeredName = commandNameOf(builder) ?? commandName;
126
- const existing = buildersByName.get(registeredName);
148
+ const key = registrationKey(builder, commandName);
149
+ const existing = buildersByCommand.get(key);
127
150
  if (existing === undefined) {
128
- buildersByName.set(registeredName, builder);
151
+ buildersByCommand.set(key, builder);
129
152
  } else if (existing !== builder) {
130
- this.logger.warn(`Command "${registeredName}" is built more than once; only the first builder is registered. ` + `Declare the builder on one @Command and give the others the plain CommandType.`);
153
+ this.logger.warn(`Command "${commandNameOf(builder) ?? commandName}" is built more than once for the same ` + `application command type; only the first builder is registered. Two builders of one type ` + `cannot both own a name, so declare the builder on a single @Command and give the others ` + `the plain CommandType.`);
131
154
  }
132
155
  }
133
156
  }
134
157
  }
135
158
  const builders = [
136
- ...buildersByName.values()
159
+ ...buildersByCommand.values()
137
160
  ];
138
161
  try {
139
162
  if (this.bot.application) {
@@ -1,4 +1,4 @@
1
- import { SlashCommandBuilder, MessageFlagsBitField } from 'discord.js';
1
+ import { ApplicationCommandType, SlashCommandBuilder, MessageFlagsBitField } from 'discord.js';
2
2
  import { Logger } from '../common/logger.js';
3
3
  import '../common/theme.js';
4
4
  import { getCommandMap, findAmbiguousRoutes, PARAM_SEPARATOR, getAutocompleteHandlers, getMessageHandlers, getReactionHandlers } from '../decorator/controller.decorator.js';
@@ -8,23 +8,44 @@ import { describeInteraction, hasCustomId, matchesCommandType, resolveCommandPat
8
8
  import { ReactionHandlerAction, CommandType } from '../enum/controller.enum.js';
9
9
  import CliTable3 from 'cli-table3';
10
10
 
11
+ /** What Discord assumes a command body is when it carries no type of its own. */ const DEFAULT_APPLICATION_COMMAND_TYPE = ApplicationCommandType.ChatInput;
11
12
  /**
12
- * The name a builder registers under, which is what Discord deduplicates on.
13
+ * The body a builder registers, which is what Discord sees.
13
14
  *
14
- * Read from the built payload rather than from the `@Command` argument: a builder is
15
- * free to name the command something other than the string it was handed, and it is
16
- * the payload Discord sees.
17
- */ function commandNameOf(builder) {
15
+ * Read from the built payload rather than from the `@Command` arguments: a builder is
16
+ * free to describe the command differently from the strings it was handed.
17
+ */ function payloadOf(builder) {
18
18
  try {
19
- const json = typeof builder.toJSON === 'function' ? builder.toJSON() : builder;
20
- return typeof json?.name === 'string' ? json.name : undefined;
19
+ return typeof builder.toJSON === 'function' ? builder.toJSON() : builder;
21
20
  } catch {
22
21
  // A builder missing a required field throws from toJSON. Surfacing that is the
23
22
  // registration call's job, where it is reported against the command Discord
24
23
  // rejected -- deduplication should not be what turns it into a startup crash.
25
- return undefined;
24
+ return {};
26
25
  }
27
26
  }
27
+ /** The name a builder registers under. */ function commandNameOf(builder) {
28
+ const { name } = payloadOf(builder);
29
+ return typeof name === 'string' ? name : undefined;
30
+ }
31
+ /**
32
+ * The application command type a builder registers as.
33
+ *
34
+ * A slash builder may leave the field out, so an absent type is read as the chat input
35
+ * one Discord would infer — which keeps untyped slash builders colliding with each
36
+ * other rather than each claiming a key of its own.
37
+ */ function commandTypeOf(builder) {
38
+ const { type } = payloadOf(builder);
39
+ return typeof type === 'number' ? type : DEFAULT_APPLICATION_COMMAND_TYPE;
40
+ }
41
+ /**
42
+ * The identity Discord gives a command.
43
+ *
44
+ * The numeric type leads, so the two halves can never be read apart wrongly: everything
45
+ * before the first separator is the type, everything after it is the name.
46
+ */ function registrationKey(builder, fallbackName) {
47
+ return `${commandTypeOf(builder)}:${commandNameOf(builder) ?? fallbackName}`;
48
+ }
28
49
  class MeoCordApp {
29
50
  /**
30
51
  * Runs an event handler so a failure inside it cannot take the process down.
@@ -91,11 +112,13 @@ class MeoCordApp {
91
112
  }
92
113
  }
93
114
  async registerCommands() {
94
- // Keyed by registered name: a command whose subcommands live in separate methods
95
- // declares the same name more than once, and sending its builder twice makes
96
- // Discord reject the whole payload. The first builder wins, and a second one that
97
- // is not the same object is reported rather than silently dropped.
98
- const buildersByName = new Map();
115
+ // Keyed by type and name together, because that pair is what Discord treats as one
116
+ // command: a user context menu and a message context menu are free to share a name,
117
+ // and keying on the name alone would drop one of them from the payload. Within a
118
+ // single type the name is unique, so a command whose subcommands live in separate
119
+ // methods still contributes its builder once — sending it twice makes Discord
120
+ // reject the whole payload.
121
+ const buildersByCommand = new Map();
99
122
  for (const controllerClass of this.controllerClasses){
100
123
  const instance = this.getInstance(controllerClass);
101
124
  const commandMap = getCommandMap(instance);
@@ -104,18 +127,18 @@ class MeoCordApp {
104
127
  if (!Array.isArray(commandMetadataArray)) continue;
105
128
  for (const { builder, type } of commandMetadataArray){
106
129
  if (!(type in CommandType) || !builder) continue;
107
- const registeredName = commandNameOf(builder) ?? commandName;
108
- const existing = buildersByName.get(registeredName);
130
+ const key = registrationKey(builder, commandName);
131
+ const existing = buildersByCommand.get(key);
109
132
  if (existing === undefined) {
110
- buildersByName.set(registeredName, builder);
133
+ buildersByCommand.set(key, builder);
111
134
  } else if (existing !== builder) {
112
- this.logger.warn(`Command "${registeredName}" is built more than once; only the first builder is registered. ` + `Declare the builder on one @Command and give the others the plain CommandType.`);
135
+ this.logger.warn(`Command "${commandNameOf(builder) ?? commandName}" is built more than once for the same ` + `application command type; only the first builder is registered. Two builders of one type ` + `cannot both own a name, so declare the builder on a single @Command and give the others ` + `the plain CommandType.`);
113
136
  }
114
137
  }
115
138
  }
116
139
  }
117
140
  const builders = [
118
- ...buildersByName.values()
141
+ ...buildersByCommand.values()
119
142
  ];
120
143
  try {
121
144
  if (this.bot.application) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "meocord",
3
3
  "description": "Decorator-based Discord bot framework built on discord.js. Brings NestJS-style controllers, dependency injection, guards, and testing utilities to bot development — with a full CLI and TypeScript-first design.",
4
- "version": "3.2.0",
4
+ "version": "3.2.1",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22"