-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add interaction handler #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,102 @@ | ||
| import { EmbedBuilder } from "discord.js"; | ||
| import { | ||
| ActionRowBuilder, | ||
| EmbedBuilder, | ||
| StringSelectMenuBuilder, | ||
| } from "discord.js"; | ||
| import path from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| import type { CommandCallbackOpts } from "../../types/command.ts"; | ||
| import { readFile } from "../../utils/fileOps.ts"; | ||
|
|
||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
|
|
||
| interface VersionSection { | ||
| title: string; | ||
| content: string; | ||
| } | ||
|
|
||
| function parseChangelog(content: string): VersionSection[] { | ||
| if (!content?.trim()) return []; | ||
|
|
||
| const parts = content.split(/^## /m); | ||
| const versions: VersionSection[] = []; | ||
|
|
||
| for (let i = 1; i < parts.length; i++) { | ||
| const section = parts[i]?.trim(); | ||
| if (!section) continue; | ||
|
|
||
| const firstNewline = section.indexOf("\n"); | ||
| const title = ( | ||
| firstNewline === -1 ? section : section.slice(0, firstNewline) | ||
| ).trim(); | ||
| const body = ( | ||
| firstNewline === -1 ? "" : section.slice(firstNewline) | ||
| ).trim(); | ||
|
|
||
| if (title) { | ||
| versions.push({ | ||
| title, | ||
| content: `## ${title}\n\n${body}`, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return versions; | ||
| } | ||
|
|
||
| export default { | ||
| name: "changelog", | ||
| description: "See the latest changes made to the bot.", | ||
| usage: "changelog", | ||
| aliases: ["changes", "updates"], | ||
| async callback({ message }: CommandCallbackOpts) { | ||
| const changelog = await readFile( | ||
| path.join(__dirname, "..", "..", "..", "CHANGELOG.md"), | ||
| ); | ||
| const latestChanges = | ||
| "## " + | ||
| changelog | ||
| ?.split(/^## /m)[1] | ||
| ?.trim() | ||
| .replace(/\r?\n\r?\n/g, "\n"); | ||
|
|
||
| const changelogEmbed = new EmbedBuilder() | ||
| .setTitle("📝 Latest Changes") | ||
| .setDescription(latestChanges || "No changelog available.") | ||
| .setColor(0x7289da); | ||
|
|
||
| await message.reply({ embeds: [changelogEmbed] }); | ||
| try { | ||
| const changelogContent = | ||
| (await readFile( | ||
| path.join(__dirname, "..", "..", "..", "CHANGELOG.md"), | ||
| )) || ""; | ||
|
|
||
| const versions = parseChangelog(changelogContent); | ||
|
|
||
| if (versions.length === 0) { | ||
| return message.reply({ | ||
| embeds: [ | ||
| new EmbedBuilder() | ||
| .setTitle("📝 Changelog") | ||
| .setDescription("No changelog available.") | ||
| .setColor(0x7289da), | ||
| ], | ||
| }); | ||
| } | ||
|
|
||
| const latest = versions[0]; | ||
|
|
||
| const embed = new EmbedBuilder() | ||
| .setTitle("📝 Latest Changes") | ||
| .setDescription( | ||
| latest?.content || "No changes listed for the latest version.", | ||
| ) | ||
| .setColor(0x7289da); | ||
|
|
||
| const selectMenu = new StringSelectMenuBuilder() | ||
| .setCustomId("changelog_select") | ||
| .setPlaceholder("Select a version to view its changes") | ||
| .addOptions( | ||
| versions.map((v, i) => ({ | ||
| label: | ||
| v.title.length > 100 ? v.title.slice(0, 97) + "..." : v.title, | ||
| value: i.toString(), | ||
| ...(i === 0 ? { description: "Latest version" } : {}), | ||
| })), | ||
| ); | ||
|
|
||
| const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents( | ||
| selectMenu, | ||
| ); | ||
|
|
||
| await message.reply({ embeds: [embed], components: [row] }); | ||
| } catch (err) { | ||
| console.error("Error fetching changelog:", err); | ||
| } | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,121 @@ | ||||||||||||||||||
| import { Client, EmbedBuilder, type Interaction } from "discord.js"; | ||||||||||||||||||
| import "dotenv/config"; | ||||||||||||||||||
| import path, { join } from "path"; | ||||||||||||||||||
| import { fileURLToPath } from "url"; | ||||||||||||||||||
| import config from "../../../config.json" with { type: "json" }; | ||||||||||||||||||
| import getAllFiles from "../../utils/getAllFiles.ts"; | ||||||||||||||||||
|
|
||||||||||||||||||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||||||||||||||||||
|
|
||||||||||||||||||
| interface InteractionObject { | ||||||||||||||||||
| id: string; | ||||||||||||||||||
| callback: Function; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const interactionsPath = join(__dirname, "..", "..", "interactions"); | ||||||||||||||||||
|
|
||||||||||||||||||
| let cachedInteractions: InteractionObject[] = []; | ||||||||||||||||||
|
|
||||||||||||||||||
| async function loadInteractions() { | ||||||||||||||||||
| const interactionFiles = getAllFiles(interactionsPath); | ||||||||||||||||||
| const interactionPromises: Promise<any>[] = []; | ||||||||||||||||||
|
|
||||||||||||||||||
| for (const file of interactionFiles) { | ||||||||||||||||||
| interactionPromises.push(import(file)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const imported = await Promise.all(interactionPromises); | ||||||||||||||||||
| cachedInteractions = imported.map((intObj) => intObj.default).filter(Boolean); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // load at startup | ||||||||||||||||||
| await loadInteractions(); | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The top-level Confidence: 5/5 Suggested Fix
Suggested change
Wrap the top-level await in a try-catch block to handle initialization failures gracefully. Exit the process explicitly if interactions can't be loaded, as the bot cannot function without them. Prompt for AICopy this prompt to your AI IDE to fix this issue locally: |
||||||||||||||||||
|
|
||||||||||||||||||
| export default async (client: Client, interaction: Interaction) => { | ||||||||||||||||||
| const { devs } = config; | ||||||||||||||||||
|
|
||||||||||||||||||
| if ( | ||||||||||||||||||
| process.env.NODE_ENV?.toLowerCase() === "dev" && | ||||||||||||||||||
| !devs.includes(interaction.user.id) | ||||||||||||||||||
| ) | ||||||||||||||||||
| return; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!interaction || interaction.user?.bot) return; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Keep track of interaction id across try/catch scope | ||||||||||||||||||
| let interactionObject: InteractionObject | undefined; | ||||||||||||||||||
| let interactionId = ""; | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| // Determine identifier: commandName for slash/context/autocomplete, customId for components/modals | ||||||||||||||||||
| if ( | ||||||||||||||||||
| interaction.isChatInputCommand() || | ||||||||||||||||||
| interaction.isContextMenuCommand() || | ||||||||||||||||||
| interaction.isAutocomplete() | ||||||||||||||||||
| ) { | ||||||||||||||||||
| interactionId = interaction.commandName; | ||||||||||||||||||
| } else if ( | ||||||||||||||||||
| interaction.isButton() || | ||||||||||||||||||
| interaction.isStringSelectMenu() || | ||||||||||||||||||
| interaction.isUserSelectMenu() || | ||||||||||||||||||
| interaction.isRoleSelectMenu() || | ||||||||||||||||||
| interaction.isMentionableSelectMenu() || | ||||||||||||||||||
| interaction.isModalSubmit() | ||||||||||||||||||
| ) { | ||||||||||||||||||
| interactionId = interaction.customId; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!interactionId) return; | ||||||||||||||||||
|
|
||||||||||||||||||
| interactionObject = cachedInteractions.find((intObj) => { | ||||||||||||||||||
| if (!intObj?.id) return false; | ||||||||||||||||||
| return intObj.id === interactionId; | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!interactionObject) return; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (typeof interactionObject.callback === "function") { | ||||||||||||||||||
| await interactionObject.callback({ client, interaction }); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| console.error( | ||||||||||||||||||
| `Invalid interaction configuration for id: ${interactionObject.id}`, | ||||||||||||||||||
| ); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| const intId = interactionObject?.id || interactionId || "unknown"; | ||||||||||||||||||
|
|
||||||||||||||||||
| console.error(`Error executing ${intId} interaction: ${err}`); | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| const errorEmbed = new EmbedBuilder() | ||||||||||||||||||
| .setTitle("❌ Interaction Error") | ||||||||||||||||||
| .setDescription("An error occurred while running that interaction.") | ||||||||||||||||||
| .setColor(0xff0000); | ||||||||||||||||||
|
|
||||||||||||||||||
| if ((interaction as any).replied || (interaction as any).deferred) { | ||||||||||||||||||
| await (interaction as any) | ||||||||||||||||||
| .followUp({ embeds: [errorEmbed], ephemeral: true }) | ||||||||||||||||||
| .catch((followUpErr: Error) => { | ||||||||||||||||||
| console.error( | ||||||||||||||||||
| `Failed to send followUp error message for ${intId}:`, | ||||||||||||||||||
| followUpErr, | ||||||||||||||||||
| ); | ||||||||||||||||||
| }); | ||||||||||||||||||
| } else { | ||||||||||||||||||
| await (interaction as any) | ||||||||||||||||||
| .reply({ embeds: [errorEmbed], ephemeral: true }) | ||||||||||||||||||
| .catch((replyErr: Error) => { | ||||||||||||||||||
| console.error( | ||||||||||||||||||
| `Failed to send reply error message for ${intId}:`, | ||||||||||||||||||
| replyErr, | ||||||||||||||||||
| ); | ||||||||||||||||||
| }); | ||||||||||||||||||
| } | ||||||||||||||||||
| } catch (replyErr) { | ||||||||||||||||||
| console.error( | ||||||||||||||||||
| `Failed to send error reply for ${intId} interaction: ${replyErr}`, | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,55 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
| EmbedBuilder, | ||||||||||||||||||||||||||||||||||||||||||||
| PermissionFlagsBits, | ||||||||||||||||||||||||||||||||||||||||||||
| type Client, | ||||||||||||||||||||||||||||||||||||||||||||
| type Message, | ||||||||||||||||||||||||||||||||||||||||||||
| } from "discord.js"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { detectCode } from "../../utils/detectCode.ts"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export default async (client: Client, message: Message) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (message.author.bot) return; | ||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||
| !message.guild?.members.me?.permissions.has( | ||||||||||||||||||||||||||||||||||||||||||||
| PermissionFlagsBits.ManageMessages, | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const content = message.content; | ||||||||||||||||||||||||||||||||||||||||||||
| const { isCode, score, sections } = detectCode(content); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (!isCode) return; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // attempt to delete the message, if fail, return | ||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||
| await message.delete(); | ||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||
| // console.error("Failed to delete message:", error); | ||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for permission-related failures: The code attempts to delete a message but only catches generic errors. If the bot loses the Confidence: 5/5 Suggested Fix
Suggested change
Why this matters: While the current code does catch errors, adding specific error type checking would help with debugging and monitoring. However, since the user rules specify only reporting issues that cause bugs or severe problems, and this code does handle the error by returning, this is more of a code quality concern than a critical bug. Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 24-29 |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const embed = new EmbedBuilder() | ||||||||||||||||||||||||||||||||||||||||||||
| .setAuthor({ | ||||||||||||||||||||||||||||||||||||||||||||
| name: message.author.tag, | ||||||||||||||||||||||||||||||||||||||||||||
| iconURL: message.author.displayAvatarURL(), | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| .setColor("#FF0000") | ||||||||||||||||||||||||||||||||||||||||||||
| // .setTitle(`<@${message.author.id}> said:`) | ||||||||||||||||||||||||||||||||||||||||||||
| .setDescription( | ||||||||||||||||||||||||||||||||||||||||||||
| sections | ||||||||||||||||||||||||||||||||||||||||||||
| .map((section) => | ||||||||||||||||||||||||||||||||||||||||||||
| section.type === "code" | ||||||||||||||||||||||||||||||||||||||||||||
| ? `\`\`\`${section.language || "txt"}\n${section.text}\`\`\`` | ||||||||||||||||||||||||||||||||||||||||||||
| : section.text, | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| .join("\n\n"), | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const channel = message.channel; | ||||||||||||||||||||||||||||||||||||||||||||
| if (channel.isTextBased() && "send" in channel) { | ||||||||||||||||||||||||||||||||||||||||||||
| await channel.send({ | ||||||||||||||||||||||||||||||||||||||||||||
| content: `<@${message.author.id}> said:`, | ||||||||||||||||||||||||||||||||||||||||||||
| embeds: [embed], | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested Fix
Suggested change
Wrap the Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 50-53 |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing error handling for embed send operation: After successfully deleting the user's message, if the Confidence: 5/5 Suggested Fix
Suggested change
Why this matters: This is a critical user experience issue. If the bot deletes a user's message but then fails to repost it, the user's content is permanently lost. This can happen due to:
Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 48-54 |
||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Top-level await blocks the entire module loading process. If
loadInteractions()fails or takes a long time (e.g., due to slow file I/O or a large number of interaction files), it will prevent the entire bot from starting up. This creates a single point of failure during initialization and makes the bot unresponsive until all interactions are loaded.Confidence: 5/5
Suggested Fix
Move the interaction loading into the event handler's initialization or use a lazy-loading pattern. Consider wrapping it in a try-catch and providing fallback behavior:
This prevents the bot from being completely blocked if interaction loading fails and provides better error recovery.
Prompt for AI
Copy this prompt to your AI IDE to fix this issue locally: