Skip to content

Commit

Permalink
feat: add generalized action confirmations
Browse files Browse the repository at this point in the history
ThijnK committed Nov 11, 2024
1 parent fee7310 commit 74941ee
Showing 10 changed files with 144 additions and 3 deletions.
20 changes: 20 additions & 0 deletions src/commands/debug/confirm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SlashCommandBuilder } from 'discord.js';
import { command, confirmationResponse, reply } from 'utils';

// NOTE: this is just an example command to demonstrate the confirmation response
// you can remove this file and the import from `src/commands/debug/index.ts`

const meta = new SlashCommandBuilder()
.setName('confirm')
.setDescription('Test the confirmation response.');

export default command({
meta,
private: true,
exec: async ({ interaction }) => {
await reply(
interaction,
confirmationResponse('example'),
);
},
});
3 changes: 2 additions & 1 deletion src/commands/debug/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { category } from 'utils';
import ping from './ping.ts';
import confirm from './confirm.ts';

export default category(
{ name: 'Debug', description: 'Commands used for debugging.', emoji: '🐛' },
[ping],
[ping, confirm],
);
24 changes: 24 additions & 0 deletions src/events/interactionCreate/confirmation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { confirmationHandlers, EMOJIS, event, Namespace, parseId } from 'utils';

export default event('interactionCreate', async (ctx, interaction) => {
if (!interaction.isButton()) return;
const [namespace, action, key, ...args] = parseId(interaction.customId);
if (namespace !== Namespace.Confirmation) return;

const confirmed = action === 'confirm';
if (key in confirmationHandlers) {
await confirmationHandlers[key as keyof typeof confirmationHandlers](
confirmed,
ctx,
interaction,
args,
);
} else {
await interaction.deferUpdate();
await interaction.editReply({
content: `${EMOJIS.error} Oops. Something went wrong!`,
embeds: [],
components: [],
});
}
});
3 changes: 2 additions & 1 deletion src/events/interactionCreate/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import commands from './commands.ts';
import help from './help.ts';
import pagination from './pagination.ts';
import confirmation from './confirmation.ts';

const events = [commands, help, pagination];
const events = [commands, help, pagination, confirmation];

export default events;
2 changes: 1 addition & 1 deletion src/types/pagination.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { APIEmbedField, BaseInteraction } from 'discord.js';
import { BaseContext } from './context.ts';
import { BaseContext } from 'types';

export type PaginationData = APIEmbedField[];

66 changes: 66 additions & 0 deletions src/utils/confirmation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
EmbedBuilder,
InteractionReplyOptions,
} from 'discord.js';
import { COLORS, confirmationHandlers, createId, Namespace } from 'utils';
import { EventContext } from 'types';

/**
* Builds the reply options for a confirmation message.
* The key is used to determine the confirmation handler to use.
* If no embeds are provided in the reply options, a default embed will be added.
* @param key - The key for the confirmation handler.
* @param opts - The options for the confirmation message (optional).
* @param args - The arguments to pass to the confirmation handler (optional).
* @returns The given reply options with an added row of buttons and a default embed if none was provided.
* @example
* await interaction.reply(confirmationResponse('example'));
*/
export const confirmationResponse = (
key: keyof typeof confirmationHandlers,
opts?: InteractionReplyOptions,
args?: string[],
): InteractionReplyOptions => {
const buttonsRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(createId(Namespace.Confirmation, 'cancel', key, args))
.setLabel('Cancel')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(createId(Namespace.Confirmation, 'confirm', key, args))
.setLabel('Confirm')
.setStyle(ButtonStyle.Primary),
);

return {
...opts,
components: [...(opts?.components ?? []), buttonsRow],
embeds: opts?.embeds ??
[
new EmbedBuilder().setColor(COLORS.embed).setTitle('Confirmation')
.setDescription('Are you sure you want to continue?'),
],
};
};

type ConfirmationHandler = (
/** Whether the confirmation was confirmed or cancelled. */
confirmed: boolean,
/** The standard event context. */
ctx: EventContext,
/** The button interaction that triggered the confirmation. */
interaction: ButtonInteraction,
/** The optional list of arguments passed when creating the confirmation response. */
args: string[],
) => Promise<void>;

/** Simple function to type check the provided confirmation handler */
export function confirmation(
handler: ConfirmationHandler,
) {
return handler;
}
18 changes: 18 additions & 0 deletions src/utils/confirmations/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { confirmation } from 'utils';

export default confirmation(
// confirmed indicates whether the confirmation was confirmed or cancelled.
// ctx contains the context with the bot client, etc.
// interaction is the interaction that triggered the confirmation (button click).
// args is the list of arguments passed to the confirmation handler (optional).
async (confirmed, _ctx, interaction, _args) => {
// This example just edits the message that contains the confirmation embed
// to show a message indicating whether the confirmation was confirmed or cancelled.
await interaction.deferUpdate();
await interaction.editReply({
content: confirmed ? 'Confirmed!' : 'Cancelled!',
embeds: [],
components: [],
});
},
);
8 changes: 8 additions & 0 deletions src/utils/confirmations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import example from './example.ts';

export const confirmationHandlers = {
// NOTE: this is just an example handler to demonstrate the confirmation response
// This handler is used by the `/confirm` command in `src/commands/debug/confirm.ts`
'example': example,
// Add more confirmation handlers here
};
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -10,3 +10,5 @@ export * from './pagination.ts';
export * from './split.ts';
export * from './date.ts';
export * from './style.ts';
export * from './confirmation.ts';
export * from './confirmations/index.ts';
1 change: 1 addition & 0 deletions src/utils/interaction.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
export enum Namespace {
Pagination = 'pagination',
Help = 'help',
Confirmation = 'confirmation',
}

/**

0 comments on commit 74941ee

Please sign in to comment.