UNPKG

@sapphire/discord.js-utilities

Version:

Discord.js specific utilities for your JavaScript/TypeScript bots

1,228 lines (1,220 loc) • 109 kB
import { EmbedLimits } from '@sapphire/discord-utilities'; export * from '@sapphire/discord-utilities'; import { isFunction, isNullish, deepClone, isObject, partition, chunk, isNullishOrEmpty, isNullishOrZero } from '@sapphire/utilities'; import { ComponentType, ButtonStyle, PermissionsBitField, PermissionFlagsBits, isJSONEncodable, EmbedBuilder, Partials, IntentsBitField, GatewayIntentBits, MessageFlags, InteractionCollector, InteractionType, ButtonBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, MentionableSelectMenuBuilder, ChannelSelectMenuBuilder, StringSelectMenuBuilder, BaseInteraction, ActionRowBuilder, Message, ChannelType, GuildMember, userMention } from 'discord.js'; import { Time } from '@sapphire/duration'; var __defProp = Object.defineProperty; var __typeError = (msg) => { throw TypeError(msg); }; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); // src/lib/builders/MessageBuilder.ts var _MessageBuilder = class _MessageBuilder { constructor(options) { /** * Whether or not the message should be spoken aloud. * @default false */ __publicField(this, "tts"); /** * The nonce for the message. * @default '' */ __publicField(this, "nonce"); /** * The content for the message. If set to undefined and the builder is used to edit, the content will not be * replaced. */ __publicField(this, "content"); /** * The embeds for the message. If set to undefined and the builder is used to edit, the embed will not be replaced. * @remark There is a maximum of 10 embeds in 1 message */ __publicField(this, "embeds"); /** * The components for the message. If set to undefined and the builder is used to edit, the components will not be replaced. */ __publicField(this, "components"); /** * Which mentions should be parsed from the message content. */ __publicField(this, "allowedMentions"); /** * Files to send with the message. This should not be set when editing a message, as Discord does not support * editing file attachments. */ __publicField(this, "files"); this.tts = options?.tts ?? _MessageBuilder.defaults.tts; this.nonce = options?.nonce ?? _MessageBuilder.defaults.nonce; this.content = options?.content ?? _MessageBuilder.defaults.content; this.embeds = options?.embeds ?? _MessageBuilder.defaults.embeds; this.components = options?.components ?? _MessageBuilder.defaults.components; this.allowedMentions = options?.allowedMentions ?? _MessageBuilder.defaults.allowedMentions; this.files = options?.files ?? _MessageBuilder.defaults.files; } /** * Sets the value for the {@link MessageBuilder.tts} field. * @param tts Whether or not the message should be spoken aloud. */ setTTS(tts) { this.tts = tts; return this; } /** * Sets the value for the {@link MessageBuilder.nonce} field. * @param nonce The nonce for the message. */ setNonce(nonce) { this.nonce = nonce; return this; } /** * Sets the value for the {@link MessageBuilder.content} field. * @param content The content for the message. If set to undefined and the builder is used to edit, the content will * not be replaced. */ setContent(content) { this.content = content; return this; } /** * Sets the value for the {@link MessageBuilder.embed} field. * @param embeds The embeds for the message. If set to undefined and the builder is used to edit, the embed will not be * replaced. There is a maximum of 10 embeds per message * @remark When providing more than 10 embeds, the array will automatically be sliced down to the first 10. */ setEmbeds(embeds) { if (embeds && embeds.length > 10) { embeds = embeds.slice(0, 10); } this.embeds = embeds; return this; } /** * Sets the value for the {@link MessageBuilder.components} field. * @param components The components for the message. If set to undefined and the builder is used to edit, the components will * not be replaced. */ setComponents(components) { this.components = components; return this; } /** * Sets the value for the {@link MessageBuilder.allowedMentions} field. * @param allowedMentions Which mentions should be parsed from the message content. */ setAllowedMentions(allowedMentions) { this.allowedMentions = allowedMentions; return this; } /** * Adds a new value for the {@link MessageBuilder.files} field array. * @param file The file to add to the {@link MessageBuilder.files} field array. */ addFile(file) { this.files = this.files?.concat(file) ?? [file]; return this; } /** * Sets a single value for the {@link MessageBuilder.files} field array. * @param file The file to send with the message. This should not be set when editing a message, as Discord does not * support editing file attachments. */ setFile(file) { this.files = [file]; return this; } /** * Sets the value for the {@link MessageBuilder.files} field. * @param files The files to send with the message. This should not be set when editing a message, as Discord does * not support editing file attachments. */ setFiles(files) { this.files = files; return this; } }; __name(_MessageBuilder, "MessageBuilder"); /** * The default values for all MessageBuilder instances. */ __publicField(_MessageBuilder, "defaults", {}); var MessageBuilder = _MessageBuilder; function isCategoryChannel(channel) { return channel?.type === ChannelType.GuildCategory; } __name(isCategoryChannel, "isCategoryChannel"); function isDMChannel(channel) { return channel?.type === ChannelType.DM; } __name(isDMChannel, "isDMChannel"); function isGroupChannel(channel) { return channel?.type === ChannelType.GroupDM; } __name(isGroupChannel, "isGroupChannel"); function isGuildBasedChannel(channel) { return channel?.type !== ChannelType.DM; } __name(isGuildBasedChannel, "isGuildBasedChannel"); function isGuildBasedChannelByGuildKey(channel) { return Reflect.has(channel ?? {}, "guild"); } __name(isGuildBasedChannelByGuildKey, "isGuildBasedChannelByGuildKey"); function isNewsChannel(channel) { return channel?.type === ChannelType.GuildAnnouncement; } __name(isNewsChannel, "isNewsChannel"); function isTextChannel(channel) { return channel?.type === ChannelType.GuildText; } __name(isTextChannel, "isTextChannel"); function isVoiceChannel(channel) { return channel?.type === ChannelType.GuildVoice; } __name(isVoiceChannel, "isVoiceChannel"); function isStageChannel(channel) { return channel?.type === ChannelType.GuildStageVoice; } __name(isStageChannel, "isStageChannel"); function isThreadChannel(channel) { return channel?.isThread() ?? false; } __name(isThreadChannel, "isThreadChannel"); function isNewsThreadChannel(channel) { return channel?.type === ChannelType.AnnouncementThread; } __name(isNewsThreadChannel, "isNewsThreadChannel"); function isPublicThreadChannel(channel) { return channel?.type === ChannelType.PublicThread; } __name(isPublicThreadChannel, "isPublicThreadChannel"); function isPrivateThreadChannel(channel) { return channel?.type === ChannelType.PrivateThread; } __name(isPrivateThreadChannel, "isPrivateThreadChannel"); function isTextBasedChannel(channel) { if (isNullish(channel) || // channel.partial || isGroupChannel(channel) || isStageChannel(channel)) { return false; } return !isNullish(channel.send); } __name(isTextBasedChannel, "isTextBasedChannel"); function isVoiceBasedChannel(channel) { if (isNullish(channel)) return false; return channel.isVoiceBased(); } __name(isVoiceBasedChannel, "isVoiceBasedChannel"); function isNsfwChannel(channel) { if (isNullish(channel)) return false; switch (channel.type) { case ChannelType.DM: case ChannelType.GroupDM: case ChannelType.GuildCategory: case ChannelType.GuildStageVoice: case ChannelType.GuildVoice: case ChannelType.GuildDirectory: return false; case ChannelType.GuildAnnouncement: case ChannelType.GuildText: case ChannelType.GuildForum: case ChannelType.GuildMedia: return channel.nsfw; case ChannelType.AnnouncementThread: case ChannelType.PrivateThread: case ChannelType.PublicThread: return Boolean(channel.parent?.nsfw); } } __name(isNsfwChannel, "isNsfwChannel"); function isMessageInstance(message) { return message instanceof Message; } __name(isMessageInstance, "isMessageInstance"); function isAnyInteraction(messageOrInteraction) { return messageOrInteraction instanceof BaseInteraction; } __name(isAnyInteraction, "isAnyInteraction"); function isAnyInteractableInteraction(messageOrInteraction) { if (isAnyInteraction(messageOrInteraction)) { return !messageOrInteraction.isAutocomplete(); } return false; } __name(isAnyInteractableInteraction, "isAnyInteractableInteraction"); function isGuildMember(member) { return member instanceof GuildMember; } __name(isGuildMember, "isGuildMember"); function isMediaAttachment(attachment) { if (isNullishOrEmpty(attachment.contentType)) return false; if (attachment.contentType.startsWith("audio/")) return true; return attachment.contentType.startsWith("image/") || attachment.contentType.startsWith("video/") ? hasDimensionsDefined(attachment) : false; } __name(isMediaAttachment, "isMediaAttachment"); function isImageAttachment(attachment) { return ( // A content type is required for an image attachment: !isNullishOrEmpty(attachment.contentType) && // // An image attachment must have a content type starting with 'image/': attachment.contentType.startsWith("image/") && // An image attachment must have dimensions defined: hasDimensionsDefined(attachment) ); } __name(isImageAttachment, "isImageAttachment"); function hasDimensionsDefined(attachment) { return !isNullishOrZero(attachment.width) && !isNullishOrZero(attachment.height); } __name(hasDimensionsDefined, "hasDimensionsDefined"); // src/lib/MessagePrompter/strategies/MessagePrompterBaseStrategy.ts var _MessagePrompterBaseStrategy = class _MessagePrompterBaseStrategy { /** * Constructor for the {@link MessagePrompterBaseStrategy} class * @param type - The type of message prompter strategy * @param message - The message that this prompt is for * @param options - Overrideable options if needed. */ constructor(type, message, options) { /** * The type of strategy that was used */ __publicField(this, "type"); /** * The timeout that was used in the collector */ __publicField(this, "timeout"); /** * Whether to return an explicit object with data, or the strategies' default */ __publicField(this, "explicitReturn"); /** * The message that has been sent in {@link MessagePrompter.run} */ __publicField(this, "appliedMessage", null); /** * The message that will be sent in {@link MessagePrompter.run} */ __publicField(this, "message"); /** * The message the bot will edit to send its prompt in {@link MessagePrompter.run} */ __publicField(this, "editMessage"); this.type = type; this.timeout = options?.timeout ?? _MessagePrompterBaseStrategy.defaultStrategyOptions.timeout ?? 10 * 1e3; this.explicitReturn = options?.explicitReturn ?? _MessagePrompterBaseStrategy.defaultStrategyOptions.explicitReturn ?? false; this.editMessage = options?.editMessage ?? _MessagePrompterBaseStrategy.defaultStrategyOptions.editMessage ?? void 0; this.message = message; } async collectReactions(channel, authorOrFilter, reactions) { if (isTextBasedChannel(channel) && !isStageChannel(channel)) { if (!isNullish(this.editMessage) && this.editMessage.editable) { this.appliedMessage = await this.editMessage.edit(this.message); } else { this.appliedMessage = await channel.send(this.message); } const collector = this.appliedMessage.createReactionCollector({ ...this.createReactionPromptFilter(reactions, authorOrFilter), max: 1, time: this.timeout }); let resolved = false; const collected = new Promise((resolve, reject) => { collector.on("collect", (r) => { resolve(r); resolved = true; collector.stop(); }); collector.on("end", (collected2) => { resolved = true; if (!collected2.size) reject(new Error("Collector has ended")); }); }); for (const reaction2 of reactions) { if (resolved) break; await this.appliedMessage.react(reaction2); } const firstReaction = await collected; const emoji = firstReaction?.emoji; const reaction = reactions.find((r) => (emoji?.id ?? emoji?.name) === r); return { emoji, reaction, strategy: this, appliedMessage: this.appliedMessage, message: this.message }; } throw new Error("A channel was provided to which I am not able to send messages"); } /** * Creates a filter for the collector to filter on * @return The filter for awaitReactions function */ createReactionPromptFilter(reactions, authorOrFilter) { return { filter: /* @__PURE__ */ __name(async (reaction, user) => reactions.includes(reaction.emoji.id ?? reaction.emoji.name ?? "") && (typeof authorOrFilter === "function" ? await authorOrFilter(reaction, user) : user.id === authorOrFilter.id) && !user.bot, "filter") }; } }; __name(_MessagePrompterBaseStrategy, "MessagePrompterBaseStrategy"); /** * The default strategy options */ __publicField(_MessagePrompterBaseStrategy, "defaultStrategyOptions", { timeout: 10 * 1e3, explicitReturn: false, editMessage: void 0 }); var MessagePrompterBaseStrategy = _MessagePrompterBaseStrategy; // src/lib/MessagePrompter/strategies/MessagePrompterConfirmStrategy.ts var _MessagePrompterConfirmStrategy = class _MessagePrompterConfirmStrategy extends MessagePrompterBaseStrategy { /** * Constructor for the {@link MessagePrompterBaseStrategy} class * @param message The message to be sent {@link MessagePrompter} * @param options Overrideable options if needed. */ constructor(message, options) { super("confirm", message, options); /** * The confirm emoji used */ __publicField(this, "confirmEmoji"); /** * The cancel emoji used */ __publicField(this, "cancelEmoji"); this.confirmEmoji = options?.confirmEmoji ?? _MessagePrompterConfirmStrategy.confirmEmoji; this.cancelEmoji = options?.cancelEmoji ?? _MessagePrompterConfirmStrategy.cancelEmoji; } /** * This executes the {@link MessagePrompter} and sends the message if {@link IMessagePrompterOptions.type} equals confirm. * The handler will wait for one (1) reaction. * @param channel The channel to use. * @param authorOrFilter An author object to validate or a {@linkplain https://discord.js.org/docs/packages/discord.js/main/CollectorFilter:TypeAlias CollectorFilter} predicate callback. * @returns A promise that resolves to a boolean denoting the value of the input (`true` for yes, `false` for no). */ async run(channel, authorOrFilter) { const response = await this.collectReactions(channel, authorOrFilter, [this.confirmEmoji, this.cancelEmoji]); const confirmed = (response?.emoji?.id ?? response?.emoji?.name) === this.confirmEmoji; return this.explicitReturn ? { ...response, confirmed } : confirmed; } }; __name(_MessagePrompterConfirmStrategy, "MessagePrompterConfirmStrategy"); /** * The default confirm emoji used for {@link MessagePrompterConfirmStrategy} */ __publicField(_MessagePrompterConfirmStrategy, "confirmEmoji", "\u{1F1FE}"); /** * The default cancel emoji used for {@link MessagePrompterConfirmStrategy} */ __publicField(_MessagePrompterConfirmStrategy, "cancelEmoji", "\u{1F1F3}"); var MessagePrompterConfirmStrategy = _MessagePrompterConfirmStrategy; var _MessagePrompterMessageStrategy = class _MessagePrompterMessageStrategy extends MessagePrompterBaseStrategy { /** * Constructor for the {@link MessagePrompterBaseStrategy} class * @param message The message instance for this {@link MessagePrompter} * @param options Overrideable options if needed. */ constructor(message, options) { super("message", message, options); } /** * This executes the {@link MessagePrompter} and sends the message if {@link IMessagePrompterOptions.type} equals message. * The handler will wait for one (1) message. * @param channel The channel to use. * @param authorOrFilter An author object to validate or a {@linkplain https://discord.js.org/docs/packages/discord.js/main/CollectorFilter:TypeAlias CollectorFilter} predicate callback. * @returns A promise that resolves to the message object received. */ async run(channel, authorOrFilter) { if (isTextBasedChannel(channel) && !isStageChannel(channel)) { if (!isNullish(this.editMessage) && this.editMessage.editable) { this.appliedMessage = await this.editMessage.edit(this.message); } else { this.appliedMessage = await channel.send(this.message); } const collector = await channel.awaitMessages({ ...this.createMessagePromptFilter(authorOrFilter), max: 1, time: this.timeout, errors: ["time"] }); const response = collector.first(); if (!response) { throw new Error("No messages received"); } return this.explicitReturn ? { response, strategy: this, appliedMessage: this.appliedMessage, message: this.message } : response; } throw new Error("A channel was provided to which I am not able to send messages"); } /** * Creates a filter for the collector to filter on * @return The filter for awaitMessages function */ createMessagePromptFilter(authorOrFilter) { return { filter: /* @__PURE__ */ __name(async (message) => (typeof authorOrFilter === "function" ? await authorOrFilter(message) : message.author.id === authorOrFilter.id) && !message.author.bot, "filter") }; } }; __name(_MessagePrompterMessageStrategy, "MessagePrompterMessageStrategy"); var MessagePrompterMessageStrategy = _MessagePrompterMessageStrategy; // src/lib/MessagePrompter/strategies/MessagePrompterNumberStrategy.ts var _MessagePrompterNumberStrategy = class _MessagePrompterNumberStrategy extends MessagePrompterBaseStrategy { /** * Constructor for the {@link MessagePrompterBaseStrategy} class * @param message The message instance for this {@link MessagePrompter} * @param options Overrideable options if needed. */ constructor(message, options) { super("number", message, options); /** * The available number emojis */ __publicField(this, "numberEmojis"); /** * The available number emojis */ __publicField(this, "start"); /** * The available number emojis */ __publicField(this, "end"); this.numberEmojis = options?.numberEmojis ?? _MessagePrompterNumberStrategy.numberEmojis; this.start = options?.start ?? 0; this.end = options?.end ?? 10; } /** * This executes the {@link MessagePrompter} and sends the message if {@link IMessagePrompterOptions.type} equals number. * The handler will wait for one (1) reaction. * @param channel The channel to use. * @param authorOrFilter An author object to validate or a {@linkplain https://discord.js.org/docs/packages/discord.js/main/CollectorFilter:TypeAlias CollectorFilter} predicate callback. * @returns A promise that resolves to the selected number within the range. */ async run(channel, authorOrFilter) { if (this.start < 0) throw new TypeError("Starting number cannot be less than 0."); if (this.end > 10) throw new TypeError("Ending number cannot be more than 10."); const numbers = Array.from({ length: this.end - this.start + 1 }, (_, n) => n + this.start); const emojis = this.numberEmojis.slice(this.start, this.end); const response = await this.collectReactions(channel, authorOrFilter, emojis); const emojiIndex = emojis.findIndex((emoji) => (response?.emoji?.id ?? response?.emoji?.name) === emoji); const number = numbers[emojiIndex]; return this.explicitReturn ? { ...response, number } : number; } }; __name(_MessagePrompterNumberStrategy, "MessagePrompterNumberStrategy"); /** * The default available number emojis */ __publicField(_MessagePrompterNumberStrategy, "numberEmojis", ["0\uFE0F\u20E3", "1\uFE0F\u20E3", "2\uFE0F\u20E3", "3\uFE0F\u20E3", "4\uFE0F\u20E3", "5\uFE0F\u20E3", "6\uFE0F\u20E3", "7\uFE0F\u20E3", "8\uFE0F\u20E3", "9\uFE0F\u20E3", "\u{1F51F}"]); var MessagePrompterNumberStrategy = _MessagePrompterNumberStrategy; // src/lib/MessagePrompter/strategies/MessagePrompterReactionStrategy.ts var _MessagePrompterReactionStrategy = class _MessagePrompterReactionStrategy extends MessagePrompterBaseStrategy { /** * Constructor for the {@link MessagePrompterReactionStrategy} class * @param message The message instance for this {@link MessagePrompter} * @param options Overrideable options if needed. */ constructor(message, options) { super("reactions", message, options); /** * The emojis used */ __publicField(this, "reactions"); this.reactions = options?.reactions; } /** * This executes the {@link MessagePrompterReactionStrategy} and sends the message. * The handler will wait for one (1) reaction. * @param channel The channel to use. * @param authorOrFilter An author object to validate or a {@linkplain https://discord.js.org/docs/packages/discord.js/main/CollectorFilter:TypeAlias CollectorFilter} predicate callback. * @returns A promise that resolves to the reaction object. */ async run(channel, authorOrFilter) { if (!this.reactions?.length) throw new TypeError("There are no reactions provided."); const response = await this.collectReactions(channel, authorOrFilter, this.reactions); return this.explicitReturn ? response : response.reaction ?? response; } }; __name(_MessagePrompterReactionStrategy, "MessagePrompterReactionStrategy"); var MessagePrompterReactionStrategy = _MessagePrompterReactionStrategy; // src/lib/MessagePrompter/MessagePrompter.ts var _MessagePrompter = class _MessagePrompter { /** * Constructor for the {@link MessagePrompter} class * @param message The message to send. * @param strategy The strategy name or Instance to use * @param strategyOptions The options that are passed to the strategy */ constructor(message, strategy, strategyOptions) { /** * The strategy used in {@link MessagePrompter.run} */ __publicField(this, "strategy"); let strategyToRun; if (message instanceof MessagePrompterBaseStrategy) { strategyToRun = message; } else { const mapStrategy = _MessagePrompter.strategies.get(strategy ?? _MessagePrompter.defaultStrategy); if (!mapStrategy) { throw new Error("No strategy provided"); } strategyToRun = new mapStrategy(message, strategyOptions); } this.strategy = strategyToRun; } /** * This executes the {@link MessagePrompter} and sends the message. * @param channel The channel to use. * @param authorOrFilter An author object to validate or a {@linkplain https://discord.js.org/docs/packages/discord.js/main/CollectorFilter:TypeAlias CollectorFilter} predicate callback. */ run(channel, authorOrFilter) { return this.strategy.run(channel, authorOrFilter); } }; __name(_MessagePrompter, "MessagePrompter"); /** * The available strategies */ __publicField(_MessagePrompter, "strategies", /* @__PURE__ */ new Map([ ["confirm", MessagePrompterConfirmStrategy], ["number", MessagePrompterNumberStrategy], ["reaction", MessagePrompterReactionStrategy], ["message", MessagePrompterMessageStrategy] ])); /** * The default strategy to use */ __publicField(_MessagePrompter, "defaultStrategy", "confirm"); var MessagePrompter = _MessagePrompter; function actionIsButtonOrMenu(action) { return action.type === ComponentType.Button || action.type === ComponentType.StringSelect || action.type === ComponentType.UserSelect || action.type === ComponentType.RoleSelect || action.type === ComponentType.MentionableSelect || action.type === ComponentType.ChannelSelect; } __name(actionIsButtonOrMenu, "actionIsButtonOrMenu"); function actionIsLinkButton(action) { return action.type === ComponentType.Button && action.style === ButtonStyle.Link; } __name(actionIsLinkButton, "actionIsLinkButton"); function isMessageButtonInteractionData(interaction) { return interaction.type === ComponentType.Button; } __name(isMessageButtonInteractionData, "isMessageButtonInteractionData"); function isMessageStringSelectInteractionData(interaction) { return interaction.type === ComponentType.StringSelect; } __name(isMessageStringSelectInteractionData, "isMessageStringSelectInteractionData"); function isMessageUserSelectInteractionData(interaction) { return interaction.type === ComponentType.UserSelect; } __name(isMessageUserSelectInteractionData, "isMessageUserSelectInteractionData"); function isMessageRoleSelectInteractionData(interaction) { return interaction.type === ComponentType.RoleSelect; } __name(isMessageRoleSelectInteractionData, "isMessageRoleSelectInteractionData"); function isMessageMentionableSelectInteractionData(interaction) { return interaction.type === ComponentType.MentionableSelect; } __name(isMessageMentionableSelectInteractionData, "isMessageMentionableSelectInteractionData"); function isMessageChannelSelectInteractionData(interaction) { return interaction.type === ComponentType.ChannelSelect; } __name(isMessageChannelSelectInteractionData, "isMessageChannelSelectInteractionData"); function isButtonComponentBuilder(component) { return component.data.type === ComponentType.Button; } __name(isButtonComponentBuilder, "isButtonComponentBuilder"); function isActionButton(action) { return action.type === ComponentType.Button && action.style !== ButtonStyle.Link; } __name(isActionButton, "isActionButton"); function isActionLink(action) { return action.type === ComponentType.Button && action.style === ButtonStyle.Link; } __name(isActionLink, "isActionLink"); function isActionStringMenu(action) { return action.type === ComponentType.StringSelect; } __name(isActionStringMenu, "isActionStringMenu"); function isActionUserMenu(action) { return action.type === ComponentType.UserSelect; } __name(isActionUserMenu, "isActionUserMenu"); function isActionRoleMenu(action) { return action.type === ComponentType.RoleSelect; } __name(isActionRoleMenu, "isActionRoleMenu"); function isActionMentionableMenu(action) { return action.type === ComponentType.MentionableSelect; } __name(isActionMentionableMenu, "isActionMentionableMenu"); function isActionChannelMenu(action) { return action.type === ComponentType.ChannelSelect; } __name(isActionChannelMenu, "isActionChannelMenu"); function createPartitionedMessageRow(components) { const [messageButtons, selectMenus] = partition(components, isButtonComponentBuilder); const [actionButtons, linkButtons] = partition(messageButtons, (value) => value.data.style !== ButtonStyle.Link); const chunkedActionButtonComponents = chunk(actionButtons, 5); const messageActionButtonActionRows = chunkedActionButtonComponents.map( (componentsChunk) => new ActionRowBuilder().setComponents(componentsChunk) ); const selectMenuActionRows = selectMenus.map( (component) => new ActionRowBuilder().setComponents(component) ); const chunkedLinkButtonComponents = chunk(linkButtons, 5); const messageLinkButtonActionRows = chunkedLinkButtonComponents.map( (componentsChunk) => new ActionRowBuilder().setComponents(componentsChunk) ); return [...messageActionButtonActionRows, ...selectMenuActionRows, ...messageLinkButtonActionRows].map( (actionRow) => actionRow.toJSON() ); } __name(createPartitionedMessageRow, "createPartitionedMessageRow"); async function safelyReplyToInteraction(parameters) { if (isAnyInteractableInteraction(parameters.messageOrInteraction)) { if (parameters.messageOrInteraction.replied || parameters.messageOrInteraction.deferred) { await parameters.messageOrInteraction.editReply(parameters.interactionEditReplyContent); } else if (parameters.messageOrInteraction.isMessageComponent()) { await parameters.messageOrInteraction.update(parameters.componentUpdateContent); } else { await parameters.messageOrInteraction.reply(parameters.interactionReplyContent); } } else if (parameters.messageMethodContent && parameters.messageMethod && isMessageInstance(parameters.messageOrInteraction)) { await parameters.messageOrInteraction[parameters.messageMethod](parameters.messageMethodContent); } } __name(safelyReplyToInteraction, "safelyReplyToInteraction"); // src/lib/PaginatedMessages/PaginatedMessage.ts var _thisMazeWasNotMeantForYouContent; var _PaginatedMessage = class _PaginatedMessage { // #endregion /** * Constructor for the {@link PaginatedMessage} class * @param __namedParameters The {@link PaginatedMessageOptions} for this instance of the {@link PaginatedMessage} class */ constructor({ pages, actions, template, pageIndexPrefix, embedFooterSeparator, paginatedMessageData = null } = {}) { // #endregion // #region public class properties /** * The pages to be converted to {@link PaginatedMessage.messages} */ __publicField(this, "pages", []); /** * The response message used to edit on page changes. */ __publicField(this, "response", null); /** * The collector used for handling component interactions. */ __publicField(this, "collector", null); /** * The pages which were converted from {@link PaginatedMessage.pages} */ __publicField(this, "messages", []); /** * The actions which are to be used. */ __publicField(this, "actions", /* @__PURE__ */ new Map()); /** * The page-specific actions which are to be used. */ __publicField(this, "pageActions", []); /** * The handler's current page/message index. */ __publicField(this, "index", 0); /** * The amount of milliseconds to idle before the paginator is closed. * @default 14.5 minutes * @remark This is to ensure it is a bit before interactions expire. */ __publicField(this, "idle", Time.Minute * 14.5); /** * The template for this {@link PaginatedMessage}. * You can use templates to set defaults that will apply to each and every page in the {@link PaginatedMessage} */ __publicField(this, "template"); /** * Custom text to show in front of the page index in the embed footer. * PaginatedMessage will automatically add a space (` `) after the given text. You do not have to add it yourself. * @default ```PaginatedMessage.pageIndexPrefix``` (static property) */ __publicField(this, "pageIndexPrefix", _PaginatedMessage.pageIndexPrefix); /** * Custom separator to show after the page index in the embed footer. * PaginatedMessage will automatically add a space (` `) after the given text. You do not have to add it yourself. * @default ```PaginatedMessage.embedFooterSeparator``` (static property) */ __publicField(this, "embedFooterSeparator", _PaginatedMessage.embedFooterSeparator); /** * A list of `customId` that are bound to actions that will stop the {@link PaginatedMessage} * @default ```PaginatedMessage.stopPaginatedMessageCustomIds``` (static property) */ __publicField(this, "stopPaginatedMessageCustomIds", _PaginatedMessage.stopPaginatedMessageCustomIds); /** * Whether to emit the warning about running a {@link PaginatedMessage} in a DM channel without the client having the `'CHANNEL'` partial. * @remark When using message based commands (as opposed to Application Commands) then you will also need to specify the `DIRECT_MESSAGE` intent for {@link PaginatedMessage} to work. * * @default ```PaginatedMessage.emitPartialDMChannelWarning``` (static property) */ __publicField(this, "emitPartialDMChannelWarning", _PaginatedMessage.emitPartialDMChannelWarning); // #endregion // #region protected class properties /** * Data for the paginated message. */ __publicField(this, "paginatedMessageData", null); /** * The placeholder for the select menu. */ __publicField(this, "selectMenuPlaceholder"); /** * Tracks whether a warning was already emitted for this {@link PaginatedMessage} * concerning the maximum amount of pages in the {@link SelectMenu}. * * @default false */ __publicField(this, "hasEmittedMaxPageWarning", false); /** * Tracks whether a warning was already emitted for this {@link PaginatedMessage} * concerning the {@link PaginatedMessage} being called in a `DMChannel` * without the client having the `'Channel'` partial. * * @remark When using message based commands (as opposed to Application Commands) then you will also need to specify the `DIRECT_MESSAGE` intent for {@link PaginatedMessage} to work. * @default false */ __publicField(this, "hasEmittedPartialDMChannelWarning", false); /** * Determines whether the default footer that shows the current page number should be added to the embeds. * * @note If this is set to false, i.e.e through {@link setShouldAddFooterToEmbeds}, then {@link embedFooterSeparator} * is never applied. * * @default true */ __publicField(this, "shouldAddFooterToEmbeds", true); /** * Function that returns the select menu options for the paginated message. * @param message The paginated message. * @returns The select menu options. */ __publicField(this, "selectMenuOptions", _PaginatedMessage.selectMenuOptions); /** * Function that handles the reply when a user interacts with the paginated message incorrectly. */ __publicField(this, "wrongUserInteractionReply", _PaginatedMessage.wrongUserInteractionReply); // #endregion // #region private class fields /** The response we send when someone gets into an invalid flow */ __privateAdd(this, _thisMazeWasNotMeantForYouContent, { content: "This maze wasn't meant for you...what did you do." }); if (pages) this.addPages(pages); this.addActions(actions ?? this.constructor.defaultActions); this.template = _PaginatedMessage.resolveTemplate(template); this.pageIndexPrefix = pageIndexPrefix ?? _PaginatedMessage.pageIndexPrefix; this.embedFooterSeparator = embedFooterSeparator ?? _PaginatedMessage.embedFooterSeparator; this.paginatedMessageData = paginatedMessageData; } // #endregion // #region private static class properties /** * Resolves the template for the PaginatedMessage. * * @param template - The template to resolve. * @returns The resolved template as a BaseMessageOptions object. */ static resolveTemplate(template) { if (template === void 0) { return {}; } if (isJSONEncodable(template)) { return { embeds: [template.toJSON()] }; } return template; } // #region property setters /** * Sets the {@link PaginatedMessage.selectMenuOptions} for this instance of {@link PaginatedMessage}. * This will only apply to this one instance and no others. * @param newOptions The new options generator to set * @returns The current instance of {@link PaginatedMessage} */ setSelectMenuOptions(newOptions) { this.selectMenuOptions = newOptions; return this; } /** * Sets the {@link PaginatedMessage.selectMenuPlaceholder} for this instance of {@link PaginatedMessage}. * * This applies only to the string select menu from the {@link PaginatedMessage.defaultActions} * that offers "go to page" (we internally check the customId for this) * * This will only apply to this one instance and no others. * @param placeholder The new placeholder to set * @returns The current instance of {@link PaginatedMessage} */ setSelectMenuPlaceholder(placeholder) { this.selectMenuPlaceholder = placeholder; return this; } /** * Sets the {@link PaginatedMessage.wrongUserInteractionReply} for this instance of {@link PaginatedMessage}. * This will only apply to this one instance and no others. * @param wrongUserInteractionReply The new `wrongUserInteractionReply` to set * @returns The current instance of {@link PaginatedMessage} */ setWrongUserInteractionReply(wrongUserInteractionReply) { this.wrongUserInteractionReply = wrongUserInteractionReply; return this; } /** * Sets the {@link PaginatedMessage.stopPaginatedMessageCustomIds} for this instance of {@link PaginatedMessage}. * This will only apply to this one instance and no others. * @param stopPaginatedMessageCustomIds The new `stopPaginatedMessageCustomIds` to set * @returns The current instance of {@link PaginatedMessage} */ setStopPaginatedMessageCustomIds(stopPaginatedMessageCustomIds) { this.stopPaginatedMessageCustomIds = stopPaginatedMessageCustomIds; return this; } /** * Sets the {@link PaginatedMessage.emitPartialDMChannelWarning} for this instance of {@link PaginatedMessage}. * This will only apply to this one instance and no others. * @param emitPartialDMChannelWarning The new `emitPartialDMChannelWarning` to set * @returns The current instance of {@link PaginatedMessage} */ setEmitPartialDMChannelWarning(emitPartialDMChannelWarning) { this.emitPartialDMChannelWarning = emitPartialDMChannelWarning; return this; } /** * Sets the handler's current page/message index. * @param index The number to set the index to. */ setIndex(index) { this.index = index; return this; } /** * Sets the amount of time to idle before the paginator is closed. * @param idle The number to set the idle to. */ setIdle(idle) { this.idle = idle; return this; } /** * Sets the value of {@link shouldAddFooterToEmbeds} property and returns the instance of the class. * @param newValue - The new value for {@link shouldAddFooterToEmbeds}. * @returns The instance of the class with the updated {@link shouldAddFooterToEmbeds} value. */ setShouldAddFooterToEmbeds(newValue) { this.shouldAddFooterToEmbeds = newValue; return this; } // #endregion // #region actions related methods /** * Clears all current actions and sets them. The order given is the order they will be used. * @param actions The actions to set. This can be either a Button, Link Button, or Select Menu. * @param includeDefaultActions Whether to merge in the {@link PaginatedMessage.defaultActions} when setting the actions. * If you set this to true then you do not need to manually add `...PaginatedMessage.defaultActions` as seen in the first example. * The default value is `false` for backwards compatibility within the current major version. * * @remark You can retrieve the default actions for the regular pagination * @example * ```typescript * const display = new PaginatedMessage(); * * display.setActions([ * ...PaginatedMessage.defaultActions, * ]) * ``` * * @remark You can add custom Message Buttons by providing `style`, `customId`, `type`, `run` and at least one of `label` or `emoji`. * @example * ```typescript * const display = new PaginatedMessage(); * * display.setActions([ * { * style: 'PRIMARY', * label: 'My Button', * customId: 'custom_button', * type: ComponentType.Button, * run: (context) => console.log(context) * } * ], true); * ``` * * @remark You can add custom Message **Link** Buttons by providing `style`, `url`, `type`, and at least one of `label` or `emoji`. * @example * ```typescript * const display = new PaginatedMessage(); * * display.setActions([ * { * style: 'LINK', * label: 'Sapphire Website', * emoji: '🔷', * url: 'https://sapphirejs.dev', * type: ComponentType.Button * } * ], true); * ``` * * @remark You can add custom Select Menus by providing `customId`, `type`, and `run`. * @example * ```typescript * const display = new PaginatedMessage(); * * display.setActions([ * { * customId: 'custom_menu', * type: ComponentType.StringSelect, * run: (context) => console.log(context) // Do something here * } * ], true); * ``` */ setActions(actions, includeDefaultActions = false) { this.actions.clear(); return this.addActions([...includeDefaultActions ? _PaginatedMessage.defaultActions : [], ...actions]); } /** * Adds actions to the existing ones. The order given is the order they will be used. * @param actions The actions to add. * @see {@link PaginatedMessage.setActions} for examples on how to structure the actions. */ addActions(actions) { for (const action of actions) this.addAction(action); return this; } /** * Adds an action to the existing ones. This will be added as the last action. * @param action The action to add. * @see {@link PaginatedMessage.setActions} for examples on how to structure the action. */ addAction(action) { if (actionIsLinkButton(action)) { this.actions.set(action.url, action); } else if (actionIsButtonOrMenu(action)) { this.actions.set(action.customId, action); } return this; } // #endregion // #region page related methods /** * Checks whether or not the handler has a specific page. * @param index The index to check. */ hasPage(index) { return index >= 0 && index < this.pages.length; } /** * Clears all current pages and messages and sets them. The order given is the order they will be used. * @param pages The pages to set. */ setPages(pages) { this.pages = []; this.messages = []; this.addPages(pages); return this; } /** * Adds a page to the existing ones. This will be added as the last page. * @remark While you can use this method you should first check out * {@link PaginatedMessage.addPageBuilder}, * {@link PaginatedMessage.addPageContent} and * {@link PaginatedMessage.addPageEmbed} as * these are easier functional methods of adding pages and will likely already suffice for your needs. * * @param page The page to add. */ addPage(page) { if (this.pages.length === 25) { if (!this.hasEmittedMaxPageWarning) { process.emitWarning( "Maximum amount of pages exceeded for PaginatedMessage. Please check your instance of PaginatedMessage and ensure that you do not exceed 25 pages total.", { type: "PaginatedMessageExceededMessagePageAmount", code: "PAGINATED_MESSAGE_EXCEEDED_MAXIMUM_AMOUNT_OF_PAGES", detail: `If you do need more than 25 pages you can extend the class and overwrite the actions in the constructor.` } ); this.hasEmittedMaxPageWarning = true; } return this; } this.pages.push(page); return this; } /** * Update the current page. * @param page The content to update the page with. * * @remark This method can only be used after {@link PaginatedMessage.run} has been used. */ async updateCurrentPage(page) { const interaction = this.response; const currentIndex = this.index; if (interaction === null) { throw new Error("You cannot update a page before responding to the interaction."); } this.pages[currentIndex] = page; this.messages[currentIndex] = null; this.pageActions[currentIndex]?.clear(); const target = isAnyInteraction(interaction) ? interaction.user : interaction.author; await this.resolvePage(interaction, target, currentIndex); return this; } /** * Adds a page to the existing ones using a {@link MessageBuilder}. This will be added as the last page. * @param builder Either a callback whose first parameter is `new MessageBuilder()`, or an already constructed {@link MessageBuilder} * @example * ```typescript * const { PaginatedMessage } = require('@sapphire/discord.js-utilities'); * const { EmbedBuilder } = require('discord.js'); * * const paginatedMessage = new PaginatedMessage() * .addPageBuilder((builder) => { * const embed = new EmbedBuilder() * .setColor('#FF0000') * .setDescription('example description'); * * return builder * .setContent('example content') * .setEmbeds([embed]); * }); * ``` * @example * ```typescript * const { EmbedBuilder } = require('discord.js'); * const { MessageBuilder, PaginatedMessage } = require('@sapphire/discord.js-utilities'); * * const embed = new EmbedBuilder() * .setColor('#FF0000') * .setDescription('example description'); * * const builder = new MessageBuilder() * .setContent('example content') * .setEmbeds([embed]); * * const paginatedMessage = new PaginatedMessage() * .addPageBuilder(builder); * ``` */ addPageBuilder(builder) { return this.addPage(isFunction(builder) ? builder(new MessageBuilder()) : builder); } /** * Adds a page to the existing ones asynchronously using a {@link MessageBuilder}. This wil be added as the last page. * @param builder Either a callback whose first parameter is `new MessageBuilder()`, or an already constructed {@link MessageBuilder} * @example * ```typescript * const { PaginatedMessage } = require('@sapphire/discord.js-utilities'); * const { EmbedBuilder } = require('discord.js'); * * const paginatedMessage = new PaginatedMessage() * .addAsyncPageBuilder(async (builder) => { * const someRemoteData = await fetch('https://contoso.com/api/users'); * * const embed = new EmbedBuilder() * .setColor('#FF0000') * .setDescription(someRemoteData.data); * * return builder * .setContent('example content') * .setEmbeds([embed]); * }); * ``` */ addAsyncPageBuilder(builder) { return this.addPage(async () => isFunction(builder) ? builder(new MessageBuilder()) : builder); } /** * Adds a page to the existing ones using simple message content. This will be added as the last page. * @param content The content to set. * @example * ```typescript * const { PaginatedMessage } = require('@sapphire/discord.js-utilities'); * * const paginatedMessage = new PaginatedMessage() * .addPageContent('example content'); * ``` */ addPageContent(content) { return this.addPage({ content }); } /** * Adds a page to the existing ones using a {@link EmbedBuilder}. This wil be added as the last page. * @param embed Either a callback whose first parameter is `new EmbedBuilder()`, or an already constructed {@link EmbedBuilder} * @example * ```typescript * const { PaginatedMessage } = require('@sapphire/discord.js-utilities'); * * const paginatedMessage = new PaginatedMessage() * .addPageEmbed((embed) => { * embed * .setColor('#FF0000') * .setDescription('example description'); * * return embed; * }); * ``` * @example * ```typescript * const { PaginatedMessage } = require('@sapphire/discord.js-utilities'); * * const embed = new EmbedBuilder() * .setColor('#FF0000') * .setDescription('example description'); * * const paginatedMessage = new PaginatedMessage() * .addPageEmbed(embed); * ``` */ addPageEmbed(embed) { return this.addPage({ embeds: isFunction(embed) ? [embed(new EmbedBuilder())] : [embed] }); } /** * Adds a page to the existing ones asynchronously using a {@link EmbedBuilder}. This wil be added as the last page. * @param embed Either a callback whose first parameter is `new EmbedBuilder()`, or an already constructed {@link EmbedBuilder} * @example * ```typescript * const { PaginatedMessage } = require('@sapphire/discord.js-utilities'); * * const paginatedMessage = new PaginatedMessage() * .addAsyncPageEmbed(async (embed) => { * const someRemoteData = await fetch('https://contoso.com/api/users'); * * embed * .setColor('#FF0000') * .setDescription(someRemoteData.data); * * return embed; * }); * ``` */ addAsyncPageEmbed(embed) { return this.addPage(async () => ({ embeds: isFunction(embed) ? [await embed(new EmbedBuilder())] : [embed] })); } /** * Adds a page to the existing ones asynchronously using multiple {@link EmbedBuilder}'s. This wil be added as the last page. * @remark When using this with a callback this will construct 10 {@link EmbedBuilder}'s in the callback parameters, regardless of how many are