Skip to content
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

feat: add more error logs and refactor main script #241

Merged
merged 3 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`);
};