diff --git a/package-lock.json b/package-lock.json index 8bd83db..c1c7dff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "discord.js": "^14.8.0", "fold-to-ascii": "^5.0.1", "gpt-3-encoder": "^1.1.4", - "openai": "^3.2.1" + "openai": "^3.2.1", + "require-directory": "^2.1.1" }, "devDependencies": { "@types/fold-to-ascii": "^5.0.0", + "@types/require-directory": "^2.1.2", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "eslint": "^8.36.0", @@ -295,6 +297,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.2.tgz", "integrity": "sha512-sDPHm2wfx2QhrMDK0pOt2J4KLJMAcerqWNvnED0itPRJWvI+bK+uNHzcH1dFsBlf7G3u8tqXmRF3wkvL9yUwMw==" }, + "node_modules/@types/require-directory": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/require-directory/-/require-directory-2.1.2.tgz", + "integrity": "sha512-FUG5PJ2rsV2TssSspVZefTR8+wH3Ahr6KdAB6WanLNroSDu0A5ew4WVUxnnGU/E1k6nzip9ZawGAAe/ZqzGn5g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -1710,6 +1721,14 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index aba236c..fc0484d 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,12 @@ "discord.js": "^14.8.0", "fold-to-ascii": "^5.0.1", "gpt-3-encoder": "^1.1.4", - "openai": "^3.2.1" + "openai": "^3.2.1", + "require-directory": "^2.1.1" }, "devDependencies": { "@types/fold-to-ascii": "^5.0.0", + "@types/require-directory": "^2.1.2", "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.55.0", "eslint": "^8.36.0", diff --git a/src/command.ts b/src/command.ts new file mode 100644 index 0000000..4ef8a64 --- /dev/null +++ b/src/command.ts @@ -0,0 +1,41 @@ +import { PermissionsBitField } from "discord.js"; +import { RESTPostAPIApplicationCommandsJSONBody } from "discord.js"; +import { ApplicationCommandOption, ApplicationCommandType, ChatInputCommandInteraction, LocalizationMap, MessageInteraction, PermissionResolvable, UserSelectMenuInteraction } from "discord.js"; + +type InteractionTypeMap = { + [ApplicationCommandType.ChatInput]: [ChatInputCommandInteraction, string]; + [ApplicationCommandType.Message]: [MessageInteraction, never]; + [ApplicationCommandType.User]: [UserSelectMenuInteraction, never]; +}; + +interface Command { + readonly name: string; + readonly name_localizations?: LocalizationMap; + readonly description: InteractionTypeMap[Type][1]; + readonly description_localizations?: LocalizationMap; + readonly options?: ApplicationCommandOption[]; + readonly default_member_permissions?: PermissionResolvable; + readonly type: Type; + readonly nsfw?: boolean; + readonly dm_permission?: boolean; +} + +abstract class Command { + abstract execute(interaction: InteractionTypeMap[Type][0]): Promise; + + toRESTPostApplicationCommands(): RESTPostAPIApplicationCommandsJSONBody { + return { + name: this.name, + name_localizations: this.name_localizations, + description: this.description, + description_localizations: this.description_localizations, + options: this.options, + default_member_permissions: this.default_member_permissions !== undefined ? new PermissionsBitField(this.default_member_permissions).bitfield.toString() : undefined, + type: this.type, + nsfw: this.nsfw, + dm_permission: this.dm_permission, + }; + } +} + +export default Command; diff --git a/src/commands/summon.ts b/src/commands/summon.ts new file mode 100644 index 0000000..0ce7407 --- /dev/null +++ b/src/commands/summon.ts @@ -0,0 +1,16 @@ +import { ApplicationCommandType, ChatInputCommandInteraction } from "discord.js"; + +import Command from "../command"; +import { queueRequest } from "../execution"; + + +export default class Summon extends Command { + name = "summon"; + description = "Summons a bot to reply in chat without sending any message"; + type = ApplicationCommandType.ChatInput; + dm_permission = false; + + async execute(interaction: ChatInputCommandInteraction) { + queueRequest(interaction); + } +} diff --git a/src/index.ts b/src/index.ts index cba9b7b..35f01ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { PrismaClient } from "@prisma/client"; import config from "./config"; import { queueRequest } from "./execution"; +import InteractionManager from "./interactionManager"; const discord = new DiscordApi.Client({ intents: [ @@ -19,6 +20,9 @@ export const openai = new OpenAIApi(new OpenAIApiConfiguration({ export const database = new PrismaClient(); +const interactionManager = new InteractionManager(); +interactionManager.bindClient(discord); + discord.on("ready", async event => { console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`); }); diff --git a/src/interactionManager.ts b/src/interactionManager.ts new file mode 100644 index 0000000..3b7968b --- /dev/null +++ b/src/interactionManager.ts @@ -0,0 +1,40 @@ +import { Interaction, Client as DiscordClient } from "discord.js"; +import requireDirectory from "require-directory"; + +import Command from "./command"; + +export default class CommandManager { + readonly commands: Command[] = []; + + constructor(directory = "./commands") { + const files = requireDirectory(module, directory); + for (const i in files ) { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.commands.push(new (files[i].default as Command)()); + } + catch (e) { + console.error(`Failed to construct command ${i} (${typeof e}):`); + console.error(e); + } + } + } + + onInteraction(interaction: Interaction) { + if ( + interaction.isChatInputCommand() || + interaction.isMessageContextMenuCommand() || + interaction.isUserContextMenuCommand() + ) { + const foundCommand = this.commands.find((command) => command.name == interaction.commandName ); + if (!foundCommand) throw new Error(`Unknown command received (${interaction.commandName}). Did you forgot to push updated commands?`); + foundCommand.execute(interaction); + return; + } + } + + bindClient(client: DiscordClient) { + client.on("interactionCreate", (e) => this.onInteraction(e)); + } +}