@backs/bot-base
Version:
The 6th version of back's bot base.
1 lines • 132 kB
Source Map (JSON)
{"version":3,"sources":["../node_modules/tsup/assets/esm_shims.js","../src/index.ts","../node_modules/colorette/index.js","../src/managers/commands.ts","../src/typings/commands.ts","../src/managers/plugins.ts","../src/managers/multipageembed.ts","../src/structures/multipageembed.ts","../src/builders/button.ts","../src/builders/modal.ts","../src/builders/selectmenus.ts","../src/structures/commands.ts","../src/structures/events.ts","../src/structures/plugins.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport { fileURLToPath } from 'url'\nimport path from 'path'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import { Client, ColorResolvable } from \"discord.js\";\r\nimport { BotOptions, Emoji, PlainBotOptions } from \"./typings/bot\";\r\nimport { crop, highlight, isTypescript } from \"@backs/utils\";\r\nimport { magenta, green, yellow, red, bold, blackBright, bgGreen, bgYellow, bgRed, italic } from \"colorette\";\r\nimport { glob } from \"glob\";\r\nimport { Event } from \"./structures/events\";\r\nimport { AnyCommand } from \"./typings/commands\";\r\nimport { Plugin } from \"./structures\";\r\nimport path from \"path\";\r\nimport isGlob from \"is-glob\";\r\nimport createLogger, { TypedLogger } from \"@backs/logger\";\r\nimport ConfigParser from \"@backs/config-parser\";\r\nimport CommandManager from \"./managers/commands\";\r\nimport PluginManager from \"./managers/plugins\";\r\nimport MultiPageEmbedManager from \"./managers/multipageembed\";\r\n\r\nexport default async function createClient<\r\n Configs extends Record<string, any>,\r\n Emojis extends Record<string, Emoji>,\r\n Colors extends Record<string, ColorResolvable>,\r\n>(options: BotOptions<Configs, Emojis, Colors>) {\r\n const configParser = new ConfigParser<Configs>({\r\n ...options.configOptions,\r\n logging: {\r\n debug: process.env.NODE_ENV === \"debug\",\r\n },\r\n start: false,\r\n });\r\n await configParser.start();\r\n const parsedOptions = parseOptions(configParser.configs, options);\r\n return new Bot(parsedOptions, configParser, options);\r\n}\r\n\r\nexport class Bot<\r\n Configs extends Record<string, any>,\r\n Emojis extends Record<string, Emoji>,\r\n Colors extends Record<string, ColorResolvable>,\r\n> extends Client<true> {\r\n private configParser: ConfigParser<Configs>;\r\n botOptions: PlainBotOptions<Configs, Emojis, Colors>;\r\n private _botOptions: BotOptions<Configs, Emojis, Colors>;\r\n logger: TypedLogger<[\"error\", \"warn\", \"debug\", \"log\", \"info\"]>;\r\n commands: CommandManager<Configs, Emojis, Colors>;\r\n plugins: PluginManager<Configs, Emojis, Colors>;\r\n multiPageEmbed: MultiPageEmbedManager<Configs, Emojis, Colors>;\r\n\r\n get configs() {\r\n return this.configParser.configs;\r\n }\r\n\r\n get clientEmojis(): Emojis {\r\n const emojis = this._botOptions.emojis;\r\n if (typeof emojis === \"function\") return emojis(this.configs);\r\n else return (this.botOptions.emojis as Emojis) ?? {};\r\n }\r\n\r\n get clientColors(): Colors {\r\n const colors = this._botOptions.colors;\r\n if (typeof colors === \"function\") return colors(this.configs);\r\n else return (this.botOptions.colors as Colors) ?? {};\r\n }\r\n\r\n constructor(\r\n options: PlainBotOptions<Configs, Emojis, Colors>,\r\n configParser: ConfigParser<Configs>,\r\n BotBaseOptions: BotOptions<Configs, Emojis, Colors>,\r\n ) {\r\n options.clientOptions.ws ??= {};\r\n // @ts-expect-error - DiscordJS doesnt have these typings\r\n options.clientOptions.ws.properties ??= {};\r\n // @ts-expect-error - DiscordJS doesnt have these typings\r\n options.clientOptions.ws.properties.$browser = options.clientOptions.browser;\r\n\r\n super(options.clientOptions);\r\n this.configParser = configParser;\r\n this.botOptions = options;\r\n this._botOptions = BotBaseOptions;\r\n this.botOptions.debug ??= process.env.NODE_ENV === \"debug\";\r\n this.botOptions.loggerOptions ??= {};\r\n this.botOptions.loggerOptions.log ??= {} as Record<\r\n \"error\" | \"warn\" | \"debug\" | \"log\" | \"info\",\r\n boolean | (() => boolean)\r\n >;\r\n this.botOptions.loggerOptions.log.debug ??= this.botOptions.debug;\r\n\r\n this.logger = createLogger(this.botOptions.loggerOptions);\r\n this.commands = new CommandManager(this);\r\n this.plugins = new PluginManager(this);\r\n this.multiPageEmbed = new MultiPageEmbedManager(this);\r\n\r\n process.on(\"uncaughtException\", (err) => {\r\n this.logger.error(err);\r\n });\r\n process.on(\"unhandledRejection\", (err, promise) => {\r\n this.logger.error(err);\r\n promise.catch((err) => this.logger.error(err));\r\n });\r\n }\r\n\r\n emoji(name: keyof Emojis) {\r\n return this.clientEmojis[name];\r\n }\r\n\r\n color(name: keyof Colors) {\r\n return this.clientColors[name];\r\n }\r\n\r\n override async login(token?: string) {\r\n if (!this.configParser.isStarted) await this.configParser.start();\r\n token =\r\n token ??\r\n process.env.TOKEN ??\r\n process.env.DISCORD_TOKEN ??\r\n process.env.BOT_TOKEN ??\r\n process.env.DISCORD_BOT_TOKEN ??\r\n this.botOptions.token;\r\n\r\n await this.loadPlugins();\r\n await this.loadEvents();\r\n await this.loadCommands();\r\n if (this.botOptions.debug) this.on(\"debug\", (info) => this.logger.debug(highlight(info, magenta)));\r\n this.once(\"ready\", () => this.logger.log(\"Client logged in as %bl_i.\", `@${this.user!.tag}`));\r\n\r\n const loggedIn = await super.login(token).catch((err) => {\r\n this.logger.error(\"Failed to login to Discord. %r\", \"Exiting...\");\r\n this.logger.error(err);\r\n process.exit(1);\r\n });\r\n await this.application?.fetch();\r\n this.logger.debug(\r\n `Logged in.\\n\\t%m ${this.guilds.cache.size}\\n\\t%m ${this.ws.shards.size}`,\r\n \"Guilds:\",\r\n \"Shards:\",\r\n );\r\n\r\n return loggedIn;\r\n }\r\n\r\n override async destroy() {\r\n this.logger.debug(\"Stopping config parser...\");\r\n this.configParser.stop();\r\n this.logger.debug(\"Clearing commands...\");\r\n this.commands.destroy();\r\n this.logger.debug(\"Destroying client...\");\r\n await super.destroy();\r\n }\r\n\r\n private async load<T>(filePath: string): Promise<{\r\n loaded: T[];\r\n files: Record<\r\n string,\r\n {\r\n status: string;\r\n index?: number;\r\n reason?: string;\r\n }\r\n >;\r\n }> {\r\n if (isGlob(filePath)) {\r\n const globbedFiles = await glob(filePath);\r\n const toLoad: string[] = [];\r\n const files = {};\r\n const loaded: T[] = [];\r\n\r\n for (let file of globbedFiles) {\r\n if (!path.isAbsolute(file)) file = path.join(path.relative(__dirname, process.cwd()), file);\r\n\r\n if ((path.extname(file) !== \".ts\" && isTypescript) || (path.extname(file) !== \".js\" && !isTypescript)) {\r\n files[file] = {\r\n status: \"skipped\",\r\n reason: isTypescript ? \"JS file in TS environment\" : \"TS file in JS environment\",\r\n };\r\n continue;\r\n }\r\n\r\n toLoad.push(file);\r\n }\r\n\r\n for (const file of toLoad) {\r\n try {\r\n const loadedFiles = require(file).default;\r\n loaded.push(loadedFiles);\r\n files[file] = {\r\n status: \"loaded\",\r\n index: loaded.length - 1,\r\n };\r\n } catch (err) {\r\n this.logger.error(err);\r\n files[file] = {\r\n status: \"errored\",\r\n reason: crop(err.message, 50),\r\n };\r\n }\r\n }\r\n\r\n return {\r\n loaded,\r\n files,\r\n };\r\n } else {\r\n if (!path.isAbsolute(filePath)) filePath = path.relative(__dirname, path.join(process.cwd(), filePath));\r\n if (\r\n (path.extname(filePath) !== \".ts\" && isTypescript) ||\r\n (path.extname(filePath) !== \".js\" && !isTypescript)\r\n )\r\n return {\r\n loaded: [],\r\n files: {\r\n [filePath]: {\r\n status: \"skipped\",\r\n reason: isTypescript ? \"JS file in TS environment\" : \"TS file in JS environment\",\r\n },\r\n },\r\n };\r\n\r\n try {\r\n const file = require(filePath).default;\r\n return {\r\n loaded: [file],\r\n files: {\r\n [filePath]: {\r\n status: \"loaded\",\r\n index: 0,\r\n },\r\n },\r\n };\r\n } catch (err) {\r\n this.logger.error(err);\r\n return {\r\n loaded: [],\r\n files: {\r\n [filePath]: {\r\n status: \"errored\",\r\n reason: err.message,\r\n },\r\n },\r\n };\r\n }\r\n }\r\n }\r\n\r\n private async loadCommands() {\r\n const commands = await Promise.all(\r\n this.botOptions.commands.map(async (command) => await this.load<AnyCommand>(command)),\r\n );\r\n const loadedCommands = commands.flatMap((e) => e.loaded);\r\n const loadedCommandsData: {\r\n name: string;\r\n type: string;\r\n status: string;\r\n loadStatus: string;\r\n filePath: string;\r\n }[] = [],\r\n commandFiles = commands.flatMap((e) => Object.entries(e.files));\r\n\r\n for (let i = 0; i < loadedCommands.length; i++) {\r\n try {\r\n const command = loadedCommands[i],\r\n filePath = commandFiles.find((e) => e[1].index === i)![0];\r\n if (command.isIDBased()) {\r\n if (command.isSelectMenuCommand()) this.commands.stores.selectMenu.set(command.id, command);\r\n else if (command.isButtonCommand()) this.commands.stores.button.set(command.id, command);\r\n else if (command.isModalCommand()) this.commands.stores.modal.set(command.id, command);\r\n loadedCommandsData.push({\r\n name: command.id,\r\n type: command.isSelectMenuCommand()\r\n ? \"Select Menu\"\r\n : command.isButtonCommand()\r\n ? \"Button\"\r\n : \"Modal\",\r\n status: command.enabled ? green(\"Enabled\") : red(\"Disabled\"),\r\n loadStatus: bgGreen(`${commandFiles[i][1].status}\\n${commandFiles[i][1].reason ?? \"\"}`),\r\n filePath,\r\n });\r\n } else {\r\n if (command.isSlashCommand()) this.commands.stores.slash.set(command.data.name, command);\r\n else if (command.isContextMenuCommand()) {\r\n if (command.isUserContextMenuCommand())\r\n this.commands.stores.contextMenuUser.set(command.data.name, command);\r\n else this.commands.stores.contextMenuMessage.set(command.data.name, command);\r\n } else if (command.isMessageCommand()) {\r\n this.commands.stores.message.set(command.data.name, command);\r\n for (const name of Object.values(command.data.nameLocalizations ?? {}))\r\n this.commands.stores.message.set(name, command);\r\n for (const alias of command.data.aliases ?? [])\r\n this.commands.stores.message.set(alias, command);\r\n }\r\n loadedCommandsData.push({\r\n name: command.data.name,\r\n type: command.isSlashCommand()\r\n ? \"Slash\"\r\n : command.isContextMenuCommand()\r\n ? \"Context Menu\"\r\n : \"Message\",\r\n status: command.enabled ?? true ? green(\"Enabled\") : red(\"Disabled\"),\r\n loadStatus: bgGreen(`${commandFiles[i][1].status}\\n${commandFiles[i][1].reason ?? \"\"}`),\r\n filePath,\r\n });\r\n }\r\n } catch (err) {\r\n this.logger.error(err);\r\n loadedCommandsData.push({\r\n name: path.basename(commandFiles[i][0], path.extname(commandFiles[i][0])),\r\n type: italic(\"Unknown\"),\r\n status: red(\"errored\"),\r\n loadStatus: bgRed(`${commandFiles[i][1].status}\\n${commandFiles[i][1].reason ?? \"\"}`),\r\n filePath: commandFiles[i][0],\r\n });\r\n }\r\n }\r\n\r\n for (const [filePath, command] of commandFiles) {\r\n if (command.status === \"loaded\") continue;\r\n loadedCommandsData.push({\r\n name: path.basename(filePath, path.extname(filePath)),\r\n type: italic(\"Unknown\"),\r\n status: blackBright(\"Unknown\"),\r\n loadStatus:\r\n command.status === \"skipped\"\r\n ? bgYellow(`${command.status}\\n${command.reason ?? \"\"}`)\r\n : bgRed(`${command.status}\\n${command.reason ?? \"\"}`),\r\n filePath,\r\n });\r\n }\r\n\r\n if (this.botOptions.debug) {\r\n this.logger.debug(\" \");\r\n this.logger.table(\r\n [\r\n [\"Command Name\", \"Type\", \"Status\", \"Load Status\", \"File Path\"].map((h) => bold(h)),\r\n ...loadedCommandsData.map((e) => [e.name, e.type, e.status, e.loadStatus, e.filePath]),\r\n [\r\n bold(\"Total\"),\r\n ...Object.entries(\r\n Object.values(commands).reduce(\r\n (acc, e) => {\r\n Object.values(e.files).forEach((e) => acc[e.status]++);\r\n return acc;\r\n },\r\n { loaded: 0, skipped: 0, errored: 0 },\r\n ),\r\n ).map(([k, v]) => `${k === \"loaded\" ? green(k) : k === \"skipped\" ? yellow(k) : red(k)}: ${v}`),\r\n \"\",\r\n ],\r\n ],\r\n {\r\n header: {\r\n content: magenta(\"Loaded Commands\"),\r\n },\r\n },\r\n );\r\n }\r\n }\r\n\r\n private async loadEvents() {\r\n const events = await Promise.all(\r\n this.botOptions.events.map(async (event) => await this.load<Event<any>>(event)),\r\n );\r\n const loadedEvents = events.flatMap((e) => e.loaded);\r\n\r\n for (const event of loadedEvents) {\r\n try {\r\n if (!(event.enabled ??= true)) continue;\r\n if (event.once)\r\n this.once(event.name, async (...args) => {\r\n for (const prerun of this.plugins.events[event.name]?.prerun ?? []) await prerun(...args);\r\n const result = await event.run.bind(event)(...args);\r\n for (const postrun of this.plugins.events[event.name]?.postrun ?? [])\r\n await postrun(result, ...args);\r\n });\r\n else\r\n this.on(event.name, async (...args) => {\r\n for (const prerun of this.plugins.events[event.name]?.prerun ?? []) await prerun(...args);\r\n const result = await event.run.bind(event)(...args);\r\n for (const postrun of this.plugins.events[event.name]?.postrun ?? [])\r\n await postrun(result, ...args);\r\n });\r\n } catch (err) {\r\n this.logger.error(err);\r\n }\r\n }\r\n\r\n if (this.botOptions.debug) {\r\n this.logger.debug(\" \");\r\n this.logger.table(\r\n [\r\n [\"Event Name\", \"Event\", \"Status\", \"Load Status\", \"File Path\"].map((h) => bold(h)),\r\n ...events.flatMap((e, i) =>\r\n Object.entries(e.files).map(([file, { status, reason, index }]) => [\r\n events[i].loaded[index ?? -1]?.readableName ??\r\n events[i].loaded[index ?? -1]?.name ??\r\n path.basename(file, path.extname(file)),\r\n events[i].loaded[index ?? -1]?.name ?? path.basename(file, path.extname(file)),\r\n events[i].loaded[index ?? -1]\r\n ? events[i].loaded[index ?? -1]?.enabled\r\n ? green(\"Enabled\")\r\n : red(\"Disabled\")\r\n : blackBright(\"Unknown\"),\r\n status === \"loaded\"\r\n ? bgGreen(`${status}\\n${reason ?? \"\"}`)\r\n : status === \"skipped\"\r\n ? bgYellow(`${status}\\n${reason ?? \"\"}`)\r\n : bgRed(`${status}\\n${reason ?? \"\"}`),\r\n file,\r\n ]),\r\n ),\r\n [\r\n bold(\"Total\"),\r\n ...Object.entries(\r\n Object.values(events).reduce(\r\n (acc, e) => {\r\n Object.values(e.files).forEach((e) => acc[e.status]++);\r\n return acc;\r\n },\r\n { loaded: 0, skipped: 0, errored: 0 },\r\n ),\r\n ).map(([k, v]) => `${k === \"loaded\" ? green(k) : k === \"skipped\" ? yellow(k) : red(k)}: ${v}`),\r\n \"\",\r\n ],\r\n ],\r\n {\r\n header: {\r\n content: magenta(\"Loaded Events\"),\r\n },\r\n },\r\n );\r\n }\r\n }\r\n\r\n private async loadPlugins() {\r\n this.botOptions.plugins ??= [];\r\n const plugins = await Promise.all(\r\n this.botOptions.plugins.map(async (plugin) => await this.load<Plugin>(plugin)),\r\n );\r\n const loadedPlugins = plugins.flatMap((e) => e.loaded);\r\n\r\n for (const plugin of loadedPlugins) this.plugins.register(plugin);\r\n\r\n if (this.botOptions.debug) {\r\n this.logger.debug(\" \");\r\n this.logger.table(\r\n [\r\n [\"Plugin Name\", \"Version\", \"Status\", \"Load Status\", \"File Path\"].map((h) => bold(h)),\r\n ...plugins.flatMap((e, i) =>\r\n Object.entries(e.files).map(([file, { status, reason, index }]) => [\r\n plugins[i].loaded[index ?? -1]?.name ?? path.basename(file, path.extname(file)),\r\n plugins[i].loaded[index ?? -1]?.version ?? italic(\"Unknown\"),\r\n plugins[i].loaded[index ?? -1]\r\n ? this.botOptions.enablePlugins === false\r\n ? plugins[i].loaded[index ?? -1]?.enabled === true\r\n ? green(\"Enabled - Forced\")\r\n : red(\"Disabled\")\r\n : plugins[i].loaded[index ?? -1]?.enabled\r\n ? green(\"Enabled\")\r\n : red(\"Disabled\")\r\n : blackBright(\"Unknown\"),\r\n status === \"loaded\"\r\n ? bgGreen(`${status}\\n${reason ?? \"\"}`)\r\n : status === \"skipped\"\r\n ? bgYellow(`${status}\\n${reason ?? \"\"}`)\r\n : bgRed(`${status}\\n${reason ?? \"\"}`),\r\n file,\r\n ]),\r\n ),\r\n [\r\n bold(\"Total\"),\r\n ...Object.entries(\r\n Object.values(plugins).reduce(\r\n (acc, e) => {\r\n Object.values(e.files).forEach((e) => acc[e.status]++);\r\n return acc;\r\n },\r\n { loaded: 0, skipped: 0, errored: 0 },\r\n ),\r\n ).map(([k, v]) => `${k === \"loaded\" ? green(k) : k === \"skipped\" ? yellow(k) : red(k)}: ${v}`),\r\n \"\",\r\n ],\r\n ],\r\n {\r\n header: {\r\n content: magenta(\"Loaded Plugins\"),\r\n },\r\n },\r\n );\r\n }\r\n }\r\n}\r\n\r\nconst parseOptions = <\r\n Configs extends Record<string, any>,\r\n Emojis extends Record<string, Emoji>,\r\n Colors extends Record<string, ColorResolvable>,\r\n>(\r\n configs: Configs,\r\n options: BotOptions<Configs, Emojis, Colors>,\r\n): PlainBotOptions<Configs, Emojis, Colors> => {\r\n const parsedOptions = {} as PlainBotOptions<Configs, Emojis, Colors>;\r\n\r\n for (const [key, value] of Object.entries(options)) {\r\n if (typeof value === \"function\") {\r\n parsedOptions[key] = (value as (configs: Configs) => any)(configs);\r\n } else {\r\n parsedOptions[key] = value;\r\n }\r\n }\r\n\r\n return parsedOptions;\r\n};\r\n\r\nexport * from \"./builders\";\r\nexport * from \"./managers\";\r\nexport * from \"./structures\";\r\nexport * from \"./typings\";\r\n","import * as tty from \"tty\"\n\nconst {\n env = {},\n argv = [],\n platform = \"\",\n} = typeof process === \"undefined\" ? {} : process\n\nconst isDisabled = \"NO_COLOR\" in env || argv.includes(\"--no-color\")\nconst isForced = \"FORCE_COLOR\" in env || argv.includes(\"--color\")\nconst isWindows = platform === \"win32\"\nconst isDumbTerminal = env.TERM === \"dumb\"\n\nconst isCompatibleTerminal =\n tty && tty.isatty && tty.isatty(1) && env.TERM && !isDumbTerminal\n\nconst isCI =\n \"CI\" in env &&\n (\"GITHUB_ACTIONS\" in env || \"GITLAB_CI\" in env || \"CIRCLECI\" in env)\n\nexport const isColorSupported =\n !isDisabled &&\n (isForced || (isWindows && !isDumbTerminal) || isCompatibleTerminal || isCI)\n\nconst replaceClose = (\n index,\n string,\n close,\n replace,\n head = string.substring(0, index) + replace,\n tail = string.substring(index + close.length),\n next = tail.indexOf(close)\n) => head + (next < 0 ? tail : replaceClose(next, tail, close, replace))\n\nconst clearBleed = (index, string, open, close, replace) =>\n index < 0\n ? open + string + close\n : open + replaceClose(index, string, close, replace) + close\n\nconst filterEmpty =\n (open, close, replace = open, at = open.length + 1) =>\n (string) =>\n string || !(string === \"\" || string === undefined)\n ? clearBleed(\n (\"\" + string).indexOf(close, at),\n string,\n open,\n close,\n replace\n )\n : \"\"\n\nconst init = (open, close, replace) =>\n filterEmpty(`\\x1b[${open}m`, `\\x1b[${close}m`, replace)\n\nconst colors = {\n reset: init(0, 0),\n bold: init(1, 22, \"\\x1b[22m\\x1b[1m\"),\n dim: init(2, 22, \"\\x1b[22m\\x1b[2m\"),\n italic: init(3, 23),\n underline: init(4, 24),\n inverse: init(7, 27),\n hidden: init(8, 28),\n strikethrough: init(9, 29),\n black: init(30, 39),\n red: init(31, 39),\n green: init(32, 39),\n yellow: init(33, 39),\n blue: init(34, 39),\n magenta: init(35, 39),\n cyan: init(36, 39),\n white: init(37, 39),\n gray: init(90, 39),\n bgBlack: init(40, 49),\n bgRed: init(41, 49),\n bgGreen: init(42, 49),\n bgYellow: init(43, 49),\n bgBlue: init(44, 49),\n bgMagenta: init(45, 49),\n bgCyan: init(46, 49),\n bgWhite: init(47, 49),\n blackBright: init(90, 39),\n redBright: init(91, 39),\n greenBright: init(92, 39),\n yellowBright: init(93, 39),\n blueBright: init(94, 39),\n magentaBright: init(95, 39),\n cyanBright: init(96, 39),\n whiteBright: init(97, 39),\n bgBlackBright: init(100, 49),\n bgRedBright: init(101, 49),\n bgGreenBright: init(102, 49),\n bgYellowBright: init(103, 49),\n bgBlueBright: init(104, 49),\n bgMagentaBright: init(105, 49),\n bgCyanBright: init(106, 49),\n bgWhiteBright: init(107, 49),\n}\n\nexport const createColors = ({ useColor = isColorSupported } = {}) =>\n useColor\n ? colors\n : Object.keys(colors).reduce(\n (colors, key) => ({ ...colors, [key]: String }),\n {}\n )\n\nexport const {\n reset,\n bold,\n dim,\n italic,\n underline,\n inverse,\n hidden,\n strikethrough,\n black,\n red,\n green,\n yellow,\n blue,\n magenta,\n cyan,\n white,\n gray,\n bgBlack,\n bgRed,\n bgGreen,\n bgYellow,\n bgBlue,\n bgMagenta,\n bgCyan,\n bgWhite,\n blackBright,\n redBright,\n greenBright,\n yellowBright,\n blueBright,\n magentaBright,\n cyanBright,\n whiteBright,\n bgBlackBright,\n bgRedBright,\n bgGreenBright,\n bgYellowBright,\n bgBlueBright,\n bgMagentaBright,\n bgCyanBright,\n bgWhiteBright,\n} = createColors()\n","import { APIInteractionGuildMember, Collection, ColorResolvable, EmbedBuilder, GuildMember, Interaction, LocaleString, Message, PermissionsBitField } from \"discord.js\";\r\nimport { MessageArguments, ICommandManager, SelectMenus, PermissionsMode, RunIn, AnyCommand } from \"../typings/commands\";\r\nimport { Bot } from \"..\";\r\nimport { Emoji } from \"../typings/bot\";\r\nimport { escape } from \"@backs/utils\"\r\nimport { ButtonCommand, ContextMenuCommand, MessageCommand, ModalCommand, SelectMenuCommand, SlashCommand } from \"../structures\";\r\n\r\nexport default class CommandManager<Configs extends Record<string, any>, Emojis extends Record<string, Emoji>, Colors extends Record<string, ColorResolvable>> implements ICommandManager {\r\n public client: Bot<Configs, Emojis, Colors>;\r\n\r\n constructor(client: Bot<Configs, Emojis, Colors>) {\r\n this.client = client;\r\n }\r\n\r\n public stores = {\r\n slash: new Collection<string, SlashCommand>(),\r\n contextMenuUser: new Collection<string, ContextMenuCommand<\"user\">>(),\r\n contextMenuMessage: new Collection<string, ContextMenuCommand<\"message\">>(),\r\n message: new Collection<string, MessageCommand<any>>(),\r\n selectMenu: new Collection<string, SelectMenuCommand<keyof SelectMenus, any>>(),\r\n button: new Collection<string, ButtonCommand<any>>(),\r\n modal: new Collection<string, ModalCommand<any>>(),\r\n };\r\n\r\n public ratelimits = {\r\n user: new Collection<string, { command: string; type: \"message\" | \"slash\" | \"contextMenuUser\" | \"contextMenuMessage\"; expires: number; }[]>(),\r\n guild: new Collection<string, { command: string; type: \"message\" | \"slash\" | \"contextMenuUser\" | \"contextMenuMessage\"; expires: number; }[]>(),\r\n channel: new Collection<string, { command: string; type: \"message\" | \"slash\" | \"contextMenuUser\" | \"contextMenuMessage\"; expires: number; }[]>(),\r\n global: new Collection<string, { command: string; type: \"message\" | \"slash\" | \"contextMenuUser\" | \"contextMenuMessage\"; expires: number; }[]>(),\r\n };\r\n\r\n destroy() {\r\n for (const store of Object.values(this.stores)) store.clear();\r\n for (const ratelimit of Object.values(this.ratelimits)) ratelimit.clear();\r\n }\r\n\r\n async run(intOrMsg: Interaction<\"cached\"> | Message, { commandName, precommand, postcommand, postprocess, postrun, behavior, translations }: {\r\n commandName?: string,\r\n precommand?: (intOrMsg: Interaction<\"cached\"> | Message) => boolean | Promise<boolean>,\r\n postcommand?: (intOrMsg: Interaction<\"cached\"> | Message, command: AnyCommand) => boolean | Promise<boolean>,\r\n postprocess?: (intOrMsg: Interaction<\"cached\"> | Message, command: AnyCommand) => boolean | Promise<boolean>,\r\n postrun?: (intOrMsg: Interaction<\"cached\"> | Message, command: AnyCommand, result: any) => boolean | Promise<boolean>,\r\n behavior?: \"preferIgnore\" | \"preferReply\" | \"auto\",\r\n translations?: Partial<Record<\"notallowed\" | \"ratelimit\" | \"unavailable\", Partial<Record<LocaleString, string>>>>;\r\n }) {\r\n try {\r\n if ((await precommand?.(intOrMsg)) === false) return;\r\n \r\n if (intOrMsg instanceof Message) {\r\n const command = this.stores.message.get(commandName!);\r\n if (!command) {\r\n if (behavior === \"preferReply\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ]\r\n });\r\n else return;\r\n }\r\n \r\n if ((await postcommand?.(intOrMsg, command)) === false) return;\r\n \r\n if (command.enabled === false) {\r\n if (behavior === \"preferReply\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ]\r\n });\r\n else return; \r\n }\r\n \r\n if (\r\n (intOrMsg.inGuild() && command.options?.runIn === RunIn.DM) ||\r\n (!intOrMsg.inGuild() && command.options?.runIn === RunIn.Guild) ||\r\n (intOrMsg.inGuild() && !this.checkPermissions(command, intOrMsg.member!))\r\n ) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.notallowed?.[(intOrMsg.guild?.preferredLocale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"You cannot use this command.\")\r\n .setColor(\"Red\")\r\n ]\r\n })\r\n else return;\r\n }\r\n \r\n if (this.ratelimit(command, intOrMsg.author.id)) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.ratelimit?.[intOrMsg.guild?.preferredLocale ?? \"en_US\"] ?? \"You're a bit too quick there.\")\r\n .setColor(\"Red\")\r\n ]\r\n })\r\n else return;\r\n }\r\n \r\n const args = command.data.arguments ? this.parseArgs(intOrMsg.content.slice(intOrMsg.content.indexOf(commandName!) - 1 + commandName!.length), command.data.arguments) : {};\r\n \r\n if ((await postprocess?.(intOrMsg, command)) === false) return;\r\n \r\n const result = await command.run(intOrMsg, args); \r\n \r\n if ((await postrun?.(intOrMsg, command, result)) === false) return;\r\n } else if (intOrMsg.isCommand()) {\r\n let command: SlashCommand | ContextMenuCommand<\"user\"> | ContextMenuCommand<\"message\"> | undefined;\r\n intOrMsg.member\r\n if (intOrMsg.isChatInputCommand()) command = this.stores.slash.get(intOrMsg.commandName);\r\n else {\r\n if (intOrMsg.isUserContextMenuCommand()) command = this.stores.contextMenuUser.get(intOrMsg.commandName);\r\n else command = this.stores.contextMenuMessage.get(intOrMsg.commandName)\r\n }\r\n \r\n if (!command) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.locale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n })\r\n else return;\r\n }\r\n \r\n if ((await postcommand?.(intOrMsg, command)) === false) return;\r\n \r\n if (command.enabled === false) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n });\r\n else return; \r\n }\r\n \r\n if (\r\n (intOrMsg.inGuild() && command.options?.runIn === RunIn.DM) ||\r\n (!intOrMsg.inGuild() && command.options?.runIn === RunIn.Guild) ||\r\n (intOrMsg.inGuild() && !this.checkPermissions(command, intOrMsg.member))\r\n ) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.notallowed?.[(intOrMsg.locale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"You cannot use this command.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n })\r\n else return;\r\n }\r\n \r\n if (this.ratelimit(command, intOrMsg.user.id)) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.ratelimit?.[(intOrMsg.locale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"You're a bit too quick there.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n })\r\n else return;\r\n }\r\n \r\n if ((await postprocess?.(intOrMsg, command)) === false) return;\r\n \r\n const result = await (command.run as (interaction: Interaction) => any | Promise<any>)(intOrMsg)\r\n \r\n if ((await postrun?.(intOrMsg, command, result)) === false) return;\r\n } else if (intOrMsg.isAutocomplete()) {\r\n const command = this.stores.slash.get(intOrMsg.commandName);\r\n if (!command || command.enabled === false || !(intOrMsg.options.getFocused(true).name in (command.autocomplete ?? {}))) return await intOrMsg.respond([]);\r\n \r\n if ((await postcommand?.(intOrMsg, command)) === false) return;\r\n \r\n const result = await command.autocomplete);\r\n \r\n if ((await postrun?.(intOrMsg, command, result)) === false) return;\r\n } else {\r\n let command: SelectMenuCommand<any, any> | ButtonCommand<any> | ModalCommand<any> | undefined;\r\n const args = this.parseArgs(intOrMsg.customId, command?.arguments ?? []);\r\n \r\n if (!args || !args.id) {\r\n if (behavior !== \"preferIgnore\" || intOrMsg.isModalSubmit()) return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.locale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n })\r\n else return await intOrMsg.update({});\r\n }\r\n\r\n if (intOrMsg.isButton() && args.id === \"multipageembed\") return;\r\n \r\n if (intOrMsg.isButton()) command = this.stores.button.get(args.id);\r\n else if (intOrMsg.isAnySelectMenu()) command = this.stores.selectMenu.get(args.id);\r\n else command = this.stores.modal.get(args.id);\r\n \r\n if (!command) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.locale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n })\r\n else return;\r\n }\r\n \r\n if ((await postcommand?.(intOrMsg, command)) === false) return;\r\n \r\n if (command.enabled === false) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.unavailable?.[(intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"This command is unavailable.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n });\r\n else return; \r\n }\r\n \r\n if (args.userId && intOrMsg.user.id !== args.userId) {\r\n if (behavior !== \"preferIgnore\") return await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(translations?.notallowed?.[(intOrMsg.locale ?? intOrMsg.guild?.preferredLocale) ?? \"en_US\"] ?? \"You are not allowed to use this command.\")\r\n .setColor(\"Red\")\r\n ],\r\n ephemeral: true\r\n })\r\n else return;\r\n }\r\n \r\n if ((await postprocess?.(intOrMsg, command)) === false) return;\r\n \r\n const result = await command.run(intOrMsg, args);\r\n \r\n if ((await postrun?.(intOrMsg, command, result)) === false) return;\r\n }\r\n } catch (err) {\r\n this.client.logger.error(err);\r\n if (intOrMsg instanceof Message) await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(\"An error occurred while executing this command.\")\r\n .setColor(this.client.color(\"error\"))\r\n ]\r\n });\r\n else {\r\n if (intOrMsg.isAutocomplete()) {\r\n if (!intOrMsg.responded) await intOrMsg.respond([]).catch(() => {});\r\n } else if (intOrMsg.replied) await intOrMsg.followUp({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(\"An error occurred while executing this command.\")\r\n .setColor(this.client.color(\"error\"))\r\n ]\r\n }).catch(() => {});\r\n else if (intOrMsg.deferred) await intOrMsg.editReply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(\"An error occurred while executing this command.\")\r\n .setColor(this.client.color(\"error\"))\r\n ]\r\n }).catch(() => {});\r\n else await intOrMsg.reply({\r\n embeds: [\r\n new EmbedBuilder()\r\n .setDescription(\"An error occurred while executing this command.\")\r\n .setColor(this.client.color(\"error\"))\r\n ],\r\n ephemeral: true\r\n }).catch(() => {});\r\n }\r\n }\r\n }\r\n\r\n checkPermissions(command: MessageCommand<any> | SlashCommand | ContextMenuCommand<\"user\" | \"message\">, user: GuildMember | APIInteractionGuildMember) {\r\n if (this.client.botOptions.owners?.includes(user.user.id)) return true;\r\n\r\n const permissions = user instanceof GuildMember ? user.permissions : new PermissionsBitField(BigInt(user.permissions));\r\n const restricted = command.options?.restricted ?? false;\r\n if (!command.options?.permissions) return !restricted;\r\n\r\n if (command.options.permissions.allow) {\r\n const mode = command.options.permissions.allowMode ?? PermissionsMode.MatchOne;\r\n \r\n if (mode === PermissionsMode.MatchOne) {\r\n for (const permission of command.options.permissions.allow)\r\n if (permissions.has(permission, true)) return true;\r\n\r\n return false;\r\n } else if (mode === PermissionsMode.MatchAll) {\r\n for (const permission of command.options.permissions.allow)\r\n if (!permissions.has(permission, true)) return false;\r\n\r\n return true;\r\n } else {\r\n for (const permission of command.options.permissions.allow)\r\n if (permissions.has(permission, true)) return false;\r\n\r\n return true;\r\n }\r\n }\r\n\r\n if (command.options.permissions.deny) {\r\n const mode = command.options.permissions.allowMode ?? PermissionsMode.MatchOne;\r\n \r\n if (mode === PermissionsMode.MatchOne) {\r\n for (const permission of command.options.permissions.deny)\r\n if (permissions.has(permission, true)) return false;\r\n\r\n return true;\r\n } else if (mode === PermissionsMode.MatchAll) {\r\n for (const permission of command.options.permissions.deny)\r\n if (!permissions.has(permission, true)) return true;\r\n\r\n return false;\r\n } else {\r\n for (const permission of command.options.permissions.deny)\r\n if (permissions.has(permission, true)) return true;\r\n\r\n return false;\r\n }\r\n }\r\n\r\n return !restricted;\r\n }\r\n\r\n ratelimit(command: MessageCommand<any> | SlashCommand | ContextMenuCommand<\"user\" | \"message\">, userId: string) {\r\n const ratelimit = command.options?.ratelimit\r\n if (!ratelimit) return false;\r\n\r\n for (const [key, value] of Object.entries(ratelimit)) {\r\n if (value === 0) continue;\r\n const now = Date.now();\r\n const ratelimits = this.ratelimits[key as keyof typeof this.ratelimits];\r\n \r\n if (ratelimits.has(userId)) {\r\n const userRatelimits = ratelimits.get(userId)!;\r\n ratelimits.set(userId, userRatelimits.filter(r => r.expires > now));\r\n const commandRatelimit = userRatelimits.find(r => r.command === command.data.name && r.type === command.commandType);\r\n if (commandRatelimit && commandRatelimit.expires > now) return true;\r\n }\r\n \r\n const expires = now + value;\r\n ratelimits.set(userId, [...(ratelimits.get(userId) ?? []), { command: command.data.name, type: command.commandType, expires }]);\r\n }\r\n\r\n return false;\r\n }\r\n\r\n parseArgs<T extends string>(args: string, format: T): MessageArguments<T>;\r\n parseArgs<T extends string[]>(args: string, format: T): { [K in T[number]]?: string } & { id: string; userId?: string };\r\n parseArgs<T extends string | string[]>(args: string, format: T) {\r\n if (Array.isArray(format)) {\r\n const argsArray = args.split(\"/\");\r\n\r\n return {\r\n id: argsArray.shift(),\r\n userId: argsArray.shift(),\r\n ...argsArray.reduce((o, v, i) => { o[format[i]] = v; return o; }, {} as { [Key in T[number]]?: string })\r\n } as { [Key in T[number]]?: string } & { id: string; userId?: string };\r\n } else if (typeof format === \"string\") {\r\n const argDefinitions = args.match(/<[^<>\\[\\]]>|\\[[^<>\\[\\]]\\]>/g) ?? [],\r\n parsedArgs: Record<string, string | boolean | undefined> = {},\r\n content = args.split(/\\s+/);\r\n\r\n const getNext = (required: boolean) => {\r\n const c = content.shift();\r\n if (required && !c) throw new Error(\"Missing required argument.\");\r\n else if (!c) return undefined;\r\n else return c;\r\n }\r\n \r\n for (let i = 0; i < argDefinitions.length; i++) {\r\n const arg = argDefinitions[i];\r\n const [name, options] = arg.match(/(?:<|\\[)([a-zA-Z0-9_-]+)(:(?:[a-zA-Z0-9_-],? *)+|\\((?:.+)\\)|\\.\\.\\.)?(?:>|])/) ?? [];\r\n const isRequired = arg.startsWith(\"<\") && arg.endsWith(\">\");\r\n\r\n if (!isRequired && (!arg.startsWith(\"[\") || !arg.endsWith(\"]\"))) throw new Error(\"Invalid format.\");\r\n if (!name) throw new Error(\"Invalid format.\");\r\n\r\n if (options) {\r\n if (options.endsWith(\"...\")) {\r\n if (i !== (argDefinitions.length - 1)) throw new Error(\"Rest arguments must be the last argument.\");\r\n if (content.length === 0) {\r\n if (isRequired) throw new Error(\"Missing required argument.\");\r\n else continue;\r\n }\r\n\r\n const c = args.match(new RegExp(`\\\\s(${escape(content[0])}\\s+)`))![1];\r\n parsedArgs[name] = c;\r\n }\r\n else if (options.startsWith(\"(\") && options.endsWith(\")\")) {\r\n const r