diff --git a/bin/main.ts b/bin/main.ts index 426c82f8..97c0b783 100644 --- a/bin/main.ts +++ b/bin/main.ts @@ -1,40 +1,42 @@ -import { InteractionType } from 'discord-api-types/v10'; import { Result } from 'oxide.ts'; import { getDiscordClient } from '../src/clients'; import { getConfigs } from '../src/config'; import { commands as contextMenuCommandList } from '../src/context-menu-commands'; -import { deployGlobalCommands } from '../src/deploy-command'; +import { type DiscordRequestConfig, deployGlobalCommands } from '../src/deploy-command'; import { commands as slashCommandList } from '../src/slash-commands'; +import { processInteraction } from '../src/utils/interaction-processor'; import { loadEnv } from '../src/utils/load-env'; import { logger } from '../src/utils/logger'; import { processMessage } from '../src/utils/message-processor'; +const deployCommands = async ({ token, clientId }: Omit) => { + if (process.env.NODE_ENV !== 'production') { + logger.info('[deploy-commands]: Skipping command deployment in development mode'); + return; + } + + logger.info('[deploy-commands]: Deploying global commands in production mode'); + const commands = [...slashCommandList, ...contextMenuCommandList]; + const op = await Result.safe(deployGlobalCommands(commands, { token, clientId })); + if (op.isErr()) { + logger.error('[deploy-commands]: Cannot deploy global commands', op.unwrapErr()); + process.exit(1); + } + + logger.info('[deploy-commands]: Successfully deployed global commands'); +}; + const main = async () => { loadEnv(); logger.info('[main]: STARTING BOT'); + const token = process.env.TOKEN; const client = await getDiscordClient({ token }); if (!client.user) throw new Error('Something went wrong!'); logger.info(`[main]: Logged in as ${client.user.tag}!`); - if (process.env.NODE_ENV === 'production') { - // This should only be run once during the bot startup in production. - // For development usage, please use `pnpm deploy:command` - logger.info('[main]: Deploying global commands'); - const commands = [...slashCommandList, ...contextMenuCommandList]; - const op = await Result.safe( - deployGlobalCommands(commands, { - token, - clientId: client.user.id, - }) - ); - if (op.isErr()) { - logger.error('[main]: Cannot deploy global commands', op.unwrapErr()); - process.exit(1); - } - logger.info('[main]: Successfully deployed global commands'); - } + await deployCommands({ token, clientId: client.user.id }); const configs = getConfigs(); client.on('messageCreate', (msg) => { @@ -42,33 +44,7 @@ const main = async () => { }); client.on('interactionCreate', async (interaction) => { - try { - const isCommand = interaction.isChatInputCommand(); - if (isCommand) { - const { commandName } = interaction; - logger.info(`[main]: RECEIVED COMMAND. COMMAND: ${commandName}`); - const command = slashCommandList.find((cmd) => cmd.data.name === commandName); - return await command?.execute(interaction); - } - - const isContextMenuCommand = interaction.isContextMenuCommand(); - if (isContextMenuCommand) { - const { commandName } = interaction; - logger.info(`[main]: RECEIVED CONTEXT MENU COMMAND. COMMAND: ${commandName}`); - const command = contextMenuCommandList.find((cmd) => cmd.data.name === commandName); - return await command?.execute(interaction); - } - - const isAutocomplete = interaction.type === InteractionType.ApplicationCommandAutocomplete; - if (isAutocomplete) { - const { commandName } = interaction; - logger.info(`[main]: RECEIVED AUTOCOMPLETE. COMMAND: ${commandName}`); - const command = slashCommandList.find((cmd) => cmd.data.name === commandName); - return await command?.autocomplete?.(interaction); - } - } catch (error) { - logger.error(`[main]: ERROR HANDLING INTERACTION, ERROR: ${error}`); - } + return processInteraction(interaction); }); }; diff --git a/src/context-menu-commands/pin-message/index.test.ts b/src/context-menu-commands/pin-message/index.test.ts index 3bf356d0..8b6eeb64 100644 --- a/src/context-menu-commands/pin-message/index.test.ts +++ b/src/context-menu-commands/pin-message/index.test.ts @@ -26,6 +26,7 @@ describe('pinMessage context menu test', () => { const mockInteraction = mockDeep(); mockInteraction.isMessageContextMenuCommand.mockReturnValueOnce(true); mockInteraction.targetMessage.pinned = false; + mockInteraction.targetMessage.pin.mockResolvedValueOnce({} as any); await pinMessage(mockInteraction); expect(mockInteraction.targetMessage.pin).toHaveBeenCalledOnce(); diff --git a/src/context-menu-commands/pin-message/index.ts b/src/context-menu-commands/pin-message/index.ts index d731d10e..abacf1fe 100644 --- a/src/context-menu-commands/pin-message/index.ts +++ b/src/context-menu-commands/pin-message/index.ts @@ -1,4 +1,5 @@ import { ApplicationCommandType, ContextMenuCommandBuilder, type ContextMenuCommandInteraction } from 'discord.js'; +import { Result } from 'oxide.ts'; import { logger } from '../../utils/logger'; import type { ContextMenuCommand } from '../builder'; @@ -16,7 +17,13 @@ export const pinMessage = async (interaction: ContextMenuCommandInteraction) => return; } - await message.pin(); + const op = await Result.safe(message.pin()); + if (op.isErr()) { + logger.error(`[pin]: Cannot pin message ${message.id} in channel ${message.channelId}`, op.unwrapErr()); + await interaction.reply('ERROR: Cannot pin message. Please try again later.'); + return; + } + logger.info(`[pin]: Successfully pinned message ${message.id} in channel ${message.channelId}`); await interaction.reply('Message is now pinned!'); }; diff --git a/src/deploy-command.ts b/src/deploy-command.ts index 4c9a432e..2790bc83 100644 --- a/src/deploy-command.ts +++ b/src/deploy-command.ts @@ -3,7 +3,7 @@ import { Routes } from 'discord-api-types/v10'; import type { ContextMenuCommand } from './context-menu-commands/builder'; import type { SlashCommand } from './slash-commands/builder'; -interface DiscordRequestConfig { +export interface DiscordRequestConfig { token: string; clientId: string; guildId: string; diff --git a/src/slash-commands/all-cap/index.ts b/src/slash-commands/all-cap/index.ts index d4b76bfb..05bcf32d 100644 --- a/src/slash-commands/all-cap/index.ts +++ b/src/slash-commands/all-cap/index.ts @@ -34,7 +34,7 @@ export const allCapExpandText = async (interaction: ChatInputCommandInteraction) // If it's still blank at this point, then exit if (fetchedMessage.isErr() || isBlank(fetchedMessage.unwrap().content)) { - logger.info('[allcap]: Cannot fetch latest message'); + logger.error('[allcap]: Cannot fetch message to allcap.', fetchedMessage.unwrapErr()); await interaction.reply('Cannot fetch latest message. Please try again later.'); return; } diff --git a/src/slash-commands/autobump-threads/add-thread.ts b/src/slash-commands/autobump-threads/add-thread.ts index 5d0a2fa3..e59410cd 100644 --- a/src/slash-commands/autobump-threads/add-thread.ts +++ b/src/slash-commands/autobump-threads/add-thread.ts @@ -23,7 +23,7 @@ export const addAutobumpThreadCommand: SlashCommandHandler = async (interaction) const op = await Result.safe(addAutobumpThread(guildId, thread.id)); if (op.isErr()) { - logger.error(`[add-autobump-thread]: Cannot save thread ${thread.id} to be autobumped for guild ${guildId}`); + logger.error(`[add-autobump-thread]: Cannot save thread ${thread.id} to be autobumped for guild ${guildId}`, op.unwrapErr()); await interaction.reply('ERROR: Cannot save this thread to be autobumped for this server. Please try again.'); return; } diff --git a/src/slash-commands/autobump-threads/list-threads.ts b/src/slash-commands/autobump-threads/list-threads.ts index 0dc337f1..f460aeff 100644 --- a/src/slash-commands/autobump-threads/list-threads.ts +++ b/src/slash-commands/autobump-threads/list-threads.ts @@ -20,7 +20,7 @@ export const listAutobumpThreadsCommand: SlashCommandHandler = async (interactio logger.info(`[list-autobump-threads]: Listing autobump threads for guild ${guildId}`); if (threads.isErr()) { - logger.error(`[list-autobump-threads]: Cannot get list of threads from the database for guild ${guildId}`); + logger.error(`[list-autobump-threads]: Cannot get list of threads from the database for guild ${guildId}`, threads.unwrapErr()); await interaction.reply("ERROR: Cannot get list of threads from the database, maybe the server threads aren't setup yet?"); return; } diff --git a/src/slash-commands/autobump-threads/remove-thread.ts b/src/slash-commands/autobump-threads/remove-thread.ts index da668a98..a9f29bbb 100644 --- a/src/slash-commands/autobump-threads/remove-thread.ts +++ b/src/slash-commands/autobump-threads/remove-thread.ts @@ -16,6 +16,7 @@ export const removeAutobumpThreadCommand: SlashCommandHandler = async (interacti const op = await Result.safe(removeAutobumpThread(guildId, thread.id)); if (op.isErr()) { + logger.error(`[remove-autobump-thread]: Cannot remove thread ${thread.id} from autobump list for guild ${guildId}`, op.unwrapErr()); await interaction.reply(`ERROR: Cannot remove thread id <#${thread.id}> from the bump list for this server. Please try again.`); return; } diff --git a/src/slash-commands/mock-someone/index.ts b/src/slash-commands/mock-someone/index.ts index 84ae8e22..2cd01f62 100644 --- a/src/slash-commands/mock-someone/index.ts +++ b/src/slash-commands/mock-someone/index.ts @@ -45,7 +45,7 @@ export const mockSomeone = async (interaction: ChatInputCommandInteraction) => { const { content } = fetchedMessage.unwrap(); if (isBlank(content)) { - logger.info('[mock]: Fetched message is blank.'); + logger.error('[mock]: Cannot fetch message to mock'); await interaction.reply('Cannot fetch latest message. Please try again later.'); return; } diff --git a/src/utils/interaction-processor.ts b/src/utils/interaction-processor.ts new file mode 100644 index 00000000..c053ba69 --- /dev/null +++ b/src/utils/interaction-processor.ts @@ -0,0 +1,61 @@ +import { type Interaction, InteractionType } from 'discord.js'; +import { Result } from 'oxide.ts'; +import { commands as contextMenuCommandList } from '../context-menu-commands'; +import { commands as slashCommandList } from '../slash-commands'; +import { logger } from './logger'; + +export const processInteraction = async (interaction: Interaction): Promise => { + const isCommand = interaction.isChatInputCommand(); + if (isCommand) { + const { commandName } = interaction; + logger.info(`[process-interaction]: RECEIVED COMMAND. COMMAND: ${commandName}`); + const command = slashCommandList.find((cmd) => cmd.data.name === commandName); + if (!command) { + logger.info(`[process-interaction]: COMMAND NOT FOUND: ${commandName}. Exiting...`); + return; + } + + const op = await Result.safe(command.execute(interaction)); + if (op.isErr()) { + logger.error(`[process-interaction]: ERROR HANDLING COMMAND: ${commandName}, ERROR: ${op.unwrapErr()}`); + return; + } + } + + const isContextMenuCommand = interaction.isContextMenuCommand(); + if (isContextMenuCommand) { + const { commandName } = interaction; + logger.info(`[process-interaction]: RECEIVED CONTEXT MENU COMMAND. COMMAND: ${commandName}`); + const command = contextMenuCommandList.find((cmd) => cmd.data.name === commandName); + if (!command) { + logger.info(`[process-interaction]: CONTEXT MENU COMMAND NOT FOUND: ${commandName}. Exiting...`); + return; + } + + const op = await Result.safe(command.execute(interaction)); + if (op.isErr()) { + logger.error(`[process-interaction]: ERROR HANDLING CONTEXT MENU COMMAND: ${commandName}, ERROR: ${op.unwrapErr()}`); + return; + } + } + + const isAutocomplete = interaction.type === InteractionType.ApplicationCommandAutocomplete; + if (isAutocomplete) { + const { commandName } = interaction; + logger.info(`[process-interaction]: RECEIVED AUTOCOMPLETE. COMMAND: ${commandName}`); + const command = slashCommandList.find((cmd) => cmd.data.name === commandName); + if (!command || !command.autocomplete) { + logger.info(`[process-interaction]: COMMAND AUTOCOMPLETE NOT FOUND: ${commandName}. Exiting...`); + return; + } + + const op = await Result.safe(command.autocomplete(interaction)); + if (op.isErr()) { + logger.error(`[process-interaction]: ERROR HANDLING AUTOCOMPLETE: ${commandName}, ERROR: ${op.unwrapErr()}`); + return; + } + return await command?.autocomplete?.(interaction); + } + + logger.info(`[process-interaction]: INTERACTION TYPE NOT RECOGNIZED. TYPE: ${interaction.type}`); +};