├── .npmignore ├── .gitignore ├── tsconfig.json ├── src ├── index.ts ├── models │ └── timeout.ts ├── classes │ ├── Client.ts │ ├── args.ts │ ├── timeout.ts │ ├── options.ts │ └── handler.ts ├── interfaces.d.ts └── utility │ └── index.ts ├── package.json └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tsconfig.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist/ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": false, 6 | "outDir": "dist" 7 | }, 8 | "include": ["./src"] 9 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Handler from "./classes/handler" 2 | import TimeoutModel from "./models/timeout" 3 | import Client from "./classes/Client" 4 | import Options from "./classes/options" 5 | 6 | export { 7 | Handler, 8 | TimeoutModel, 9 | Client, 10 | Options 11 | } -------------------------------------------------------------------------------- /src/models/timeout.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, SchemaTypes } from 'mongoose'; 2 | 3 | const timeoutSchema = new Schema({ 4 | from: { type: SchemaTypes.Number, default: Date.now() }, 5 | command: { type: SchemaTypes.String, default: "" }, 6 | user: { type: SchemaTypes.String, default: "" }, 7 | }); 8 | 9 | export default model("timeout_logs", timeoutSchema); 10 | 11 | interface Timeout { 12 | user: string, 13 | command: string, 14 | from: number, 15 | } -------------------------------------------------------------------------------- /src/classes/Client.ts: -------------------------------------------------------------------------------- 1 | import { Client, ClientOptions, Collection } from 'discord.js'; 2 | import { Command } from '../interfaces'; 3 | 4 | class client extends Client { 5 | commands: Collection; 6 | commandAliases: Collection; 7 | 8 | constructor(options: ClientOptions) { 9 | super(options); 10 | 11 | this.commands = new Collection(); 12 | this.commandAliases = new Collection(); 13 | } 14 | } 15 | 16 | export default client; -------------------------------------------------------------------------------- /src/classes/args.ts: -------------------------------------------------------------------------------- 1 | class args extends Array { 2 | private _collection: Map; 3 | 4 | constructor(args: Array | undefined) { 5 | super(); 6 | 7 | this._collection = new Map(); 8 | 9 | args?.forEach(v => { 10 | this.push(v.value || v); 11 | 12 | if (v.name) { 13 | this._collection.set(v.name, v.value); 14 | this[v.name] = v.value; 15 | } 16 | }); 17 | } 18 | 19 | get(key: string | number): String | Number | Boolean | undefined { 20 | return this._collection.get(key) || this[key]; 21 | } 22 | 23 | getWithType(key: string | number): returnType | undefined { 24 | return this._collection.get(key) || this[key]; 25 | } 26 | 27 | toArray(): args { 28 | return this; 29 | } 30 | 31 | toMap(): Map { 32 | return this._collection; 33 | } 34 | } 35 | 36 | export default args; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-slash-command-handler", 3 | "version": "3.2.2", 4 | "description": "an packaged Command handler for discord slash commands", 5 | "main": "dist/index.js", 6 | "typings": "src/", 7 | "scripts": { 8 | "test": "ts-node src/index.ts", 9 | "bug-fix": "tsc && git add . && git commit -m \"bug fix\" && git push origin master && npm version patch && npm publish" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/KartikeSingh/discord-slash-command-handler" 14 | }, 15 | "keywords": [ 16 | "discord", 17 | "slash", 18 | "command", 19 | "handler" 20 | ], 21 | "author": "shisui", 22 | "license": "MIT", 23 | "dependencies": { 24 | "discord.js": "^13.2.0", 25 | "mongoose": "^6.0.4", 26 | "ms-prettify": "^1.2.1" 27 | }, 28 | "publishConfig": { 29 | "access": "public" 30 | }, 31 | "devDependencies": { 32 | "@types/mongoose": "^5.11.97", 33 | "@types/node": "^16.10.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, ContextMenuInteraction, DMChannel, Guild, GuildMember, Message, TextBasedChannels, TextChannel, User } from 'discord.js'; 2 | import Handler from './classes/handler'; 3 | import Args from './classes/args'; 4 | import Client from './classes/Client'; 5 | 6 | export interface CommandData { 7 | client: Client, 8 | guild: Guild, 9 | channel: TextChannel | DMChannel | TextBasedChannels, 10 | interaction: CommandInteraction | undefined, 11 | args: Args, 12 | member: GuildMember | undefined, 13 | user: User, 14 | message: Message | CommandInteraction, 15 | handler: Handler, 16 | subCommand?: string | undefined, 17 | subCommandGroup?: string | undefined, 18 | } 19 | 20 | export interface Command { 21 | name: string, 22 | description: string, 23 | permissions?: string[], 24 | type?: number, 25 | aliases?: string[], 26 | category?: string, 27 | slash?: "both" | boolean, 28 | global?: boolean, 29 | guildOnly?: boolean, 30 | ownerOnly?: boolean, 31 | dm?: "only" | boolean, 32 | timeout?: number | string, 33 | args?: string, 34 | argsType?: string, 35 | argsDescription?: string, 36 | options?: Options[], 37 | 38 | error(eventName: "exception", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message, error: Error): void; 39 | error(eventName: "guildOnly", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message): void; 40 | error(eventName: "dmOnly", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message): void; 41 | error(eventName: "notOwner", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message): void; 42 | error(eventName: "timeout", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message, timeRemaining: Number): void; 43 | error(eventName: "noPermissions", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message): void; 44 | error(eventName: "lessArguments", command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message): void; 45 | 46 | run(...args: any[]): void; 47 | } 48 | 49 | interface Options { 50 | name: string, 51 | description: string, 52 | required: boolean, 53 | type?: string | number, 54 | choices?: string | number, 55 | options?: [Options] 56 | } -------------------------------------------------------------------------------- /src/classes/timeout.ts: -------------------------------------------------------------------------------- 1 | import Client from "./Client"; 2 | import { connect } from 'mongoose'; 3 | import timeout from '../models/timeout'; 4 | 5 | class Timeout { 6 | client: Client; 7 | mongoURI: string; 8 | cached: Map; 9 | constructor(client: Client, mongoURI: string = "no_uri") { 10 | this.client = client; 11 | this.mongoURI = mongoURI; 12 | this.cached = new Map(); 13 | 14 | if (mongoURI !== "no_uri") this.connect().then(v => console.log("[ discord-slash-command-handler ] : Mongoose Database Connected Successfully")).catch(e => { throw new Error("Invalid MONGO_URI was provided in Discord-Slash-Command_handler") }); 15 | } 16 | 17 | async connect() { 18 | return new Promise((resolve, reject) => { 19 | connect(this.mongoURI).then(v => resolve(v)).catch(e => reject(e)); 20 | }) 21 | } 22 | 23 | async getTimeout(user: string, command: string): Promise { 24 | return new Promise(async (resolve) => { 25 | let data = { 26 | user: "1", 27 | command: "1", 28 | from: 0, 29 | }; 30 | 31 | if (this.mongoURI === "no_uri") { 32 | if (!this.cached.has(`${user}_${command}`)) this.cached.set(`${user}_${command}`, { from: 0, user, command }); 33 | data = this.cached.get(`${user}_${command}`); 34 | } else { 35 | data = await timeout.findOne({ user, command }) || await timeout.create({ user, command, from: 0 }) 36 | } 37 | 38 | return resolve(data); 39 | }) 40 | } 41 | 42 | async setTimeout(user: string, command: string, time: number): Promise { 43 | return new Promise(async (resolve) => { 44 | let data = { 45 | user: "1", 46 | command: "1", 47 | from: 0, 48 | }; 49 | 50 | if (this.mongoURI === "no_uri") { 51 | this.cached.set(`${user}_${command}`, { from: time, command, user }); 52 | data = this.cached.get(`${user}_${command}`); 53 | } 54 | else { 55 | data = await timeout.findOneAndUpdate({ user, command }, { from: time }) || await timeout.create({ user, command, from: time }); 56 | } 57 | 58 | resolve(data); 59 | }) 60 | } 61 | } 62 | 63 | export default Timeout; 64 | 65 | interface CommandTimeout { 66 | from: number, 67 | command: string, 68 | user: string 69 | } -------------------------------------------------------------------------------- /src/classes/options.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | class HandlerOptions { 4 | /** 5 | * The location where all the commands are present. 6 | */ 7 | commandFolder: string; 8 | 9 | /** 10 | * The location where all the events are present. 11 | */ 12 | eventFolder?: string; 13 | 14 | /** 15 | * The reply to give when user do not have enough permissions. 16 | */ 17 | permissionReply?: string; 18 | 19 | /** 20 | * The reply to give when user is on timeout. 21 | */ 22 | timeoutMessage?: string; 23 | 24 | /** 25 | * The reply to give when there is a error in executing the command. 26 | */ 27 | errorReply?: string; 28 | 29 | /** 30 | * The reply to give when command is owner only and user is not owner. 31 | */ 32 | notOwnerReply?: string; 33 | 34 | /** 35 | * The reply to give when command is dm only but used in a guild. 36 | */ 37 | dmOnlyReply?: string; 38 | 39 | /** 40 | * The reply to give when command guild only, but used in dm. 41 | */ 42 | guildOnlyReply?: string; 43 | 44 | /** 45 | * The type of commands present inside command folder, possible types "folder" | "file". 46 | */ 47 | commandType?: "folder" | "file"; 48 | 49 | /** 50 | * The mongo URI, Provided when you want to connect timeout with mongo DB 51 | */ 52 | mongoURI?: string; 53 | 54 | /** 55 | * The prefix for message based commands 56 | */ 57 | prefix?: string; 58 | 59 | /** 60 | * The array of server's Discord ID where you want the commands to work. 61 | */ 62 | slashGuilds?: string[]; 63 | 64 | /** 65 | * The array of owner's Discord ID. 66 | */ 67 | owners?: string[] | string; 68 | 69 | /** 70 | * Whether the package have to handle normal commands or not. 71 | */ 72 | handleSlash?: boolean | "both"; 73 | 74 | /** 75 | * Whether the package have to handle slash commands or not. 76 | */ 77 | handleNormal?: boolean | "both"; 78 | 79 | /** 80 | * Whether the package have to add timeouts or not. 81 | */ 82 | timeout?: boolean; 83 | 84 | /** 85 | * Consider all commands as slash commands. 86 | */ 87 | allSlash?: boolean; 88 | 89 | /** 90 | * Auto defer the slash commands 91 | */ 92 | autoDefer?: boolean; 93 | 94 | /** 95 | * Choose the sequence of paramters for your run commands 96 | * for more information read docs 97 | */ 98 | runParameters: string[]; 99 | 100 | constructor(options: HandlerOptions) { 101 | const { dmOnlyReply = "{mention}, **{command}** is a Guild Only command, please use it in a guild, not in DM Channel", permissionReply = "{mention}, You don't have enough permissions to use {command} command", timeoutMessage = "{mention}, please wait for {remaining} before using {command} command", errorReply = "Unable to run this command due to error", notOwnerReply = "Only bot owner's can use this command", prefix, slashGuilds = [], owners = [], handleSlash = false, handleNormal = false, timeout = false, commandFolder, eventFolder = undefined, mongoURI = undefined, allSlash = false, commandType = "file", autoDefer = true, runParameters = ["0"] } = options; 102 | const { path } = require.main; 103 | 104 | this.commandFolder = `${path}/${commandFolder}`; 105 | this.eventFolder = eventFolder ? `${path}/${eventFolder}` : undefined; 106 | 107 | if (("commandType" in options) && (commandType !== "file" && commandType !== "folder")) throw new Error("Command type should be \"folder\" or \"folder\" but we got " + commandType) 108 | if (!fs.existsSync(this.commandFolder)) throw new Error("Invalid command folder, please provide an correct folder"); 109 | 110 | if (!prefix && handleNormal === true) throw new Error("Please provide a prefix, If you want us to handle normal commands for you"); 111 | 112 | this.runParameters = runParameters; 113 | this.autoDefer = autoDefer; 114 | this.dmOnlyReply = dmOnlyReply; 115 | this.permissionReply = permissionReply; 116 | this.timeoutMessage = timeoutMessage; 117 | this.errorReply = errorReply; 118 | this.notOwnerReply = notOwnerReply; 119 | this.commandType = commandType; 120 | this.mongoURI = mongoURI || undefined; 121 | this.prefix = prefix; 122 | this.slashGuilds = slashGuilds || []; 123 | this.owners = typeof owners === "string" ? owners.split(",") : owners || []; 124 | this.handleSlash = handleSlash || false; 125 | this.handleNormal = handleNormal || false; 126 | this.timeout = timeout || false; 127 | this.allSlash = allSlash || false; 128 | } 129 | } 130 | 131 | export default HandlerOptions; -------------------------------------------------------------------------------- /src/utility/index.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction, CommandInteraction, ContextMenuInteraction, Interaction, MessageComponentInteraction, SelectMenuInteraction } from "discord.js"; 2 | import Handler from "../classes/handler"; 3 | import { Command, Options } from "../interfaces"; 4 | 5 | class Utils { 6 | // command type 7 | static fixType(type: string | number = 1): number { 8 | if (typeof type !== "number" && typeof type !== "string") throw new Error("Command Type should be a string or a number"); 9 | 10 | type = typeof type === "string" ? type?.toLowerCase() : type; 11 | 12 | if (!type || type > 3 || type < 1 || type === "chat" || type === "chat_input") return 1; 13 | else if (type === "user") return 2; 14 | else if (type === "message") return 4; 15 | else if (typeof type === "number") return type; 16 | else return 1; 17 | } 18 | 19 | static getOptions(args: string = "", argsDescription: string = "", argsType: string = "") { 20 | let last = "", lastIndex = 0, index = 0; 21 | 22 | const ArgsDescription: string[] = argsDescription.length > 1 ? argsDescription.split("|") : [] || [], ArgsType: string[] = argsType.length > 1 ? argsType.split("|") : [] || []; 23 | 24 | const req = [], opt = []; 25 | 26 | for (let i = 0; i < args.length; i++) { 27 | if (last === "<" ? args[i] === ">" : args[i] === "]") { 28 | last === "<" ? req.push({ name: args.substring(lastIndex + 1, i).replace(/ /g, "-").toLowerCase(), required: true, description: ArgsDescription[index] || args.substring(lastIndex + 1, i), type: Utils.getType(ArgsType[index]) || "STRING" }) : opt.push({ name: args.substring(lastIndex + 1, i).replace(/ /g, "-").toLowerCase(), required: false, description: ArgsDescription[index] || args.substring(lastIndex + 1, i), type: Utils.getType(ArgsType[index]) || "STRING" }); 29 | index++; 30 | } 31 | 32 | if (args[i] === "<" || args[i] === "[") { 33 | last = args[i]; 34 | lastIndex = i; 35 | } 36 | } 37 | 38 | return req.concat(opt); 39 | } 40 | 41 | // option type 42 | static getType(type: string | number) { 43 | try { 44 | type = typeof type === "string" ? type?.toUpperCase()?.trim() : type; 45 | 46 | if (typeof type === "number" && type > 0 && type < 9) return type; 47 | if (!type) return 3; 48 | 49 | return type === "SUB_COMMAND" ? 1 : type === "SUB_COMMAND_GROUP" ? 2 : type === "STRING" ? 3 : type === "INTEGER" ? 4 : type === "BOOLEAN" ? 5 : type === "USER" ? 6 : type === "CHANNEL" ? 7 : type === "ROLE" ? 8 : 3; 50 | } catch (e) { 51 | return 3; 52 | } 53 | } 54 | 55 | static async fixOptions(this: Handler, options: Options[]) { 56 | return new Promise(async (res) => { 57 | if (!options || !options?.length) return res(undefined); 58 | 59 | for (let i = 0; i < options.length; i++) { 60 | options[i].type = this.Utils.getType(options[i].type); 61 | options[i].name = options[i].name?.trim()?.replace(/ /g, "-"); 62 | 63 | if (options[i]?.options?.length > 0) options[i].options = await this.Utils.fixOptions.bind(this)(options[i]?.options); 64 | } 65 | res(options); 66 | }) 67 | } 68 | 69 | static add(this: Handler, commands: string[], extraFolder: string = "") { 70 | return new Promise(async (res) => { 71 | const globalCommands = [], guildCommands = []; 72 | for (let i = 0; i < commands.length; i++) { 73 | const command: Command = require(`${this.options.commandFolder}${extraFolder}/${commands[i]}`) || {}; 74 | 75 | if (!command.name || !command.run) continue; 76 | 77 | if (Utils.fixType(command.type) !== 1) command.name = command.name.replace(/ /g, "-").toLowerCase(); 78 | if (Utils.fixType(command.type) !== 1) command.description = ""; 79 | 80 | this.client.commands.set(command.name, command); 81 | 82 | command.aliases ? command.aliases.forEach((v) => this.client.commandAliases.set(v, command.name)) : null; 83 | 84 | if (command.slash !== true && command.slash !== "both" && this.options.allSlash !== true) continue; 85 | 86 | if (!command.description) throw new Error("Description is required in a slash command\n Description was not found in " + command.name) 87 | 88 | if ((!command.options || command.options && command.options.length < 1) && command.args) command.options = this.Utils.getOptions(command.args, command.argsDescription, command.argsType); 89 | 90 | command.options = await this.Utils.fixOptions.bind(this)(command.options); 91 | 92 | const command_data = { 93 | name: command.name, 94 | description: command.description, 95 | options: command.options || [], 96 | type: this.Utils.fixType(command.type), 97 | } 98 | 99 | if (command.guildOnly) guildCommands.push(command_data); 100 | else globalCommands.push(command_data); 101 | } 102 | 103 | res({ globalCommands, guildCommands }); 104 | }) 105 | } 106 | 107 | static getParameters(keys: any, values: any, runParameters: string[]): Array { 108 | const parameters = []; 109 | 110 | runParameters.forEach(v => { 111 | const data = {}; 112 | if (v.includes("0")) { 113 | for (let i = 0; i < 10; i++) { 114 | if (values[i.toString()]) data[keys[i.toString()]] = values[i.toString()]; 115 | } 116 | 117 | parameters.push(data); 118 | } else if (v.length === 1) { 119 | if (values[v]) parameters.push(values[v]); 120 | } else { 121 | const V = v.split(""), data = {}; 122 | 123 | V.forEach(i => { 124 | if (values[i.toString()]) data[keys[i.toString()]] = values[i.toString()]; 125 | }) 126 | 127 | if (Object.keys(data).length > 0) parameters.push(data) 128 | } 129 | }) 130 | 131 | return parameters; 132 | } 133 | 134 | static replyInteraction(interaction: CommandInteraction | ContextMenuInteraction | MessageComponentInteraction | ButtonInteraction | SelectMenuInteraction, message: string) { 135 | try { 136 | if (interaction.replied) interaction.followUp(message); 137 | else interaction.reply(message); 138 | } catch (e) { 139 | interaction.channel.send(`${interaction.user.toString()}, ${message}`); 140 | } 141 | } 142 | } 143 | 144 | export default Utils; -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Installations 2 | 3 | ``` 4 | npm i discord-slash-command-handler 5 | ``` 6 | 7 | # Why use our package? 8 | 9 | * Fast and secure. 10 | * Easy to use. 11 | * Active support on discord. 12 | * Easily convert normal commands to slash commands. 13 | * Supports Database for timeouts. 14 | * Automatic Handling 15 | * Advanced methods to handle commands and errors ( like timeouts, less arguments etc ) and can be automated too. 16 | * We support discord.js@13.x.x 17 | * Example [source code](https://github.com/KartikeSingh/discord-slash-command-bot) 18 | 19 | ## Request 20 | Please provide suggestions, bug reports either on [discord](https://discord.gg/PBFj276RUw) or [github issues](https://github.com/KartikeSingh/discord-slash-command-handler/issues/) 21 | 22 | # Basic handler example 23 | 24 | ```js 25 | // NOTE: This package only supports Discord.js V 13 26 | const { Handler, Client } = require('discord-slash-command-handler'); 27 | // Client class is same as discord js client but with additional properties 28 | const client = new Client(options); 29 | 30 | client.on('ready', () => { 31 | // replace src/commands to the path with your commands folder. 32 | // if your commands folder contain files then use commandType: "file". otherwise commandType: "folder" 33 | const handler = new Handler(client, { guilds: ["guild id"], commandFolder: "/commands",commandType: "file" || "folder"}); 34 | 35 | console.log("bot is up!"); 36 | }); 37 | 38 | client.login(token); 39 | ``` 40 | 41 | # Complex handler example 42 | 43 | ```js 44 | const { Handler, Client } = require('discord-slash-command-handler'); 45 | const client = new Client(options); 46 | 47 | client.on('ready', () => { 48 | // replace src/commands to the path to your commands folder. 49 | const handler = new Handler(client, { 50 | // Locations of folder should be provided with respect to the main file 51 | // Location of the command folder 52 | commandFolder: "/commands", 53 | 54 | // Folder contains files or folders ? 55 | commandType: "file" || "folder", 56 | 57 | // Location of the event folder 58 | eventFolder: "/events", 59 | 60 | // Guild ID(s) where you want to enable slash commands (if slash command isn't global) 61 | slashGuilds: ["guild id"], 62 | 63 | // Add MONGO URI for timeouts 64 | mongoURI: "some_mongo_uri", 65 | 66 | // Make all commands slash commands 67 | allSlash: true, 68 | 69 | // User ID(s), these users will be considered as bot owners 70 | owners: ["user id"], 71 | 72 | handleSlash: true, 73 | /* True => If you want automatic slash handler 74 | * False => if you want to handle commands yourself 75 | * 'both' => in this case instead of running the command itself we will invoke an event called 'slashCommand' 76 | */ 77 | 78 | handleNormal: false, 79 | /* True => If you want automatic normal handler 80 | * False => if you want to handle commands yourself 81 | * 'both' => in this case instead of running the command itself we will invoke an event called 'normalCommand' 82 | */ 83 | 84 | prefix: "k!", // Bot's prefix 85 | timeout: true, // If you want to add timeouts in commands 86 | 87 | // reply to send when user don't have enough permissions to use the command 88 | permissionReply: "You don't have enough permissions to use this command", 89 | 90 | // reply to send when user is on a timeout 91 | timeoutMessage: "You are on a timeout", 92 | 93 | // reply to send when there is an error in command 94 | errorReply: "Unable to run this command due to errors", 95 | 96 | // reply to send when command is ownerOnly and user isn't a owner 97 | notOwnerReply: "Only bot owners can use this command", 98 | }); 99 | 100 | console.log("bot is up"); 101 | }); 102 | 103 | client.login(token); 104 | ``` 105 | 106 | # Custom Command Handler (Slash/Normal) 107 | 108 | ```js 109 | ... 110 | bot.on('ready', () => { 111 | ... 112 | 113 | // Custom normal command handler, this function works when handleNormal is 'both' 114 | handler.on('normalCommand', (command,command_data) => { 115 | // handle the command 116 | // command is your normal command object, for command_data go down below to data types 117 | }) 118 | 119 | 120 | // Custom slash command handler, this function works when handleSlash is 'both' 121 | handler.on('slashCommand', (command,command_data) => { 122 | // handle the command 123 | // command is your normal command object, for command_data go down below to data types 124 | }) 125 | ... 126 | }) 127 | ... 128 | ``` 129 | 130 | # Handle Arguments for Slash Commands 131 | ```js 132 | run: async ({ args }) => { 133 | // Wanna get an specific argument of a slash command? 134 | args.get("argument name goes here"); 135 | // argument name = the one specified in options. 136 | 137 | // Other ways to get options 138 | args[0] // index 139 | args["some name"] // get argument from name 140 | } 141 | ``` 142 | 143 | # How to define command 144 | 145 | ```js 146 | // file name: help.js 147 | 148 | module.exports = { 149 | name: "help", // Name of the command 150 | 151 | description: "Get some help", // Description of the command 152 | 153 | aliases: ["gethelp"], // The aliases for command ( don't works for slash command ) 154 | 155 | category: "general", // the category of command 156 | 157 | slash: "both", // true => if only slash, false => if only normal, "both" => both slash and normal 158 | 159 | global: false, // false => work in all guilds provided in options, true => works globally 160 | 161 | ownerOnly: false, // false => work for all users, true => works only for bot owners 162 | 163 | dm: false, // false => Guild Only, true => Both Guild And DM, "only" => DM Only 164 | 165 | timeout: 10000 | '10s', // the timeout on the command 166 | 167 | args: "< command category > [ command name ]", // Command arguments, <> for required arguments, [] for optional arguments ( please provide required arguments before optional arguments ) 168 | 169 | // Arguments for slash commands 170 | 171 | // first method 172 | args: "< command category > [ command name ]", // Command arguments, <> for required arguments, [] for optional arguments ( please provide required arguments before optional arguments ) 173 | 174 | argsType: "String | String", // OPTIONAL, if you want to specify the argument type 175 | // Available Types: String, Integer, Boolean, Channel, User, Role 176 | // also Sub_command, Sub_command_group but these aren't tested yet 177 | 178 | argsDescription: "The command category | the command name", // OPTIONAL, if you wanna add a cute little description for arguments 179 | 180 | 181 | // Second method 182 | // All properties are required, if not provided than you will get an error 183 | // NOTE: You can also use a command builder for the options 184 | options: [ 185 | { 186 | name: "name of argument", 187 | description: "description of the argument", 188 | require: true or false, 189 | type: "string" 190 | } 191 | ], 192 | 193 | // OPTIONAL 194 | error: async (errorType, command, message, error) => { 195 | // If you want custom error handler for each command 196 | /* 197 | * errorType: errorType ( check in data types at bottom for more info ) 198 | * command: the command 199 | * message: the message object 200 | * error: only in exceptions, the error message 201 | */ 202 | }, 203 | 204 | // Required 205 | run: async (command_data) => { // you can add custom run arguments 206 | // your command's code 207 | } 208 | } 209 | ``` 210 | 211 | # Convert Normal Command to Slash Command 212 | 213 | ## Additions 214 | ```js 215 | // Add slash porperty 216 | slash: true, // true => only slash command, "both" => slash and normal command, false => normal command 217 | 218 | // you have to fix your run method or add custom run command parameter in handler options for that check #specials 219 | 220 | // All done. but there are few limitations like, message object is not Discord.Message object 221 | // it is an custom objected created by us its properties are listen in # datatype 's slash_command 222 | ``` 223 | 224 | # All available events 225 | 226 | ```js 227 | /** 228 | * this event is invoked when Commands are added to client / Commands are loaded 229 | * @param {Collection} commands The collection of commands 230 | * @param {Collection} commandAliases The collection of command aliases 231 | */ 232 | handler.on('commandsCreated', (commands, commandAliases) => { }); 233 | 234 | /** 235 | * this event is invoked when a user used a slash command and handleSlash is 'both' 236 | * @param {command} command the command used 237 | * @param {Object} command_data the command data, check #types for more information 238 | */ 239 | handler.on('slashCommand', (command, command_data) => { }); 240 | 241 | /** 242 | * this event is invoked when a user used a normal command and handleNormal is 'both' 243 | * @param {command} command the command used 244 | * @param {Object} command_data the command data, check #types for more information 245 | */ 246 | handler.on('normalCommand', (command, command_data) => { }); 247 | 248 | /** 249 | * This event is invoked when user don't provides enough arguments in a command 250 | * @param {command} command the command used 251 | * @param {message | interaction} message The Command Interaction or the message 252 | */ 253 | handler.on('lessArguments', (command, message) => { }); 254 | 255 | /** 256 | * This event is invoked when command is owner only but user is not an owner 257 | * @param {command} command the command used 258 | * @param {message | interaction} message The Command Interaction or the message 259 | */ 260 | handler.on('notOwner', (command, message) => { }); 261 | 262 | /** 263 | * This event is invoked when user don't have enough permissions to use a command 264 | * @param {command} command the command used 265 | * @param {message | interaction} message The Command Interaction or the message 266 | */ 267 | handler.on('noPermissions', (command, message) => { 268 | /* 269 | * commands: the command used 270 | * message: the Discord message object 271 | */ 272 | }); 273 | 274 | /** 275 | * This event is invoked when user is on a mOnly to use a command 276 | * @param {command} command the command used 277 | * @param {message | interaction} message The Command Interaction or the message 278 | */ 279 | handler.on('timeout', (command, message) => { }); 280 | 281 | /** 282 | * This event is invoked when a command is DM only but used in a guild 283 | * @param {command} command the command used 284 | * @param {message | interaction} message The Command Interaction or the message 285 | */ 286 | handler.on('dmOnly', (command, message) => { }); 287 | 288 | /** 289 | * This event is invoked when a command is guild only but used in a DM 290 | * @param {command} command the command used 291 | * @param {message | interaction} message The Command Interaction or the message 292 | */ 293 | handler.on('guildOnly', (command, message) => { }); 294 | 295 | /** 296 | * This event is invoked when an unknown error occurs while running a command 297 | * @param {command} command the command used 298 | * @param {message | interaction} message The Command Interaction or the message 299 | * @param {Error} error the error 300 | */ 301 | handler.on('exception', (command, message, error) => { }); 302 | ``` 303 | 304 | # Specials 305 | - ### Reload Commands 306 | ```js 307 | ... 308 | 309 | handler.reloadCommands(); // to reload the commands 310 | 311 | ... 312 | ``` 313 | 314 | - ### Custom run parameters 315 | ```js 316 | const { Handler } = require('discord-slash-command-handler'); 317 | 318 | const handler = new Handler({ 319 | runParameters: ["1","2"] || ["12","3"] || ["0"] 320 | }); 321 | 322 | // Number refers to different values, if provided more than one number in the string than it returns a object. 323 | const type = { 324 | 1: "client", 325 | 2: "guild", 326 | 3: "channel", 327 | 4: "interaction", 328 | 5: "args", 329 | 6: "member", 330 | 7: "user", 331 | 8: "message", 332 | 9: "handler" 333 | } 334 | ``` 335 | 336 | # Date Types 337 | 338 | ```js 339 | command_data = { 340 | client, // your discord client instance 341 | guild, // the guild in which command was used 342 | channel, // the channel in which command was used 343 | interaction, // interaction if it is an slash command 344 | args, // the array of arguments 345 | member, // the guild member object 346 | message, // the message object if normal command, in slash command it have less attributes ( to check its attribute read slash_message ) 347 | handler, // the instance of your command handler 348 | } 349 | 350 | slash_message = { 351 | member, // the guild member object 352 | author, // the user 353 | client, // the instance of your client 354 | guild, // the guild where command was used 355 | channel, // the channel where command was used 356 | interaction, // the ineraction if it is an slash command 357 | content, // the message contnet 358 | createdAT, // timestamps of the message creation 359 | } 360 | 361 | errorType = "noPermissions" | "exception" | "lessArguments" | "timeout" | "dmOnly" | "guildOnly"; 362 | ``` 363 | 364 | 365 | # Report Problems at [Github](https://github.com/KartikeSingh/discord-slash-command-handler/issues) 366 | 367 | # Links 368 | [Discord Server](https://discord.gg/XYnMTQNTFh) 369 | [Refactored By](https://hug-me.vercel.app/) -------------------------------------------------------------------------------- /src/classes/handler.ts: -------------------------------------------------------------------------------- 1 | import { Collection, CommandInteraction, ContextMenuInteraction, Message } from 'discord.js'; 2 | import { EventEmitter } from 'events'; 3 | import { readdirSync, statSync, existsSync } from 'fs'; 4 | import Utils from '../utility'; 5 | import Client from './Client'; 6 | import HandlerOptions from './options'; 7 | import Options from './options'; 8 | import Timeout from './timeout'; 9 | import ms from 'ms-prettify'; 10 | import Args from './args'; 11 | import { Command, CommandData } from '../interfaces'; 12 | 13 | class Handler extends EventEmitter { 14 | client: Client; 15 | options: HandlerOptions; 16 | Utils = Utils; 17 | Timeout: Timeout; 18 | 19 | constructor(client: Client, options: HandlerOptions) { 20 | super(); 21 | this.client = client; 22 | this.client.commands = new Collection(); 23 | this.client.commandAliases = new Collection(); 24 | this.options = new Options(options); 25 | this.Timeout = new Timeout(this.client, this.options.mongoURI); 26 | this.client.commands = new Collection(); 27 | this.client.commandAliases = new Collection(); 28 | 29 | if (existsSync(this.options.eventFolder)) this.handleEvents(); 30 | 31 | this.setCommands().then(() => { 32 | this.emit("commandsCreated", this.client.commands, this.client.commandAliases); 33 | if (this.options.handleSlash) this.handleSlashCommands(); 34 | if (this.options.handleNormal) this.handleNormalCommands(); 35 | }) 36 | } 37 | 38 | setCommands() { 39 | return new Promise(async (resolve, reject) => { 40 | try { 41 | let Commands = readdirSync(this.options.commandFolder)?.filter(file => this.options.commandType === "file" ? file.endsWith(".ts") || file.endsWith(".js") : statSync(`${this.options.commandFolder}/${file}`).isDirectory()), i; 42 | if (Commands.length === 0) return reject("No Folders/file in the provided location"); 43 | 44 | if (this.options.commandType === "file") await this.Utils.add.bind(this)(Commands); 45 | else for (let i = 0; i < Commands.length; i++) await this.Utils.add.bind(this)(readdirSync(`${this.options.commandFolder}/${Commands[i]}`).filter(file => file.endsWith(".ts") || file.endsWith(".js")), `/${Commands[i]}`); 46 | 47 | const commandsGuild = this.client.commands.filter(v => v.guildOnly).map(v => { 48 | return { 49 | name: v.name, 50 | description: v.description, 51 | type: this.Utils.fixType(v.type), 52 | // @ts-ignore 53 | options: this.Utils.fixType(v.type) === 1 ? v.options.toJSON ? v.options.toJSON() : v.options : undefined 54 | }; 55 | }); 56 | 57 | const commandsGlobal = this.client.commands.filter(v => !v.guildOnly).map(v => { 58 | return { 59 | name: v.name, 60 | description: v.description, 61 | type: this.Utils.fixType(v.type), 62 | // @ts-ignore 63 | options: this.Utils.fixType(v.type) === 1 ? v.options?.toJSON ? v.options.toJSON() : v.options : undefined 64 | }; 65 | }); 66 | 67 | if (this.client.isReady() === true) { 68 | this.client.application.commands.set(commandsGlobal) 69 | this.options.slashGuilds.forEach(v => this.client.application.commands.set(commandsGuild, v)); 70 | } else { 71 | this.client.once('ready', () => { 72 | this.client.application.commands.set(commandsGlobal) 73 | if (Array.isArray(this.options.slashGuilds)) { 74 | this.options.slashGuilds.forEach(v => this.client.application.commands.set(commandsGlobal, v)); 75 | } else { 76 | this.client.application.commands.set(commandsGlobal, this.options.slashGuilds); 77 | } 78 | }) 79 | } 80 | 81 | resolve({ commands: this.client.commands, commandAliases: this.client.commandAliases }) 82 | 83 | } catch (e) { 84 | reject(e); 85 | } 86 | }) 87 | } 88 | 89 | async handleSlashCommands() { 90 | 91 | this.client.on("interactionCreate", async (interaction) => { 92 | if (!interaction.isCommand() && !interaction.isContextMenu()) return; 93 | 94 | const command = this.client.commands.get(interaction.commandName), member = interaction.guild.members.cache.get(interaction.user.id); 95 | 96 | if (this.options.autoDefer === true) await interaction.deferReply(); 97 | 98 | try { 99 | if (command.dm !== true && !interaction.guild) { 100 | if (typeof command.error === "function") command.error("guildOnly", command, interaction); 101 | else if (this.listeners("guildOnly").length > 0) this.emit("guildOnly", command, interaction); 102 | else this.Utils.replyInteraction(interaction, this.options.guildOnlyReply.replace(/{mention}/g, interaction.user.toString()).replace(/{command}/g, command.name)); 103 | 104 | return; 105 | } 106 | if (command.dm === "only" && interaction.guild) { 107 | if (typeof command.error === "function") command.error("dmOnly", command, interaction); 108 | else if (this.listeners("dmOnly").length > 0) this.emit("dmOnly", command, interaction); 109 | else this.Utils.replyInteraction(interaction, this.options.dmOnlyReply.replace(/{mention}/g, interaction.user.toString()).replace(/{command}/g, command.name)); 110 | 111 | return 112 | } 113 | 114 | if (command.ownerOnly && !this.options.owners.includes(interaction.user.id)) { 115 | if (typeof command.error === "function") command.error("notOwner", command, interaction); 116 | else if (this.listeners("notOwner").length > 0) this.emit("notOwner", command, interaction); 117 | else this.Utils.replyInteraction(interaction, this.options.notOwnerReply.replace(/{mention}/g, interaction.user.toString())); 118 | 119 | return 120 | } 121 | 122 | const tm = await this.Timeout.getTimeout(interaction.user.id, interaction.commandName); 123 | 124 | if (tm.from > Date.now()) { 125 | if (typeof command.error === "function") command.error("timeout", command, interaction, tm.from - Date.now()) 126 | else if (this.listeners("timeout").length > 0) this.emit("timeout", command, interaction, tm.from - Date.now()); 127 | else this.Utils.replyInteraction(interaction, this.options.timeoutMessage.replace(/{remaining}/g, ms(tm.from - Date.now())).replace(/{mention}/g, interaction.user.toString()).replace(/{command}/g, command.name)) 128 | 129 | return; 130 | } 131 | 132 | const args = new Args([...interaction?.options?.data] || []); 133 | 134 | const values = { 135 | 1: this.client, 136 | 2: interaction.guild, 137 | 3: interaction.channel, 138 | 4: interaction, 139 | 5: args, 140 | 6: interaction.member, 141 | 7: interaction.member.user, 142 | 8: interaction, 143 | 9: this, 144 | }, keys = { 145 | 1: "client", 146 | 2: "guild", 147 | 3: "channel", 148 | 4: "interaction", 149 | 5: "args", 150 | 6: "member", 151 | 7: "user", 152 | 8: "message", 153 | 9: "handler" 154 | } 155 | 156 | const parameters = this.Utils.getParameters(keys, values, this.options.runParameters); 157 | 158 | let allow = command.permissions ? command.permissions.length === 0 : true; 159 | 160 | // @ts-ignore 161 | if (command.permissions) command.permissions.forEach((v) => { if (member.permissions.has(v)) allow = true }); 162 | 163 | if (!allow) { 164 | if (typeof command.error === "function") command.error("noPermissions", command, interaction); 165 | else if (this.listeners("noPermissions").length > 0) this.emit("noPermissions", command, interaction) 166 | else this.Utils.replyInteraction(interaction, this.options.permissionReply.replace(/{mention}/g, interaction.user.toString()).replace(/{command}/g, command.name)); 167 | 168 | return; 169 | } 170 | 171 | let timeout; 172 | 173 | if (command.timeout) { 174 | if (typeof command.timeout === "string") timeout = ms(command.timeout) 175 | else timeout = command.timeout; 176 | } 177 | 178 | if (timeout && this.options.timeout === true) this.Timeout.setTimeout(interaction.user.id, command.name, Date.now() + timeout); 179 | 180 | if (this.options.handleSlash === true) command.run(...parameters); 181 | else this.emit("slashCommand", command, ...parameters); 182 | 183 | } catch (e) { 184 | if (typeof command.error === "function") command.error("exception", command, interaction, e); 185 | else if (this.listeners("exception").length > 0) this.emit("exception", command, interaction, e); 186 | else this.Utils.replyInteraction(interaction, this.options.errorReply); 187 | } 188 | }) 189 | } 190 | 191 | async handleNormalCommands() { 192 | this.client.on('messageCreate', async message => { 193 | let command: Command; 194 | try { 195 | if (message.author.bot || !message.content.toLowerCase().startsWith(this.options.prefix)) return; 196 | 197 | const args = message.content.slice(this.options.prefix.length).trim().split(/ +/g) || []; 198 | let cmd = args.shift().toLowerCase(); 199 | 200 | if (cmd.length == 0) return; 201 | 202 | command = this.client.commands.get(cmd) || this.client.commands.get(this.client.commandAliases.get(cmd)); 203 | 204 | if (command?.slash === true) return; 205 | 206 | if (command.ownerOnly && !this.options.owners.includes(message.author.id)) { 207 | if (typeof command.error === "function") command.error("notOwner", command, message); 208 | else if (this.listeners("notOwner").length > 0) this.emit("notOwner", command, message); 209 | else message.reply(this.options.notOwnerReply.replace(/{mention}/g, message.author.toString())); 210 | 211 | return; 212 | } 213 | 214 | if (command.dm !== true && !message.guild) { 215 | if (typeof command.error === "function") command.error("guildOnly", command, message); 216 | else if (this.listeners("guildOnly").length > 0) this.emit("guildOnly", command, message); 217 | else message.reply(this.options.guildOnlyReply.replace(/{mention}/g, message.author.toString()).replace(/{command}/g, command.name)); 218 | 219 | return; 220 | } 221 | if (command.dm === "only" && message.guild) { 222 | if (typeof command.error === "function") command.error("dmOnly", command, message); 223 | else if (this.listeners("dmOnly").length > 0) this.emit("dmOnly", command, message); 224 | else message.reply(this.options.dmOnlyReply.replace(/{mention}/g, message.author.toString()).replace(/{command}/g, command.name)); 225 | 226 | return; 227 | } 228 | 229 | const tm = await this.Timeout.getTimeout(message.author.id, command.name); 230 | 231 | if (tm.from > Date.now()) { 232 | if (typeof command.error === "function") command.error("timeout", command, message, tm.from - Date.now()) 233 | else if (this.listeners("timeout").length > 0) this.emit("timeout", command, message, tm.from - Date.now()); 234 | else message.reply(this.options.timeoutMessage.replace(/{mention}/g, message.author.toString()).replace(/{remaining}/g, ms(tm.from - Date.now())).replace(/{command}/g, command.name)) 235 | 236 | return; 237 | } 238 | 239 | const reqArgs = command.args ? this.Utils.getOptions(command.args).filter((v) => v.required === true) || [] : command.options ? command.options.filter(v => v.required === true) : []; 0 240 | 241 | if (args.length < reqArgs.length) { 242 | let args = command.args || ""; 243 | if (args === "" && command.options.length > 0) command.options.forEach(v => args += v.required ? `<${v.name}>` : `[${v.name}]`); 244 | 245 | if (typeof command.error === "function") command.error("lessArguments", command, message) 246 | else if (this.listeners("lessArguments").length > 0) this.emit("lessArguments", command, message) 247 | else message.reply({ content: `Invalid Syntax corrected syntax is : \`${this.options.prefix}${command.name} ${args}\`` }); 248 | 249 | return; 250 | } 251 | 252 | let allow = command.permissions && message.guild ? command.permissions.length === 0 : true; 253 | 254 | // @ts-ignore 255 | if (message.guild) if (command.permissions) command.permissions.forEach((v) => { if (message.member.permissions.has(v)) allow = true }); 256 | 257 | if (!allow) { 258 | if (typeof command.error === "function") command.error("noPermissions", command, message); 259 | else if (this.listeners("noPermissions").length > 0) this.emit("noPermissions", command, message) 260 | else message.reply(this.options.permissionReply.replace(/{mention}/g, message.author.toString()).replace(/{command}/g, command.name)); 261 | 262 | return; 263 | } 264 | 265 | const command_data = { 266 | client: this.client, 267 | guild: message.guild, 268 | channel: message.channel, 269 | interaction: undefined, 270 | args: new Args(args), 271 | member: message.member, 272 | message: message, 273 | handler: this, 274 | user: message.author 275 | } 276 | 277 | let timeout; 278 | 279 | if (command.timeout) { 280 | if (typeof command.timeout === "string") timeout = ms(command.timeout) 281 | else timeout = command.timeout; 282 | } 283 | 284 | if (timeout && this.options.timeout === true) { 285 | this.Timeout.setTimeout(message.author.id, command.name, Date.now() + timeout); 286 | } 287 | 288 | if (this.options.handleNormal === true) command.run(command_data); 289 | else this.emit("normalCommand", command, command_data); 290 | } catch (e) { 291 | if (typeof command.error === "function") command.error("exception", command, message, e); 292 | else if (this.listeners("exception").length > 0) this.emit("exception", command, message, e); 293 | else message.reply(this.options.errorReply); 294 | } 295 | }) 296 | } 297 | 298 | async handleEvents() { 299 | readdirSync(this.options.eventFolder).filter((f) => f.endsWith(".js")).forEach((file) => { 300 | this.client.on(`${file.split(".")[0]}`, (...args) => require(`${this.options.eventFolder}/${file}`)(this.client, ...args)); 301 | }); 302 | } 303 | 304 | async reloadCommands() { 305 | this.client.commands.clear(); 306 | this.client.commandAliases.clear(); 307 | 308 | return new Promise((res, rej) => { 309 | this.setCommands() 310 | .then((v) => { 311 | res({ commands: this.client.commands, aliases: this.client.commandAliases }) 312 | console.log("[ discord-slash-command-handler ] : Commands are reloaded") 313 | this.emit("commandsCreated", this.client.commands, this.client.commandAliases) 314 | }) 315 | .catch((e) => { 316 | rej(e); 317 | console.log("[ discord-slash-command-handler ] : There was a error in reloading the commands"); 318 | console.log(e); 319 | }) 320 | }) 321 | } 322 | 323 | on(eventName: "commandsCreated", listener: (commands: Collection, commandAliases: Collection) => void): this; 324 | on(eventName: "normalCommand", listener: (command: Command, commandData: CommandData) => void): this; 325 | on(eventName: "slashCommand", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message) => void): this; 326 | on(eventName: "exception", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message, error: Error) => void): this; 327 | on(eventName: "guildOnly", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message) => void): this; 328 | on(eventName: "dmOnly", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message) => void): this; 329 | on(eventName: "notOwner", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message) => void): this; 330 | on(eventName: "timeout", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message, timeRemaining: Number) => void): this; 331 | on(eventName: "noPermissions", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message) => void): this; 332 | on(eventName: "lessArguments", listener: (command: Command, interaction: CommandInteraction | ContextMenuInteraction | Message) => void): this; 333 | 334 | on(eventName: string | symbol, listener: (...args: any[]) => void): this { 335 | return super.on(eventName, listener); 336 | } 337 | } 338 | 339 | export default Handler; 340 | --------------------------------------------------------------------------------