UNPKG

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
'use strict'; 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;