| import { ButtonInteraction, TextInputBuilder } from "discord.js"; |
| import Discord, { |
| CommandInteraction, |
| Message, |
| ActionRowBuilder, |
| ButtonBuilder, |
| ModalSubmitInteraction, |
| ButtonStyle, |
| TextInputStyle |
| } from "discord.js"; |
| import { modalInteractionCollector } from "./dualCollector.js"; |
| import EmojiEmbed from "./generateEmojiEmbed.js"; |
| import getEmojiByName from "./getEmojiByName.js"; |
| |
| interface CustomBoolean<T> { |
| title: string; |
| disabled: boolean; |
| value: string | null; |
| notValue: string | null; |
| emoji: string | undefined; |
| active: boolean; |
| onClick: () => Promise<T>; |
| response: T | null; |
| } |
| |
| class confirmationMessage { |
| interaction: CommandInteraction | ButtonInteraction; |
| title = ""; |
| emoji = ""; |
| redEmoji: string | null = null; |
| failedMessage: string | null = null; |
| failedEmoji: string | null = null; |
| failedStatus: "Success" | "Danger" | "Warning" | null = null; |
| description = ""; |
| color: "Danger" | "Warning" | "Success" = "Success"; |
| customButtons: Record<string, CustomBoolean<unknown>> = {}; |
| inverted = false; |
| reason: string | null = null; |
| |
| modals: { |
| buttonText: string; |
| emoji: string; |
| customId: string; |
| modal: Discord.ModalBuilder; |
| values: Record<string, string>; |
| }[] = []; |
| |
| constructor(interaction: CommandInteraction | ButtonInteraction) { |
| this.interaction = interaction; |
| } |
| |
| setTitle(title: string) { |
| this.title = title; |
| return this; |
| } |
| setEmoji(emoji: string) { |
| this.emoji = emoji; |
| return this; |
| } |
| setDescription(description: string, timedOut?: string) { |
| this.description = description; |
| if (timedOut) this.failedMessage = timedOut; |
| return this; |
| } |
| setColor(color: "Danger" | "Warning" | "Success") { |
| this.color = color; |
| return this; |
| } |
| setInverted(inverted: boolean) { |
| this.inverted = inverted; |
| return this; |
| } |
| setFailedMessage( |
| text: string, |
| failedStatus: "Success" | "Danger" | "Warning" | null, |
| failedEmoji: string | null = null |
| ) { |
| this.failedMessage = text; |
| this.failedStatus = failedStatus; |
| this.failedEmoji = failedEmoji; |
| return this; |
| } |
| addCustomBoolean( |
| customId: string, |
| title: string, |
| disabled: boolean, |
| callback: (() => Promise<unknown>) | null = async () => null, |
| callbackClicked: string | null, |
| callbackNotClicked: string | null, |
| emoji?: string, |
| initial?: boolean |
| ) { |
| this.customButtons[customId] = { |
| title: title, |
| disabled: disabled, |
| value: callbackClicked, |
| notValue: callbackNotClicked, |
| emoji: emoji, |
| active: initial ?? false, |
| onClick: callback ?? (async () => null), |
| response: null |
| }; |
| return this; |
| } |
| addReasonButton(reason: string) { |
| this.reason = reason; |
| return this; |
| } |
| addModal( |
| buttonText: string, |
| emoji: string, |
| customId: string, |
| current: Record<string, string>, |
| modal: Discord.ModalBuilder |
| ) { |
| modal.setCustomId(customId); |
| this.modals.push({ buttonText, emoji, customId, modal, values: current }); |
| return this; |
| } |
| async send(editOnly?: boolean): Promise<{ |
| success?: boolean; |
| cancelled?: boolean; |
| components?: Record<string, CustomBoolean<unknown>>; |
| newReason?: string; |
| modals?: { |
| buttonText: string; |
| emoji: string; |
| customId: string; |
| modal: Discord.ModalBuilder; |
| values: Record<string, string>; |
| }[]; |
| }> { |
| let cancelled = false; |
| let success: boolean | undefined = undefined; |
| let returnComponents = false; |
| let newReason = undefined; |
| |
| while (!cancelled && success === undefined && !returnComponents && !newReason) { |
| const fullComponents = [ |
| new ButtonBuilder() |
| .setCustomId("yes") |
| .setLabel("Confirm") |
| .setStyle(this.inverted ? ButtonStyle.Success : ButtonStyle.Danger) |
| .setEmoji(getEmojiByName("CONTROL.TICK", "id")), |
| new ButtonBuilder() |
| .setCustomId("no") |
| .setLabel("Cancel") |
| .setStyle(ButtonStyle.Danger) |
| .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) |
| ]; |
| Object.entries(this.customButtons).forEach(([k, v]) => { |
| const button = new ButtonBuilder() |
| .setCustomId(k) |
| .setLabel(v.title) |
| .setStyle(v.active ? ButtonStyle.Success : ButtonStyle.Primary) |
| .setDisabled(v.disabled); |
| if (v.emoji !== undefined) button.setEmoji(getEmojiByName(v.emoji, "id")); |
| fullComponents.push(button); |
| }); |
| for (const modal of this.modals) { |
| fullComponents.push( |
| new ButtonBuilder() |
| .setCustomId(modal.customId) |
| .setLabel(modal.buttonText) |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName(modal.emoji, "id")) |
| .setDisabled(false) |
| ); |
| } |
| if (this.reason !== null) |
| fullComponents.push( |
| new ButtonBuilder() |
| .setCustomId("reason") |
| .setLabel("Edit Reason") |
| .setStyle(ButtonStyle.Primary) |
| .setEmoji(getEmojiByName("ICONS.EDIT", "id")) |
| .setDisabled(false) |
| ); |
| const components = []; |
| for (let i = 0; i < fullComponents.length; i += 5) { |
| components.push( |
| new ActionRowBuilder< |
| | ButtonBuilder |
| | Discord.StringSelectMenuBuilder |
| | Discord.RoleSelectMenuBuilder |
| | Discord.UserSelectMenuBuilder |
| >().addComponents(fullComponents.slice(i, i + 5)) |
| ); |
| } |
| const object = { |
| embeds: [ |
| new EmojiEmbed() |
| .setEmoji(this.emoji) |
| .setTitle(this.title) |
| .setDescription( |
| this.description + |
| "\n\n" + |
| Object.values(this.customButtons) |
| .map((v) => { |
| if (v.active) { |
| return v.value ? `*${v.value}*\n` : ""; |
| } else { |
| return v.notValue ? `*${v.notValue}*\n` : ""; |
| } |
| }) |
| .join("") |
| ) |
| .setStatus(this.color) |
| ], |
| components: components, |
| ephemeral: true, |
| fetchReply: true |
| }; |
| let m: Message; |
| try { |
| if (editOnly) { |
| m = (await this.interaction.editReply(object)) as unknown as Message; |
| } else { |
| m = (await this.interaction.reply(object)) as unknown as Message; |
| } |
| } catch (e) { |
| cancelled = true; |
| continue; |
| } |
| let component; |
| try { |
| component = await m.awaitMessageComponent({ |
| filter: (i) => |
| i.user.id === this.interaction.user.id && i.channel!.id === this.interaction.channel!.id, |
| time: 300000 |
| }); |
| } catch (e) { |
| success = false; |
| break; |
| } |
| if (component.customId === "yes") { |
| await component.deferUpdate(); |
| for (const v of Object.values(this.customButtons)) { |
| if (!v.active) continue; |
| try { |
| v.response = await v.onClick(); |
| } catch (e) { |
| console.log(e); |
| } |
| } |
| success = true; |
| returnComponents = true; |
| continue; |
| } else if (component.customId === "no") { |
| await component.deferUpdate(); |
| success = false; |
| returnComponents = true; |
| continue; |
| } else if (component.customId === "reason") { |
| await component.showModal( |
| new Discord.ModalBuilder() |
| .setCustomId("modal") |
| .setTitle("Editing reason") |
| .addComponents( |
| new ActionRowBuilder<TextInputBuilder>().addComponents( |
| new TextInputBuilder() |
| .setCustomId("reason") |
| .setLabel("Reason") |
| .setMaxLength(2000) |
| .setRequired(false) |
| .setStyle(TextInputStyle.Paragraph) |
| .setPlaceholder("Spammed in #general") |
| .setValue(this.reason ? this.reason : "") |
| ) |
| ) |
| ); |
| await this.interaction.editReply({ |
| embeds: [ |
| new EmojiEmbed() |
| .setTitle(this.title) |
| .setDescription("Modal opened. If you can't see it, click back and try again.") |
| .setStatus(this.color) |
| .setEmoji(this.emoji) |
| ], |
| components: [ |
| new ActionRowBuilder<ButtonBuilder>().addComponents( |
| new ButtonBuilder() |
| .setLabel("Back") |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) |
| .setStyle(ButtonStyle.Primary) |
| .setCustomId("back") |
| ) |
| ] |
| }); |
| let out; |
| try { |
| out = (await modalInteractionCollector( |
| m, |
| this.interaction.user |
| )) as Discord.ModalSubmitInteraction | null; |
| } catch (e) { |
| cancelled = true; |
| continue; |
| } |
| if (out === null) { |
| cancelled = true; |
| continue; |
| } |
| if (out.isButton()) { |
| continue; |
| } |
| if (out instanceof ModalSubmitInteraction) { |
| newReason = out.fields.getTextInputValue("reason"); |
| continue; |
| } else { |
| returnComponents = true; |
| continue; |
| } |
| } else if (this.modals.map((m) => m.customId).includes(component.customId)) { |
| const chosenModal = this.modals.find( |
| ( |
| (component) => (m) => |
| m.customId === component.customId |
| )(component) |
| ); |
| await component.showModal(chosenModal!.modal); |
| await this.interaction.editReply({ |
| embeds: [ |
| new EmojiEmbed() |
| .setTitle(this.title) |
| .setDescription("Modal opened. If you can't see it, click back and try again.") |
| .setStatus(this.color) |
| .setEmoji(this.emoji) |
| ], |
| components: [ |
| new ActionRowBuilder<ButtonBuilder>().addComponents( |
| new ButtonBuilder() |
| .setLabel("Back") |
| .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) |
| .setStyle(ButtonStyle.Primary) |
| .setCustomId("back") |
| ) |
| ] |
| }); |
| let out; |
| try { |
| out = (await modalInteractionCollector( |
| m, |
| this.interaction.user |
| )) as Discord.ModalSubmitInteraction | null; |
| } catch (e) { |
| console.log(e); |
| cancelled = true; |
| continue; |
| } |
| if (out === null) { |
| cancelled = true; |
| continue; |
| } |
| if (out.isButton()) { |
| continue; |
| } |
| if (out instanceof ModalSubmitInteraction) { |
| out.fields.fields.forEach((f, k) => { |
| chosenModal!.values[k] = f.value; |
| }); |
| } |
| returnComponents = true; |
| continue; |
| } else { |
| await component.deferUpdate(); |
| this.customButtons[component.customId]!.active = !this.customButtons[component.customId]!.active; |
| returnComponents = true; |
| continue; |
| } |
| } |
| const returnValue: Awaited<ReturnType<typeof this.send>> = {}; |
| |
| if (cancelled) { |
| await this.timeoutError(); |
| returnValue.cancelled = true; |
| } |
| if (success === false) { |
| await this.interaction.editReply({ |
| embeds: [ |
| new EmojiEmbed() |
| .setTitle(this.title) |
| .setDescription(this.failedMessage ?? "*Message timed out*") |
| .setStatus(this.failedStatus ?? "Danger") |
| .setEmoji(this.failedEmoji ?? this.redEmoji ?? this.emoji) |
| ], |
| components: [] |
| }); |
| return { success: false, cancelled: returnValue.cancelled ?? false }; |
| } |
| if (returnComponents || success !== undefined) returnValue.components = this.customButtons; |
| if (success !== undefined) returnValue.success = success; |
| if (newReason) returnValue.newReason = newReason; |
| returnValue.modals = this.modals; |
| |
| const modals = this.modals; |
| const typedReturnValue = returnValue as |
| | { cancelled: true } |
| | { |
| success: boolean; |
| components: Record<string, CustomBoolean<unknown>>; |
| modals: typeof modals; |
| newReason?: string; |
| } |
| | { newReason: string; components: Record<string, CustomBoolean<unknown>>; modals: typeof modals } |
| | { components: Record<string, CustomBoolean<unknown>>; modals: typeof modals }; |
| |
| return typedReturnValue; |
| } |
| |
| async timeoutError(): Promise<void> { |
| await this.interaction.editReply({ |
| embeds: [ |
| new EmojiEmbed() |
| .setTitle(this.title) |
| .setDescription("We closed this message because it was not used for a while.") |
| .setStatus("Danger") |
| .setEmoji("CONTROL.BLOCKCROSS") |
| ], |
| components: [] |
| }); |
| } |
| } |
| |
| export default confirmationMessage; |