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.
261 lines (253 loc) • 10.7 kB
JavaScript
;
require('reflect-metadata');
var inversify = require('inversify');
var metadataKey_enum = require('../_shared/metadata-key.enum-BzzvGUId.cjs');
var controller_decorator = require('../_shared/controller.decorator-DX5lFlPZ.cjs');
var discord_js = require('discord.js');
require('../enum/index.cjs');
/**
* `@Service()` decorator to mark a class as a service that can be injected into controllers or used as standalone services.
*
* @example
* ```typescript
* @Service()
* class MyService {
* constructor(private anotherService: AnotherService) {}
*
* doSomething() {
* this.anotherService.alsoDoSomething()
* console.log('Hello, World!')
* }
* }
* ```
* @returns A decorator function to apply to the class.
*/ function Service() {
return function(target) {
if (!Reflect.hasMetadata(metadataKey_enum.MetadataKey.Injectable, target)) {
inversify.injectable()(target);
}
};
}
/**
* This decorator is used to mark a class as a Discord command builder that later can be registered on the `@Command` decorator.
* It defines the command type using metadata and dynamically makes the class injectable if it isn't already.
*
* @example
* ```typescript
* @CommandBuilder(CommandType.SLASH)
* export class MySlashCommand implements CommandBuilderBase {
* build(commandName: string): SlashCommandBuilder {
* return new SlashCommandBuilder().setName(commandName).setDescription('A sample slash command')
* }
* }
*```
*
* @param commandType - The type of the command, specified from the `CommandType` enum.
* @returns A decorator function that makes the target class injectable
* and assigns the `commandType` metadata.
*/ function CommandBuilder(commandType) {
return function(target) {
// Check if the class is already injectable; if not, make it injectable dynamically
if (!Reflect.hasMetadata(metadataKey_enum.MetadataKey.Injectable, target)) {
inversify.injectable()(target);
}
// Define the command type metadata for the target class
Reflect.defineMetadata(metadataKey_enum.MetadataKey.CommandType, commandType, target);
};
}
function isValidContext(context) {
return context instanceof discord_js.BaseInteraction || context instanceof discord_js.Message || context instanceof discord_js.MessageReaction;
}
function applyGuards(descriptor, guards, propertyKey) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
const [context] = args;
if (!isValidContext(context)) {
throw new Error(`The first argument of ${String(propertyKey)} must be an instance of Interaction, Message, or MessageReaction.`);
}
const container = Reflect.getMetadata(metadataKey_enum.MetadataKey.Container, this.constructor);
for (const guard of guards){
let guardInstance;
if (isGuardWithParams(guard)) {
const { provide, params } = guard;
guardInstance = container.get(provide, {
autobind: true
});
Object.assign(guardInstance, params);
} else {
guardInstance = container.get(guard, {
autobind: true
});
}
if (!guardInstance.canActivate) {
throw new Error(`Guard ${guard.constructor.name} applied to ${String(propertyKey)} does not have a valid canActivate method.`);
}
const canActivate = await guardInstance.canActivate(...args);
if (!canActivate) {
return;
}
}
return originalMethod.apply(this, args);
};
}
/**
* `@Guard()` decorator to mark a class as a Guard that later can be added on `@UseGuard` decorator.
*
* @example
* ```typescript
* @Guard()
export class ButtonInteractionGuard implements GuardInterface {
private readonly logger = new Logger(ButtonInteractionGuard.name)
async canActivate(context: ButtonInteraction, { ownerId }: { ownerId: string }): Promise<boolean> {
if (context.user.id !== ownerId) {
this.logger.error(
`User with id ${context.user.id} is not allowed to use this command that initiated by user with id ${ownerId}.`,
)
const embed = generateErrorEmbed(
`Hi <@${context.user.id}>, this command can only be used by the person who initiated it: <@${ownerId}>.`,
)
await context.reply({
embeds: [embed],
flags: MessageFlagsBitField.Flags.Ephemeral,
})
return false
}
return true
}
}
* ```
*/ function Guard() {
return function(target) {
if (!Reflect.hasMetadata(metadataKey_enum.MetadataKey.Injectable, target)) {
inversify.injectable()(target);
}
};
}
/**
* Type guard to check if the object is a GuardWithParams.
* This function helps to check whether a guard is parameterized or not.
*
* @param guard - The guard to check.
* @returns `true` if the guard has parameters, otherwise `false`.
*/ function isGuardWithParams(guard) {
return typeof guard === 'object' && 'provide' in guard && 'params' in guard;
}
/**
* `@UseGuard()` decorator to apply one or more guards to methods.
* Guards are used to handle permission checks before executing a method.
* Each guard must use `@Guard` decorator and implement the `canActivate` method, which determines
* whether the method should be allowed to execute based on the provided context (Interaction, Message, or Reaction) and arguments.
* This decorator ensures that all guards pass validation before calling the original method.
* Supports guards that are parameterized (accepting additional parameters).
*
* @param guards - One or more guard classes to apply. These can be regular guards or guards with additional parameters.
* - If providing a guard with parameters, it should be an object with:
* - `provide`: The guard class to instantiate. Must implement `GuardInterface`.
* - `params`: A record of key-value pairs to be passed as additional properties to the guard instance.
* @returns A method decorator function that applies the guards to the method.
*
* @example
* ```typescript
* // Method-level usage
* @Command('profile-{id}', CommandType.BUTTON)
* @UseGuard(
* { provide: RateLimiterGuard, params: { limit: 2, window: 3000 } },
* ButtonInteractionGuard
* )
* async showProfileById(interaction: ButtonInteraction, { id }: { id: string }) {
* await interaction.reply(`Profile ID: ${id}`)
* }
*
* // Class-level usage
* @Controller()
* @UseGuard(GlobalGuard)
* class MyController {
* @Command('ping', CommandType.SLASH)
* async ping(interaction: ChatInputCommandInteraction) {
* await interaction.reply('Pong!')
* }
* }
* ```
*/ function UseGuard(...guards) {
return function(target, propertyKey, descriptor) {
if (descriptor && propertyKey) {
// Method Decorator
applyGuards(descriptor, guards, String(propertyKey));
Reflect.defineMetadata(metadataKey_enum.MetadataKey.Guards, guards, target, propertyKey);
} else if (typeof target === 'function' && !propertyKey && !descriptor) {
// Class Decorator
const prototype = target.prototype;
const methods = new Set();
const commandMap = controller_decorator.getCommandMap(prototype) || {};
Object.values(commandMap).flat().forEach((cmd)=>methods.add(cmd.methodName));
const messageHandlers = controller_decorator.getMessageHandlers(prototype) || [];
messageHandlers.forEach((handler)=>methods.add(handler.method));
const reactionHandlers = controller_decorator.getReactionHandlers(prototype) || [];
reactionHandlers.forEach((handler)=>methods.add(handler.method));
for (const methodName of methods){
const methodDescriptor = Object.getOwnPropertyDescriptor(prototype, methodName);
if (methodDescriptor) {
applyGuards(methodDescriptor, guards, methodName);
Object.defineProperty(prototype, methodName, methodDescriptor);
Reflect.defineMetadata(metadataKey_enum.MetadataKey.Guards, guards, prototype, methodName);
}
}
}
};
}
/**
* `@MeoCord()` decorator for declaring the MeoCord application class.
*
* This decorator stores the application options as metadata on the class.
* All DI wiring — container creation, client binding, controller/service
* registration — happens inside `MeoCordFactory.create()`, not here.
*
* @param {Object} options - The decorator options.
* @param {ServiceIdentifier[]} options.controllers - The list of controllers to be registered.
* @param {ClientOptions} options.clientOptions - The Discord client options for initializing the bot.
* @param {ActivityOptions[]} [options.activities] - Optional activities for the bot.
* @param {ServiceIdentifier[]} [options.services] - Optional services to be registered.
*
* @example
* ```typescript
* @MeoCord({
* controllers: [PingSlashController],
* clientOptions: {
* intents: [
* GatewayIntentBits.Guilds,
* GatewayIntentBits.GuildMembers,
* GatewayIntentBits.GuildMessages,
* GatewayIntentBits.GuildMessageReactions,
* GatewayIntentBits.MessageContent,
* ],
* partials: [Partials.Message, Partials.Channel, Partials.Reaction],
* },
* activities: [{
* name: `${sample(['Genshin', 'ZZZ'])} with Romeo`,
* type: ActivityType.Playing,
* url: 'https://enka.network/u/824957678/',
* }],
* services: [MyStandaloneService],
* })
* class MyApp {}
* ```
**/ function MeoCord(options) {
return (target)=>{
if (!Reflect.hasMetadata(metadataKey_enum.MetadataKey.Injectable, target)) {
inversify.injectable()(target);
}
Reflect.defineMetadata(metadataKey_enum.MetadataKey.AppOptions, options, target);
};
}
exports.Command = controller_decorator.Command;
exports.Controller = controller_decorator.Controller;
exports.MessageHandler = controller_decorator.MessageHandler;
exports.ReactionHandler = controller_decorator.ReactionHandler;
exports.getCommandMap = controller_decorator.getCommandMap;
exports.getMessageHandlers = controller_decorator.getMessageHandlers;
exports.getReactionHandlers = controller_decorator.getReactionHandlers;
exports.CommandBuilder = CommandBuilder;
exports.Guard = Guard;
exports.MeoCord = MeoCord;
exports.Service = Service;
exports.UseGuard = UseGuard;