Command handler stuff, presences, other stuff

This commit is contained in:
Isaac
2021-02-18 18:41:35 +00:00
parent 7c0b1311dc
commit bdcff221db
15 changed files with 435 additions and 109 deletions

View File

@@ -1,92 +1,106 @@
/* eslint-disable no-unused-vars */
const { Client } = require('discord.js');
const { Client, GuildMember, Guild, Channel } = require('discord.js');
const fs = require('fs');
const { join } = require('path');
const { path } = require('../../utils/fs');
/** A plugin */
module.exports = class Plugin {
/**
* A command
*/
module.exports = class Command {
/**
* Create a new Plugin
* @param {Client} client The Discord Client
* @param {String} id The plugin ID
* @param {Object} options Plugin options
* @param {String} options.name A human-friendly name (can be different to the name in package.json)
* A command option choice
* @typedef CommandOptionChoice
* @property {string} name - Choice name (1-100)
* @property {(string|number)} value - choice value
*/
/**
* A command option
* @typedef CommandOption
* @property {number} type - [ApplicationCommandOptionType](https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype)
* @property {string} name - Option name (1-32)
* @property {string} description - Option description (1-100)
* @property {boolean} [required] - Required?
* @property {CommandOptionChoice[]} [choices] - Array of choices
* @property {CommandOption[]} [options] - Array of options if this option is a subcommand/subcommand group
*/
constructor(client, id, options = {}) {
/** The human-friendly name of the plugin */
this.name = options.name || id;
/**
* Create a new Command
* @param {Client} client - The Discord Client
* @param {Object} data - Command data
* @param {string} data.name - The name of the command (3-32)
* @param {string} data.description - The description of the command (1-100)
* @param {CommandOption[]} data.options - The command options, max of 10
*/
constructor(client, data) {
/** The Discord Client */
this.client = client;
/** The PluginManager */
this.manager = this.client.plugins;
/** The CommandManager */
this.manager = this.client.commands;
// Object.assign(this, this.manager.plugins.get(id));
// make JSDoc happy
if (typeof data !== 'object') {
throw new TypeError(`Expected type of data to be an object, got ${typeof data}`);
}
let {
version,
author,
description
} = this.manager.plugins.get(id);
/**
* The name of the command
* @type {string}
*/
this.name = data.name;
/** The unique ID of the plugin (NPM package name) */
this.id = id;
/**
* The command description
* @type {string}
*/
this.description = data.description;
/** The version of the plugin (NPM package version) */
this.version = version;
/**
* The command options
* @type {CommandOption[]}
*/
this.options = data.options;
/** The plugin author's name (NPM package author) */
this.author = author;
/** True if command is internal, false if it is from a plugin */
this.internal = data.internal;
/** The plugin description (NPM package description) */
this.description = description;
this.directory = {};
/** A cleaned version of the plugin's ID suitable for use in the directory name */
this.directory.name = this.id.replace(/@[-_a-zA-Z0-9]+\//, '');
/** The absolute path of the plugin directory */
this.directory.path = path(`./user/plugins/${this.directory.name}`);
}
/**
* Create the plugin directory if it doesn't already exist
* @returns {Boolean} True if created, false if it already existed
*/
createDirectory() {
if (!fs.existsSync(this.directory.path)) {
this.client.log.plugins(`Creating plugin directory for "${this.name}"`);
fs.mkdirSync(this.directory.path);
return true;
} else {
return false;
this.manager.check(data); // validate
try {
this.manager.register(this); // register the command
} catch (e) {
return this.client.log.error(e);
}
this.client.api.applications(this.client.user.id).commands.post({ data }); // post command to Discord
let internal = this.internal ? 'internal ' : '';
this.client.log.commands(`Loaded ${internal}"${this.name}" command`);
}
/**
* Create the plugin config file if it doesn't already exist
* @param {Object} template The default config template
* @returns {Boolean} True if created, false if it already existed
* [ApplicationCommandInteractionDataOption](https://discord.com/developers/docs/interactions/slash-commands#interaction-applicationcommandinteractiondataoption)
* @typedef {Object} ApplicationCommandInteractionDataOption
* @property {string} name - Name of the parameter
* @property {*} value - The value
* @property {(undefined|ApplicationCommandInteractionDataOption[])} options - Present if the option is a subcommand/subcommand group
*/
createConfig(template) {
this.createDirectory();
let file = join(this.directory.path, 'config.json');
if (!fs.existsSync(file)) {
this.client.log.plugins(`Creating plugin config file for "${this.name}"`);
fs.writeFileSync(file, JSON.stringify(template, null, 2));
return true;
} else {
return false;
}
}
/**
* The main function where your code should go. Create functions and event listeners here
* The code to be executed when a command is invoked
* @abstract
* @param {Object} data - Object containing data about the command invocation
* @param {(undefined|ApplicationCommandInteractionDataOption[])} data.args - Command arguments
* @param {Channel} data.channel- The channel object
* @param {Guild} data.guild- The guild object
* @param {GuildMember} data.member - The member object
* @param {string} data.token - The token used to respond to the interaction
*/
load() {}
async execute(data) {}
};

View File

@@ -1,4 +1,14 @@
module.exports = {
CommandManager: require('./manager'),
Command: require('./command')
Command: require('./command'),
OptionTypes: {
SUB_COMMAND: 1,
SUB_COMMAND_GROUP: 2,
STRING: 3,
INTEGER: 4,
BOOLEAN: 5,
USER: 6,
CHANNEL: 7,
ROLE: 8,
},
};

View File

@@ -6,7 +6,9 @@ const Command = require('./command');
const fs = require('fs');
const { path } = require('../../utils/fs');
/** Manages the loading of commands */
/**
* Manages the loading of commands
*/
module.exports = class CommandManager {
/**
* Create a CommandManager instance
@@ -15,6 +17,7 @@ module.exports = class CommandManager {
constructor(client) {
/** The Discord Client */
this.client = client;
/** A discord.js Collection (Map) of loaded commands */
this.commands = new Collection();
}
@@ -24,10 +27,113 @@ module.exports = class CommandManager {
const files = fs.readdirSync(path('./src/commands'))
.filter(file => file.endsWith('.js'));
for (const file of files) {
const cmd = require(`../commands/${file}`);
this.commands.set(cmd, new cmd(this.client));
for (let file of files) {
try {
file = require(`../../commands/${file}`);
new file(this.client);
} catch (e) {
this.client.log.warn('An error occurred whilst loading an internal command');
this.client.log.error(e);
}
}
}
/** Register a command */
register(cmd) {
const is_internal = (this.commands.has(cmd.name) && cmd.internal)
|| (this.commands.has(cmd.name) && this.commands.get(cmd.name).internal);
if (is_internal) {
let plugin = this.client.plugins.plugins.find(p => p.commands.includes(cmd.name));
if (plugin)
this.client.log.commands(`The "${plugin.name}" plugin has overridden the internal "${cmd.name}" command`);
else
this.client.log.commands(`An unknown plugin has overridden the internal "${cmd.name}" command`);
if(cmd.internal) return;
}
else if (this.commands.has(cmd.name))
throw new Error(`A non-internal command with the name "${cmd.name}" already exists`);
this.commands.set(cmd.name, cmd);
}
/** Check the command data */
check(data) {
if (typeof data.name !== 'string')
throw new TypeError(`Expected type of command name to be a string, got ${typeof data.name}`);
if (data.name.length < 3 || data.name.length > 32)
throw new TypeError('Length of command name must be 3-32');
if (typeof data.description !== 'string')
throw new TypeError(`Expected type of command description to be a string, got ${typeof data.description}`);
if (data.name.length < 1 || data.name.length > 100)
throw new TypeError('Length of description must be 3-32');
if (typeof data.options !== 'undefined' && !(data.options instanceof Array))
throw new TypeError(`Expected type of command options to be undefined or an array, got ${typeof data.options}`);
if (data.options)
this.checkOptions(data.options);
}
/** Check the command data's options */
checkOptions(options) {
let num = 0;
options.forEach(o => {
if (typeof o.type !== 'number')
throw new TypeError(`Expected type of option ${num} type to be a number, got ${typeof o.type}`);
if (typeof o.name !== 'string')
throw new TypeError(`Expected type of option ${num} name to be a string, got ${typeof o.name}`);
if (o.name.length < 3 || o.name.length > 32)
throw new TypeError(`Length of option ${num} name must be 3-32`);
if (typeof o.description !== 'string')
throw new TypeError(`Expected type of option ${num} description to be a string, got ${typeof o.description}`);
if (o.description.length < 1 || o.description.length > 100)
throw new TypeError(`Length of option ${num} description must be 1-100`);
if (typeof o.required !== 'undefined' && typeof o.required !== 'boolean')
throw new TypeError(`Expected type of option ${num} required to be undefined or a boolean, got ${typeof o.required}`);
if (typeof o.choices !== 'undefined' && !(o.choices instanceof Array))
throw new TypeError(`Expected type of option ${num} choices to be undefined or an array, got ${typeof o.choices}`);
if (o.choices)
this.checkOptionChoices(o.choices);
if (typeof o.options !== 'undefined' && !(o.options instanceof Array))
throw new TypeError(`Expected type of option ${num} options to be undefined or an array, got ${typeof o.options}`);
if (o.options)
this.checkOptions(o.options);
num++;
});
}
/** Check command option choices */
checkOptionChoices(choices) {
let num = 0;
choices.forEach(c => {
if (typeof c.name !== 'string')
throw new TypeError(`Expected type of option choice ${num} name to be a string, got ${typeof c.name}`);
if (c.name.length < 1 || c.name.length > 100)
throw new TypeError(`Length of option choice ${num} name must be 1-100`);
if (typeof c.value !== 'string' && typeof c.value !== 'number')
throw new TypeError(`Expected type of option choice ${num} value to be a string or number, got ${typeof c.value}`);
num++;
});
}
};

View File

@@ -7,7 +7,11 @@ module.exports = client => {
for (const file of files) {
const listener = require(`../listeners/${file}`);
const exec = (...args) => listener.execute(client, ...args);
let on = listener.once ? 'once' : 'on';
client[on](listener.event, (...args) => listener.execute(client, ...args));
if (listener.raw)
client.ws[on](listener.event, exec);
else
client[on](listener.event, exec);
}
};

View File

@@ -7,7 +7,9 @@ const fs = require('fs');
const { join } = require('path');
const { path } = require('../../utils/fs');
/** Manages the loading of plugins */
/**
* Manages the loading of plugins
*/
module.exports = class PluginManager {
/**
* Create a PluginManager instance
@@ -16,6 +18,7 @@ module.exports = class PluginManager {
constructor(client) {
/** The Discord Client */
this.client = client;
/** A discord.js Collection (Map) of loaded plugins */
this.plugins = new Collection();

View File

@@ -5,7 +5,9 @@ const fs = require('fs');
const { join } = require('path');
const { path } = require('../../utils/fs');
/** A plugin */
/**
* A plugin
*/
module.exports = class Plugin {
/**
* Create a new Plugin
@@ -13,11 +15,9 @@ module.exports = class Plugin {
* @param {String} id The plugin ID
* @param {Object} options Plugin options
* @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
*/
constructor(client, id, options = {}) {
/** The human-friendly name of the plugin */
this.name = options.name || id;
/** The Discord Client */
this.client = client;
@@ -33,25 +33,53 @@ module.exports = class Plugin {
description
} = this.manager.plugins.get(id);
/** The unique ID of the plugin (NPM package name) */
/**
* The human-friendly name of the plugin
* @type {string}
*/
this.name = options.name || id;
/**
* An array of commands from this plugin
* @type {string[]}
*/
this.commands = options.commands;
/**
* The unique ID of the plugin (NPM package name)
* @type {string}
*/
this.id = id;
/** The version of the plugin (NPM package version) */
/**
* The version of the plugin (NPM package version)
* @type {string}
*/
this.version = version;
/** The plugin author's name (NPM package author) */
/**
* The plugin author's name (NPM package author)
* @type {(undefined|string)}
*/
this.author = author;
/** The plugin description (NPM package description) */
/**
* The plugin description (NPM package description)
* @type {string}
*/
this.description = description;
this.directory = {};
/** A cleaned version of the plugin's ID suitable for use in the directory name */
this.directory.name = this.id.replace(/@[-_a-zA-Z0-9]+\//, '');
let clean = this.id.replace(/@[-_a-zA-Z0-9]+\//, '');
/** The absolute path of the plugin directory */
this.directory.path = path(`./user/plugins/${this.directory.name}`);
/**
* Information about the plugin directory
* @property {string} name - A cleaned version of the plugin's ID suitable for use in the directory name
* @property {string} path - The absolute path of the plugin directory
*/
this.directory = {
name: clean,
path: path(`./user/plugins/${clean}`)
};
}
/**
@@ -85,8 +113,20 @@ module.exports = class Plugin {
}
}
/**
* Reset the plugin config file to the defaults
* @param {Object} template The default config template
*/
resetConfig(template) {
this.createDirectory();
let file = join(this.directory.path, 'config.json');
this.client.log.plugins(`Resetting plugin config file for "${this.name}"`);
fs.writeFileSync(file, JSON.stringify(template, null, 2));
}
/**
* The main function where your code should go. Create functions and event listeners here
* @abstract
*/
load() {}
};