meocord
Version:
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.
217 lines (213 loc) • 8.51 kB
JavaScript
;
require('reflect-metadata');
var inversify = require('inversify');
var discord_js = require('discord.js');
var enum_index = require('../enum/index.cjs');
var metadataKey_enum = require('./metadata-key.enum-BzzvGUId.cjs');
const COMMAND_METADATA_KEY = Symbol('commands');
const MESSAGE_HANDLER_METADATA_KEY = Symbol('message_handlers');
const REACTION_HANDLER_METADATA_KEY = Symbol('reaction_handlers');
/**
* Decorator to register message handlers in the controller.
*
* @param keyword - An optional keyword to filter messages this handler should respond to.
*
* @example
* ```typescript
* @MessageHandler('hello')
* async handleHelloMessage(message: Message) {
* await message.reply('Hello! How can I help you?');
* }
*
* @MessageHandler()
* async handleAnyMessage(message: Message) {
* console.log(`Received a message: ${message.content}`);
* }
* ```
*/ function MessageHandler(keyword) {
return function(target, propertyKey, _descriptor) {
const handlers = Reflect.getMetadata(MESSAGE_HANDLER_METADATA_KEY, target) || [];
handlers.push({
keyword,
method: propertyKey.toString()
});
Reflect.defineMetadata(MESSAGE_HANDLER_METADATA_KEY, handlers, target);
};
}
/**
* Decorator to register reaction handlers in the controller.
*
* @param emoji - Optional emoji name to filter reactions this handler should respond to.
*
* @example
* ```typescript
* @ReactionHandler('👍')
* async handleThumbsUpReaction(reaction: MessageReaction, { user }: ReactionHandlerOptions) {
* console.log(`User ${user.username} reacted with 👍`);
* }
*
* @ReactionHandler()
* async handleAnyReaction(reaction: MessageReaction, { user }: ReactionHandlerOptions) {
* console.log(`User ${user.username} reacted with ${reaction.emoji.name}`);
* }
* ```
*/ function ReactionHandler(emoji) {
return function(target, propertyKey, _descriptor) {
const handlers = Reflect.getMetadata(REACTION_HANDLER_METADATA_KEY, target) || [];
handlers.push({
emoji,
method: propertyKey.toString()
});
Reflect.defineMetadata(REACTION_HANDLER_METADATA_KEY, handlers, target);
};
}
/**
* Retrieves reaction handlers metadata from a given controller.
*
* @param controller - The controller class instance.
* @returns An array of reaction handler metadata objects.
*/ function getReactionHandlers(controller) {
return Reflect.getMetadata(REACTION_HANDLER_METADATA_KEY, controller) || [];
}
/**
* Retrieves message handlers metadata from a given controller.
*
* @param controller - The controller class instance.
* @returns An array of message handler method names.
*/ function getMessageHandlers(controller) {
return Reflect.getMetadata(MESSAGE_HANDLER_METADATA_KEY, controller) || [];
}
/**
* Helper function to create regex and parameter mappings from a pattern string.
*
* @param pattern - The pattern string to parse.
* @returns An object containing the generated regex and parameter names.
*/ function createRegexFromPattern(pattern) {
const params = [];
// Escape special characters except for {} and -
const escapedPattern = pattern.replace(/[/\\^$*+?.()|[\]]/g, '\\$&') // Removed hyphen `-` from this list
;
// Replace placeholders with named capturing groups
const regexPattern = escapedPattern.replace(/\{(\w+)}/g, (_, param)=>{
if (!/^\w+$/.test(param)) {
throw new Error(`Invalid parameter name: ${param}. Parameter names must be alphanumeric.`);
}
params.push(param);
return `(?<${param}>[a-zA-Z0-9]+)`;
});
// Construct the final regex
const regex = new RegExp(`^${regexPattern}$`);
return {
regex,
params
};
}
/**
* Decorator to register command methods in a controller.
*
* @param commandName - The name or pattern of the command.
* @param builderOrType - A command builder class or a command type from `CommandType`.
*
* @example
* ```typescript
* @Command('help', CommandType.SLASH)
* public async handleHelp(interaction: ChatInputCommandInteraction) {
* await interaction.reply('This is the help command!')
* }
*
* @Command('stats-{id}', CommandType.BUTTON)
* public async handleStats(message: ButtonInteraction, { id }) {
* await message.reply(`Fetching stats for ID: ${id}`);
* }
* ```
*/ function Command(commandName, builderOrType) {
return function(target, propertyKey, _descriptor) {
const originalMethod = _descriptor.value;
if (!originalMethod) {
throw new Error(`Missing implementation for method ${propertyKey}`);
}
// Wrap original method for interaction type validation
_descriptor.value = function(interaction, params) {
const expectedInteraction = commandType === enum_index.CommandType.BUTTON && interaction instanceof discord_js.ButtonInteraction || commandType === enum_index.CommandType.SELECT_MENU && interaction instanceof discord_js.StringSelectMenuInteraction || commandType === enum_index.CommandType.SLASH && interaction instanceof discord_js.ChatInputCommandInteraction || commandType === enum_index.CommandType.CONTEXT_MENU && interaction instanceof discord_js.ContextMenuCommandInteraction || commandType === enum_index.CommandType.MODAL_SUBMIT && interaction instanceof discord_js.ModalSubmitInteraction;
if (!expectedInteraction) {
throw new Error(`Invalid interaction type passed to @Command for method: ${propertyKey}`);
}
return originalMethod.apply(this, [
interaction,
params
]);
};
// Retrieve existing metadata or initialize it
const commands = Reflect.getMetadata(COMMAND_METADATA_KEY, target) || {};
let builderInstance;
let commandType;
let regex;
let dynamicParams = [];
// Determine command type and builder
if (typeof builderOrType === 'function') {
const builderObj = new builderOrType();
builderInstance = builderObj.build(commandName);
commandType = Reflect.getMetadata(metadataKey_enum.MetadataKey.CommandType, builderOrType);
if (!(commandType in enum_index.CommandType)) {
throw new Error(`Metadata for 'commandType' is missing on builder ${builderOrType.name}`);
}
} else {
commandType = builderOrType;
}
if (commandType !== enum_index.CommandType.SLASH && commandType !== enum_index.CommandType.CONTEXT_MENU) {
const { regex: generatedRegex, params } = createRegexFromPattern(commandName);
regex = generatedRegex;
dynamicParams = params;
}
// Ensure commandName supports multiple entries
if (!commands[commandName]) {
commands[commandName] = [];
}
commands[commandName].push({
methodName: propertyKey,
builder: builderInstance,
type: commandType,
regex,
dynamicParams
});
Reflect.defineMetadata(COMMAND_METADATA_KEY, commands, target);
};
}
/**
* Retrieves the command map for a given controller.
*
* @param controller - The controller class instance.
* @returns A record containing command metadata indexed by command names.
*/ function getCommandMap(controller) {
return Reflect.getMetadata(COMMAND_METADATA_KEY, controller);
}
/**
* Decorator to mark a class as a controller that can later be registered to the App class `(app.ts)` using the `@MeoCord` decorator.
*
* @example
* ```typescript
* @Controller()
* export class PingSlashController {
* constructor(private pingService: PingService) {}
*
* @Command('ping', PingCommandBuilder)
* async ping(interaction: ChatInputCommandInteraction) {
* const response = await this.pingService.handlePing()
* await interaction.reply(response)
* }
* }
* ```
*/ function Controller() {
return function(target) {
if (!Reflect.hasMetadata(metadataKey_enum.MetadataKey.Injectable, target)) {
inversify.injectable()(target);
}
};
}
exports.Command = Command;
exports.Controller = Controller;
exports.MessageHandler = MessageHandler;
exports.ReactionHandler = ReactionHandler;
exports.getCommandMap = getCommandMap;
exports.getMessageHandlers = getMessageHandlers;
exports.getReactionHandlers = getReactionHandlers;