feat(bot): boilerplate for adding interaction commands and event listeners

This commit is contained in:
Corban-Lee Jones 2025-05-13 16:57:07 +01:00
parent 55c6a7125a
commit cf8713c1bb
10 changed files with 224 additions and 17 deletions

View File

@ -1,23 +1,22 @@
import { Client, GatewayIntentBits, ActivityType } from "discord.js";
import { Client, GatewayIntentBits } from "discord.js";
import EventHandler from "@bot/handlers/events";
import InteractionHandler from "@bot/handlers/interactions";
import { triggerTask } from "@bot/task";
export const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildWebhooks
]
})
export default class DiscordBot extends Client {
constructor() {
super({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildWebhooks, // May not need?
] });
client.on("ready", () => {
if (!client.user) {
throw Error("Client is null");
this.login(process.env.BOT_TOKEN);
}
setInterval(() => triggerTask(client), 5000);
public events = new EventHandler(this);
public interactions = new InteractionHandler(this);
}
client.user.setActivity("new sources", {type: ActivityType.Watching});
console.log(`Discord Bot ${client.user.displayName} is online!`)
});
client.login(process.env.BOT_TOKEN);
export const client = new DiscordBot();

View File

@ -0,0 +1,15 @@
import DiscordBot from "@bot/bot";
export default class Event {
readonly client: DiscordBot;
name!: string;
once!: boolean;
constructor(client: DiscordBot) {
this.client = client;
}
execute(..._args: unknown[]) {
throw new Error("No execute override");
}
}

View File

@ -0,0 +1,32 @@
import { RESTPostAPIApplicationCommandsJSONBody, SlashCommandBuilder } from "discord.js";
import DiscordBot from "@bot/bot";
export default class Interaction {
readonly client: DiscordBot;
name!: string;
description: string = "No description";
options: any[] = [];
dmPermission!: boolean;
constructor(client: DiscordBot) {
this.client = client;
}
execute(..._args: unknown[]) {
throw new Error("No execute override");
}
toJSON(): RESTPostAPIApplicationCommandsJSONBody {
const command = new SlashCommandBuilder();
command.setName(this.name);
command.setDescription(this.description);
command.setDMPermission(this.dmPermission); // TODO: deprecated - replace
for (const option of this.options) {
command.options.push(option);
}
return command.toJSON();
}
}

View File

@ -0,0 +1,14 @@
import { Interaction } from "discord.js";
import Event from "@bot/components/event";
export default class interactionCreate extends Event {
name = "interactionCreate";
async execute(interaction: Interaction) {
if (!interaction.isChatInputCommand()) return;
await interaction.deferReply();
const command = this.client.interactions.get(interaction.commandName);
return command!.execute(interaction);
}
}

19
src/bot/events/ready.ts Normal file
View File

@ -0,0 +1,19 @@
import { ActivityType } from "discord.js";
import Event from "@bot/components/event";
import DiscordBot from "@bot/bot";
export default class Ready extends Event {
name = "ready";
once = true;
async execute(client: DiscordBot): Promise<void> {
if (!client.user) {
throw new Error("Discord client is not truthy");
}
await client.interactions.deploy();
client.user.setActivity("new sources", {type: ActivityType.Watching});
console.log(`Discord Bot ${client.user.displayName} is online!`);
}
}

View File

@ -0,0 +1,31 @@
import { join } from "path";
import { Collection } from "discord.js";
import getAllFiles from "@bot/utils/getAllFiles";
import Event from "@bot/components/event";
import DiscordBot from "@bot/bot";
export default class EventHandler extends Collection<string, Event> {
readonly client: DiscordBot;
constructor(client: DiscordBot) {
super();
this.client = client
this.init();
}
private async init() {
const eventsDirectory = join(__dirname, "../events");
const modules = getAllFiles(eventsDirectory);
for (const module of modules) {
const eventClass = ((r) => r.default || r)(require(module));
const event: Event = new eventClass(this.client);
this.set(event.name, event);
this.client[event.once ? "once" : "on"](
event.name,
(...args: unknown[]) => event.execute(...args)
);
}
}
}

View File

@ -0,0 +1,38 @@
import { join } from "path";
import { Collection, REST, Routes } from "discord.js";
import getAllFiles from "@bot/utils/getAllFiles";
import Interaction from "@bot/components/interaction";
import DiscordBot from "@bot/bot";
export default class InteractionHandler extends Collection<string, Interaction> {
readonly client: DiscordBot;
constructor(client: DiscordBot) {
super();
this.client = client;
this.init();
}
private async init() {
const interactionsDirectory = join(__dirname, "../interactions");
const modules = getAllFiles(interactionsDirectory);
for (const module of modules) {
const interactionClass = ((r) => r.default || r)(require(module))
const interaction: Interaction = new interactionClass(this.client);
this.set(interaction.name, interaction);
}
}
async deploy() {
const interactions = this.map(inter => inter.toJSON())
const rest = new REST({ version: "10" }).setToken(process.env.BOT_TOKEN!);
for (const [_, guild] of this.client.guilds.cache) {
rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID!, guild.id),
{ body: interactions }
)
}
}
}

View File

@ -0,0 +1,20 @@
import { CommandInteraction, EmbedBuilder } from "discord.js";
import Interaction from "@bot/components/interaction";
export default class Ping extends Interaction {
name = "ping";
description = "Measure the bot's ability to respond.";
async execute(interaction: CommandInteraction) {
const execTime = Math.abs(Date.now() - interaction.createdTimestamp);
const apiLatency = Math.floor(this.client.ws.ping);
const embed = new EmbedBuilder();
embed.addFields([
{ name: "Command Time", value: `${execTime}ms` },
{ name: "Discord API Latency", value: `${apiLatency}ms` }
])
return interaction.editReply({ embeds: [embed] });
}
}

View File

@ -0,0 +1,17 @@
import { CommandInteraction } from "discord.js";
import Interaction from "@bot/components/interaction";
import { triggerTask } from "@bot/task";
export default class Trigger extends Interaction {
name = "trigger";
description = "Perform a single process of the feeds."
async execute(interaction: CommandInteraction) {
await triggerTask(this.client);
const execTime = Math.abs(Date.now() - interaction.createdTimestamp);
return interaction.editReply({
content: `Completed in \`${execTime}ms\``
});
}
}

View File

@ -0,0 +1,22 @@
import fs from "fs";
import { join } from "path";
const getAllFiles = (directoryPath: string, existingResult?: string[]) => {
const fileNames = fs.readdirSync(directoryPath);
let result: string[] = existingResult ?? [];
for (const fileName of fileNames) {
if (!(fileName.endsWith(".ts") || fileName.endsWith(".js"))) continue;
const fullPath = join(directoryPath, fileName);
fs.statSync(fullPath).isDirectory()
? result = getAllFiles(fullPath, result)
: result.push(fullPath);
}
return result
};
export default getAllFiles;