From 251584c5464c99128044cfd5f698c8cc6caaf587 Mon Sep 17 00:00:00 2001 From: Nathanial Henniges <19924836+nathanialhenniges@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:56:31 -0500 Subject: [PATCH] refactor: extract duplicated patterns into shared helper utilities Create 4 shared helpers (safeErrorReply, requireOwner, sendToMainChannel, updateGuildPremiumStatus) to replace copy-pasted patterns across 28 files, removing ~200 lines of duplicated code while preserving identical behavior. Co-Authored-By: Claude Opus 4.6 --- src/commands/about.ts | 10 +++----- src/commands/admin/activity/create.ts | 8 ++---- src/commands/admin/activity/list.ts | 8 ++---- src/commands/admin/activity/remove.ts | 9 ++----- src/commands/admin/index.ts | 8 ++---- src/commands/admin/quote/create.ts | 23 +++--------------- src/commands/admin/quote/list.ts | 8 ++---- src/commands/admin/quote/remove.ts | 23 ++++++------------ src/commands/admin/suggestion/approve.ts | 17 +++---------- src/commands/admin/suggestion/list.ts | 8 ++---- src/commands/admin/suggestion/reject.ts | 17 +++---------- src/commands/admin/suggestion/stats.ts | 8 ++---- src/commands/changelog.ts | 8 ++---- src/commands/help.ts | 8 ++---- src/commands/invite.ts | 8 ++---- src/commands/owner/index.ts | 21 +++------------- src/commands/owner/premium/testCreate.ts | 24 ++++++------------ src/commands/owner/premium/testDelete.ts | 21 +++------------- src/commands/owner/premium/testList.ts | 21 +++------------- src/commands/premium.ts | 8 ++---- src/commands/quote.ts | 10 +++----- src/commands/setup/channel.ts | 8 ++---- src/commands/setup/index.ts | 8 ++---- src/commands/setup/schedule.ts | 13 ++-------- src/commands/suggestion.ts | 17 +++---------- src/events/entitlementCreate.ts | 15 ++---------- src/events/entitlementDelete.ts | 15 ++---------- src/events/entitlementUpdate.ts | 15 ++---------- src/utils/commandErrors.ts | 19 +++++++++++++++ src/utils/entitlementHelpers.ts | 29 ++++++++++++++++++++++ src/utils/mainChannel.ts | 31 ++++++++++++++++++++++++ src/utils/ownerGuard.ts | 28 +++++++++++++++++++++ 32 files changed, 189 insertions(+), 285 deletions(-) create mode 100644 src/utils/commandErrors.ts create mode 100644 src/utils/entitlementHelpers.ts create mode 100644 src/utils/mainChannel.ts create mode 100644 src/utils/ownerGuard.ts diff --git a/src/commands/about.ts b/src/commands/about.ts index ffe83b6..e152578 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -1,9 +1,10 @@ -import { EmbedBuilder, MessageFlags, SlashCommandBuilder } from "discord.js"; +import { EmbedBuilder, SlashCommandBuilder } from "discord.js"; import type { Client, CommandInteraction, User } from "discord.js"; import env from "../utils/env.js"; import logger from "../utils/logger.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; export const slashCommand = new SlashCommandBuilder() .setName("about") @@ -76,12 +77,7 @@ export async function execute(client: Client, interaction: CommandInteraction) { command: "about", }); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/activity/create.ts b/src/commands/admin/activity/create.ts index a11e5f8..435d194 100644 --- a/src/commands/admin/activity/create.ts +++ b/src/commands/admin/activity/create.ts @@ -5,6 +5,7 @@ import type { CommandInteractionOptionResolver } from "discord.js"; import type { DiscordActivityType } from "../../../database/schema.js"; import logger from "../../../utils/logger.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { discordActivities } from "../../../database/schema.js"; @@ -73,11 +74,6 @@ export default async function ( err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/activity/list.ts b/src/commands/admin/activity/list.ts index 0c59d72..142eb84 100644 --- a/src/commands/admin/activity/list.ts +++ b/src/commands/admin/activity/list.ts @@ -5,6 +5,7 @@ import { desc } from "drizzle-orm"; import type { DiscordActivity } from "../../../database/schema.js"; import logger from "../../../utils/logger.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { discordActivities } from "../../../database/schema.js"; @@ -68,11 +69,6 @@ export default async function ( err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/activity/remove.ts b/src/commands/admin/activity/remove.ts index 7bb7081..7179ac9 100644 --- a/src/commands/admin/activity/remove.ts +++ b/src/commands/admin/activity/remove.ts @@ -5,6 +5,7 @@ import type { CommandInteractionOptionResolver } from "discord.js"; import { eq } from "drizzle-orm"; import logger from "../../../utils/logger.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { discordActivities } from "../../../database/schema.js"; @@ -71,12 +72,6 @@ export default async function ( err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: - "An error occurred while deleting the activity. Please try again.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/index.ts b/src/commands/admin/index.ts index f226d2b..a10c761 100644 --- a/src/commands/admin/index.ts +++ b/src/commands/admin/index.ts @@ -10,6 +10,7 @@ import type { CommandInteractionOptionResolver, } from "discord.js"; import logger from "../../utils/logger.js"; +import { safeErrorReply } from "../../utils/commandErrors.js"; /** * Import subcommands @@ -290,12 +291,7 @@ export async function execute(client: Client, interaction: CommandInteraction) { err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/quote/create.ts b/src/commands/admin/quote/create.ts index 631df86..48bceaf 100644 --- a/src/commands/admin/quote/create.ts +++ b/src/commands/admin/quote/create.ts @@ -10,8 +10,9 @@ import type { CommandInteractionOptionResolver } from "discord.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { motivationQuotes } from "../../../database/schema.js"; -import env from "../../../utils/env.js"; import logger from "../../../utils/logger.js"; +import { sendToMainChannel } from "../../../utils/mainChannel.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( client: Client, @@ -83,18 +84,7 @@ export default async function ( }) .setTimestamp(); - if (env.MAIN_CHANNEL_ID) { - const channel = await client.channels.fetch(env.MAIN_CHANNEL_ID); - if (channel?.isTextBased() && !channel.isDMBased()) { - await channel.send({ embeds: [embed] }); - } else { - logger.warn("Admin", "Main channel not found or not text-based", { - channelId: env.MAIN_CHANNEL_ID, - }); - } - } else { - logger.warn("Admin", "MAIN_CHANNEL_ID not configured"); - } + await sendToMainChannel(client, { embeds: [embed] }); await interaction.reply({ content: `Quote created with id: ${newQuote.id}`, @@ -114,11 +104,6 @@ export default async function ( err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/quote/list.ts b/src/commands/admin/quote/list.ts index a367e8c..0eb1202 100644 --- a/src/commands/admin/quote/list.ts +++ b/src/commands/admin/quote/list.ts @@ -5,6 +5,7 @@ import { desc } from "drizzle-orm"; import type { MotivationQuote } from "../../../database/schema.js"; import logger from "../../../utils/logger.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { motivationQuotes } from "../../../database/schema.js"; @@ -67,11 +68,6 @@ export default async function ( err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/quote/remove.ts b/src/commands/admin/quote/remove.ts index 99b1928..31a969a 100644 --- a/src/commands/admin/quote/remove.ts +++ b/src/commands/admin/quote/remove.ts @@ -12,7 +12,8 @@ import logger from "../../../utils/logger.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { motivationQuotes } from "../../../database/schema.js"; -import env from "../../../utils/env.js"; +import { sendToMainChannel } from "../../../utils/mainChannel.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( client: Client, @@ -49,15 +50,10 @@ export default async function ( await db.delete(motivationQuotes).where(eq(motivationQuotes.id, quoteId)); - // send message to main channel - if (env.MAIN_CHANNEL_ID) { - const mainChannel = await client.channels.fetch(env.MAIN_CHANNEL_ID); - if (mainChannel?.isTextBased() && !mainChannel.isDMBased()) { - await mainChannel.send( - `Quote deleted by ${interaction.user.username} with id: ${quoteId}` - ); - } - } + await sendToMainChannel( + client, + `Quote deleted by ${interaction.user.username} with id: ${quoteId}` + ); await interaction.reply({ content: `Quote deleted with id: ${quoteId}`, @@ -77,11 +73,6 @@ export default async function ( err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/suggestion/approve.ts b/src/commands/admin/suggestion/approve.ts index 995b6a1..b86f2ac 100644 --- a/src/commands/admin/suggestion/approve.ts +++ b/src/commands/admin/suggestion/approve.ts @@ -12,8 +12,9 @@ import { eq } from "drizzle-orm"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { motivationQuotes, suggestionQuotes } from "../../../database/schema.js"; -import env from "../../../utils/env.js"; import logger from "../../../utils/logger.js"; +import { sendToMainChannel } from "../../../utils/mainChannel.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( client: Client, @@ -88,12 +89,7 @@ export default async function ( .setFooter({ text: `Suggestion ID: ${suggestionId}` }) .setTimestamp(); - if (env.MAIN_CHANNEL_ID) { - const channel = await client.channels.fetch(env.MAIN_CHANNEL_ID); - if (channel?.isTextBased() && !channel.isDMBased()) { - await channel.send({ embeds: [embed] }); - } - } + await sendToMainChannel(client, { embeds: [embed] }); try { const submitter = await client.users.fetch(suggestion.addedBy); @@ -135,11 +131,6 @@ export default async function ( err, ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/suggestion/list.ts b/src/commands/admin/suggestion/list.ts index 3da6fa5..a783853 100644 --- a/src/commands/admin/suggestion/list.ts +++ b/src/commands/admin/suggestion/list.ts @@ -6,6 +6,7 @@ import { eq, desc } from "drizzle-orm"; import type { SuggestionQuote } from "../../../database/schema.js"; import logger from "../../../utils/logger.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { suggestionQuotes } from "../../../database/schema.js"; @@ -73,11 +74,6 @@ export default async function ( err, ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/suggestion/reject.ts b/src/commands/admin/suggestion/reject.ts index 6afe0b8..000a847 100644 --- a/src/commands/admin/suggestion/reject.ts +++ b/src/commands/admin/suggestion/reject.ts @@ -12,8 +12,9 @@ import { eq, and } from "drizzle-orm"; import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { suggestionQuotes } from "../../../database/schema.js"; -import env from "../../../utils/env.js"; import logger from "../../../utils/logger.js"; +import { sendToMainChannel } from "../../../utils/mainChannel.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( client: Client, @@ -98,12 +99,7 @@ export default async function ( .setFooter({ text: `Suggestion ID: ${suggestionId}` }) .setTimestamp(); - if (env.MAIN_CHANNEL_ID) { - const channel = await client.channels.fetch(env.MAIN_CHANNEL_ID); - if (channel?.isTextBased() && !channel.isDMBased()) { - await channel.send({ embeds: [embed] }); - } - } + await sendToMainChannel(client, { embeds: [embed] }); try { const submitter = await client.users.fetch(suggestion.addedBy); @@ -148,11 +144,6 @@ export default async function ( err, ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/admin/suggestion/stats.ts b/src/commands/admin/suggestion/stats.ts index af4451c..239a248 100644 --- a/src/commands/admin/suggestion/stats.ts +++ b/src/commands/admin/suggestion/stats.ts @@ -6,6 +6,7 @@ import { isUserPermitted } from "../../../utils/permissions.js"; import { db } from "../../../database/index.js"; import { suggestionQuotes } from "../../../database/schema.js"; import logger from "../../../utils/logger.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( _client: Client, @@ -71,11 +72,6 @@ export default async function ( err, ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/changelog.ts b/src/commands/changelog.ts index 4062908..2fdb882 100644 --- a/src/commands/changelog.ts +++ b/src/commands/changelog.ts @@ -3,6 +3,7 @@ import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"; import type { Client, CommandInteraction } from "discord.js"; import logger from "../utils/logger.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; export const slashCommand = new SlashCommandBuilder() .setName("changelog") @@ -81,12 +82,7 @@ export async function execute(_client: Client, interaction: CommandInteraction) } ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/help.ts b/src/commands/help.ts index f83f524..ec0a11a 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -3,6 +3,7 @@ import type { Client, CommandInteraction } from "discord.js"; import { SlashCommandBuilder, MessageFlags } from "discord.js"; import logger from "../utils/logger.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; export const slashCommand = new SlashCommandBuilder() .setName("help") @@ -47,12 +48,7 @@ export async function execute(_client: Client, interaction: CommandInteraction) command: "help", }); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/invite.ts b/src/commands/invite.ts index 3817b2e..f547a2b 100644 --- a/src/commands/invite.ts +++ b/src/commands/invite.ts @@ -3,6 +3,7 @@ import { SlashCommandBuilder, OAuth2Scopes, MessageFlags } from "discord.js"; import type { Client, CommandInteraction } from "discord.js"; import logger from "../utils/logger.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; export const slashCommand = new SlashCommandBuilder() .setName("invite") @@ -46,12 +47,7 @@ export async function execute(client: Client, interaction: CommandInteraction) { command: "invite", }); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/owner/index.ts b/src/commands/owner/index.ts index 2980894..f060667 100644 --- a/src/commands/owner/index.ts +++ b/src/commands/owner/index.ts @@ -7,7 +7,8 @@ import type { } from "discord.js"; import logger from "../../utils/logger.js"; -import env from "../../utils/env.js"; +import { requireOwner } from "../../utils/ownerGuard.js"; +import { safeErrorReply } from "../../utils/commandErrors.js"; /** * Import subcommands @@ -62,16 +63,7 @@ export async function execute(client: Client, interaction: CommandInteraction) { interaction.user.id ); - if (interaction.user.id !== env.OWNER_ID) { - logger.commands.unauthorized( - "owner", - interaction.user.username, - interaction.user.id - ); - await interaction.reply({ - content: "Only the bot owner can use this command.", - flags: MessageFlags.Ephemeral, - }); + if (!(await requireOwner(interaction, "owner"))) { return; } @@ -125,12 +117,7 @@ export async function execute(client: Client, interaction: CommandInteraction) { command: "owner", }); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/owner/premium/testCreate.ts b/src/commands/owner/premium/testCreate.ts index 8fa1af0..0f67c62 100644 --- a/src/commands/owner/premium/testCreate.ts +++ b/src/commands/owner/premium/testCreate.ts @@ -3,8 +3,9 @@ import { MessageFlags } from "discord.js"; import type { Client, CommandInteraction, CommandInteractionOptionResolver } from "discord.js"; import logger from "../../../utils/logger.js"; -import env from "../../../utils/env.js"; import { getPremiumSkuId } from "../../../utils/premium.js"; +import { requireOwner } from "../../../utils/ownerGuard.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( client: Client, @@ -18,16 +19,7 @@ export default async function ( interaction.user.id ); - if (interaction.user.id !== env.OWNER_ID) { - logger.commands.unauthorized( - "owner premium test-create", - interaction.user.username, - interaction.user.id - ); - await interaction.reply({ - content: "Only the bot owner can use this command.", - flags: MessageFlags.Ephemeral, - }); + if (!(await requireOwner(interaction, "owner premium test-create"))) { return; } @@ -93,11 +85,9 @@ export default async function ( } ); - if (!interaction.replied) { - await interaction.reply({ - content: `Failed to create test entitlement: ${err instanceof Error ? err.message : String(err)}`, - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply( + interaction, + `Failed to create test entitlement: ${err instanceof Error ? err.message : String(err)}` + ); } } diff --git a/src/commands/owner/premium/testDelete.ts b/src/commands/owner/premium/testDelete.ts index e454de5..284bebc 100644 --- a/src/commands/owner/premium/testDelete.ts +++ b/src/commands/owner/premium/testDelete.ts @@ -3,7 +3,8 @@ import { MessageFlags } from "discord.js"; import type { Client, CommandInteraction, CommandInteractionOptionResolver } from "discord.js"; import logger from "../../../utils/logger.js"; -import env from "../../../utils/env.js"; +import { requireOwner } from "../../../utils/ownerGuard.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function ( client: Client, @@ -17,16 +18,7 @@ export default async function ( interaction.user.id ); - if (interaction.user.id !== env.OWNER_ID) { - logger.commands.unauthorized( - "owner premium test-delete", - interaction.user.username, - interaction.user.id - ); - await interaction.reply({ - content: "Only the bot owner can use this command.", - flags: MessageFlags.Ephemeral, - }); + if (!(await requireOwner(interaction, "owner premium test-delete"))) { return; } @@ -69,11 +61,6 @@ export default async function ( } ); - if (!interaction.replied) { - await interaction.reply({ - content: "Failed to delete test entitlement. Check bot logs for details.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction, "Failed to delete test entitlement. Check bot logs for details."); } } diff --git a/src/commands/owner/premium/testList.ts b/src/commands/owner/premium/testList.ts index c91d469..b2fe9e2 100644 --- a/src/commands/owner/premium/testList.ts +++ b/src/commands/owner/premium/testList.ts @@ -3,7 +3,8 @@ import { MessageFlags } from "discord.js"; import type { Client, CommandInteraction } from "discord.js"; import logger from "../../../utils/logger.js"; -import env from "../../../utils/env.js"; +import { requireOwner } from "../../../utils/ownerGuard.js"; +import { safeErrorReply } from "../../../utils/commandErrors.js"; export default async function (client: Client, interaction: CommandInteraction): Promise { try { @@ -13,16 +14,7 @@ export default async function (client: Client, interaction: CommandInteraction): interaction.user.id ); - if (interaction.user.id !== env.OWNER_ID) { - logger.commands.unauthorized( - "owner premium test-list", - interaction.user.username, - interaction.user.id - ); - await interaction.reply({ - content: "Only the bot owner can use this command.", - flags: MessageFlags.Ephemeral, - }); + if (!(await requireOwner(interaction, "owner premium test-list"))) { return; } @@ -92,11 +84,6 @@ export default async function (client: Client, interaction: CommandInteraction): } ); - if (!interaction.replied) { - await interaction.reply({ - content: "Failed to list entitlements. Check bot logs for details.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction, "Failed to list entitlements. Check bot logs for details."); } } diff --git a/src/commands/premium.ts b/src/commands/premium.ts index 4abed13..c541664 100644 --- a/src/commands/premium.ts +++ b/src/commands/premium.ts @@ -10,6 +10,7 @@ import { import type { Client, CommandInteraction } from "discord.js"; import logger from "../utils/logger.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; import { isPremiumEnabled, hasEntitlement, getPremiumSkuId } from "../utils/premium.js"; export const slashCommand = new SlashCommandBuilder() @@ -90,12 +91,7 @@ export async function execute(_client: Client, interaction: CommandInteraction) command: "premium", }); - if (!interaction.replied) { - await interaction.reply({ - content: "An error occurred while processing your request. Please try again later.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/quote.ts b/src/commands/quote.ts index 06cb0db..c4f0a7b 100644 --- a/src/commands/quote.ts +++ b/src/commands/quote.ts @@ -1,10 +1,11 @@ -import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"; +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; import type { Client, ChatInputCommandInteraction } from "discord.js"; import { count } from "drizzle-orm"; import logger from "../utils/logger.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; import { db } from "../database/index.js"; import { motivationQuotes } from "../database/schema.js"; @@ -95,12 +96,7 @@ export async function execute(client: Client, interaction: ChatInputCommandInter command: "quote", }); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/setup/channel.ts b/src/commands/setup/channel.ts index 5d0482d..b5a6fc7 100644 --- a/src/commands/setup/channel.ts +++ b/src/commands/setup/channel.ts @@ -9,6 +9,7 @@ import type { import { eq } from "drizzle-orm"; import logger from "../../utils/logger.js"; +import { safeErrorReply } from "../../utils/commandErrors.js"; import { db } from "../../database/index.js"; import { guilds } from "../../database/schema.js"; import { guildExists } from "../../utils/guildDatabase.js"; @@ -69,11 +70,6 @@ export default async function ( } ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/setup/index.ts b/src/commands/setup/index.ts index 87f2fca..190b53d 100644 --- a/src/commands/setup/index.ts +++ b/src/commands/setup/index.ts @@ -14,6 +14,7 @@ import type { } from "discord.js"; import logger from "../../utils/logger.js"; +import { safeErrorReply } from "../../utils/commandErrors.js"; /** * Import subcommands @@ -119,12 +120,7 @@ export async function execute(client: Client, interaction: CommandInteraction) { command: "setup", }); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/setup/schedule.ts b/src/commands/setup/schedule.ts index b7775b0..768d72c 100644 --- a/src/commands/setup/schedule.ts +++ b/src/commands/setup/schedule.ts @@ -4,6 +4,7 @@ import type { Client, ChatInputCommandInteraction, AutocompleteInteraction } fro import { eq } from "drizzle-orm"; import logger from "../../utils/logger.js"; +import { safeErrorReply } from "../../utils/commandErrors.js"; import { db } from "../../database/index.js"; import { guilds } from "../../database/schema.js"; import type { MotivationFrequency } from "../../database/schema.js"; @@ -141,17 +142,7 @@ export default async function schedule(_client: Client, interaction: ChatInputCo command: "setup schedule", }); - if (interaction.replied || interaction.deferred) { - await interaction.followUp({ - content: "An error occurred while setting up the schedule.", - flags: MessageFlags.Ephemeral, - }); - } else { - await interaction.reply({ - content: "An error occurred while setting up the schedule.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/commands/suggestion.ts b/src/commands/suggestion.ts index 43eb48f..3d0eb21 100644 --- a/src/commands/suggestion.ts +++ b/src/commands/suggestion.ts @@ -12,7 +12,8 @@ import { eq } from "drizzle-orm"; import logger from "../utils/logger.js"; import { db } from "../database/index.js"; import { guilds, suggestionQuotes } from "../database/schema.js"; -import env from "../utils/env.js"; +import { sendToMainChannel } from "../utils/mainChannel.js"; +import { safeErrorReply } from "../utils/commandErrors.js"; export const slashCommand = new SlashCommandBuilder() .setName("suggestion") @@ -134,12 +135,7 @@ export async function execute(client: Client, interaction: ChatInputCommandInter text: `Created with ID ${newQuote.id}`, }); - if (env.MAIN_CHANNEL_ID) { - const mainChannel = await client.channels.fetch(env.MAIN_CHANNEL_ID); - if (mainChannel?.isTextBased() && !mainChannel.isDMBased()) { - await mainChannel.send({ embeds: [embed] }); - } - } + await sendToMainChannel(client, { embeds: [embed] }); logger.commands.success( "suggestion", @@ -154,12 +150,7 @@ export async function execute(client: Client, interaction: ChatInputCommandInter err ); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "An error occurred while processing your request.", - flags: MessageFlags.Ephemeral, - }); - } + await safeErrorReply(interaction); } } diff --git a/src/events/entitlementCreate.ts b/src/events/entitlementCreate.ts index 7cf17e3..9a5e216 100644 --- a/src/events/entitlementCreate.ts +++ b/src/events/entitlementCreate.ts @@ -1,10 +1,7 @@ import type { Entitlement } from "discord.js"; import logger from "../utils/logger.js"; -import { eq } from "drizzle-orm"; - -import { db } from "../database/index.js"; -import { guilds } from "../database/schema.js"; +import { updateGuildPremiumStatus } from "../utils/entitlementHelpers.js"; export async function entitlementCreateEvent(entitlement: Entitlement): Promise { logger.info("Discord - Event (Entitlement Create)", "New premium subscription", { @@ -14,13 +11,5 @@ export async function entitlementCreateEvent(entitlement: Entitlement): Promise< timestamp: new Date().toISOString(), }); - if (entitlement.guildId) { - try { - await db.update(guilds).set({ isPremium: true }).where(eq(guilds.guildId, entitlement.guildId)); - } catch (err) { - logger.error("Discord - Event (Entitlement Create)", "Failed to update guild premium status", err, { - guildId: entitlement.guildId, - }); - } - } + await updateGuildPremiumStatus(entitlement, true, "Entitlement Create"); } diff --git a/src/events/entitlementDelete.ts b/src/events/entitlementDelete.ts index 7b0e270..1c29b3b 100644 --- a/src/events/entitlementDelete.ts +++ b/src/events/entitlementDelete.ts @@ -1,10 +1,7 @@ import type { Entitlement } from "discord.js"; import logger from "../utils/logger.js"; -import { eq } from "drizzle-orm"; - -import { db } from "../database/index.js"; -import { guilds } from "../database/schema.js"; +import { updateGuildPremiumStatus } from "../utils/entitlementHelpers.js"; export async function entitlementDeleteEvent(entitlement: Entitlement): Promise { logger.info("Discord - Event (Entitlement Delete)", "Premium entitlement removed", { @@ -14,13 +11,5 @@ export async function entitlementDeleteEvent(entitlement: Entitlement): Promise< timestamp: new Date().toISOString(), }); - if (entitlement.guildId) { - try { - await db.update(guilds).set({ isPremium: false }).where(eq(guilds.guildId, entitlement.guildId)); - } catch (err) { - logger.error("Discord - Event (Entitlement Delete)", "Failed to update guild premium status", err, { - guildId: entitlement.guildId, - }); - } - } + await updateGuildPremiumStatus(entitlement, false, "Entitlement Delete"); } diff --git a/src/events/entitlementUpdate.ts b/src/events/entitlementUpdate.ts index 0cf9082..894cd83 100644 --- a/src/events/entitlementUpdate.ts +++ b/src/events/entitlementUpdate.ts @@ -1,10 +1,7 @@ import type { Entitlement } from "discord.js"; import logger from "../utils/logger.js"; -import { eq } from "drizzle-orm"; - -import { db } from "../database/index.js"; -import { guilds } from "../database/schema.js"; +import { updateGuildPremiumStatus } from "../utils/entitlementHelpers.js"; export async function entitlementUpdateEvent( _oldEntitlement: Entitlement | null, @@ -24,13 +21,5 @@ export async function entitlementUpdateEvent( } ); - if (newEntitlement.guildId) { - try { - await db.update(guilds).set({ isPremium: !isCancelled }).where(eq(guilds.guildId, newEntitlement.guildId)); - } catch (err) { - logger.error("Discord - Event (Entitlement Update)", "Failed to update guild premium status", err, { - guildId: newEntitlement.guildId, - }); - } - } + await updateGuildPremiumStatus(newEntitlement, !isCancelled, "Entitlement Update"); } diff --git a/src/utils/commandErrors.ts b/src/utils/commandErrors.ts new file mode 100644 index 0000000..db11556 --- /dev/null +++ b/src/utils/commandErrors.ts @@ -0,0 +1,19 @@ +import { MessageFlags } from "discord.js"; + +import type { CommandInteraction, ChatInputCommandInteraction } from "discord.js"; + +/** + * Safely reply with an ephemeral error message if the interaction + * hasn't already been replied to or deferred. + */ +export async function safeErrorReply( + interaction: CommandInteraction | ChatInputCommandInteraction, + message = "An error occurred while processing your request." +): Promise { + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ + content: message, + flags: MessageFlags.Ephemeral, + }); + } +} diff --git a/src/utils/entitlementHelpers.ts b/src/utils/entitlementHelpers.ts new file mode 100644 index 0000000..f11fc19 --- /dev/null +++ b/src/utils/entitlementHelpers.ts @@ -0,0 +1,29 @@ +import type { Entitlement } from "discord.js"; + +import { eq } from "drizzle-orm"; + +import { db } from "../database/index.js"; +import { guilds } from "../database/schema.js"; +import logger from "./logger.js"; + +/** + * Update a guild's premium status in the database based on an entitlement event. + * Handles the guildId check, DB update, and error logging. + */ +export async function updateGuildPremiumStatus( + entitlement: Entitlement, + isPremium: boolean, + eventName: string +): Promise { + if (!entitlement.guildId) { + return; + } + + try { + await db.update(guilds).set({ isPremium }).where(eq(guilds.guildId, entitlement.guildId)); + } catch (err) { + logger.error(`Discord - Event (${eventName})`, "Failed to update guild premium status", err, { + guildId: entitlement.guildId, + }); + } +} diff --git a/src/utils/mainChannel.ts b/src/utils/mainChannel.ts new file mode 100644 index 0000000..58ae3d4 --- /dev/null +++ b/src/utils/mainChannel.ts @@ -0,0 +1,31 @@ +import type { Client, EmbedBuilder } from "discord.js"; + +import env from "./env.js"; +import logger from "./logger.js"; + +/** + * Fetch the configured main channel and send content to it. + * Handles the MAIN_CHANNEL_ID check, channel fetch, and text-based type guard. + */ +export async function sendToMainChannel( + client: Client, + content: { embeds: EmbedBuilder[] } | string +): Promise { + if (!env.MAIN_CHANNEL_ID) { + logger.warn("Admin", "MAIN_CHANNEL_ID not configured"); + return; + } + + const channel = await client.channels.fetch(env.MAIN_CHANNEL_ID); + if (channel?.isTextBased() && !channel.isDMBased()) { + if (typeof content === "string") { + await channel.send(content); + } else { + await channel.send(content); + } + } else { + logger.warn("Admin", "Main channel not found or not text-based", { + channelId: env.MAIN_CHANNEL_ID, + }); + } +} diff --git a/src/utils/ownerGuard.ts b/src/utils/ownerGuard.ts new file mode 100644 index 0000000..858bac7 --- /dev/null +++ b/src/utils/ownerGuard.ts @@ -0,0 +1,28 @@ +import { MessageFlags } from "discord.js"; + +import type { CommandInteraction } from "discord.js"; + +import env from "./env.js"; +import logger from "./logger.js"; + +/** + * Check if the user is the bot owner. If not, replies with an + * ephemeral rejection and logs the unauthorized attempt. + * + * @returns `true` if the user is the owner, `false` otherwise. + */ +export async function requireOwner( + interaction: CommandInteraction, + commandName: string +): Promise { + if (interaction.user.id === env.OWNER_ID) { + return true; + } + + logger.commands.unauthorized(commandName, interaction.user.username, interaction.user.id); + await interaction.reply({ + content: "Only the bot owner can use this command.", + flags: MessageFlags.Ephemeral, + }); + return false; +}