diff --git a/README.md b/README.md new file mode 100644 index 0000000..6483131 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +

+ OPCommands +

+ +--- + +## What's this? + +OPCommands is an advanced Discord.js Command Handler that supports slash commands and more! + +## Requirements + +* Discord.js v13+ +* Node.js v16.6.0+ + +## Installation + +```bash +npm install --save opcommands +``` + +## Documentation + +Here is the official Documentation of OPCommands: https://opcommands.lukedev.tk + +## Contributing + +Feel free to report bugs, suggest new things or make pull requests to the GitHub repository of OPCommands. It would be really appreciated! \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..618f2f0 Binary files /dev/null and b/assets/logo.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..94e7a8b --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "opcommands", + "version": "1.0.0", + "description": "An advanced Discord.js Command Handler that supports slash commands and more!", + "main": "src/OPCommands.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "discord.js", + "discord", + "handler", + "command", + "slash" + ], + "author": "LukeIsHereToDevelop (https://lukedev.tk)", + "homepage": "https://opcommands.lukedev.tk", + "bugs": { + "url": "https://github.com/LukeIsHereToDevelop/opcommands/issues" + }, + "repository": "github:LukeIsHereToDevelop/opcommands", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^16.6.0" + }, + "dependencies": { + "@discordjs/rest": "^0.1.0-canary.0", + "discord-api-types": "^0.22.0", + "discord.js": "^13.0.1", + "humanize-ms": "^1.2.1", + "ms": "^2.1.3" + } +} diff --git a/src/OPCommands.js b/src/OPCommands.js new file mode 100644 index 0000000..40346a2 --- /dev/null +++ b/src/OPCommands.js @@ -0,0 +1,65 @@ +const Discord = require("discord.js"); +const EventHandler = require("./handlers/Event.js"); +const CommandHandler = require("./handlers/Command.js"); + +/** + * The main class of OPCommands. + * @class + * @requires [Node.js v16.6.0](https://nodejs.org/en/download/current/) & Discord.js v13 + * @param {Discord.Client} client + * @param {Object} options + * @example + * const OPCommands = require("opcommands"); + * const Discord = require("discord.js"); + * const client = new Discord.Client(); + * new OPCommands(client, { + * commandsDir: "commands", + * eventsDir: "events" + * }); + */ +class OPCommands { + constructor(client, options) { + if (!client) throw new Error("[OPCommands] Missing Discord client."); + if (!options) throw new Error("[OPCommands] Missing options.") + if (!options.commandsDir) throw new Error("[OPCommands] Missing commands' directory parameter on options."); + if (!options.eventsDir) throw new Error("[OPCommands] Missing events' directory parameter on options."); + this.client = client; + this.options = options; + + new CommandHandler(this, options.commandsDir); + new EventHandler(this, options.eventsDir); + } + + /** + * Sets the owner(s) of the bot. + * @param {*} owner Owner ID(s) + */ + async setBotOwner(owner) { + if (!owner) throw new Error("[OPCommands] Missing Owner ID(s)."); + if (typeof owner === "string") return this.client.owners = [owner]; + if (typeof owner === "array") return this.client.owners = owner; + throw new Error("[OPCommands] Owner parameter must be a String or Array."); + } + + /** + * Adds owner(s) of the bot. + * @param {String} owner Owner ID + */ + async addBotOwner(owner) { + if (!owner) throw new Error("[OPCommands] Missing Owner ID."); + if (typeof owner != "string") throw new Error("[OPCommands] Owner parameter must be a String."); + return this.client.owners.push(owner); + } + + /** + * Sets the limit messages. + * @param {Object} msgs New messages + */ + async setMessages(msgs) { + if (!msgs) throw new Error("[OPCommands] Missing the new messages."); + if (typeof msgs != "object") throw new Error("[OPCommands] Msgs parameter must be an Object."); + return this.client.msgs = msgs; + } +} + +module.exports = OPCommands; \ No newline at end of file diff --git a/src/handlers/Command.js b/src/handlers/Command.js new file mode 100644 index 0000000..5dc7765 --- /dev/null +++ b/src/handlers/Command.js @@ -0,0 +1,102 @@ +const Discord = require("discord.js"); +const { REST } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v9'); +const hms = require("humanize-ms"); +const ms = require("ms"); +const fs = require("fs"); +const path = require("path"); +const { time } = require("console"); + +/** + * Command Handler + * @class + * @param {*} _this + * @param {String} commandsDir + */ +class CommandHandler { + constructor (_this, commandsDir) { + if (!this) throw new Error("[OPCommands] Internal error: missing _this parameter on Command Handler."); + if (!commandsDir) throw new Error("[OPCommands] Internal error: missing eventsDir parameter on Command Handler."); + if (!fs.existsSync(commandsDir)) throw new Error("[OPCommands] Unexisting command directory."); + + _this.client.commands = new Discord.Collection(); + const files = fs.readdirSync(commandsDir).filter(file => file.endsWith(".js")); + const commands = []; + + if (_this.options.logs) console.log("[OPCommands] Loaded " + files.length + " commands."); + for (const file of files) { + const commandFile = require(path.join(require.main.path, commandsDir, file)); + _this.client.commands.set(commandFile.name, commandFile); + const commandObj = { + name: commandFile.name, + description: commandFile.description + }; + if (commandFile.options) Object.assign(commandObj, {options: commandFile.options}); + commands.push(JSON.stringify(commandObj)); + }; + + _this.client.on("ready", async () => { + const rest = new REST({ version: "9" }).setToken(_this.client.token); + + (async () => { + try { + await rest.put( + Routes.applicationGuildCommands(_this.client.application?.id, "678352388251451433"), + { body: _this.client.commands }, + ); + } catch (error) { + console.error(error); + } + })(); + }); + + const cooldowns = new Discord.Collection(); + _this.client.on("interactionCreate", (interaction) => { + if (!interaction.isCommand()) return; + if (!_this.client.commands.has(interaction.commandName)) return; + const commandFile = _this.client.commands.get(interaction.commandName); + if (commandFile.limits.owner && !_this.client.owners.includes(interaction.user.id)) { + const ownerOnly = _this.client.msgs.ownerOnly; + if (ownerOnly) { + return ownerOnly(interaction); + } else { + return interaction.reply({ content: "You must be a Bot Owner to execute this command!", ephemeral: true }); + }; + }; + if (commandFile.limits.permissions && !interaction.member?.permissions.has(commandFile.limits.permissions)) { + const permissions = _this.client.msgs.permissions; + if (permissions) { + return permissions(interaction, commandFile.limits.permissions); + } else { + return interaction.reply({ content: "You must have the following permissions: " + commandFile.limits.permissions.join(", "), ephemeral: true }); + }; + }; + if (!cooldowns.has(`${commandFile.name}_${interaction.user.id}`)) { + if (!commandFile.limits.cooldown) return; + cooldowns.set(`${commandFile.name}_${interaction.user.id}`, Date.now()); + setTimeout(() => {cooldowns.delete(`${commandFile.name}_${interaction.user.id}`)}, hms(commandFile.limits.cooldown)) + } else { + const expirationTime = cooldowns.get(`${commandFile.name}_${interaction.user.id}`) + hms(commandFile.limits.cooldown); + if (Date.now() < expirationTime) { + const timeLeft = ms(expirationTime - Date.now()); + const cooldown = _this.client.msgs.cooldown; + if (cooldown) { + return cooldown(interaction, timeLeft); + } else { + return interaction.reply({ content: `You are in a cooldown! Please wait **${timeLeft}**.`, ephemeral: true }); + }; + }; + }; + + try { + if (_this.options.logs) console.log("[OPCommands] Command executed: " + interaction.commandName); + _this.client.commands.get(interaction.commandName).run(_this.client, interaction); + } catch (e) { + if (_this.options.logs) console.log("[OPCommands] Command error: " + interaction.commandName); + console.error(e); + } + }); + } +} + +module.exports = CommandHandler; \ No newline at end of file diff --git a/src/handlers/Event.js b/src/handlers/Event.js new file mode 100644 index 0000000..c480b24 --- /dev/null +++ b/src/handlers/Event.js @@ -0,0 +1,31 @@ +const Discord = require("discord.js"); +const fs = require("fs"); +const path = require("path"); + +/** + * Event Handler. + * @class + * @param {*} _this + * @param {String} eventsDir + */ +class EventHandler { + constructor(_this, eventsDir) { + if (!_this) throw new Error("[OPCommands] Internal error: missing _this parameter on Event Handler."); + if (!eventsDir) throw new Error("[OPCommands] Internal error: missing eventsDir parameter on Event Handler."); + if (!fs.existsSync(eventsDir)) throw new Error("[OPCommands] Unexisting event directory."); + + const files = fs.readdirSync(eventsDir).filter(file => file.endsWith(".js")); + + if (_this.options.logs) console.log("[OPCommands] Loaded " + files.length + " events."); + for (const file of files) { + const eventFile = require(path.join(require.main.path, eventsDir, file)); + if (eventFile.settings.once) { + _this.client.once(eventFile.name, (...args) => eventFile.run(_this.client, ...args)); + } else { + _this.client.on(eventFile.name, (...args) => eventFile.run(_this.client, ...args)); + }; + } + } +} + +module.exports = EventHandler; \ No newline at end of file diff --git a/test/commands/say.js b/test/commands/say.js new file mode 100644 index 0000000..ad0c4c3 --- /dev/null +++ b/test/commands/say.js @@ -0,0 +1,20 @@ +module.exports = { + name: "say", + description: "Says something.", + options: [ + { + type: 3, + name: "input", + description: "The input to return.", + required: true + } + ], + limits: { + owner: false, + permissions: ["MANAGE_MESSAGES"], + cooldown: "3s" + }, + run: (client, interaction) => { + interaction.reply({ content: interaction.options.getString("input") }); + } +} \ No newline at end of file diff --git a/test/commands/test.js b/test/commands/test.js new file mode 100644 index 0000000..5a78538 --- /dev/null +++ b/test/commands/test.js @@ -0,0 +1,12 @@ +module.exports = { + name: "test", + description: "Test command", + limits: { + owner: true, + permissions: ["ADMINISTRATOR"], + cooldown: "20s" + }, + run: (client, interaction) => { + interaction.reply({ content: "test" }); + } +} \ No newline at end of file diff --git a/test/events/ready.js b/test/events/ready.js new file mode 100644 index 0000000..8b8d7b6 --- /dev/null +++ b/test/events/ready.js @@ -0,0 +1,10 @@ +module.exports = { + name: "ready", + settings: { + once: false + }, + run: (client) => { + console.log("I'm online!"); + client.user.setActivity("OPCommands"); + } +} \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..7c2db3e --- /dev/null +++ b/test/index.js @@ -0,0 +1,19 @@ +const OPCommands = require("../src/OPCommands.js"); +const Discord = require("discord.js"); +const client = new Discord.Client({ + intents: 32767 +}); + +const handler = new OPCommands(client, { + commandsDir: "commands", + eventsDir: "events", + logs: true +}); +handler.setBotOwner("OWNER_ID"); +handler.setMessages({ + ownerOnly: (interaction) => interaction.reply("Missing **Bot Owner** permission."), + permissions: (interaction, perms) => interaction.reply(`You are missing the following permissions: **${perms.join(", ")}**`), + cooldown: (interaction, cooldown) => interaction.reply(`You must wait **${cooldown}** before executing another command.`) +}); + +client.login("BOT_TOKEN"); \ No newline at end of file