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
This commit is contained in:
Wroclaw 2023-05-08 08:53:06 +02:00
parent 56a0e686b0
commit 8b4b35454b
6 changed files with 124 additions and 2 deletions

21
package-lock.json generated
View file

@ -13,10 +13,12 @@
"discord.js": "^14.8.0", "discord.js": "^14.8.0",
"fold-to-ascii": "^5.0.1", "fold-to-ascii": "^5.0.1",
"gpt-3-encoder": "^1.1.4", "gpt-3-encoder": "^1.1.4",
"openai": "^3.2.1" "openai": "^3.2.1",
"require-directory": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fold-to-ascii": "^5.0.0", "@types/fold-to-ascii": "^5.0.0",
"@types/require-directory": "^2.1.2",
"@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0", "@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0", "eslint": "^8.36.0",
@ -295,6 +297,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.2.tgz",
"integrity": "sha512-sDPHm2wfx2QhrMDK0pOt2J4KLJMAcerqWNvnED0itPRJWvI+bK+uNHzcH1dFsBlf7G3u8tqXmRF3wkvL9yUwMw==" "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": { "node_modules/@types/semver": {
"version": "7.3.13", "version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
@ -1710,6 +1721,14 @@
"url": "https://github.com/sponsors/Borewit" "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": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",

View file

@ -14,10 +14,12 @@
"discord.js": "^14.8.0", "discord.js": "^14.8.0",
"fold-to-ascii": "^5.0.1", "fold-to-ascii": "^5.0.1",
"gpt-3-encoder": "^1.1.4", "gpt-3-encoder": "^1.1.4",
"openai": "^3.2.1" "openai": "^3.2.1",
"require-directory": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/fold-to-ascii": "^5.0.0", "@types/fold-to-ascii": "^5.0.0",
"@types/require-directory": "^2.1.2",
"@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0", "@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0", "eslint": "^8.36.0",

41
src/command.ts Normal file
View file

@ -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<Type extends keyof InteractionTypeMap = ApplicationCommandType> {
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<Type extends keyof InteractionTypeMap = ApplicationCommandType> {
abstract execute(interaction: InteractionTypeMap[Type][0]): Promise<void>;
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;

16
src/commands/summon.ts Normal file
View file

@ -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);
}
}

View file

@ -4,6 +4,7 @@ import { PrismaClient } from "@prisma/client";
import config from "./config"; import config from "./config";
import { queueRequest } from "./execution"; import { queueRequest } from "./execution";
import InteractionManager from "./interactionManager";
const discord = new DiscordApi.Client({ const discord = new DiscordApi.Client({
intents: [ intents: [
@ -19,6 +20,9 @@ export const openai = new OpenAIApi(new OpenAIApiConfiguration({
export const database = new PrismaClient(); export const database = new PrismaClient();
const interactionManager = new InteractionManager();
interactionManager.bindClient(discord);
discord.on("ready", async event => { discord.on("ready", async event => {
console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`); console.log(`Connected to Discord as ${event.user.tag} (${event.user.id})`);
}); });

40
src/interactionManager.ts Normal file
View file

@ -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));
}
}