diff --git a/.prettierignore b/.prettierignore index 28e6c5e..e9b5295 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ .husky/ Dockerfile LICENSE +*.txt # Specific to this repository dist/ diff --git a/package-lock.json b/package-lock.json index edb79de..6dd4a8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@discordjs/builders": "^0.8.2", "canvas": "^2.8.0", "chalk": "^4.1.2", "discord.js": "^13.3.0", + "dotenv": "^10.0.0", "figlet": "^1.5.2", "glob": "^7.2.0", "inquirer": "^8.2.0", @@ -653,13 +655,13 @@ "dev": true }, "node_modules/@discordjs/builders": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.1.tgz", - "integrity": "sha512-kYJMvZ/BjRD1/6G2t1pQop2yoJNUmYvvKeG4mOBUCHFmfb7WIeBFmN/eSiP3cVSfRx3lbNiyxkdd5JzhjQnGbg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.2.tgz", + "integrity": "sha512-/YRd11SrcluqXkKppq/FAVzLIPRVlIVmc6X8ZklspzMIHDtJ+A4W37D43SHvLdH//+NnK+SHW/WeOF4Ts54PeQ==", "dependencies": { "@sindresorhus/is": "^4.2.0", "discord-api-types": "^0.24.0", - "ow": "^0.28.1", + "ow": "^0.27.0", "ts-mixer": "^6.0.0", "tslib": "^2.3.1" }, @@ -2172,6 +2174,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -4523,15 +4533,15 @@ } }, "node_modules/ow": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.1.tgz", - "integrity": "sha512-1EZTywPZeUKac9gD7q8np3Aj+V54kvfIcjNEVNDSbG2Ys5xA5foW2HquvMMqgyWGLqIFMlc0Iq/HmyMHqN48sA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz", + "integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==", "dependencies": { - "@sindresorhus/is": "^4.2.0", + "@sindresorhus/is": "^4.0.1", "callsites": "^3.1.0", "dot-prop": "^6.0.1", "lodash.isequal": "^4.5.0", - "type-fest": "^2.3.4", + "type-fest": "^1.2.1", "vali-date": "^1.0.0" }, "engines": { @@ -4542,11 +4552,11 @@ } }, "node_modules/ow/node_modules/type-fest": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.5.2.tgz", - "integrity": "sha512-WMbytmAs5PUTqwGJRE+WoRrD2S0bYFtHX8k4Y/1l18CG5kqA3keJud9pPQ/r30FE9n8XRFCXF9BbccHIZzRYJw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "engines": { - "node": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6378,13 +6388,13 @@ "dev": true }, "@discordjs/builders": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.1.tgz", - "integrity": "sha512-kYJMvZ/BjRD1/6G2t1pQop2yoJNUmYvvKeG4mOBUCHFmfb7WIeBFmN/eSiP3cVSfRx3lbNiyxkdd5JzhjQnGbg==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.2.tgz", + "integrity": "sha512-/YRd11SrcluqXkKppq/FAVzLIPRVlIVmc6X8ZklspzMIHDtJ+A4W37D43SHvLdH//+NnK+SHW/WeOF4Ts54PeQ==", "requires": { "@sindresorhus/is": "^4.2.0", "discord-api-types": "^0.24.0", - "ow": "^0.28.1", + "ow": "^0.27.0", "ts-mixer": "^6.0.0", "tslib": "^2.3.1" } @@ -7576,6 +7586,11 @@ "is-obj": "^2.0.0" } }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -9265,22 +9280,22 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "ow": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.1.tgz", - "integrity": "sha512-1EZTywPZeUKac9gD7q8np3Aj+V54kvfIcjNEVNDSbG2Ys5xA5foW2HquvMMqgyWGLqIFMlc0Iq/HmyMHqN48sA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz", + "integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==", "requires": { - "@sindresorhus/is": "^4.2.0", + "@sindresorhus/is": "^4.0.1", "callsites": "^3.1.0", "dot-prop": "^6.0.1", "lodash.isequal": "^4.5.0", - "type-fest": "^2.3.4", + "type-fest": "^1.2.1", "vali-date": "^1.0.0" }, "dependencies": { "type-fest": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.5.2.tgz", - "integrity": "sha512-WMbytmAs5PUTqwGJRE+WoRrD2S0bYFtHX8k4Y/1l18CG5kqA3keJud9pPQ/r30FE9n8XRFCXF9BbccHIZzRYJw==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==" } } }, diff --git a/package.json b/package.json index 7d554df..2f1ed92 100644 --- a/package.json +++ b/package.json @@ -5,19 +5,21 @@ "main": "dist/index.js", "scripts": { "build": "rimraf dist && tsc --project tsconfig.prod.json && npm prune --production", - "start": "node .", + "start": "node -r dotenv/config .", "once": "tsc && npm start", "dev": "tsc-watch --onSuccess \"npm run dev-instance\"", - "dev-fast": "tsc-watch --onSuccess \"node . dev\"", - "dev-instance": "rimraf dist && tsc && node . dev", + "dev-fast": "tsc-watch --onSuccess \"node -r dotenv/config . dev\"", + "dev-instance": "rimraf dist && tsc && node -r dotenv/config . dev", "test": "jest", "format": "prettier --write **/*", "postinstall": "husky install" }, "dependencies": { + "@discordjs/builders": "^0.8.2", "canvas": "^2.8.0", "chalk": "^4.1.2", "discord.js": "^13.3.0", + "dotenv": "^10.0.0", "figlet": "^1.5.2", "glob": "^7.2.0", "inquirer": "^8.2.0", diff --git a/prettier.config.js b/prettier.config.js index f27d170..dad4ad0 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -8,7 +8,7 @@ module.exports = { jsxSingleQuote: false, trailingComma: "none", bracketSpacing: false, - jsxBracketSameLine: false, + bracketSameLine: false, arrowParens: "always", endOfLine: "auto" // Apparently, the GitHub repository still uses CRLF. I don't know how to force it to use LF, and until someone figures that out, I'm changing this to auto because I don't want more than one line ending commit. }; diff --git a/slash-cmd-brainstorming.txt b/slash-cmd-brainstorming.txt new file mode 100644 index 0000000..dadd3e0 --- /dev/null +++ b/slash-cmd-brainstorming.txt @@ -0,0 +1,91 @@ +-= [Fun] =- +/8ball +/cookie all +/cookie (<@user>) +/eco show (<@user>) +/eco daily - "... Same as /eco get." +/eco get - "... Same as /eco daily." +/eco pay <@user> +/eco guild +/eco leaderboard - "... Same as /eco top." +/eco top - "... Same as /eco leaderboard." +/eco buy +/eco shop +/eco monday +/eco bet <@user> +/eco award <@user> () +/eco post +/eco delete --- "Operation successful. 5000 rows affected." +/figlet +/insult +/love +/ok +/owoify +/party +/pat (<@user>) +/poll () +/ravi () +/thonk () +/urban +/vaporwave +/weather +/whoami +/whois <@user> + +-= [System] =- ("/admin other ..." if needed) +/admin prefix () +/admin messageembeds +/admin welcome type <[none, text, graphical]: string> +/admin welcome channel <#channel> +/admin stream <#channel> +/admin defaultname () +/admin logs () +/admin status +/admin clear +/admin nickname +/admin guilds +/admin activity () +/admin syslog +/admin autoroles list +/admin autoroles reset +/admin autoroles add <@role> +/admin autoroles remove <@role> +/admin streamroles set <@role> +/admin streamroles remove <@role> +/webhook register +/webhook delete + +-= [Utility] =- +/calc +/code +/desc +/docs +/emote +/info +/info avatar (<@user>) +/info bot +/info guild +/info guild +/info <@user> +/invite () +/lsemotes () +/purge +/react () +/say +/scanemotes () +/shorten +/stream description set () +/stream description remove +/stream thumbnail set () +/stream thumbnail remove +/stream category set () +/stream category remove +/time show (<@user>) +/time setup +/time delete +/time utc +/time daylight +/todo show +/todo add +/todo remove +/todo clear \ No newline at end of file diff --git a/src/commands/fun/whoami.ts b/src/commands/fun/whoami.ts new file mode 100644 index 0000000..23995a4 --- /dev/null +++ b/src/commands/fun/whoami.ts @@ -0,0 +1,16 @@ +import {SlashCommandBuilder} from "@discordjs/builders"; +import {CommandInteraction} from "discord.js"; +import {registry} from "./whois"; + +export const header = new SlashCommandBuilder().setDescription("Tells you who you are"); + +export function handler(interaction: CommandInteraction) { + const {user} = interaction; + const id = user.id; + + if (id in registry) { + interaction.reply({content: `${user} ${registry[id]}`, allowedMentions: {parse: []}}); + } else { + interaction.reply("You haven't been added to the registry yet!"); + } +} diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index 08a7e59..7047ae2 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -1,8 +1,10 @@ import {User} from "discord.js"; import {Command, NamedCommand, getUserByNickname, RestCommand} from "onion-lasers"; +import {SlashCommandBuilder} from "@discordjs/builders"; +import {CommandInteraction} from "discord.js"; // Quotes must be used here or the numbers will change -const registry: {[id: string]: string} = { +export const registry: {[id: string]: string} = { "465662909645848577": "You're an idiot, that's what.", "306499531665833984": "Kuma, you eldritch fuck, I demand you to release me from this Discord bot and let me see my Chromebook!", @@ -50,6 +52,24 @@ const registry: {[id: string]: string} = { "138840343855497216": "your face is a whois entry" }; +export const header = new SlashCommandBuilder() + .setDescription("Tells you who the specified user is") + .addUserOption((option) => + option.setName("target").setDescription("The person to inquire about").setRequired(true) + ); + +export function handler(interaction: CommandInteraction) { + const {options} = interaction; + const user = options.getUser("target", true); + const id = user.id; + + if (id in registry) { + interaction.reply({content: `${user} ${registry[id]}`, allowedMentions: {parse: []}}); + } else { + interaction.reply({content: `${user} hasn't been added to the registry yet!`, allowedMentions: {parse: []}}); + } +} + export default new NamedCommand({ description: "Tells you who you or the specified user is.", aliases: ["whoami"], diff --git a/src/index.ts b/src/index.ts index 4de5ccf..dc8aef6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,9 @@ export const client = new Client({ Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS, Intents.FLAGS.DIRECT_MESSAGES + ], + partials: [ + "CHANNEL" // Needed so the bot can receive DM messages ] }); @@ -76,6 +79,7 @@ launch(client, path.join(__dirname, "commands"), { }); // Initialize Modules // +import "./modules/slashCommands"; import "./modules/ready"; import "./modules/presence"; // TODO: Reimplement entire music system, contact Sink diff --git a/src/modules/slashCommands.ts b/src/modules/slashCommands.ts new file mode 100644 index 0000000..a8ce34f --- /dev/null +++ b/src/modules/slashCommands.ts @@ -0,0 +1,54 @@ +import {client} from "../index"; +import {join} from "path"; +import {loadCommands} from "../modules/slashCommandsLoader"; + +loadCommands(join(__dirname, "..", "commands")).then((result) => { + const [headers, handlers] = result; + const useDevGuild = IS_DEV_MODE && !!process.env.DEV_GUILD; + + // Send slash command data to Discord + + client.on("ready", async () => { + try { + if (useDevGuild) { + await client.guilds.cache + .get(process.env.DEV_GUILD!)! + .commands.set(headers.map((header) => header.toJSON())); + } else { + await client.application!.commands.set(headers.map((header) => header.toJSON())); + } + + console.log( + `Successfully loaded command definitions into Discord using ${ + useDevGuild ? "development" : "production" + } mode.` + ); + } catch (error) { + console.error(error); + } + }); + + // Listen for slash commands + + client.on("interactionCreate", async (interaction) => { + if (interaction.isCommand()) { + if (handlers.has(interaction.commandName)) { + try { + await handlers.get(interaction.commandName)!(interaction); + } catch (error) { + console.error(error); + } + + // Use these when implementing subcommands and subcommand groups + // interaction.options.getSubcommandGroup(false); // string if exists, null if not + // interaction.options.getSubcommand(false); // string if exists, null if not + } else { + interaction.reply({ + content: + "**Error:** Invalid command name! This probably means that the command definitions forgot to be updated.", + ephemeral: true + }); + } + } + }); +}); diff --git a/src/modules/slashCommandsLoader.ts b/src/modules/slashCommandsLoader.ts new file mode 100644 index 0000000..a12f855 --- /dev/null +++ b/src/modules/slashCommandsLoader.ts @@ -0,0 +1,76 @@ +import {Collection, CommandInteraction} from "discord.js"; +import {SlashCommandBuilder} from "@discordjs/builders"; +import glob from "glob"; +import path from "path"; + +export async function loadCommands( + commandsDir: string +): Promise< + [Collection, Collection Promise>] +> { + // Add a trailing separator so that the reduced filename list will reliably cut off the starting part. + // "C:/some/path/to/commands" --> "C:/some/path/to/commands/" (and likewise for \) + commandsDir = path.normalize(commandsDir); + if (!commandsDir.endsWith(path.sep)) commandsDir += path.sep; + + const headers = new Collection(); + const handlers = new Collection Promise>(); + const files = await globP(path.join(commandsDir, "**", "*.js")); // This stage filters out source maps (.js.map). + // Because glob will use / regardless of platform, the following regex pattern can rely on / being the case. + const filesClean = files.map((filename) => filename.substring(commandsDir.length)); + // Extract the usable parts from commands directory if the path is 1 to 2 subdirectories (a or a/b, not a/b/c). + // No further checks will be made to exclude template command files or test command files, keeping it structure-agnostic. + const pattern = /^([^/]+(?:\/[^/]+)?)\.js$/; + + for (let i = 0; i < files.length; i++) { + const match = pattern.exec(filesClean[i]); + if (!match) continue; + const commandID = match[1]; // e.g. "utilities/info" + const slashIndex = commandID.indexOf("/"); + const isMiscCommand = slashIndex !== -1; + const commandName = isMiscCommand ? commandID.substring(slashIndex + 1) : commandID; // e.g. "info" + + // This try-catch block MUST be here or Node.js' dynamic require() will silently fail. + try { + // If the dynamic import works, it must be an object at the very least. Then, just test to see if it's a proper instance. + const {header, handler} = (await import(files[i])) as {header: unknown; handler: unknown}; + + if (header instanceof SlashCommandBuilder && handler instanceof Function) { + if (headers.has(commandName) || handlers.has(commandName)) { + console.warn( + `Command "${commandID}" already exists! Make sure to make each command uniquely identifiable across categories!` + ); + } else { + // Set the slash command name to the filename only if there isn't already a name set + if (header.name === undefined) { + header.setName(commandName); + } + + headers.set(header.name, header); + handlers.set(header.name, handler as any); // Just got to hope that the user puts in good data + console.log(`Loaded Command: "${commandID}" as "${header.name}"`); // Use header.name to show what the slash command name is (should be the same) + } + } else { + console.warn( + `Command "${commandID}" doesn't export a "header" property (SlashCommandBuilder instance) or a "handler" property (function)!` + ); + } + } catch (error) { + console.error(error); + } + } + + return [headers, handlers]; +} + +function globP(path: string) { + return new Promise((resolve, reject) => { + glob(path, (error, files) => { + if (error) { + reject(error); + } else { + resolve(files); + } + }); + }); +}