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.
- package/dist/cjs/core/index.cjs +41 -18
- package/dist/esm/core/meocord.app.js +42 -19
- package/package.json +1 -1
package/dist/cjs/core/index.cjs
CHANGED
|
@@ -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
|
|
31
|
+
* The body a builder registers, which is what Discord sees.
|
|
31
32
|
*
|
|
32
|
-
* Read from the built payload rather than from the `@Command`
|
|
33
|
-
* free to
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
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
|
|
126
|
-
const existing =
|
|
148
|
+
const key = registrationKey(builder, commandName);
|
|
149
|
+
const existing = buildersByCommand.get(key);
|
|
127
150
|
if (existing === undefined) {
|
|
128
|
-
|
|
151
|
+
buildersByCommand.set(key, builder);
|
|
129
152
|
} else if (existing !== builder) {
|
|
130
|
-
this.logger.warn(`Command "${
|
|
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
|
-
...
|
|
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
|
|
13
|
+
* The body a builder registers, which is what Discord sees.
|
|
13
14
|
*
|
|
14
|
-
* Read from the built payload rather than from the `@Command`
|
|
15
|
-
* free to
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
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
|
|
108
|
-
const existing =
|
|
130
|
+
const key = registrationKey(builder, commandName);
|
|
131
|
+
const existing = buildersByCommand.get(key);
|
|
109
132
|
if (existing === undefined) {
|
|
110
|
-
|
|
133
|
+
buildersByCommand.set(key, builder);
|
|
111
134
|
} else if (existing !== builder) {
|
|
112
|
-
this.logger.warn(`Command "${
|
|
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
|
-
...
|
|
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.
|
|
4
|
+
"version": "3.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22"
|