@sodacore/discord
Version:
Sodacore Discord is a plugin that offers Discord SSO/OAuth2 support and the ability to create bots in a similar controller pattern.
379 lines (378 loc) • 18.1 kB
JavaScript
import { Registry } from '@sodacore/registry';
import { Utils } from '@sodacore/di';
export default class Router {
constructor(logger) {
this.logger = logger;
this.controllers = [];
}
async init() {
// Collect the controllers.
const modules = Registry.all();
for (const module of modules) {
// Validate the type and service.
const type = Utils.getMeta('type', 'autowire')(module.constructor);
const services = Utils.getMeta('services', 'controller')(module.constructor, undefined, []);
if (type !== 'controller' || !services.includes('discord'))
continue;
// Collect the commands.
const builder = Utils.getMeta('builder', 'discord')(module.constructor);
const methods = Utils.getMeta('methods', 'discord')(module, undefined, []);
this.controllers.push({
className: module.constructor.name,
name: builder?.name ?? null,
methods,
module,
});
}
console.log(this.controllers, { depth: true });
}
async onCommand(interaction) {
// Get the controller.
const command = interaction.commandName;
const controller = this.getController(command);
if (!controller)
return this.sendResponse(interaction, `No available controller found for: ${command}.`);
// Now get the method.
const method = this.getControllerMethod(controller, false);
if (!method)
return this.sendResponse(interaction, `No available handler for: ${command}.`);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return this.sendResponse(interaction, 'You are not authorized to use this command.');
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'command', methodArguments);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
return this.sendResponse(interaction, result);
}
catch (err) {
this.logger.error(`There was an error executing the command ${command}, with error: ${err.message}.`);
return this.sendResponse(interaction, false);
}
}
async onSubCommand(interaction) {
// Get the controller.
const command = interaction.commandName;
const subCommand = interaction.options.getSubcommand(false);
const controller = this.getController(command);
if (!controller || !subCommand)
return this.sendResponse(interaction, `No available controller found for: ${command}.`);
// Now get the method.
const method = this.getControllerMethod(controller, subCommand);
if (!method)
return this.sendResponse(interaction, `No available handler for: ${subCommand}.`);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return this.sendResponse(interaction, 'You are not authorized to use this command.');
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'subcommand', methodArguments);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
return this.sendResponse(interaction, result);
}
catch (err) {
this.logger.error(`There was an error executing the command ${command}, with error: ${err.message}.`);
return this.sendResponse(interaction, false);
}
}
async onButton(interaction) {
// Get the unique ID and validate it's not internal.
const uniqueId = interaction.customId;
if (uniqueId.startsWith('internal:'))
return;
// Get the controller.
const { controller, method } = this.getControllerbyUnique('button', uniqueId);
if (!controller || !method)
return this.sendResponse(interaction, `No available controller found for: ${uniqueId}.`);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return this.sendResponse(interaction, 'You are not authorized to use this command.');
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'button', methodArguments);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
return this.sendResponse(interaction, result);
}
catch (err) {
this.logger.error(`There was an error executing that action, with error: ${err.message}.`);
return this.sendResponse(interaction, false);
}
}
async onSelectMenu(interaction) {
// Get the unique ID and validate it's not internal.
const uniqueId = interaction.customId;
if (uniqueId.startsWith('internal:'))
return;
// Get the controller.
const { controller, method } = this.getControllerbyUnique('selectmenu', uniqueId);
if (!controller || !method)
return this.sendResponse(interaction, `No available controller found for: ${uniqueId}.`);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return this.sendResponse(interaction, 'You are not authorized to use this command.');
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'selectmenu', methodArguments);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
return this.sendResponse(interaction, result);
}
catch (err) {
this.logger.error(`There was an error executing that action, with error: ${err.message}.`);
return this.sendResponse(interaction, false);
}
}
async onContextMenu(interaction) {
// Get the unique ID and validate it's not internal.
const uniqueId = interaction.commandName;
if (uniqueId.startsWith('internal:'))
return;
// Get the controller.
const { controller, method } = this.getControllerbyUnique('contextmenu', uniqueId);
if (!controller || !method)
return this.sendResponse(interaction, `No available controller found for: ${uniqueId}.`);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return this.sendResponse(interaction, 'You are not authorized to use this command.');
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'contextmenu', methodArguments);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
return this.sendResponse(interaction, result);
}
catch (err) {
this.logger.error(`There was an error executing that action, with error: ${err.message}.`);
return this.sendResponse(interaction, false);
}
}
async onAutocomplete(interaction) {
// Define the params.
const commandName = interaction.commandName;
const subCommand = interaction.options.getSubcommand(false);
const { value, name } = interaction.options.getFocused();
// Let's get the controller.
const controller = this.getController(commandName);
if (!controller)
return interaction.respond([]);
// Let's get the method.
const method = this.getControllerMethodByMultiple(controller, 'autocomplete', name, subCommand ?? undefined);
if (!method)
return interaction.respond([]);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return interaction.respond([]);
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'autocomplete', methodArguments, value);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
if (['boolean', 'null', 'undefined'].includes(typeof result))
return await interaction.respond([]);
if (['string', 'number'].includes(typeof result))
return await interaction.respond([{ name: String(result), value: result }]);
if (Array.isArray(result))
return await interaction.respond(result);
if (typeof result === 'object')
return await interaction.respond([result]);
return await interaction.respond([]);
}
catch (err) {
this.logger.error(`There was an error executing that action, with error: ${err.message}.`);
return await interaction.respond([]);
}
}
async onModalSubmit(interaction) {
// Get the unique ID and validate it's not internal.
const uniqueId = interaction.customId;
if (uniqueId.startsWith('internal:'))
return;
// Get the controller.
const { controller, method } = this.getControllerbyUnique('modalsubmit', uniqueId);
if (!controller || !method)
return this.sendResponse(interaction, `No available controller found for: ${uniqueId}.`);
// Validate the authentication guards.
const status = await this.verifyAuthentication(interaction.client, method.auth, interaction.user, interaction.guildId);
if (!status)
return this.sendResponse(interaction, 'You are not authorized to use this command.');
// Define the function arguments.
const methodArguments = Utils.getMeta('args', 'discord')(controller.module, method.key, []);
const functionArguments = this.getMethodArguments(interaction, 'modalsubmit', methodArguments);
// Execute the method and catch any issues.
try {
const result = await controller.module[method.key].bind(controller.module)(...functionArguments);
return this.sendResponse(interaction, result);
}
catch (err) {
this.logger.error(`There was an error executing that action, with error: ${err.message}.`);
return this.sendResponse(interaction, false);
}
}
async onEvent(event, ...data) {
try {
for (const controller of this.controllers) {
for (const method of controller.methods) {
// Does the method match.
if (method.type !== 'event' || method.unique !== event)
continue;
// Execute the method.
await controller.module[method.key].bind(controller.module)(...data);
}
}
}
catch (err) {
this.logger.error(`There was an error executing the event ${event}, with error: ${err.message}.`);
}
}
getMethodArguments(interaction, type, args, query) {
const params = [];
args.forEach(arg => {
// Validate the argument.
if (arg.type === 'query' && type !== 'autocomplete')
throw new Error('Query arguments are only available for autocomplete methods.');
if (arg.type === 'field' && type !== 'modalsubmit')
throw new Error('Field arguments are only available for modal submit methods.');
// Get the params data.
if (arg.type === 'interaction')
params.push(interaction);
if (arg.type === 'user')
params.push(interaction.user);
if (arg.type === 'channel')
params.push(interaction.channel);
if (arg.type === 'guild')
params.push(interaction.guild);
if (arg.type === 'client')
params.push(interaction.client);
if (arg.type === 'query')
params.push(query ?? null);
if (arg.type === 'option') {
if (interaction.isChatInputCommand() && arg.name) {
const value = interaction.options.get(arg.name);
if (!value)
return params.push(null);
return params.push(arg.format ? value : value.value);
}
else {
return params.push(null);
}
}
if (arg.type === 'field' && arg.name) {
if (interaction.isModalSubmit()) {
const value = interaction.fields.getTextInputValue(arg.name);
return params.push(value);
}
else {
return params.push(null);
}
}
});
return params;
}
getController(command) {
return this.controllers.find((item) => item.name === command);
}
getControllerMethod(controller, unique, type) {
const methodType = type ?? typeof unique === 'string' ? 'subcommand' : 'command';
return controller.methods.find((item) => {
return item.unique === unique && item.type === methodType;
});
}
getControllerMethodByMultiple(controller, type, unique, subType) {
return controller.methods.find((item) => {
return item.unique === unique && item.type === type && (subType ?? item.subType === subType);
});
}
getControllerbyUnique(type, uniqueId) {
// Should return a controller and method.
let controller = null;
let method = null;
// Loop through the controllers.
for (const item of this.controllers) {
const found = item.methods.find((method) => {
return method.unique === uniqueId && method.type === type;
});
if (found) {
controller = item;
method = found;
break;
}
}
return { controller, method };
}
sendResponse(interaction, result) {
// Define the methodName.
const replyCallback = this.getReplyMethod(interaction, String(result));
// Is null.
if (result === null || result === undefined) {
return replyCallback({
content: 'That action was executed.',
embeds: [],
components: [],
});
}
// Is boolean.
if (typeof result === 'boolean') {
return replyCallback({
content: result ? '✅ That action was successful.' : '❌ Failed to complete that action.',
embeds: [],
components: [],
});
}
// Is primitive.
if (['string', 'number'].includes(typeof result)) {
return replyCallback({
content: String(result).replace('$', ''),
embeds: [],
components: [],
});
}
// Is object.
if (typeof result === 'object' && !Array.isArray(result)) {
return replyCallback(result);
}
// Is array.
if (Array.isArray(result)) {
return replyCallback({
content: result.join(', '),
embeds: [],
components: [],
});
}
}
getReplyMethod(interaction, result) {
if (interaction.replied && result.startsWith('$'))
return interaction.followUp.bind(interaction);
if (interaction.replied)
return interaction.editReply.bind(interaction);
if (interaction.deferred)
return interaction.editReply.bind(interaction);
return interaction.reply.bind(interaction);
}
async verifyAuthentication(client, auth, user, guildId) {
// Get the guild member if available.
let guildMember = null;
if (guildId) {
guildMember = await client.guilds.cache.get(guildId)?.members.fetch(user.id) ?? null;
}
// Loop the auth items and get the output.
return (await Promise.all(auth.map(async (item) => {
const member = item.wants === 'guildmember' ? guildMember ?? user : user;
return await item.callback(member);
}))).every(Boolean);
}
}