meocord 1.2.1 → 1.2.2
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/CHANGELOG.md +35 -0
- package/README.md +152 -140
- package/dist/cjs/_shared/meocord.app-CHjdCAA_.cjs +496 -0
- package/dist/cjs/_shared/theme-BdtbtMZX.cjs +176 -0
- package/dist/cjs/common/index.cjs +16 -0
- package/dist/cjs/core/index.cjs +35 -0
- package/dist/cjs/decorator/index.cjs +360 -0
- package/dist/cjs/enum/index.cjs +20 -0
- package/dist/cjs/interface/index.cjs +2 -0
- package/dist/esm/bin/generator.js +92 -0
- package/dist/esm/bin/helper/controller-generator.helper.js +105 -0
- package/dist/esm/bin/helper/guard-generator.helper.js +33 -0
- package/dist/esm/bin/helper/service-generator.helper.js +33 -0
- package/dist/esm/bin/meocord.js +333 -0
- package/dist/esm/common/index.js +2 -0
- package/dist/esm/common/logger.js +72 -0
- package/dist/{core/index.d.ts → esm/common/theme.js} +8 -2
- package/dist/esm/core/index.js +1 -0
- package/dist/esm/core/meocord-factory.js +28 -0
- package/dist/esm/core/meocord.app.js +267 -0
- package/dist/esm/decorator/app.decorator.js +99 -0
- package/dist/esm/decorator/command-builder.decorator.js +32 -0
- package/dist/esm/decorator/container.js +6 -0
- package/dist/esm/decorator/controller.decorator.js +218 -0
- package/dist/esm/decorator/guard.decorator.js +165 -0
- package/dist/esm/decorator/index.js +6 -0
- package/dist/esm/decorator/service.decorator.js +58 -0
- package/dist/esm/enum/controller.enum.js +43 -0
- package/dist/esm/enum/index.js +1 -0
- package/dist/esm/interface/index.js +1 -0
- package/dist/esm/package.json.js +5 -0
- package/dist/esm/util/common.util.js +68 -0
- package/dist/esm/util/embed.util.js +13 -0
- package/dist/esm/util/generator-cli.util.js +107 -0
- package/dist/{util → esm/util}/json.util.js +10 -6
- package/dist/esm/util/meocord-cli.util.js +172 -0
- package/dist/esm/util/meocord-config-loader.util.js +48 -0
- package/dist/esm/util/tsconfig.util.js +83 -0
- package/dist/{util → esm/util}/wait.util.js +5 -1
- package/dist/{common/logger.d.ts → types/common/index.d.ts} +30 -1
- package/dist/{core/meocord.app.d.ts → types/core/index.d.ts} +30 -2
- package/dist/types/decorator/index.d.ts +425 -0
- package/dist/types/enum/index.d.ts +18 -0
- package/dist/{interface → types/interface}/index.d.ts +11 -7
- package/package.json +64 -48
- package/webpack.config.js +2 -2
- package/dist/bin/generator.d.ts +0 -29
- package/dist/bin/generator.js +0 -17
- package/dist/bin/helper/controller-generator.helper.d.ts +0 -67
- package/dist/bin/helper/controller-generator.helper.js +0 -50
- package/dist/bin/helper/guard-generator.helper.d.ts +0 -32
- package/dist/bin/helper/guard-generator.helper.js +0 -25
- package/dist/bin/helper/service-generator.helper.d.ts +0 -32
- package/dist/bin/helper/service-generator.helper.js +0 -25
- package/dist/bin/meocord.d.ts +0 -19
- package/dist/bin/meocord.js +0 -34
- package/dist/common/index.d.ts +0 -19
- package/dist/common/index.js +0 -17
- package/dist/common/logger.js +0 -17
- package/dist/common/theme.d.ts +0 -24
- package/dist/common/theme.js +0 -17
- package/dist/core/index.js +0 -17
- package/dist/core/meocord-factory.d.ts +0 -24
- package/dist/core/meocord-factory.js +0 -17
- package/dist/core/meocord.app.js +0 -17
- package/dist/decorator/app.decorator.d.ts +0 -59
- package/dist/decorator/app.decorator.js +0 -61
- package/dist/decorator/command-builder.decorator.d.ts +0 -39
- package/dist/decorator/command-builder.decorator.js +0 -35
- package/dist/decorator/container.d.ts +0 -20
- package/dist/decorator/container.js +0 -17
- package/dist/decorator/controller.decorator.d.ts +0 -125
- package/dist/decorator/controller.decorator.js +0 -113
- package/dist/decorator/guard.decorator.d.ts +0 -101
- package/dist/decorator/guard.decorator.js +0 -94
- package/dist/decorator/index.d.ts +0 -23
- package/dist/decorator/index.js +0 -17
- package/dist/decorator/service.decorator.d.ts +0 -36
- package/dist/decorator/service.decorator.js +0 -36
- package/dist/enum/controller.enum.d.ts +0 -42
- package/dist/enum/controller.enum.js +0 -19
- package/dist/enum/index.d.ts +0 -18
- package/dist/enum/index.js +0 -17
- package/dist/interface/command-decorator.interface.d.ts +0 -43
- package/dist/interface/command-decorator.interface.js +0 -1
- package/dist/interface/index.js +0 -1
- package/dist/util/common.util.d.ts +0 -40
- package/dist/util/common.util.js +0 -38
- package/dist/util/embed.util.d.ts +0 -19
- package/dist/util/embed.util.js +0 -17
- package/dist/util/generator-cli.util.d.ts +0 -65
- package/dist/util/generator-cli.util.js +0 -49
- package/dist/util/index.d.ts +0 -18
- package/dist/util/index.js +0 -17
- package/dist/util/json.util.d.ts +0 -27
- package/dist/util/meocord-cli.util.d.ts +0 -62
- package/dist/util/meocord-cli.util.js +0 -50
- package/dist/util/meocord-config-loader.util.d.ts +0 -32
- package/dist/util/meocord-config-loader.util.js +0 -34
- package/dist/util/tsconfig.util.d.ts +0 -29
- package/dist/util/tsconfig.util.js +0 -32
- package/dist/util/wait.util.d.ts +0 -18
- /package/dist/{bin → esm/bin}/builder-template/builder/context-menu.builder.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/builder/slash.builder.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/button.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/context-menu.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/message.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/modal-submit.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/reaction.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/select-menu.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/controller/slash.controller.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/guard.template +0 -0
- /package/dist/{bin → esm/bin}/builder-template/service.template +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { SlashCommandBuilder, MessageFlagsBitField } from 'discord.js';
|
|
2
|
+
import { Logger } from '../common/logger.js';
|
|
3
|
+
import '../common/theme.js';
|
|
4
|
+
import { getCommandMap, getMessageHandlers, getReactionHandlers } from '../decorator/controller.decorator.js';
|
|
5
|
+
import { mainContainer } from '../decorator/container.js';
|
|
6
|
+
import { sample } from 'lodash-es';
|
|
7
|
+
import { createErrorEmbed } from '../util/embed.util.js';
|
|
8
|
+
import { ReactionHandlerAction, CommandType } from '../enum/controller.enum.js';
|
|
9
|
+
|
|
10
|
+
class MeoCordApp {
|
|
11
|
+
async start() {
|
|
12
|
+
try {
|
|
13
|
+
this.logger.log('Starting bot...');
|
|
14
|
+
this.bot.on('clientReady', async ()=>{
|
|
15
|
+
this.activityInterval = setInterval(()=>{
|
|
16
|
+
this.bot.user?.setActivity(sample(this.activities));
|
|
17
|
+
}, 10000);
|
|
18
|
+
await this.registerCommands();
|
|
19
|
+
});
|
|
20
|
+
this.bot.on('interactionCreate', async (interaction)=>{
|
|
21
|
+
await this.handleInteraction(interaction);
|
|
22
|
+
});
|
|
23
|
+
this.bot.on('messageCreate', async (message)=>{
|
|
24
|
+
await this.handleMessage(message);
|
|
25
|
+
});
|
|
26
|
+
this.bot.on('messageReactionAdd', async (reaction, user)=>{
|
|
27
|
+
await this.handleReaction(reaction, {
|
|
28
|
+
user,
|
|
29
|
+
action: ReactionHandlerAction.ADD
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
this.bot.on('messageReactionRemove', async (reaction, user)=>{
|
|
33
|
+
await this.handleReaction(reaction, {
|
|
34
|
+
user,
|
|
35
|
+
action: ReactionHandlerAction.REMOVE
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
await this.bot.login(this.discordToken);
|
|
39
|
+
this.logger.log('Bot is online!');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this.logger.error('Error during bot startup:', error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async registerCommands() {
|
|
45
|
+
const builders = [];
|
|
46
|
+
for (const controller of this.controllers){
|
|
47
|
+
const commandMap = getCommandMap(controller);
|
|
48
|
+
for(const commandName in commandMap){
|
|
49
|
+
const commandMetadataArray = commandMap[commandName];
|
|
50
|
+
if (!Array.isArray(commandMetadataArray)) continue;
|
|
51
|
+
for (const { builder, type } of commandMetadataArray){
|
|
52
|
+
if (type in CommandType && builder) {
|
|
53
|
+
builders.push(builder);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
if (this.bot.application) {
|
|
60
|
+
await this.bot.application.commands.set(builders);
|
|
61
|
+
this.logger.log(`Registered ${builders.length} bot commands:`, builders.map((builder)=>{
|
|
62
|
+
const json = typeof builder.toJSON === 'function' ? builder.toJSON() : builder;
|
|
63
|
+
const typeName = json?.type === 1 ? 'SlashCommand' : json?.type === 2 ? 'UserContextMenu' : json?.type === 3 ? 'MessageContextMenu' : builder instanceof SlashCommandBuilder ? 'SlashCommand' : 'Command';
|
|
64
|
+
const subCommands = Array.isArray(json?.options) && json.options.length ? json.options.map((opt)=>({
|
|
65
|
+
name: opt.name,
|
|
66
|
+
options: opt.options.map((opt)=>opt.name)
|
|
67
|
+
})) : undefined;
|
|
68
|
+
const name = json?.name || builder.name;
|
|
69
|
+
return subCommands ? {
|
|
70
|
+
type: typeName,
|
|
71
|
+
name,
|
|
72
|
+
subCommands
|
|
73
|
+
} : {
|
|
74
|
+
type: typeName,
|
|
75
|
+
name
|
|
76
|
+
};
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.logger.error('Error during command registration:', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async handleInteraction(interaction) {
|
|
84
|
+
for (const controller of this.controllers){
|
|
85
|
+
let controllerInstance = this.controllerInstancesCache.get(controller);
|
|
86
|
+
if (!controllerInstance) {
|
|
87
|
+
controllerInstance = mainContainer.get(controller.constructor);
|
|
88
|
+
this.controllerInstancesCache.set(controller, controllerInstance);
|
|
89
|
+
}
|
|
90
|
+
const commandMap = getCommandMap(controllerInstance);
|
|
91
|
+
if (!commandMap) continue;
|
|
92
|
+
let commandMetadataArray = undefined;
|
|
93
|
+
let commandIdentifier = undefined;
|
|
94
|
+
if (interaction.isChatInputCommand() || interaction.isContextMenuCommand()) {
|
|
95
|
+
commandIdentifier = interaction.commandName;
|
|
96
|
+
commandMetadataArray = commandMap[commandIdentifier];
|
|
97
|
+
} else if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
|
|
98
|
+
commandIdentifier = interaction.customId;
|
|
99
|
+
const foundEntry = Object.entries(commandMap).find(([commandName, metaArray])=>{
|
|
100
|
+
if (!Array.isArray(metaArray)) return false;
|
|
101
|
+
return metaArray.some((meta)=>{
|
|
102
|
+
if (!meta.regex || !commandIdentifier) return false;
|
|
103
|
+
const match = meta.regex.exec(commandIdentifier);
|
|
104
|
+
if (match?.groups) {
|
|
105
|
+
interaction.dynamicParams = match.groups;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return commandIdentifier === commandName;
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
if (foundEntry) {
|
|
112
|
+
commandMetadataArray = foundEntry[1];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (commandMetadataArray && commandMetadataArray.length > 0) {
|
|
116
|
+
const commandMetadata = commandMetadataArray[0];
|
|
117
|
+
const { methodName, type } = commandMetadata;
|
|
118
|
+
try {
|
|
119
|
+
if (type === CommandType.SLASH && interaction.isChatInputCommand() || type === CommandType.BUTTON && interaction.isButton() || type === CommandType.SELECT_MENU && interaction.isStringSelectMenu() || type === CommandType.CONTEXT_MENU && interaction.isUserContextMenuCommand() || type === CommandType.CONTEXT_MENU && interaction.isMessageContextMenuCommand() || type === CommandType.MODAL_SUBMIT && interaction.isModalSubmit()) {
|
|
120
|
+
this.logger.log('[INTERACTION]', `[${CommandType[type]}]`, `[${methodName}]`);
|
|
121
|
+
let dynamicParams = {};
|
|
122
|
+
if (interaction.isChatInputCommand() && interaction.options) {
|
|
123
|
+
dynamicParams = interaction.options.data.reduce((acc, opt)=>{
|
|
124
|
+
acc[opt.name] = opt.value;
|
|
125
|
+
return acc;
|
|
126
|
+
}, {});
|
|
127
|
+
} else if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isModalSubmit()) {
|
|
128
|
+
dynamicParams = interaction.dynamicParams || {};
|
|
129
|
+
}
|
|
130
|
+
await controllerInstance[methodName](interaction, dynamicParams);
|
|
131
|
+
return;
|
|
132
|
+
} else {
|
|
133
|
+
this.logger.debug(type, methodName, CommandType.BUTTON, interaction.isButton());
|
|
134
|
+
this.logger.warn(`Interaction type mismatch for command "${commandIdentifier}". Interaction type: ${interaction.type}.`);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
this.logger.error(`Error executing command "${commandIdentifier}":`, error);
|
|
138
|
+
if (interaction.isRepliable()) {
|
|
139
|
+
const embed = createErrorEmbed('An error occurred while executing the command.');
|
|
140
|
+
await interaction.reply({
|
|
141
|
+
embeds: [
|
|
142
|
+
embed
|
|
143
|
+
],
|
|
144
|
+
flags: MessageFlagsBitField.Flags.Ephemeral
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// If no matching command is found
|
|
152
|
+
if (interaction.isRepliable()) {
|
|
153
|
+
const embed = createErrorEmbed('Command not found!');
|
|
154
|
+
await interaction.reply({
|
|
155
|
+
embeds: [
|
|
156
|
+
embed
|
|
157
|
+
],
|
|
158
|
+
flags: MessageFlagsBitField.Flags.Ephemeral
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async handleMessage(message) {
|
|
163
|
+
if (message.author.bot || !message.content?.trim()) return;
|
|
164
|
+
const messageContent = message.content.trim();
|
|
165
|
+
const relevantControllers = this.controllers.filter((controller)=>{
|
|
166
|
+
const messageHandlers = getMessageHandlers(controller);
|
|
167
|
+
return messageHandlers.some((handler)=>!handler.keyword || handler.keyword === messageContent);
|
|
168
|
+
});
|
|
169
|
+
for (const controller of relevantControllers){
|
|
170
|
+
let controllerInstance = this.controllerInstancesCache.get(controller.constructor);
|
|
171
|
+
if (!controllerInstance) {
|
|
172
|
+
const container = Reflect.getMetadata('inversify:container', controller.constructor);
|
|
173
|
+
controllerInstance = container.get(controller.constructor, {
|
|
174
|
+
autobind: true
|
|
175
|
+
});
|
|
176
|
+
this.controllerInstancesCache.set(controller.constructor, controllerInstance);
|
|
177
|
+
}
|
|
178
|
+
let messageHandlers = getMessageHandlers(controller);
|
|
179
|
+
messageHandlers = messageHandlers.sort((a, b)=>{
|
|
180
|
+
if (a.keyword && !b.keyword) return -1;
|
|
181
|
+
if (!a.keyword && b.keyword) return 1;
|
|
182
|
+
return 0;
|
|
183
|
+
});
|
|
184
|
+
for (const handler of messageHandlers){
|
|
185
|
+
const { keyword, method } = handler;
|
|
186
|
+
if (!keyword || keyword === messageContent) {
|
|
187
|
+
try {
|
|
188
|
+
await controllerInstance[method](message);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.error(`Error handling message "${messageContent}" for method "${method}":`, error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async handleReaction(reaction, { user, action }) {
|
|
197
|
+
await reaction.message.fetch();
|
|
198
|
+
const relevantControllers = this.controllers.filter((controller)=>{
|
|
199
|
+
const reactionHandlers = getReactionHandlers(controller);
|
|
200
|
+
return reactionHandlers.some((handler)=>!handler.emoji || handler.emoji === reaction.emoji.name);
|
|
201
|
+
});
|
|
202
|
+
for (const controller of relevantControllers){
|
|
203
|
+
let controllerInstance = this.controllerInstancesCache.get(controller.constructor);
|
|
204
|
+
if (!controllerInstance) {
|
|
205
|
+
const container = Reflect.getMetadata('inversify:container', controller.constructor);
|
|
206
|
+
controllerInstance = container.get(controller.constructor, {
|
|
207
|
+
autobind: true
|
|
208
|
+
});
|
|
209
|
+
this.controllerInstancesCache.set(controller.constructor, controllerInstance);
|
|
210
|
+
}
|
|
211
|
+
let reactionHandlers = getReactionHandlers(controller);
|
|
212
|
+
reactionHandlers = reactionHandlers.sort((a, b)=>{
|
|
213
|
+
if (a.emoji && !b.emoji) return -1;
|
|
214
|
+
if (!a.emoji && b.emoji) return 1;
|
|
215
|
+
return 0;
|
|
216
|
+
});
|
|
217
|
+
for (const handler of reactionHandlers){
|
|
218
|
+
const { emoji, method } = handler;
|
|
219
|
+
if (!emoji || emoji === reaction.emoji.name) {
|
|
220
|
+
try {
|
|
221
|
+
await controllerInstance[method](reaction, {
|
|
222
|
+
user,
|
|
223
|
+
action
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
this.logger.error(`Error handling reaction "${reaction.emoji.name}" for method "${method}":`, error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async gracefulShutdown() {
|
|
233
|
+
if (this.isShuttingDown) {
|
|
234
|
+
// Second signal received while shutting down — force exit immediately
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
if (this.bot) {
|
|
238
|
+
try {
|
|
239
|
+
this.isShuttingDown = true;
|
|
240
|
+
this.logger.log('Shutting down bot...');
|
|
241
|
+
if (this.activityInterval) clearInterval(this.activityInterval);
|
|
242
|
+
this.bot.removeAllListeners();
|
|
243
|
+
await this.bot.destroy();
|
|
244
|
+
this.logger.log('Bot has shut down');
|
|
245
|
+
process.exit(0);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.logger.error('Error during shutdown:', error);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
constructor(controllers, discordClient, discordToken, activities){
|
|
253
|
+
this.controllers = controllers;
|
|
254
|
+
this.discordClient = discordClient;
|
|
255
|
+
this.discordToken = discordToken;
|
|
256
|
+
this.activities = activities;
|
|
257
|
+
this.logger = new Logger(MeoCordApp.name);
|
|
258
|
+
this.isShuttingDown = false;
|
|
259
|
+
this.activityInterval = null;
|
|
260
|
+
this.controllerInstancesCache = new Map();
|
|
261
|
+
this.bot = this.discordClient;
|
|
262
|
+
process.on('SIGINT', ()=>this.gracefulShutdown());
|
|
263
|
+
process.on('SIGTERM', ()=>this.gracefulShutdown());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export { MeoCordApp };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { mainContainer } from './container.js';
|
|
3
|
+
import { injectable } from 'inversify';
|
|
4
|
+
import { Client } from 'discord.js';
|
|
5
|
+
import { MeoCordApp } from '../core/meocord.app.js';
|
|
6
|
+
import { loadMeoCordConfig } from '../util/meocord-config-loader.util.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Binds a class and its dependencies to the Inversify container in singleton scope.
|
|
10
|
+
*
|
|
11
|
+
* @param {Container} container - The Inversify container instance.
|
|
12
|
+
* @param {any} cls - The class to be bound to the container.
|
|
13
|
+
*/ function bindDependencies(container, cls) {
|
|
14
|
+
if (!container.isBound(cls)) {
|
|
15
|
+
container.bind(cls).toSelf().inSingletonScope();
|
|
16
|
+
const dependencies = Reflect.getMetadata('design:paramtypes', cls) || [];
|
|
17
|
+
dependencies.forEach((dep)=>bindDependencies(container, dep));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolves dependencies for a given class by binding them to the container and returning the resolved instances.
|
|
22
|
+
*
|
|
23
|
+
* @param {Container} container - The Inversify container instance.
|
|
24
|
+
* @param {any} target - The target class whose dependencies are to be resolved.
|
|
25
|
+
* @returns {any[]} - An array of resolved instances of the target's dependencies.
|
|
26
|
+
*/ function resolveDependencies(container, target) {
|
|
27
|
+
const injectables = Reflect.getMetadata('design:paramtypes', target) || [];
|
|
28
|
+
return injectables.map((dep)=>{
|
|
29
|
+
bindDependencies(container, dep);
|
|
30
|
+
return container.get(dep);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `@MeoCord()` decorator for initializing and setting up the MeoCord application.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} options - The decorator options.
|
|
37
|
+
* @param {ServiceIdentifier[]} options.controllers - The list of controllers to be registered.
|
|
38
|
+
* @param {ClientOptions} options.clientOptions - The Discord client options for initializing the bot.
|
|
39
|
+
* @param {ActivityOptions[]} [options.activities] - Optional activities for the bot.
|
|
40
|
+
* @param {ServiceIdentifier[]} [options.services] - Optional services to be registered.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* @MeoCord({
|
|
45
|
+
* controllers: [PingSlashController],
|
|
46
|
+
* clientOptions: {
|
|
47
|
+
* intents: [
|
|
48
|
+
* GatewayIntentBits.Guilds,
|
|
49
|
+
* GatewayIntentBits.GuildMembers,
|
|
50
|
+
* GatewayIntentBits.GuildMessages,
|
|
51
|
+
* GatewayIntentBits.GuildMessageReactions,
|
|
52
|
+
* GatewayIntentBits.MessageContent,
|
|
53
|
+
* ],
|
|
54
|
+
* partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
55
|
+
* },
|
|
56
|
+
* activities: [{
|
|
57
|
+
* name: `${sample(['Genshin', 'ZZZ'])} with Romeo`,
|
|
58
|
+
* type: ActivityType.Playing,
|
|
59
|
+
* url: 'https://enka.network/u/824957678/',
|
|
60
|
+
* }],
|
|
61
|
+
* services: [MyStandaloneService],
|
|
62
|
+
* })
|
|
63
|
+
* class MyApp {}
|
|
64
|
+
* ```
|
|
65
|
+
**/ function MeoCord(options) {
|
|
66
|
+
return (target)=>{
|
|
67
|
+
if (!Reflect.hasMetadata('inversify:injectable', target)) {
|
|
68
|
+
injectable()(target); // Make target injectable (inversify-specific)
|
|
69
|
+
}
|
|
70
|
+
const meocordConfig = loadMeoCordConfig();
|
|
71
|
+
if (!meocordConfig) return;
|
|
72
|
+
const discordClient = new Client(options.clientOptions);
|
|
73
|
+
mainContainer.bind(Client).toConstantValue(discordClient);
|
|
74
|
+
[
|
|
75
|
+
...options.controllers,
|
|
76
|
+
...options.services || []
|
|
77
|
+
].forEach((dep)=>{
|
|
78
|
+
bindDependencies(mainContainer, dep);
|
|
79
|
+
});
|
|
80
|
+
// Bind other static values to the container
|
|
81
|
+
mainContainer.bind(target).toConstantValue(options.clientOptions);
|
|
82
|
+
if (options.activities) {
|
|
83
|
+
mainContainer.bind(target).toConstantValue(options.activities);
|
|
84
|
+
}
|
|
85
|
+
if (options.services) {
|
|
86
|
+
mainContainer.bind(target).toConstantValue(options.services.map((s)=>mainContainer.get(s)));
|
|
87
|
+
}
|
|
88
|
+
const meocordApp = new MeoCordApp(options.controllers.map((c)=>mainContainer.get(c)), discordClient, meocordConfig.discordToken, options?.activities);
|
|
89
|
+
mainContainer.bind(MeoCordApp).toConstantValue(meocordApp);
|
|
90
|
+
// Bind the App class dynamically with resolved dependencies
|
|
91
|
+
mainContainer.bind(target).toDynamicValue(()=>{
|
|
92
|
+
const dependencies = resolveDependencies(mainContainer, target);
|
|
93
|
+
return new target(...dependencies);
|
|
94
|
+
}).inSingletonScope();
|
|
95
|
+
Reflect.defineMetadata('inversify:container', mainContainer, target);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { MeoCord };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { injectable } from 'inversify';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This decorator is used to mark a class as a Discord command builder that later can be registered on the `@Command` decorator.
|
|
6
|
+
* It defines the command type using metadata and dynamically makes the class injectable if it isn't already.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* @CommandBuilder(CommandType.SLASH)
|
|
11
|
+
* export class MySlashCommand implements CommandBuilderBase {
|
|
12
|
+
* build(commandName: string): SlashCommandBuilder {
|
|
13
|
+
* return new SlashCommandBuilder().setName(commandName).setDescription('A sample slash command')
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*```
|
|
17
|
+
*
|
|
18
|
+
* @param commandType - The type of the command, specified from the `CommandType` enum.
|
|
19
|
+
* @returns A decorator function that makes the target class injectable
|
|
20
|
+
* and assigns the `commandType` metadata.
|
|
21
|
+
*/ function CommandBuilder(commandType) {
|
|
22
|
+
return function(target) {
|
|
23
|
+
// Check if the class is already injectable; if not, make it injectable dynamically
|
|
24
|
+
if (!Reflect.hasMetadata('inversify:injectable', target)) {
|
|
25
|
+
injectable()(target);
|
|
26
|
+
}
|
|
27
|
+
// Define the command type metadata for the target class
|
|
28
|
+
Reflect.defineMetadata('commandType', commandType, target);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { CommandBuilder };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { injectable } from 'inversify';
|
|
3
|
+
import { mainContainer } from './container.js';
|
|
4
|
+
import { ButtonInteraction, StringSelectMenuInteraction, ChatInputCommandInteraction, ContextMenuCommandInteraction, ModalSubmitInteraction } from 'discord.js';
|
|
5
|
+
import { CommandType } from '../enum/controller.enum.js';
|
|
6
|
+
|
|
7
|
+
const COMMAND_METADATA_KEY = Symbol('commands');
|
|
8
|
+
const MESSAGE_HANDLER_METADATA_KEY = Symbol('message_handlers');
|
|
9
|
+
const REACTION_HANDLER_METADATA_KEY = Symbol('reaction_handlers');
|
|
10
|
+
/**
|
|
11
|
+
* Decorator to register message handlers in the controller.
|
|
12
|
+
*
|
|
13
|
+
* @param keyword - An optional keyword to filter messages this handler should respond to.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* @MessageHandler('hello')
|
|
18
|
+
* async handleHelloMessage(message: Message) {
|
|
19
|
+
* await message.reply('Hello! How can I help you?');
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @MessageHandler()
|
|
23
|
+
* async handleAnyMessage(message: Message) {
|
|
24
|
+
* console.log(`Received a message: ${message.content}`);
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/ function MessageHandler(keyword) {
|
|
28
|
+
return function(target, propertyKey, _descriptor) {
|
|
29
|
+
const handlers = Reflect.getMetadata(MESSAGE_HANDLER_METADATA_KEY, target) || [];
|
|
30
|
+
handlers.push({
|
|
31
|
+
keyword,
|
|
32
|
+
method: propertyKey.toString()
|
|
33
|
+
});
|
|
34
|
+
Reflect.defineMetadata(MESSAGE_HANDLER_METADATA_KEY, handlers, target);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Decorator to register reaction handlers in the controller.
|
|
39
|
+
*
|
|
40
|
+
* @param emoji - Optional emoji name to filter reactions this handler should respond to.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* @ReactionHandler('👍')
|
|
45
|
+
* async handleThumbsUpReaction(reaction: MessageReaction, { user }: ReactionHandlerOptions) {
|
|
46
|
+
* console.log(`User ${user.username} reacted with 👍`);
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* @ReactionHandler()
|
|
50
|
+
* async handleAnyReaction(reaction: MessageReaction, { user }: ReactionHandlerOptions) {
|
|
51
|
+
* console.log(`User ${user.username} reacted with ${reaction.emoji.name}`);
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/ function ReactionHandler(emoji) {
|
|
55
|
+
return function(target, propertyKey, _descriptor) {
|
|
56
|
+
const handlers = Reflect.getMetadata(REACTION_HANDLER_METADATA_KEY, target) || [];
|
|
57
|
+
handlers.push({
|
|
58
|
+
emoji,
|
|
59
|
+
method: propertyKey.toString()
|
|
60
|
+
});
|
|
61
|
+
Reflect.defineMetadata(REACTION_HANDLER_METADATA_KEY, handlers, target);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Retrieves reaction handlers metadata from a given controller.
|
|
66
|
+
*
|
|
67
|
+
* @param controller - The controller class instance.
|
|
68
|
+
* @returns An array of reaction handler metadata objects.
|
|
69
|
+
*/ function getReactionHandlers(controller) {
|
|
70
|
+
return Reflect.getMetadata(REACTION_HANDLER_METADATA_KEY, controller) || [];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves message handlers metadata from a given controller.
|
|
74
|
+
*
|
|
75
|
+
* @param controller - The controller class instance.
|
|
76
|
+
* @returns An array of message handler method names.
|
|
77
|
+
*/ function getMessageHandlers(controller) {
|
|
78
|
+
return Reflect.getMetadata(MESSAGE_HANDLER_METADATA_KEY, controller) || [];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Helper function to create regex and parameter mappings from a pattern string.
|
|
82
|
+
*
|
|
83
|
+
* @param pattern - The pattern string to parse.
|
|
84
|
+
* @returns An object containing the generated regex and parameter names.
|
|
85
|
+
*/ function createRegexFromPattern(pattern) {
|
|
86
|
+
const params = [];
|
|
87
|
+
// Escape special characters except for {} and -
|
|
88
|
+
const escapedPattern = pattern.replace(/[/\\^$*+?.()|[\]]/g, '\\$&') // Removed hyphen `-` from this list
|
|
89
|
+
;
|
|
90
|
+
// Replace placeholders with named capturing groups
|
|
91
|
+
const regexPattern = escapedPattern.replace(/\{(\w+)}/g, (_, param)=>{
|
|
92
|
+
if (!/^\w+$/.test(param)) {
|
|
93
|
+
throw new Error(`Invalid parameter name: ${param}. Parameter names must be alphanumeric.`);
|
|
94
|
+
}
|
|
95
|
+
params.push(param);
|
|
96
|
+
return `(?<${param}>[a-zA-Z0-9]+)`;
|
|
97
|
+
});
|
|
98
|
+
// Construct the final regex
|
|
99
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
100
|
+
return {
|
|
101
|
+
regex,
|
|
102
|
+
params
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Decorator to register command methods in a controller.
|
|
107
|
+
*
|
|
108
|
+
* @param commandName - The name or pattern of the command.
|
|
109
|
+
* @param builderOrType - A command builder class or a command type from `CommandType`.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* @Command('help', CommandType.SLASH)
|
|
114
|
+
* public async handleHelp(interaction: ChatInputCommandInteraction) {
|
|
115
|
+
* await interaction.reply('This is the help command!')
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* @Command('stats-{id}', CommandType.BUTTON)
|
|
119
|
+
* public async handleStats(message: ButtonInteraction, { id }) {
|
|
120
|
+
* await message.reply(`Fetching stats for ID: ${id}`);
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*/ function Command(commandName, builderOrType) {
|
|
124
|
+
return function(target, propertyKey, _descriptor) {
|
|
125
|
+
const originalMethod = _descriptor.value;
|
|
126
|
+
if (!originalMethod) {
|
|
127
|
+
throw new Error(`Missing implementation for method ${propertyKey}`);
|
|
128
|
+
}
|
|
129
|
+
// Wrap original method for interaction type validation
|
|
130
|
+
_descriptor.value = function(interaction, params) {
|
|
131
|
+
const expectedInteraction = commandType === CommandType.BUTTON && interaction instanceof ButtonInteraction || commandType === CommandType.SELECT_MENU && interaction instanceof StringSelectMenuInteraction || commandType === CommandType.SLASH && interaction instanceof ChatInputCommandInteraction || commandType === CommandType.CONTEXT_MENU && interaction instanceof ContextMenuCommandInteraction || commandType === CommandType.MODAL_SUBMIT && interaction instanceof ModalSubmitInteraction;
|
|
132
|
+
if (!expectedInteraction) {
|
|
133
|
+
throw new Error(`Invalid interaction type passed to @Command for method: ${propertyKey}`);
|
|
134
|
+
}
|
|
135
|
+
return originalMethod.apply(this, [
|
|
136
|
+
interaction,
|
|
137
|
+
params
|
|
138
|
+
]);
|
|
139
|
+
};
|
|
140
|
+
// Retrieve existing metadata or initialize it
|
|
141
|
+
const commands = Reflect.getMetadata(COMMAND_METADATA_KEY, target) || {};
|
|
142
|
+
let builderInstance;
|
|
143
|
+
let commandType;
|
|
144
|
+
let regex;
|
|
145
|
+
let dynamicParams = [];
|
|
146
|
+
// Determine command type and builder
|
|
147
|
+
if (typeof builderOrType === 'function') {
|
|
148
|
+
const builderObj = new builderOrType();
|
|
149
|
+
builderInstance = builderObj.build(commandName);
|
|
150
|
+
commandType = Reflect.getMetadata('commandType', builderOrType);
|
|
151
|
+
if (!(commandType in CommandType)) {
|
|
152
|
+
throw new Error(`Metadata for 'commandType' is missing on builder ${builderOrType.name}`);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
commandType = builderOrType;
|
|
156
|
+
}
|
|
157
|
+
if (commandType !== CommandType.SLASH && commandType !== CommandType.CONTEXT_MENU) {
|
|
158
|
+
const { regex: generatedRegex, params } = createRegexFromPattern(commandName);
|
|
159
|
+
regex = generatedRegex;
|
|
160
|
+
dynamicParams = params;
|
|
161
|
+
}
|
|
162
|
+
// Ensure commandName supports multiple entries
|
|
163
|
+
if (!commands[commandName]) {
|
|
164
|
+
commands[commandName] = [];
|
|
165
|
+
}
|
|
166
|
+
commands[commandName].push({
|
|
167
|
+
methodName: propertyKey,
|
|
168
|
+
builder: builderInstance,
|
|
169
|
+
type: commandType,
|
|
170
|
+
regex,
|
|
171
|
+
dynamicParams
|
|
172
|
+
});
|
|
173
|
+
Reflect.defineMetadata(COMMAND_METADATA_KEY, commands, target);
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Retrieves the command map for a given controller.
|
|
178
|
+
*
|
|
179
|
+
* @param controller - The controller class instance.
|
|
180
|
+
* @returns A record containing command metadata indexed by command names.
|
|
181
|
+
*/ function getCommandMap(controller) {
|
|
182
|
+
return Reflect.getMetadata(COMMAND_METADATA_KEY, controller);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Decorator to mark a class as a controller that can later be registered to the App class `(app.ts)` using the `@MeoCord` decorator.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* @Controller()
|
|
190
|
+
* export class PingSlashController {
|
|
191
|
+
* constructor(private pingService: PingService) {}
|
|
192
|
+
*
|
|
193
|
+
* @Command('ping', PingCommandBuilder)
|
|
194
|
+
* async ping(interaction: ChatInputCommandInteraction) {
|
|
195
|
+
* const response = await this.pingService.handlePing()
|
|
196
|
+
* await interaction.reply(response)
|
|
197
|
+
* }
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/ function Controller() {
|
|
201
|
+
return function(target) {
|
|
202
|
+
if (!Reflect.hasMetadata('inversify:injectable', target)) {
|
|
203
|
+
injectable()(target);
|
|
204
|
+
}
|
|
205
|
+
const injectables = Reflect.getMetadata('design:paramtypes', target) || [];
|
|
206
|
+
injectables.map((dep)=>{
|
|
207
|
+
if (!mainContainer.isBound(dep)) {
|
|
208
|
+
if (!Reflect.hasMetadata('inversify:injectable', dep)) {
|
|
209
|
+
injectable()(dep);
|
|
210
|
+
}
|
|
211
|
+
mainContainer.bind(dep).toSelf().inSingletonScope();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
Reflect.defineMetadata('inversify:container', mainContainer, target);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { Command, Controller, MessageHandler, ReactionHandler, getCommandMap, getMessageHandlers, getReactionHandlers };
|