From 8b4b35454b4670dd259636a5921cfca144189975 Mon Sep 17 00:00:00 2001 From: Wroclaw Date: Mon, 8 May 2023 08:53:06 +0200 Subject: [PATCH] Add commandManager and the first slash command the command allows for summining the bot without sending an actual mention message that might hang in the chat log sent to openAi, consuming tokens --- package-lock.json | 21 +++++++++++++++++++- package.json | 4 +++- src/command.ts | 41 +++++++++++++++++++++++++++++++++++++++ src/commands/summon.ts | 16 +++++++++++++++ src/index.ts | 4 ++++ src/interactionManager.ts | 40 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/command.ts create mode 100644 src/commands/summon.ts create mode 100644 src/interactionManager.ts 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)); + } +}