Improved plugin manager and added to command manager

This commit is contained in:
Isaac 2021-02-19 21:26:02 +00:00
parent bdcff221db
commit 67e7d36c47
No known key found for this signature in database
GPG Key ID: 279D1F53391CED07
15 changed files with 238 additions and 79 deletions

View File

@ -1,3 +1,4 @@
const { MessageEmbed } = require('discord.js');
const { const {
Command, Command,
OptionTypes OptionTypes
@ -8,7 +9,7 @@ module.exports = class NewCommand extends Command {
super(client, { super(client, {
internal: true, internal: true,
name: 'new', name: 'new',
description: 'Create a new ticket', description: 'Create a new support ticket',
options: [ options: [
// { // {
// name: 'category', // name: 'category',
@ -26,11 +27,15 @@ module.exports = class NewCommand extends Command {
}); });
} }
async execute(data) { async execute({ guild, member, channel, args}, interaction) {
console.log(data.args); console.log(args);
console.log(data.channel.name); // console.log(channel.name);
console.log(data.member.user.tag); // console.log(member.user.tag);
console.log(data.guild.name); // console.log(guild.name);
console.log(data.token);
const i18n = this.client.i18n.get(/* GET GUILD LOCALE FROM SETTINGS */);
return new MessageEmbed()
.setTitle(i18n('bot.version', require('../../package.json').version));
} }
}; };

View File

@ -146,7 +146,7 @@ new Bot();
const { version } = require('../package.json'); const { version } = require('../package.json');
process.on('unhandledRejection', error => { process.on('unhandledRejection', error => {
log.notice('PLEASE INCLUDE THIS INFORMATION IF YOU ASK FOR HELP ABOUT THE FOLLOWING ERROR:'); log.notice('PLEASE INCLUDE THIS INFORMATION IF YOU ASK FOR HELP ABOUT THE FOLLOWING ERROR:');
log.warn(`Discord Tickets v${version}, Node v${process.versions.node} on ${process.platform}`); log.notice(`Discord Tickets v${version}, Node v${process.versions.node} on ${process.platform}`);
log.warn('An error was not caught'); log.warn('An error was not caught');
if (error instanceof Error) log.warn(`Uncaught ${error.name}: ${error}`); if (error instanceof Error) log.warn(`Uncaught ${error.name}: ${error}`);
log.error(error); log.error(error);

7
src/listeners/debug.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
event: 'debug',
execute: (client, data) => {
if (client.config.debug)
client.log.debug(data);
}
};

View File

@ -3,25 +3,22 @@ module.exports = {
raw: true, raw: true,
execute: async (client, interaction) => { execute: async (client, interaction) => {
if (interaction.type !== 2) return; if (interaction.type === 1) {
client.log.debug('Received interaction ping, responding with pong');
return await client.api.interactions(interaction.id, interaction.token).callback.post({
data: {
type: 1,
}
});
}
const cmd = interaction.data.name; const cmd = interaction.data.name;
if (!client.commands.commands.has(cmd)) if (!client.commands.commands.has(cmd))
return client.log.warn(`Received "${cmd}" command invocation, but the command manager does not have a "${cmd}" command`); return client.log.warn(`[COMMANDS] Received "${cmd}" command invocation, but the command manager does not have a "${cmd}" command`);
let data = {
args: interaction.data.options,
channel: await client.channels.fetch(interaction.channel_id),
guild: await client.guilds.fetch(interaction.guild_id),
token: interaction.token
};
data.member = await data.guild.members.fetch(interaction.member.user.id);
try { try {
client.log.commands(`Executing ${cmd} command (invoked by ${data.member.user.username.tag})`); client.commands.execute(cmd, interaction);
client.commands.commands.get(cmd).execute(data);
} catch (e) { } catch (e) {
client.log.warn(`[COMMANDS] An error occurred whilst executed the ${cmd} command`); client.log.warn(`[COMMANDS] An error occurred whilst executed the ${cmd} command`);
client.log.error(e); client.log.error(e);

View File

@ -23,6 +23,8 @@ module.exports = {
client.commands.load(); // load internal commands client.commands.load(); // load internal commands
client.plugins.plugins.forEach(p => p.load()); // call load function for each plugin
if (client.config.presence.presences.length > 1) { if (client.config.presence.presences.length > 1) {
const { selectPresence } = require('../utils/discord'); const { selectPresence } = require('../utils/discord');
setInterval(() => { setInterval(() => {

View File

@ -1 +1,5 @@
{} {
"bot": {
"version": "DiscordTickets v%s by eartharoid"
}
}

View File

@ -4,6 +4,8 @@ const { Client, GuildMember, Guild, Channel } = require('discord.js');
const fs = require('fs'); const fs = require('fs');
const { join } = require('path'); const { join } = require('path');
const { path } = require('../../utils/fs'); const { path } = require('../../utils/fs');
const { createMessage, flags } = require('../../utils/discord');
const Plugin = require('../plugins/plugin');
/** /**
* A command * A command
@ -33,6 +35,8 @@ module.exports = class Command {
* @param {Object} data - Command data * @param {Object} data - Command data
* @param {string} data.name - The name of the command (3-32) * @param {string} data.name - The name of the command (3-32)
* @param {string} data.description - The description of the command (1-100) * @param {string} data.description - The description of the command (1-100)
* @param {boolean} staff_only - Only allow staff to use this command?
* @param {string[]} permissions - Array of permissions needed for a user to use this command
* @param {CommandOption[]} data.options - The command options, max of 10 * @param {CommandOption[]} data.options - The command options, max of 10
*/ */
constructor(client, data) { constructor(client, data) {
@ -59,15 +63,37 @@ module.exports = class Command {
*/ */
this.description = data.description; this.description = data.description;
/**
* Only allow staff to use this command?
* @type {boolean}
*/
this.staff_only = data.staff_only;
/**
* Array of permissions needed for a user to use this command
* @type {string[]}
*/
this.permissions = data.permissions;
/** /**
* The command options * The command options
* @type {CommandOption[]} * @type {CommandOption[]}
*/ */
this.options = data.options; this.options = data.options;
/** True if command is internal, false if it is from a plugin */ /**
* True if command is internal, false if it is from a plugin
* @type {boolean}
*/
this.internal = data.internal; this.internal = data.internal;
if (!this.internal) {
/**
* The plugin this command belongs to, if any
* @type {(undefined|Plugin)}
*/
this.plugin = this.client.plugins.plugins.find(p => p.commands?.includes(this.name));
}
this.manager.check(data); // validate this.manager.check(data); // validate
@ -92,15 +118,83 @@ module.exports = class Command {
* @property {(undefined|ApplicationCommandInteractionDataOption[])} options - Present if the option is a subcommand/subcommand group * @property {(undefined|ApplicationCommandInteractionDataOption[])} options - Present if the option is a subcommand/subcommand group
*/ */
/**
* [Interaction](https://discord.com/developers/docs/interactions/slash-commands#interaction) object
* @typedef {Object} Interaction
* @property {string} interaction.id - ID of the interaction
* @property {number} interaction.type - Type of interaction
* @property {ApplicationCommandInteractionData} interaction.data - Interaction data
* @property {Guild} interaction.guild- The guild object
* @property {Channel} interaction.channel- The channel object
* @property {GuildMember} interaction.member - The member object
* @property {string} interaction.token - The token used to respond to the interaction
*/
/** /**
* The code to be executed when a command is invoked * The code to be executed when a command is invoked
* @abstract * @abstract
* @param {Object} data - Object containing data about the command invocation * @param {Object} data - Object containing data about the command invocation
* @param {(undefined|ApplicationCommandInteractionDataOption[])} data.args - Command arguments * @param {Object} data.args - Command arguments
* @param {Channel} data.channel- The channel object * @param {Channel} data.channel- The channel object
* @param {Guild} data.guild- The guild object * @param {Guild} data.guild- The guild object
* @param {GuildMember} data.member - The member object * @param {GuildMember} data.member - The member object
* @param {string} data.token - The token used to respond to the interaction * @param {string} data.token - The token used to respond to the interaction
* @param {Interaction} interaction - Interaction object
*/ */
async execute(data) {} async execute(data, interaction) { }
/**
* Defer the response to respond later
* @param {Interaction} interaction - Interaction object
* @param {boolean} secret - Ephemeral message? **NOTE: EMBEDS AND ATTACHMENTS DO NOT RENDER IF TRUE**
*/
async deferResponse(interaction, secret) {
this.client.api.interactions(interaction.id, interaction.token).callback.post({
data: {
type: 5,
flags: flags(secret),
}
});
}
/**
* Send an interaction response
* @param {Interaction} interaction - Interaction object
* @param {*} content - Message content
* @param {boolean} secret - Ephemeral message? **NOTE: EMBEDS AND ATTACHMENTS DO NOT RENDER IF TRUE**
*/
async sendResponse(interaction, content, secret) {
if (typeof content === 'object')
this.client.api.interactions(interaction.id, interaction.token).callback.post({
data: {
type: 4,
flags: flags(secret),
data: await createMessage(this.client, interaction.channel_id, content)
}
});
else
this.client.api.interactions(interaction.id, interaction.token).callback.post({
data: {
type: 4,
flags: flags(secret),
content
}
});
}
/**
* Edit the original interaction response
* @param {Interaction} interaction - Interaction object
* @param {*} content - Message content
*/
async editResponse(interaction, content) {
if (typeof content === 'object')
this.client.api.interactions(interaction.id, interaction.token).messages.patch({
embeds: content
});
else
this.client.api.interactions(interaction.id, interaction.token).messages.patch({
content
});
}
}; };

View File

@ -11,4 +11,9 @@ module.exports = {
CHANNEL: 7, CHANNEL: 7,
ROLE: 8, ROLE: 8,
}, },
ResponseTypes: {
Pong: 1,
ChannelMessageWithSource: 4,
DeferredChannelMessageWithSource: 5,
},
}; };

View File

@ -40,8 +40,9 @@ module.exports = class CommandManager {
/** Register a command */ /** Register a command */
register(cmd) { register(cmd) {
const is_internal = (this.commands.has(cmd.name) && cmd.internal) const exists = this.commands.has(cmd.name);
|| (this.commands.has(cmd.name) && this.commands.get(cmd.name).internal); const is_internal = (exists && cmd.internal)
|| (exists && this.commands.get(cmd.name).internal);
if (is_internal) { if (is_internal) {
let plugin = this.client.plugins.plugins.find(p => p.commands.includes(cmd.name)); let plugin = this.client.plugins.plugins.find(p => p.commands.includes(cmd.name));
@ -51,7 +52,7 @@ module.exports = class CommandManager {
this.client.log.commands(`An unknown plugin has overridden the internal "${cmd.name}" command`); this.client.log.commands(`An unknown plugin has overridden the internal "${cmd.name}" command`);
if(cmd.internal) return; if(cmd.internal) return;
} }
else if (this.commands.has(cmd.name)) else if (exists)
throw new Error(`A non-internal command with the name "${cmd.name}" already exists`); throw new Error(`A non-internal command with the name "${cmd.name}" already exists`);
this.commands.set(cmd.name, cmd); this.commands.set(cmd.name, cmd);
@ -136,4 +137,46 @@ module.exports = class CommandManager {
}); });
} }
/**
* Execute a command
* @param {string} cmd_name - Name of the command
* @param {interaction} interaction - Command interaction
*/
async execute(cmd_name, interaction) {
if (!this.commands.has(cmd_name))
throw new Error(`Unregistered command: "${cmd_name}"`);
let args = {};
if (interaction.data.options)
interaction.data.options.forEach(({ name, value }) => args[name] = value);
let data = { args };
data.guild = await this.client.guilds.fetch(interaction.guild_id);
data.channel = await this.client.channels.fetch(interaction.channel_id),
data.member = await data.guild.members.fetch(interaction.member.user.id);
const cmd = this.commands.get(cmd_name);
// if (cmd.staff_only) {
// return await cmd.sendResponse(interaction, msg, true);
// }
const no_perm = cmd.permissions instanceof Array
&& !data.member.hasPermission(cmd.permissions);
if (no_perm) {
let perms = cmd.permissions.map(p => `\`${p}\``).join(', ');
let msg = `❌ You do not have the permissions required to use this command:\n${perms}`;
return await cmd.sendResponse(interaction, msg, true);
}
await cmd.deferResponse(interaction, true);
this.client.log.commands(`Executing "${cmd_name}" command (invoked by ${data.member.user.tag})`);
let res = await cmd.execute(data, interaction); // run the command
if (typeof res === 'object' || typeof res === 'string')
cmd.sendResponse(interaction, res, res.secret);
}
}; };

View File

@ -62,15 +62,10 @@ module.exports = class PluginManager {
description description
}; };
this.plugins.set(id, about);
try { try {
let plugin = new Main(this.client, id); let plugin = new Main(this.client, about);
plugin.load(); this.plugins.set(id, plugin);
plugin.preload();
this.plugins.set(id, Object.assign(about, {
name: plugin.name || id,
}));
} catch (e) { } catch (e) {
if (npm) { if (npm) {

View File

@ -17,7 +17,7 @@ module.exports = class Plugin {
* @param {String} options.name A human-friendly name (can be different to the name in package.json) * @param {String} options.name A human-friendly name (can be different to the name in package.json)
* @param {String[]} options.commands An array of command names the plugin registers * @param {String[]} options.commands An array of command names the plugin registers
*/ */
constructor(client, id, options = {}) { constructor(client, about, options = {}) {
/** The Discord Client */ /** The Discord Client */
this.client = client; this.client = client;
@ -28,10 +28,11 @@ module.exports = class Plugin {
// make JSDoc happy // make JSDoc happy
let { let {
id,
version, version,
author, author,
description description
} = this.manager.plugins.get(id); } = about;
/** /**
* The human-friendly name of the plugin * The human-friendly name of the plugin
@ -125,7 +126,15 @@ module.exports = class Plugin {
} }
/** /**
* The main function where your code should go. Create functions and event listeners here * The function where any code that needs to be executed before the client is ready should go.
* **This is executed _BEFORE_ the ready event**
* @abstract
*/
preload() { }
/**
* The main function where your code should go. Create commands and event listeners here.
* **This is executed _after_ the ready event**
* @abstract * @abstract
*/ */
load() {} load() {}

View File

@ -0,0 +1,3 @@
module.exports = {
};

View File

@ -1,25 +0,0 @@
const { Structures } = require('discord.js');
Structures.extend('TextChannel', TextChannel => {
return class extends TextChannel {
constructor(client, data) {
super(client, data);
}
get isTicket() {
return !!this.client.db.Ticket.findOne({
where: {
id: this.id
}
});
}
get ticket() {
return new class {
constructor(channel) {
this.channel = channel;
}
}(this);
}
};
});

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line no-unused-vars const Discord = require('discord.js');
const { PresenceData } = require('discord.js');
const config = require('../../user/config'); const config = require('../../user/config');
@ -7,10 +6,30 @@ let current_presence = -1;
module.exports = { module.exports = {
/** /**
* Select a presence from the config * Resolves data and files so embeds can be sent as a response to a slash command
* @returns {PresenceData} * @param {Discord.Client} channel_id - Text channel ID
* @param {string} channel_id - Text channel ID
* @param {*} content - Message content
* @returns {Object}
*/ */
selectPresence() { createMessage: async (client, channel_id, content) => {
let msg = await Discord.APIMessage.create(client.channels.resolve(channel_id), content)
.resolveData()
.resolveFiles();
return { ...msg.data, files: msg.files };
},
/**
* Generate flags
* @param {boolean} secret - Ephemeral message?
*/
flags: (secret) => secret ? 1 << 64 : undefined,
/**
* Select a presence from the config
* @returns {Discord.PresenceData}
*/
selectPresence: () => {
let length = config.presence.presences.length; let length = config.presence.presences.length;
if (length === 0) return {}; if (length === 0) return {};
@ -41,5 +60,5 @@ module.exports = {
}, },
status status
}; };
} },
}; };

View File

@ -26,7 +26,8 @@ module.exports = {
debug: false, debug: false,
defaults: { defaults: {
colour: '#009999', colour: '#009999',
locale: 'en-GB' locale: 'en-GB',
log_messages: true, // required for transcripts/archives
}, },
logs: { logs: {
enabled: true, enabled: true,
@ -50,12 +51,12 @@ module.exports = {
activity: 'for new tickets | /help', activity: 'for new tickets | /help',
type: 'WATCHING' type: 'WATCHING'
}, },
{ /* { // an example
activity: 'Minecraft', activity: 'Minecraft',
type: 'STREAMING', type: 'STREAMING',
status: 'dnd', status: 'dnd',
url: 'https://www.twitch.tv/twitch' url: 'https://www.twitch.tv/twitch'
}, }, */
], ],
randomise: true, randomise: true,
duration: 60 duration: 60