├── tests ├── index.test.ts └── helpers.ts ├── .gitignore ├── .npmignore ├── .github └── workflows │ └── action.yml ├── jest.config.js ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── src ├── types.ts └── index.ts /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | experiment 3 | node_modules 4 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # src files 3 | node_modules/ 4 | src/ 5 | package-lock.json 6 | 7 | # testing files 8 | coverage/ 9 | tests/ 10 | test.js 11 | sandbox.js 12 | experiment 13 | 14 | # config files 15 | .gitignore 16 | .npmignore 17 | tsconfig.json 18 | .prettierrc.js 19 | jest.config.js 20 | .github/ 21 | .vscode/ 22 | .env -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | export function getMocks(messageContent: string) { 2 | const mockedMessage = { 3 | channel: { 4 | send: jest.fn(), 5 | }, 6 | reply: jest.fn(), 7 | content: messageContent, 8 | }; 9 | const mockedInteraction = { 10 | channel: { 11 | send: jest.fn(), 12 | }, 13 | reply: jest.fn(), 14 | content: messageContent, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | jobs: 7 | publish-npm: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: 16.x 14 | registry-url: https://registry.npmjs.org/ 15 | - run: npm ci 16 | - run: npm run build 17 | - run: npm publish --access=public 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | rootDir: process.cwd(), 4 | testTimeout: 30000, 5 | testEnvironment: "node", 6 | verbose: true, 7 | testPathIgnorePatterns: ["/node_modules", "/dist"], 8 | modulePathIgnorePatterns: ["/dist"], 9 | roots: ["/tests"], 10 | testMatch: ["/tests/**/*.test.ts"], 11 | transform: { 12 | "^.+\\.(ts)$": "ts-jest", 13 | }, 14 | collectCoverageFrom: ["src/**/*.ts"], 15 | collectCoverage: true, 16 | coverageDirectory: "./coverage", 17 | coverageThreshold: { 18 | global: { 19 | branches: 80, 20 | functions: 80, 21 | lines: 80, 22 | statements: -10, 23 | }, 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "esnext" 7 | ], 8 | "strict": false, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "declaration": true, 12 | "moduleResolution": "node", 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "allowSyntheticDefaultImports": true, 22 | "esModuleInterop": true, 23 | "emitDecoratorMetadata": true, 24 | "experimentalDecorators": true, 25 | "resolveJsonModule": true, 26 | "incremental": false, 27 | "baseUrl": "./src", 28 | "watch": false, 29 | "types": [ 30 | "node", 31 | ], 32 | "outDir": "./dist", 33 | "rootDir": "./src", 34 | }, 35 | "include": [ 36 | "./src/**/*.ts" 37 | ], 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 xImouto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start:watch": "nodemon experiment/index.ts", 4 | "build": "tsc --sourceMap false" 5 | }, 6 | "dependencies": { 7 | "discord.js": "^14.3.0" 8 | }, 9 | "devDependencies": { 10 | "@types/jest": "^29.0.3", 11 | "@types/node": "^18.7.14", 12 | "concurrently": "^7.3.0", 13 | "dotenv": "^16.0.2", 14 | "jest": "^29.0.3", 15 | "nodemon": "^2.0.19", 16 | "ts-jest": "^29.0.2", 17 | "ts-node": "^10.9.1", 18 | "typescript": "^4.8.2" 19 | }, 20 | "name": "@reinforz/cordmand", 21 | "version": "0.0.4", 22 | "description": "A utility package for making discord-bot commands much easier to write with discord.js", 23 | "main": "dist/index.js", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Reinforz/cordmand.git" 27 | }, 28 | "keywords": [ 29 | "discord.js", 30 | "discord-message-command-handler" 31 | ], 32 | "author": "imoxto", 33 | "license": "MIT", 34 | "types": "./dist/index.d.ts", 35 | "bugs": { 36 | "url": "https://github.com/Reinforz/cordmand/issues" 37 | }, 38 | "homepage": "https://github.com/Reinforz/cordmand#readme" 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordmand 2 | 3 | ## About 4 | A utility package for making discord-bot commands much easier to write with discord.js. 5 | 6 | ## Usage Example 7 | 8 | #### Install this package: 9 | ```bash 10 | npm i @reinforz/cordmand 11 | ``` 12 | or, 13 | ```bash 14 | yarn add @reinforz/cordmand 15 | ``` 16 | 17 | #### Example typescript file: 18 | ```ts 19 | import { Client, GatewayIntentBits } from "discord.js"; 20 | import { addCommands } from "@reinforz/cordmand"; 21 | import { Commands } from "@reinforz/cordmand/types"; 22 | 23 | // initiate discord.js client 24 | const client = new Client({ 25 | intents: [ 26 | GatewayIntentBits.Guilds, 27 | GatewayIntentBits.GuildMessages, 28 | GatewayIntentBits.MessageContent, 29 | GatewayIntentBits.GuildMembers, 30 | ], 31 | }); 32 | 33 | // define your commands similar to 34 | const commands: Commands = { 35 | interactionCreate: [ 36 | // define interactions here 37 | { 38 | name: "ping", 39 | cb: async (interaction) => { 40 | await interaction.reply("Pong!"); 41 | }, 42 | }, 43 | { 44 | name: "hello", 45 | message: { 46 | content: "hello", 47 | ephemeral: true, 48 | }, 49 | }, 50 | ], 51 | 52 | messageCreate: [ 53 | { 54 | regex: /ping/i, 55 | message: "pong", 56 | }, 57 | { 58 | regex: /hi/i, 59 | // message can also be a callback function which can access the discord message object 60 | message: (_, message) => `hello <@${message.author.id}>`, 61 | }, 62 | { 63 | regex: /bye/i, 64 | message: (_, message) => `bye ${message.author.username}`, 65 | reply: true, // uses discord's message.reply intead of just sending the message in the same channel 66 | }, 67 | { 68 | regex: /args/i, 69 | message: (args) => `The arguments are: ${args.join(", ")}`, 70 | reply: true, 71 | }, 72 | ], 73 | }; 74 | 75 | // add commands to the client by calling the addCommands function provided by the client 76 | addCommands(client, commands, { 77 | messageCommandPrefix: /^i!/i, 78 | // Add your command prefix regex. Make sure to include ^ (starts with) in the regex 79 | }); 80 | 81 | // login 82 | client.login(process.env.BOT_TOKEN!); 83 | ``` 84 | 85 | #### Example with just using `makeDiscordClient` function: 86 | ```ts 87 | import { makeDiscordClient } from "@reinforz/cordmand"; 88 | import { Commands, MakeDiscordClientOptions } from "@reinforz/cordmand/types"; 89 | import { commands } from "./some-file" 90 | 91 | const makeClientOptions: MakeDiscordClientOptions = { 92 | botToken: process.env.BOT_TOKEN!, 93 | clientOptions: { 94 | intents: ["Guilds", "GuildMessages", "MessageContent", "GuildMembers"], 95 | }, 96 | commands, // the same command object as previous one, It will work in the same way as the previous example 97 | addCommandsOptions: { 98 | messageCommandPrefix: /^i!/i, 99 | }, 100 | }; 101 | 102 | makeDiscordClient(makeClientOptions); 103 | ``` 104 | 105 | ## Contributors 106 | 107 | - [imoxto](https://github.com/imoxto) -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MessagePayload, 3 | ReplyMessageOptions, 4 | MessageOptions, 5 | InteractionReplyOptions, 6 | ChatInputCommandInteraction, 7 | Message, 8 | ClientOptions, 9 | } from "discord.js"; 10 | 11 | export type CommonReply = string | MessagePayload; 12 | export type MessageReplies = CommonReply | ReplyMessageOptions; 13 | export type MessageSends = CommonReply | MessageOptions; 14 | export type InteractionReplies = CommonReply | InteractionReplyOptions; 15 | 16 | /** 17 | * Function to help send message in discord 18 | * @param messageArgs message arguments string array returned by MessageArgumentParser. Default parser excludes the message command prefix 19 | * @param message The discord Message object. Accessing it allows using author, channel etc. of the message 20 | * @returns string message to send in discord 21 | */ 22 | export type MessageArgCb = (messageArgs: string[], message: Message) => MessageReplies | MessageSends; 23 | 24 | export type InteractionArgCb = (interaction: ChatInputCommandInteraction) => InteractionReplies; 25 | /** 26 | * Function to do anything you want to instead of just sending a message 27 | * @param message The discord Message object. Accessing it allows using author, channel etc. of the message 28 | */ 29 | export type MessageCb = (message?: Message) => void; 30 | 31 | /** 32 | * Function to do anything you want to with the interaction 33 | * @param interaction The discord Chat Command Interaction object. Accessing it allows using author, channel etc. 34 | */ 35 | export type InteractionCb = (interaction: ChatInputCommandInteraction) => void; 36 | /** 37 | * Function to parse arguments incase the default one is not according to expectations 38 | * @param messageContent the string message content to process into arguments 39 | * @returns should return arguments in the form of an array of string 40 | */ 41 | export type MessageArgumentParserCb = (messageContent: string) => string[]; 42 | 43 | /** 44 | * Object explaining what a command does 45 | * @property regex - the regex used to match with the message content detect the command 46 | * @property cb - A function to give users complete control with what to do with the message object 47 | * @property message - a message to send or funtion returning a message to send 48 | * @property reply - boolean indicating whether to send the message by replying to the command message or not 49 | */ 50 | export interface MessageCommand { 51 | regex: RegExp; 52 | cb?: MessageCb; 53 | message?: MessageReplies | MessageSends | MessageArgCb; 54 | reply?: boolean; 55 | } 56 | 57 | /** 58 | * Object explaining what a command does 59 | * @property name - the name of the command 60 | * @property cb - A function to give users complete control with what to do with the interaction object 61 | * @property message - A string or a function returning a string to reply to the interaction 62 | */ 63 | export interface InteractionCommand { 64 | name: string; 65 | cb?: InteractionCb; 66 | message?: InteractionReplies | InteractionArgCb; 67 | } 68 | 69 | /** 70 | * Main Object to pass to add defferent types of commands 71 | * @property messageCreate - regular message commands 72 | * @property interactionCreate - interaction (or slash) commands 73 | */ 74 | export interface Commands { 75 | messageCreate?: MessageCommand[]; 76 | interactionCreate?: InteractionCommand[]; 77 | } 78 | 79 | export interface AddCommandsOptions { 80 | messageCommandPrefix: RegExp; 81 | messageArgumentParser?: (messageContent: string) => string[]; 82 | } 83 | 84 | export interface MakeDiscordClientOptions { 85 | botToken: string; 86 | clientOptions: ClientOptions; 87 | onceReady?: () => void; 88 | commands?: Commands; 89 | addCommandsOptions?: AddCommandsOptions; 90 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Client, Message } from "discord.js"; 2 | import { Commands, AddCommandsOptions, MessageArgCb, MakeDiscordClientOptions } from "types"; 3 | 4 | export const allMatch = /.*/; 5 | 6 | /** 7 | * The default function to parse arguments 8 | * @param messageContent the string message content to process into arguments 9 | * @param messageCommandPrefix the regex command prefix to not pass it into the arguments array 10 | * @returns arguments in the form of an array of string 11 | */ 12 | export function messageArgumentParser(messageContent: string, messageCommandPrefix: RegExp | string) { 13 | return messageContent.toLowerCase().replace(messageCommandPrefix, "").trim().replace(/\s\s+/g, " ").split(" "); 14 | } 15 | 16 | /** 17 | * Main function to add commands to the discord bot client 18 | * @param client The discord client to "add" commands on 19 | * @param commands Object representing the information of different types of commands 20 | * @param options Optional. Needed for passing command prefix regex and/or message argument parser 21 | */ 22 | export function addCommands( 23 | client: Client, 24 | { messageCreate = [], interactionCreate = [] }: Commands, 25 | options?: AddCommandsOptions 26 | ) { 27 | client.on("messageCreate", async (message) => { 28 | if (!message.content.match(options?.messageCommandPrefix || allMatch)) { 29 | return; 30 | } 31 | const args = options?.messageArgumentParser 32 | ? options.messageArgumentParser(message.content) 33 | : messageArgumentParser(message.content, options?.messageCommandPrefix || ""); 34 | for (let command of messageCreate) { 35 | const { regex, reply, cb, message: msgFromCommand } = command; 36 | if (!args[0]?.match(regex)) { 37 | continue; 38 | } 39 | 40 | if (cb) { 41 | await cb(message); 42 | break; 43 | } 44 | if (!msgFromCommand) break; 45 | if (typeof msgFromCommand === "function") { 46 | if (reply) await message.reply(await msgFromCommand(args, message)); 47 | else await message.channel.send(await msgFromCommand(args, message)); 48 | break; 49 | } 50 | if (reply) await message.reply(msgFromCommand); 51 | else await message.channel.send(msgFromCommand); 52 | break; 53 | } 54 | }); 55 | 56 | client.on("interactionCreate", async (interaction) => { 57 | if (!interaction.isChatInputCommand()) return; 58 | 59 | for (let command of interactionCreate) { 60 | const { name, cb, message } = command; 61 | if (interaction.commandName !== name) { 62 | continue; 63 | } 64 | if (cb) { 65 | await cb(interaction); 66 | break; 67 | } 68 | if (!message) break; 69 | if (typeof message === "function") { 70 | await interaction.reply(await message(interaction)); 71 | break; 72 | } else { 73 | await interaction.reply(message); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | /** 80 | * @deprecated Since version 0.0.4. Use the addCommands function instead. 81 | */ 82 | export function sendMessage( 83 | message: string | MessageArgCb, 84 | discordMessage: Message, 85 | messageArgs: string[], 86 | reply?: boolean 87 | ) { 88 | if (typeof message === "string") reply ? discordMessage.reply(message) : discordMessage.channel.send(message); 89 | else 90 | reply 91 | ? discordMessage.reply(message(messageArgs, discordMessage)) 92 | : discordMessage.channel.send(message(messageArgs, discordMessage)); 93 | } 94 | 95 | /** 96 | * function to make the discord client 97 | * @param makeDiscordClientOptions an object which has the necessary fields to make a discord.js client. 98 | * i.e. botToken, ClientOptions (which includes intents), onceReady function, commands, addCommandsOptions 99 | */ 100 | export function makeDiscordClient({ 101 | botToken, 102 | clientOptions, 103 | onceReady = () => console.log("BOT is online"), 104 | commands = undefined, 105 | addCommandsOptions = undefined, 106 | }: MakeDiscordClientOptions) { 107 | const client = new Client(clientOptions); 108 | 109 | client.once("ready", onceReady); 110 | 111 | if (commands) { 112 | addCommands(client, commands, addCommandsOptions); 113 | } 114 | 115 | client.login(botToken); 116 | 117 | return client; 118 | } 119 | 120 | export * from "./types"; 121 | --------------------------------------------------------------------------------