Skip to content

Commit

Permalink
feat: add more error logs and refactor main script (#241)
Browse files Browse the repository at this point in the history
* WIP

* feat: move process interaction into a different module

* feat: move deploy command into a separate function in entrypoint script
  • Loading branch information
samhwang authored Jul 26, 2024
1 parent 734f87f commit 4b6d7de
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 52 deletions.
68 changes: 22 additions & 46 deletions bin/main.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,50 @@
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<DiscordRequestConfig, 'guildId'>) => {
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) => {
return processMessage(msg, configs);
});

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);
});
};

Expand Down
1 change: 1 addition & 0 deletions src/context-menu-commands/pin-message/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('pinMessage context menu test', () => {
const mockInteraction = mockDeep<MessageContextMenuCommandInteraction>();
mockInteraction.isMessageContextMenuCommand.mockReturnValueOnce(true);
mockInteraction.targetMessage.pinned = false;
mockInteraction.targetMessage.pin.mockResolvedValueOnce({} as any);

await pinMessage(mockInteraction);
expect(mockInteraction.targetMessage.pin).toHaveBeenCalledOnce();
Expand Down
9 changes: 8 additions & 1 deletion src/context-menu-commands/pin-message/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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!');
};
Expand Down
2 changes: 1 addition & 1 deletion src/deploy-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/slash-commands/all-cap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/slash-commands/autobump-threads/add-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/slash-commands/autobump-threads/list-threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/slash-commands/autobump-threads/remove-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/slash-commands/mock-someone/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
61 changes: 61 additions & 0 deletions src/utils/interaction-processor.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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}`);
};

0 comments on commit 4b6d7de

Please sign in to comment.