diff --git a/src/bot/bot.ts b/src/bot/bot.ts index ca9e306..638f485 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -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); \ No newline at end of file +export const client = new DiscordBot(); diff --git a/src/bot/components/event.ts b/src/bot/components/event.ts new file mode 100644 index 0000000..e248c3d --- /dev/null +++ b/src/bot/components/event.ts @@ -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"); + } +} diff --git a/src/bot/components/interaction.ts b/src/bot/components/interaction.ts new file mode 100644 index 0000000..fd8592c --- /dev/null +++ b/src/bot/components/interaction.ts @@ -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(); + } +} diff --git a/src/bot/events/interactionCreate.ts b/src/bot/events/interactionCreate.ts new file mode 100644 index 0000000..c442708 --- /dev/null +++ b/src/bot/events/interactionCreate.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/bot/events/ready.ts b/src/bot/events/ready.ts new file mode 100644 index 0000000..f238b62 --- /dev/null +++ b/src/bot/events/ready.ts @@ -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 { + 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!`); + } +} diff --git a/src/bot/handlers/events.ts b/src/bot/handlers/events.ts new file mode 100644 index 0000000..5df6547 --- /dev/null +++ b/src/bot/handlers/events.ts @@ -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 { + 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) + ); + } + } +} \ No newline at end of file diff --git a/src/bot/handlers/interactions.ts b/src/bot/handlers/interactions.ts new file mode 100644 index 0000000..d7d7011 --- /dev/null +++ b/src/bot/handlers/interactions.ts @@ -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 { + 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 } + ) + } + } +} diff --git a/src/bot/interactions/ping.ts b/src/bot/interactions/ping.ts new file mode 100644 index 0000000..967f41d --- /dev/null +++ b/src/bot/interactions/ping.ts @@ -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] }); + } +} diff --git a/src/bot/interactions/trigger.ts b/src/bot/interactions/trigger.ts new file mode 100644 index 0000000..9e6f3e8 --- /dev/null +++ b/src/bot/interactions/trigger.ts @@ -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\`` + }); + } +} diff --git a/src/bot/utils/getAllFiles.ts b/src/bot/utils/getAllFiles.ts new file mode 100644 index 0000000..f8e9429 --- /dev/null +++ b/src/bot/utils/getAllFiles.ts @@ -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; \ No newline at end of file