Skip to content
This repository has been archived by the owner on Sep 3, 2024. It is now read-only.

Commit

Permalink
feat!: Extracted handling of interactions into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusbegby committed Aug 28, 2023
1 parent 8e9cb30 commit 780e20d
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 153 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./dist/index.js",
"start-pretty": "node ./dist/index.js | pino-pretty --ignore environment,source,module,action,name,context,executionId,shardId,guildId",
"start-pretty": "node ./dist/index.js | pino-pretty --ignore environment,source,module,action,name,context,executionId,executionTime,shardId,guildId,interactionType",
"start-pretty-no-ignore": "node ./dist/index.js | pino-pretty",
"deploy": "node ./dist/utils/deploySlashCommands.js",
"deploy-pretty": "node ./dist/utils/deploySlashCommands.js | pino-pretty --ignore environment,source,module,action,name,context,executionId,shardId,guildId",
"deploy-pretty": "node ./dist/utils/deploySlashCommands.js | pino-pretty --ignore environment,source,module,action,name,context,executionId,executionTime,shardId,guildId,interactionType",
"eslint": "eslint ./src",
"build": "tsc"
},
Expand Down
144 changes: 17 additions & 127 deletions src/events/interactions/interactionCreate.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import config from 'config';
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
EmbedBuilder,
Events,
Interaction,
InteractionType,
MessageComponentInteraction
} from 'discord.js';
import { v4 as uuidv4 } from 'uuid';

import { handleAutocomplete } from '../../handlers/interactionAutocompleteHandler';
import { handleCommand } from '../../handlers/interactionCommandHandler';
import { handleComponent } from '../../handlers/interactionComponentHandler';
import { handleError } from '../../handlers/interactionErrorHandler';
import loggerModule from '../../services/logger';
import { Command, ExtendedClient } from '../../types/clientTypes';
import { BotOptions, EmbedOptions } from '../../types/configTypes';
import { cannotSendMessageInChannel } from '../../utils/validation/permissionValidator';
import { ExtendedClient } from '../../types/clientTypes';
import { CustomError } from '../../types/interactionTypes';

const embedOptions: EmbedOptions = config.get('embedOptions');
const botOptions: BotOptions = config.get('botOptions');
module.exports = {
name: Events.InteractionCreate,
isDebug: false,
Expand Down Expand Up @@ -46,131 +44,19 @@ module.exports = {
interactionIdentifier = (interaction as MessageComponentInteraction).customId;
}

// Todo: Extract to own file
const handleComponent = async (interaction: MessageComponentInteraction) => {
await interaction.deferReply();
logger.debug('Interaction deferred.');

const componentId = interaction.customId.split('_')[0];
const referenceId = interaction.customId.split('_')[1];

logger.debug(`Parsed componentId: ${componentId}`);

const componentModule = await import(`../../interactions/components/${componentId}.js`);
const { default: component } = componentModule;

logger.debug('Executing component interaction.');
await component.execute({ interaction, referenceId, executionId });
};

const handleAutocomplete = async (interaction: AutocompleteInteraction) => {
const autocompleteModule = await import(`../../interactions/autocomplete/${interaction.commandName}.js`);
const { default: autocomplete } = autocompleteModule;

logger.debug('Executing autocomplete interaction.');
await autocomplete.execute({ interaction, executionId });
};

const handleCommand = async (interaction: ChatInputCommandInteraction) => {
await interaction.deferReply();
logger.debug('Interaction deferred.');

const command = client.commands?.get(interaction.commandName) as Command;
if (!command) {
logger.warn(`Interaction created but command '${interaction.commandName}' was not found.`);
return;
}

logger.debug(`Chat input command interaction created for '${interaction.commandName}'.`);

if (await cannotSendMessageInChannel({ interaction, executionId })) {
return;
}

logger.debug('Executing command interaction.');
await command.execute({ interaction, client, executionId });
};

// TODO: Extract the text to a config file?
const errorReply = {
embeds: [
new EmbedBuilder()
.setDescription(
`**${embedOptions.icons.error} Uh-oh... _Something_ went wrong!**\nThere was an unexpected error while trying to perform this action. You can try again.\n\n_If this problem persists, please submit a bug report in the **[support server](${botOptions.serverInviteUrl})**._`
)
.setColor(embedOptions.colors.error)
.setFooter({ text: `Execution ID: ${executionId}` })
]
};

// TODO: extract handlError to own file (create errorHandler and export handleInteractionError?)
const handleError = async (interaction: Interaction, error: CustomError) => {
logger.error(error, `Error handling interaction '${interactionIdentifier}'`);

if (interaction instanceof ChatInputCommandInteraction && interaction.deferred) {
switch (interaction.replied) {
case true:
// This means interaction has received a reply already.
// Most likely command executed successfully or error is already handled.
logger.warn(
error,
`Interaction '${interaction.id}' threw an error but has already been replied to.`
);
return;
case false:
// If the interaction has not been replied to, most likely command execution failed.
logger.debug('Responding with error embed');
return await interaction.editReply(errorReply);
}
} else if (interaction instanceof MessageComponentInteraction && interaction.deferred) {
switch (interaction.replied) {
case true:
// This means interaction has received a reply already.
// Most likely command executed successfully or error is already handled.
logger.warn(
error,
`Interaction '${interaction.id}' threw an error but has already been replied to.`
);
return;
case false:
// If the interaction has not been replied to, most likely command execution failed.
logger.debug('Responding with error embed');
return await interaction.editReply(errorReply);
}
} else {
logger.warn(
'Interaction threw an error but was not deferred or replied to, or was an autocomplete interaction. Cannot send error reply.'
);

if (
error.code === 'InteractionCollectorError' ||
error.message === 'Collector received no interactions before ending with reason: time'
) {
logger.debug('Interaction response timed out.');
return;
}

// If this code is reached there is an error that is not handled at all.
// This should not happen, but if it does, we log it with 'fatal' level to investigate.
// This is to prevent the bot from crashing or doing something very unexpected.
logger.fatal(error, 'Unhandled error while awaiting or handling component interaction.');
return;
}
};

try {
logger.debug('Started handling interaction.');
switch (interaction.type as InteractionType) {
switch (interaction.type) {
case InteractionType.ApplicationCommand:
await handleCommand(interaction as ChatInputCommandInteraction);
await handleCommand(interaction as ChatInputCommandInteraction, client, executionId);
break;

case InteractionType.ApplicationCommandAutocomplete:
await handleAutocomplete(interaction as AutocompleteInteraction);
await handleAutocomplete(interaction as AutocompleteInteraction, executionId);
break;

case InteractionType.MessageComponent:
await handleComponent(interaction as MessageComponentInteraction);
await handleComponent(interaction as MessageComponentInteraction, executionId);
break;

default:
Expand All @@ -182,21 +68,25 @@ module.exports = {
}
} catch (error) {
logger.debug('Error while handling received interaction.');
await handleError(interaction, error as CustomError);
await handleError(interaction, error as CustomError, interactionIdentifier, executionId);
}

const outputTime: number = new Date().getTime();
const executionTime: number = outputTime - inputTime;
const interactionType = InteractionType[interaction.type];

logger.info(
{
executionTime: executionTime
executionTime: executionTime,
interactionType: interactionType
},
`Interaction '${interactionIdentifier}' successfully handled in ${executionTime} ms.`
`${interactionType} interaction '${interactionIdentifier}' successfully handled in ${executionTime} ms.`
);

if (executionTime > 10000) {
logger.warn(`Interaction '${interactionIdentifier}' took ${executionTime} ms to execute.`);
logger.warn(
`${interactionType} interaction '${interactionIdentifier}' took ${executionTime} ms to execute.`
);
}

return;
Expand Down
16 changes: 16 additions & 0 deletions src/handlers/interactionAutocompleteHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AutocompleteInteraction } from 'discord.js';
import loggerModule from '../services/logger';

const logger = loggerModule.child({
source: 'interactionCutocompleteHandler.ts',
module: 'handler',
name: 'interactionAutocompleteHandler'
});

export const handleAutocomplete = async (interaction: AutocompleteInteraction, executionId: string) => {
const autocompleteModule = await import(`../interactions/autocomplete/${interaction.commandName}.js`);
const { default: autocomplete } = autocompleteModule;

logger.debug('Executing autocomplete interaction.');
await autocomplete.execute({ interaction, executionId });
};
32 changes: 32 additions & 0 deletions src/handlers/interactionCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChatInputCommandInteraction } from 'discord.js';
import { Command, ExtendedClient } from '../types/clientTypes';
import { cannotSendMessageInChannel } from '../utils/validation/permissionValidator';
import loggerModule from '../services/logger';

const logger = loggerModule.child({
source: 'interactionCommandHandler.ts',
module: 'handler',
name: 'interactionCommandHandler'
});

export const handleCommand = async (
interaction: ChatInputCommandInteraction,
client: ExtendedClient,
executionId: string
) => {
await interaction.deferReply();
logger.debug('Interaction deferred.');

const command = client.commands?.get(interaction.commandName) as Command;
if (!command) {
logger.warn(`Interaction created but command '${interaction.commandName}' was not found.`);
return;
}

if (await cannotSendMessageInChannel({ interaction, executionId })) {
return Promise.resolve();
}

logger.debug('Executing command interaction.');
await command.execute({ interaction, client, executionId });
};
24 changes: 24 additions & 0 deletions src/handlers/interactionComponentHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MessageComponentInteraction } from 'discord.js';
import loggerModule from '../services/logger';

const logger = loggerModule.child({
source: 'interactionComponentHandler.js',
module: 'handler',
name: 'interactionComponentHandler'
});

export const handleComponent = async (interaction: MessageComponentInteraction, executionId: string) => {
await interaction.deferReply();
logger.debug('Interaction deferred.');

const componentId = interaction.customId.split('_')[0];
const referenceId = interaction.customId.split('_')[1];

logger.debug(`Parsed componentId: ${componentId}`);

const componentModule = await import(`../interactions/components/${componentId}.js`);
const { default: component } = componentModule;

logger.debug('Executing component interaction.');
await component.execute({ interaction, referenceId, executionId });
};
69 changes: 69 additions & 0 deletions src/handlers/interactionErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import config from 'config';
import { ChatInputCommandInteraction, EmbedBuilder, Interaction, MessageComponentInteraction } from 'discord.js';
import { BotOptions, EmbedOptions } from '../types/configTypes';
import loggerModule from '../services/logger';
import { CustomError } from '../types/interactionTypes';

const embedOptions: EmbedOptions = config.get('embedOptions');
const botOptions: BotOptions = config.get('botOptions');

const logger = loggerModule.child({
source: 'interactionErrorHandler.ts',
module: 'handler',
name: 'interactionErrorHandler'
});

export const handleError = async (
interaction: Interaction,
error: CustomError,
interactionIdentifier: string,
executionId: string
) => {
const errorReply = {
embeds: [
new EmbedBuilder()
.setDescription(
`**${embedOptions.icons.error} Uh-oh... _Something_ went wrong!**\nThere was an unexpected error while trying to perform this action. You can try again.\n\n_If this problem persists, please submit a bug report in the **[support server](${botOptions.serverInviteUrl})**._`
)
.setColor(embedOptions.colors.error)
.setFooter({ text: `Execution ID: ${executionId}` })
]
};

logger.error(error, `Error handling interaction '${interactionIdentifier}'`);

if (interaction instanceof ChatInputCommandInteraction && interaction.deferred) {
switch (interaction.replied) {
case true:
logger.warn(error, `Interaction '${interaction.id}' threw an error but has already been replied to.`);
return;
case false:
logger.debug('Responding with error embed');
return await interaction.editReply(errorReply);
}
} else if (interaction instanceof MessageComponentInteraction && interaction.deferred) {
switch (interaction.replied) {
case true:
logger.warn(error, `Interaction '${interaction.id}' threw an error but has already been replied to.`);
return;
case false:
logger.debug('Responding with error embed');
return await interaction.editReply(errorReply);
}
} else {
logger.warn(
'Interaction threw an error but was not deferred or replied to, or was an autocomplete interaction. Cannot send error reply.'
);

if (
error.code === 'InteractionCollectorError' ||
error.message === 'Collector received no interactions before ending with reason: time'
) {
logger.debug('Interaction response timed out.');
return;
}

logger.fatal(error, 'Unhandled error while awaiting or handling component interaction.');
return;
}
};
Loading

0 comments on commit 780e20d

Please sign in to comment.