@sapphire/discord.js-utilities
Version:
Discord.js specific utilities for your JavaScript/TypeScript bots
1,225 lines (1,216 loc) • 113 kB
JavaScript
'use strict';
var discordUtilities = require('@sapphire/discord-utilities');
var utilities = require('@sapphire/utilities');
var discord_js = require('discord.js');
var duration = require('@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 === discord_js.ChannelType.GuildCategory;
}
__name(isCategoryChannel, "isCategoryChannel");
function isDMChannel(channel) {
return channel?.type === discord_js.ChannelType.DM;
}
__name(isDMChannel, "isDMChannel");
function isGroupChannel(channel) {
return channel?.type === discord_js.ChannelType.GroupDM;
}
__name(isGroupChannel, "isGroupChannel");
function isGuildBasedChannel(channel) {
return channel?.type !== discord_js.ChannelType.DM;
}
__name(isGuildBasedChannel, "isGuildBasedChannel");
function isGuildBasedChannelByGuildKey(channel) {
return Reflect.has(channel ?? {}, "guild");
}
__name(isGuildBasedChannelByGuildKey, "isGuildBasedChannelByGuildKey");
function isNewsChannel(channel) {
return channel?.type === discord_js.ChannelType.GuildAnnouncement;
}
__name(isNewsChannel, "isNewsChannel");
function isTextChannel(channel) {
return channel?.type === discord_js.ChannelType.GuildText;
}
__name(isTextChannel, "isTextChannel");
function isVoiceChannel(channel) {
return channel?.type === discord_js.ChannelType.GuildVoice;
}
__name(isVoiceChannel, "isVoiceChannel");
function isStageChannel(channel) {
return channel?.type === discord_js.ChannelType.GuildStageVoice;
}
__name(isStageChannel, "isStageChannel");
function isThreadChannel(channel) {
return channel?.isThread() ?? false;
}
__name(isThreadChannel, "isThreadChannel");
function isNewsThreadChannel(channel) {
return channel?.type === discord_js.ChannelType.AnnouncementThread;
}
__name(isNewsThreadChannel, "isNewsThreadChannel");
function isPublicThreadChannel(channel) {
return channel?.type === discord_js.ChannelType.PublicThread;
}
__name(isPublicThreadChannel, "isPublicThreadChannel");
function isPrivateThreadChannel(channel) {
return channel?.type === discord_js.ChannelType.PrivateThread;
}
__name(isPrivateThreadChannel, "isPrivateThreadChannel");
function isTextBasedChannel(channel) {
if (utilities.isNullish(channel) || //
channel.partial || isGroupChannel(channel) || isStageChannel(channel)) {
return false;
}
return !utilities.isNullish(channel.send);
}
__name(isTextBasedChannel, "isTextBasedChannel");
function isVoiceBasedChannel(channel) {
if (utilities.isNullish(channel)) return false;
return channel.isVoiceBased();
}
__name(isVoiceBasedChannel, "isVoiceBasedChannel");
function isNsfwChannel(channel) {
if (utilities.isNullish(channel)) return false;
switch (channel.type) {
case discord_js.ChannelType.DM:
case discord_js.ChannelType.GroupDM:
case discord_js.ChannelType.GuildCategory:
case discord_js.ChannelType.GuildStageVoice:
case discord_js.ChannelType.GuildVoice:
case discord_js.ChannelType.GuildDirectory:
return false;
case discord_js.ChannelType.GuildAnnouncement:
case discord_js.ChannelType.GuildText:
case discord_js.ChannelType.GuildForum:
case discord_js.ChannelType.GuildMedia:
return channel.nsfw;
case discord_js.ChannelType.AnnouncementThread:
case discord_js.ChannelType.PrivateThread:
case discord_js.ChannelType.PublicThread:
return Boolean(channel.parent?.nsfw);
}
}
__name(isNsfwChannel, "isNsfwChannel");
function isMessageInstance(message) {
return message instanceof discord_js.Message;
}
__name(isMessageInstance, "isMessageInstance");
function isAnyInteraction(messageOrInteraction) {
return messageOrInteraction instanceof discord_js.BaseInteraction;
}
__name(isAnyInteraction, "isAnyInteraction");
function isAnyInteractableInteraction(messageOrInteraction) {
if (isAnyInteraction(messageOrInteraction)) {
return !messageOrInteraction.isAutocomplete();
}
return false;
}
__name(isAnyInteractableInteraction, "isAnyInteractableInteraction");
function isGuildMember(member) {
return member instanceof discord_js.GuildMember;
}
__name(isGuildMember, "isGuildMember");
function isMediaAttachment(attachment) {
if (utilities.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:
!utilities.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 !utilities.isNullishOrZero(attachment.width) && !utilities.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 (!utilities.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 (!utilities.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 === discord_js.ComponentType.Button || action.type === discord_js.ComponentType.StringSelect || action.type === discord_js.ComponentType.UserSelect || action.type === discord_js.ComponentType.RoleSelect || action.type === discord_js.ComponentType.MentionableSelect || action.type === discord_js.ComponentType.ChannelSelect;
}
__name(actionIsButtonOrMenu, "actionIsButtonOrMenu");
function actionIsLinkButton(action) {
return action.type === discord_js.ComponentType.Button && action.style === discord_js.ButtonStyle.Link;
}
__name(actionIsLinkButton, "actionIsLinkButton");
function isMessageButtonInteractionData(interaction) {
return interaction.type === discord_js.ComponentType.Button;
}
__name(isMessageButtonInteractionData, "isMessageButtonInteractionData");
function isMessageStringSelectInteractionData(interaction) {
return interaction.type === discord_js.ComponentType.StringSelect;
}
__name(isMessageStringSelectInteractionData, "isMessageStringSelectInteractionData");
function isMessageUserSelectInteractionData(interaction) {
return interaction.type === discord_js.ComponentType.UserSelect;
}
__name(isMessageUserSelectInteractionData, "isMessageUserSelectInteractionData");
function isMessageRoleSelectInteractionData(interaction) {
return interaction.type === discord_js.ComponentType.RoleSelect;
}
__name(isMessageRoleSelectInteractionData, "isMessageRoleSelectInteractionData");
function isMessageMentionableSelectInteractionData(interaction) {
return interaction.type === discord_js.ComponentType.MentionableSelect;
}
__name(isMessageMentionableSelectInteractionData, "isMessageMentionableSelectInteractionData");
function isMessageChannelSelectInteractionData(interaction) {
return interaction.type === discord_js.ComponentType.ChannelSelect;
}
__name(isMessageChannelSelectInteractionData, "isMessageChannelSelectInteractionData");
function isButtonComponentBuilder(component) {
return component.data.type === discord_js.ComponentType.Button;
}
__name(isButtonComponentBuilder, "isButtonComponentBuilder");
function isActionButton(action) {
return action.type === discord_js.ComponentType.Button && action.style !== discord_js.ButtonStyle.Link;
}
__name(isActionButton, "isActionButton");
function isActionLink(action) {
return action.type === discord_js.ComponentType.Button && action.style === discord_js.ButtonStyle.Link;
}
__name(isActionLink, "isActionLink");
function isActionStringMenu(action) {
return action.type === discord_js.ComponentType.StringSelect;
}
__name(isActionStringMenu, "isActionStringMenu");
function isActionUserMenu(action) {
return action.type === discord_js.ComponentType.UserSelect;
}
__name(isActionUserMenu, "isActionUserMenu");
function isActionRoleMenu(action) {
return action.type === discord_js.ComponentType.RoleSelect;
}
__name(isActionRoleMenu, "isActionRoleMenu");
function isActionMentionableMenu(action) {
return action.type === discord_js.ComponentType.MentionableSelect;
}
__name(isActionMentionableMenu, "isActionMentionableMenu");
function isActionChannelMenu(action) {
return action.type === discord_js.ComponentType.ChannelSelect;
}
__name(isActionChannelMenu, "isActionChannelMenu");
function createPartitionedMessageRow(components) {
const [messageButtons, selectMenus] = utilities.partition(components, isButtonComponentBuilder);
const [actionButtons, linkButtons] = utilities.partition(messageButtons, (value) => value.data.style !== discord_js.ButtonStyle.Link);
const chunkedActionButtonComponents = utilities.chunk(actionButtons, 5);
const messageActionButtonActionRows = chunkedActionButtonComponents.map(
(componentsChunk) => new discord_js.ActionRowBuilder().setComponents(componentsChunk)
);
const selectMenuActionRows = selectMenus.map(
(component) => new discord_js.ActionRowBuilder().setComponents(component)
);
const chunkedLinkButtonComponents = utilities.chunk(linkButtons, 5);
const messageLinkButtonActionRows = chunkedLinkButtonComponents.map(
(componentsChunk) => new discord_js.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", duration.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 (discord_js.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(utilities.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 () => utilities.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: utilities.isFunction(embed) ? [embed(new discord_js.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: utilities.isFunction(embed) ? [await embed(new discord_js.EmbedBuilder(