@sapphire/discord.js-utilities
Version:
Discord.js specific utilities for your JavaScript/TypeScript bots
1,078 lines (1,065 loc) âĸ 112 kB
text/typescript
export * from '@sapphire/discord-utilities';
import { MessageCreateOptions, CategoryChannel, DMChannel, DirectoryChannel, PartialDMChannel, NewsChannel, StageChannel, TextChannel, ThreadChannel, VoiceChannel, GuildChannel, Channel, Message, ChatInputCommandInteraction, UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction, AutocompleteInteraction, StringSelectMenuInteraction, ButtonInteraction, Interaction, PartialTextBasedChannelFields, EmojiIdentifierResolvable, User, CollectorFilter, MessageReaction, CollectorOptions, GuildEmoji, ReactionEmoji, ApplicationEmoji, EmojiResolvable, InteractionButtonComponentData, LinkButtonComponentData, StringSelectMenuComponentData, UserSelectMenuComponentData, RoleSelectMenuComponentData, MentionableSelectMenuComponentData, ChannelSelectMenuComponentData, APIMessage, CommandInteraction, InteractionCollector, EmbedBuilder, BaseMessageOptions, WebhookMessageEditOptions, SelectMenuComponentOptionData, MessageComponentInteraction, APIEmbed, JSONEncodable, CollectedInteraction, ModalSubmitInteraction, APIActionRowComponent, APIMessageActionRowComponent, ActionRowData, ActionRowComponentOptions, MessageActionRowComponentBuilder, Guild, InteractionReplyOptions, InteractionUpdateOptions, MessageReplyOptions, MessageEditOptions, Collection, Snowflake, EmbedData, EmbedField, ButtonComponentData, ButtonBuilder, PartialGroupDMChannel, PublicThreadChannel, PrivateThreadChannel, VoiceBasedChannel, BaseInteraction, GuildMember, APIGuildMember, APIInteractionGuildMember, APIInteractionDataResolvedGuildMember, Attachment } from 'discord.js';
import { ArgumentTypes, Awaitable, Ctor, Nullish } from '@sapphire/utilities';
type MessageBuilderFileResolvable = NonNullable<MessageCreateOptions['files']>[number];
type MessageBuilderResolvable = Omit<MessageCreateOptions, 'embed' | 'disableMentions' | 'reply'> & {
embeds?: MessageCreateOptions['embeds'];
components?: MessageCreateOptions['components'];
};
/**
* A message builder class, it implements the {@link MessageCreateOptions} interface.
*/
declare class MessageBuilder implements MessageCreateOptions {
/**
* Whether or not the message should be spoken aloud.
* @default false
*/
tts?: MessageCreateOptions['tts'];
/**
* The nonce for the message.
* @default ''
*/
nonce?: MessageCreateOptions['nonce'];
/**
* The content for the message. If set to undefined and the builder is used to edit, the content will not be
* replaced.
*/
content?: MessageCreateOptions['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
*/
embeds?: MessageCreateOptions['embeds'];
/**
* The components for the message. If set to undefined and the builder is used to edit, the components will not be replaced.
*/
components?: MessageCreateOptions['components'];
/**
* Which mentions should be parsed from the message content.
*/
allowedMentions?: MessageCreateOptions['allowedMentions'];
/**
* Files to send with the message. This should not be set when editing a message, as Discord does not support
* editing file attachments.
*/
files?: MessageCreateOptions['files'];
constructor(options?: MessageBuilderResolvable);
/**
* Sets the value for the {@link MessageBuilder.tts} field.
* @param tts Whether or not the message should be spoken aloud.
*/
setTTS(tts?: boolean): this;
/**
* Sets the value for the {@link MessageBuilder.nonce} field.
* @param nonce The nonce for the message.
*/
setNonce(nonce?: string): 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?: string): 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?: MessageCreateOptions['embeds']): 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?: MessageCreateOptions['components']): this;
/**
* Sets the value for the {@link MessageBuilder.allowedMentions} field.
* @param allowedMentions Which mentions should be parsed from the message content.
*/
setAllowedMentions(allowedMentions?: MessageCreateOptions['allowedMentions']): 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: MessageBuilderFileResolvable): 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: MessageBuilderFileResolvable): 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?: MessageBuilderFileResolvable[]): this;
/**
* The default values for all MessageBuilder instances.
*/
static defaults: MessageBuilderResolvable;
}
/**
* A union of all the various types of channels that Discord.js has
*/
type ChannelTypes = CategoryChannel | DMChannel | DirectoryChannel | PartialDMChannel | NewsChannel | StageChannel | TextChannel | ThreadChannel | VoiceChannel | GuildChannel | Channel;
/**
* A union of all the channel types that a message can come from
*/
type TextBasedChannelTypes = Message['channel'];
/**
* A union of all the voice-based channel types that Discord.js has
*/
type VoiceBasedChannelTypes = VoiceChannel | StageChannel;
/**
* A union of all the channel types that belong to a guild, not including {@link ThreadChannel}
*/
type NonThreadGuildBasedChannelTypes = Extract<ChannelTypes, GuildChannel>;
/**
* A union of all the channel types that belong to a guild, including {@link ThreadChannel}
*/
type GuildBasedChannelTypes = NonThreadGuildBasedChannelTypes | ThreadChannel;
/**
* A union of guild based message channels, not including {@link ThreadChannel}
*/
type NonThreadGuildTextBasedChannelTypes = Extract<TextBasedChannelTypes, GuildChannel>;
/**
* A union of guild based message channels, including {@link ThreadChannel}
*/
type GuildTextBasedChannelTypes = NonThreadGuildTextBasedChannelTypes | ThreadChannel;
/**
* The types of a channel, with the addition of `'UNKNOWN'`
*/
type ChannelTypeString = ChannelTypes['type'] | 'UNKNOWN';
/**
* A union of {@link ChatInputCommandInteraction}, {@link UserContextMenuCommandInteraction} and {@link MessageContextMenuCommandInteraction}. Similar to {@link CommandInteraction} class but as a type union.
*/
type ChatInputOrContextMenuCommandInteraction = ChatInputCommandInteraction | UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction;
/**
* A union of {@link ChatInputCommandInteraction}{@link UserContextMenuCommandInteraction}, {@link MessageContextMenuCommandInteraction}, {@link AutocompleteInteraction}, {@link StringSelectMenuInteraction} and {@link ButtonInteraction}
*/
type NonModalInteraction = ChatInputOrContextMenuCommandInteraction | AutocompleteInteraction | StringSelectMenuInteraction | ButtonInteraction;
/**
* A union of {@link ChatInputCommandInteraction}{@link UserContextMenuCommandInteraction}, {@link MessageContextMenuCommandInteraction}, {@link AutocompleteInteraction}, {@link StringSelectMenuInteraction}, {@link ButtonInteraction}, and {@link ModalSubmitInteraction}
*/
type AnyInteraction = Interaction;
/**
* A union of {@link ChatInputCommandInteraction}, {@link UserContextMenuCommandInteraction}, {@link MessageContextMenuCommandInteraction}, {@link StringSelectMenuInteraction}, {@link ButtonInteraction}, and {@link ModalSubmitInteraction}
*/
type AnyInteractableInteraction = Exclude<AnyInteraction, AutocompleteInteraction>;
/**
* A type to extend multiple discord types and simplify usage in {@link MessagePrompter}
*/
type MessagePrompterMessage = ArgumentTypes<PartialTextBasedChannelFields['send']>[0];
type MessagePrompterChannelTypes = Exclude<ChannelTypes, VoiceBasedChannelTypes | CategoryChannel>;
interface IMessagePrompterStrategyOptions {
timeout?: number;
explicitReturn?: boolean;
editMessage?: Message;
}
interface IMessagePrompterConfirmStrategyOptions extends IMessagePrompterStrategyOptions {
confirmEmoji?: string | EmojiIdentifierResolvable;
cancelEmoji?: string | EmojiIdentifierResolvable;
}
interface IMessagePrompterNumberStrategyOptions extends IMessagePrompterStrategyOptions {
start?: number;
end?: number;
numberEmojis?: string[] | EmojiIdentifierResolvable[];
}
interface IMessagePrompterReactionStrategyOptions extends IMessagePrompterStrategyOptions {
reactions: string[] | EmojiIdentifierResolvable[];
}
declare abstract class MessagePrompterBaseStrategy {
/**
* The type of strategy that was used
*/
type: string;
/**
* The timeout that was used in the collector
*/
timeout: number;
/**
* Whether to return an explicit object with data, or the strategies' default
*/
explicitReturn: boolean;
/**
* The message that has been sent in {@link MessagePrompter.run}
*/
appliedMessage: Message | null;
/**
* The message that will be sent in {@link MessagePrompter.run}
*/
message: MessagePrompterMessage;
/**
* The message the bot will edit to send its prompt in {@link MessagePrompter.run}
*/
editMessage: Message | undefined;
/**
* 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: string, message: MessagePrompterMessage, options?: IMessagePrompterStrategyOptions);
abstract run(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<unknown[]>): Awaitable<unknown>;
protected collectReactions(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<[MessageReaction, User]>, reactions: string[] | EmojiIdentifierResolvable[]): Promise<IMessagePrompterExplicitReturnBase>;
/**
* Creates a filter for the collector to filter on
* @return The filter for awaitReactions function
*/
protected createReactionPromptFilter(reactions: string[] | EmojiIdentifierResolvable[], authorOrFilter: User | CollectorFilter<[MessageReaction, User]>): CollectorOptions<[MessageReaction, User]>;
/**
* The default strategy options
*/
static defaultStrategyOptions: IMessagePrompterStrategyOptions;
}
interface IMessagePrompterExplicitReturnBase {
emoji?: GuildEmoji | ReactionEmoji | ApplicationEmoji;
reaction?: string | EmojiIdentifierResolvable;
strategy: MessagePrompterBaseStrategy;
appliedMessage: Message;
message: MessagePrompterMessage;
}
interface IMessagePrompterExplicitConfirmReturn extends IMessagePrompterExplicitReturnBase {
confirmed: boolean;
}
interface IMessagePrompterExplicitNumberReturn extends IMessagePrompterExplicitReturnBase {
number: number;
}
interface IMessagePrompterExplicitMessageReturn extends IMessagePrompterExplicitReturnBase {
response?: Message;
}
declare class MessagePrompterConfirmStrategy extends MessagePrompterBaseStrategy implements IMessagePrompterConfirmStrategyOptions {
/**
* The confirm emoji used
*/
confirmEmoji: string | EmojiResolvable;
/**
* The cancel emoji used
*/
cancelEmoji: string | EmojiResolvable;
/**
* Constructor for the {@link MessagePrompterBaseStrategy} class
* @param message The message to be sent {@link MessagePrompter}
* @param options Overrideable options if needed.
*/
constructor(message: MessagePrompterMessage, options?: IMessagePrompterConfirmStrategyOptions);
/**
* 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).
*/
run(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<[MessageReaction, User]>): Promise<IMessagePrompterExplicitConfirmReturn | boolean>;
/**
* The default confirm emoji used for {@link MessagePrompterConfirmStrategy}
*/
static confirmEmoji: string | EmojiResolvable;
/**
* The default cancel emoji used for {@link MessagePrompterConfirmStrategy}
*/
static cancelEmoji: string | EmojiResolvable;
}
declare class MessagePrompterMessageStrategy extends MessagePrompterBaseStrategy implements IMessagePrompterStrategyOptions {
/**
* Constructor for the {@link MessagePrompterBaseStrategy} class
* @param message The message instance for this {@link MessagePrompter}
* @param options Overrideable options if needed.
*/
constructor(message: MessagePrompterMessage, options: IMessagePrompterStrategyOptions);
/**
* 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.
*/
run(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<[Message]>): Promise<IMessagePrompterExplicitMessageReturn | Message>;
/**
* Creates a filter for the collector to filter on
* @return The filter for awaitMessages function
*/
private createMessagePromptFilter;
}
declare class MessagePrompterNumberStrategy extends MessagePrompterBaseStrategy implements IMessagePrompterNumberStrategyOptions {
/**
* The available number emojis
*/
numberEmojis: EmojiIdentifierResolvable[];
/**
* The available number emojis
*/
start: number;
/**
* The available number emojis
*/
end: number;
/**
* Constructor for the {@link MessagePrompterBaseStrategy} class
* @param message The message instance for this {@link MessagePrompter}
* @param options Overrideable options if needed.
*/
constructor(message: MessagePrompterMessage, options: IMessagePrompterNumberStrategyOptions);
/**
* 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.
*/
run(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<[MessageReaction, User]>): Promise<IMessagePrompterExplicitNumberReturn | number>;
/**
* The default available number emojis
*/
static numberEmojis: string[];
}
declare class MessagePrompterReactionStrategy extends MessagePrompterBaseStrategy implements MessagePrompterReactionStrategy {
/**
* The emojis used
*/
reactions: EmojiIdentifierResolvable[];
/**
* Constructor for the {@link MessagePrompterReactionStrategy} class
* @param message The message instance for this {@link MessagePrompter}
* @param options Overrideable options if needed.
*/
constructor(message: MessagePrompterMessage, options: IMessagePrompterReactionStrategyOptions);
/**
* 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.
*/
run(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<[MessageReaction, User]>): Promise<IMessagePrompterExplicitReturnBase | string | EmojiResolvable>;
}
interface StrategyReturns {
confirm: IMessagePrompterExplicitConfirmReturn | boolean;
message: IMessagePrompterExplicitMessageReturn | Message;
number: IMessagePrompterExplicitNumberReturn | number;
reaction: IMessagePrompterExplicitReturnBase | string | EmojiResolvable;
}
interface StrategyOptions {
confirm: IMessagePrompterConfirmStrategyOptions;
message: IMessagePrompterStrategyOptions;
number: IMessagePrompterNumberStrategyOptions;
reaction: IMessagePrompterReactionStrategyOptions;
}
interface StrategyFilters {
confirm: [MessageReaction, User];
message: [Message];
number: [MessageReaction, User];
reaction: [MessageReaction, User];
}
/**
* This is a {@link MessagePrompter}, a utility that sends a message, prompting for user input. The prompt can resolve to any kind of input.
* There are several specifiable types to prompt for user input, and they are as follows:
* - Confirm
* This will send a simple Yes/No prompt, using reactions.
* - Number
* This will prompt for an integer. By default it will be a number between 0 and 10 (inclusive), however you can also specify your own custom range (inclusive).
* - Reactions
* This can be any kind of reaction emoji that Discord supports, and as many as you want. This type will return that reaction instead of a boolean.
* - Message
* This will prompt the user and require a response in the form of a message. This can be helpful if you require a user to upload an image for example, or give text input.
*
* You must either use this class directly or extend it.
*
* {@link MessagePrompter} uses reactions to prompt for a yes/no answer and returns it.
* You can modify the confirm and cancel reaction used for each message, or use the {@link MessagePrompter.defaultPrompts}.
* {@link MessagePrompter.defaultPrompts} is also static so you can modify these directly.
*
* @example
* ```typescript
* const { MessagePrompter } = require('@sapphire/discord.js-utilities');
*
* const handler = new MessagePrompter('Are you sure you want to continue?');
* const result = await handler.run(channel, author);
* ```
*
* @example
* ```typescript
* const { MessagePrompter } = require('@sapphire/discord.js-utilities');
*
* const handler = new MessagePrompter('Choose a number between 5 and 10?', 'number', {
* start: 5,
* end: 10
* });
* const result = await handler.run(channel, author);
* ```
*
* @example
* ```typescript
* const { MessagePrompter } = require('@sapphire/discord.js-utilities');
*
* const handler = new MessagePrompter('Are you happy or sad?', 'reaction', {
* reactions: ['đ', 'đ']
* });
* const result = await handler.run(channel, author);
* ```
*
* @example
* ```typescript
* const { MessagePrompter } = require('@sapphire/discord.js-utilities');
*
* const handler = new MessagePrompter('Do you love me?', 'message');
* const result = await handler.run(channel, author);
* ```
*/
declare class MessagePrompter<S extends keyof StrategyReturns = 'confirm'> {
/**
* The strategy used in {@link MessagePrompter.run}
*/
strategy: MessagePrompterBaseStrategy;
/**
* 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: MessagePrompterMessage | MessagePrompterBaseStrategy, strategy?: S, strategyOptions?: S extends keyof StrategyOptions ? StrategyOptions[S] : never);
/**
* 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<Filter extends S extends keyof StrategyFilters ? StrategyFilters[S] : unknown[]>(channel: MessagePrompterChannelTypes, authorOrFilter: User | CollectorFilter<Filter>): S extends keyof StrategyReturns ? Promise<StrategyReturns[S]> : never;
/**
* The available strategies
*/
static readonly strategies: Map<keyof StrategyReturns, Ctor<ConstructorParameters<typeof MessagePrompterConfirmStrategy> | ConstructorParameters<typeof MessagePrompterNumberStrategy> | ConstructorParameters<typeof MessagePrompterReactionStrategy> | ConstructorParameters<typeof MessagePrompterMessageStrategy>, MessagePrompterConfirmStrategy | MessagePrompterNumberStrategy | MessagePrompterReactionStrategy | MessagePrompterMessageStrategy>>;
/**
* The default strategy to use
*/
static defaultStrategy: keyof StrategyReturns;
}
/**
* Represents an action that can be performed in a paginated message.
*/
type PaginatedMessageAction = PaginatedMessageActionButton | PaginatedMessageActionLink | PaginatedMessageActionStringMenu | PaginatedMessageActionUserMenu | PaginatedMessageActionRoleMenu | PaginatedMessageActionMentionableMenu | PaginatedMessageActionChannelMenu;
/**
* Represents an action that can be run in a paginated message.
*/
interface PaginatedMessageActionRun {
/**
* Runs the action with the given context.
* @param context The context object containing information about the paginated message.
* @returns A promise that resolves when the action is complete.
*/
run?(context: PaginatedMessageActionContext): Awaitable<unknown>;
}
/**
* To utilize buttons you can pass an object with the structure of {@link PaginatedMessageActionButton} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* const StopAction: PaginatedMessageActionButton = {
* customId: 'CustomStopAction',
* emoji: 'âšī¸',
* run: ({ collector }) => {
* collector.stop();
* }
* }
* ```
*/
type PaginatedMessageActionButton = InteractionButtonComponentData & PaginatedMessageActionRun;
/**
* To utilize links you can pass an object with the structure of {@link PaginatedMessageActionLink} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* You can also give the object directly.
*
* const LinkSapphireJs: PaginatedMessageActionLink = {
* url: 'https://sapphirejs.dev',
* label: 'Sapphire Website',
* emoji: 'đ'
* }
* ```
*/
type PaginatedMessageActionLink = LinkButtonComponentData;
/**
* To utilize String Select Menus you can pass an object with the structure of {@link PaginatedMessageActionStringMenu} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* const StringMenu: PaginatedMessageActionStringMenu = {
* customId: 'CustomStringSelectMenu',
* type: ComponentType.StringSelect,
* run: ({ handler, interaction }) => interaction.isStringSelectMenu() && (handler.index = parseInt(interaction.values[0], 10))
* }
* ```
*/
type PaginatedMessageActionStringMenu = PaginatedMessageActionRun & StringSelectMenuComponentData;
/**
* To utilize User Select Menus you can pass an object with the structure of {@link PaginatedMessageActionUserMenu} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* const UserMenu: PaginatedMessageActionUserMenu = {
* customId: 'CustomUserSelectMenu',
* type: ComponentType.UserSelect,
* run: ({ interaction }) => {
* if (interaction.isChannelSelectMenu()) {
* console.log(interaction.values[0])
* }
* }
* }
* ```
*/
type PaginatedMessageActionUserMenu = PaginatedMessageActionRun & UserSelectMenuComponentData & {
options?: never;
};
/**
* To utilize Role Select Menus you can pass an object with the structure of {@link PaginatedMessageActionRoleMenu} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* const RoleMenu: PaginatedMessageActionRoleMenu = {
* customId: 'CustomRoleSelectMenu',
* type: ComponentType.RoleSelect,
* run: ({ interaction }) => {
* if (interaction.isRoleSelectMenu()) {
* console.log(interaction.values[0])
* }
* }
* }
* ```
*/
type PaginatedMessageActionRoleMenu = PaginatedMessageActionRun & RoleSelectMenuComponentData & {
options?: never;
};
/**
* To utilize Mentionable Select Menus you can pass an object with the structure of {@link PaginatedMessageActionMentionableMenu} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* const MentionableMenu: PaginatedMessageActionMentionableMenu = {
* customId: 'CustomMentionableSelectMenu',
* type: ComponentType.MentionableSelect,
* run: ({ interaction }) => {
* if (interaction.isMentionableSelectMenu()) {
* console.log(interaction.values[0])
* }
* }
* }
* ```
*/
type PaginatedMessageActionMentionableMenu = PaginatedMessageActionRun & MentionableSelectMenuComponentData & {
options?: never;
};
/**
* To utilize Channel Select Menus you can pass an object with the structure of {@link PaginatedMessageActionChannelMenu} to {@link PaginatedMessage} actions.
* @example
* ```typescript
* const ChannelMenu: PaginatedMessageActionChannelMenu = {
* customId: 'CustomChannelSelectMenu',
* type: ComponentType.ChannelSelect,
* channelTypes: [ChannelType.GuildText],
* run: ({ interaction }) => {
* if (interaction.isChannelSelectMenu()) {
* console.log(interaction.values[0])
* }
* }
* }
* ```
*/
type PaginatedMessageActionChannelMenu = PaginatedMessageActionRun & ChannelSelectMenuComponentData & {
options?: never;
};
/**
* The context to be used in {@link PaginatedMessageActionButton}.
*/
interface PaginatedMessageActionContext {
interaction: PaginatedMessageInteractionUnion;
handler: PaginatedMessage;
author: User;
channel: Message['channel'];
response: APIMessage | Message | CommandInteraction | ButtonInteraction | PaginatedMessageInteractionUnion;
collector: InteractionCollector<PaginatedMessageInteractionUnion>;
}
/**
* Options for configuring a paginated message.
*/
interface PaginatedMessageOptions {
/**
* The pages to display in this {@link PaginatedMessage}.
*/
pages?: PaginatedMessagePage[];
/**
* Custom actions to provide when sending the paginated message.
*/
actions?: PaginatedMessageAction[];
/**
* The {@link EmbedBuilder} or {@link MessageOptions} options to apply to the entire {@link PaginatedMessage}.
*/
template?: EmbedBuilder | BaseMessageOptions;
/**
* The prefix to display before the page index.
* @seealso {@link PaginatedMessage.pageIndexPrefix}
*/
pageIndexPrefix?: string;
/**
* The separator to display between the embed footer and the page index.
* @seealso {@link PaginatedMessage.embedFooterSeparator}
*/
embedFooterSeparator?: string;
/**
* Additional options that are applied to each message when sending it to Discord.
* Be careful with using this, misusing it can cause issues, such as sending empty messages.
* @remark **This is for advanced usages only!**
*
* @default null
*/
paginatedMessageData?: Omit<PaginatedMessageMessageOptionsUnion, 'components'> | null;
}
/**
* The pages that are used for {@link PaginatedMessage.pages}
*
* Pages can be either a {@link Message},
* or an {@link Awaitable} function that returns a {@link Message}.
*
* Furthermore, {@link MessageOptions} can be used to
* construct the pages without state. This library also provides {@link MessageBuilder}, which can be used as a chainable
* alternative to raw objects, similar to how {@link MessageEmbed}
* works.
*
* Ideally, however, you should use the utility functions
* {@link PaginatedMessage.addPageBuilder `addPageBuilder`}, {@link PaginatedMessage.addPageContent `addPageContent`}, and {@link PaginatedMessage.addPageEmbed `addPageEmbed`}
* as opposed to manually constructing {@link PaginatedMessagePage `MessagePages`}. This is because a {@link PaginatedMessage} does a lot of post-processing
* on the provided pages and we can only guarantee this will work properly when using the utility methods.
*/
type PaginatedMessagePage = ((index: number, pages: PaginatedMessagePage[], handler: PaginatedMessage) => Awaitable<PaginatedMessageMessageOptionsUnion>) | PaginatedMessageMessageOptionsUnion;
/**
* Represents a resolved page for a paginated message.
* It can be either a `BaseMessageOptions` object with the `flags` property omitted,
* or a `WebhookMessageEditOptions` object.
*/
type PaginatedMessageResolvedPage = Omit<BaseMessageOptions, 'flags'> | WebhookMessageEditOptions;
/**
* The type of the custom function that can be set for the {@link PaginatedMessage.selectMenuOptions}
*/
type PaginatedMessageSelectMenuOptionsFunction = (pageIndex: number, internationalizationContext: PaginatedMessageInternationalizationContext) => Awaitable<Omit<SelectMenuComponentOptionData, 'value'>>;
/**
* The type of the custom function that can be set for the {@link PaginatedMessage.wrongUserInteractionReply}
*/
type PaginatedMessageWrongUserInteractionReplyFunction = (targetUser: User, interactionUser: User, internationalizationContext: PaginatedMessageInternationalizationContext) => Awaitable<Parameters<MessageComponentInteraction['reply']>[0]>;
/**
* Represents the resolvable type for the embeds property of a paginated message.
*/
type PaginatedMessageEmbedResolvable = BaseMessageOptions['embeds'];
/**
* A non nullable writeable variant of {@link PaginatedMessageEmbedResolvable}.
* This removes:
*
* - The union with `| undefined`
* - The `readonly` constraint
*/
type PaginatedMessageWriteableEmbedResolvable = (APIEmbed | JSONEncodable<APIEmbed>)[];
/**
* Represents the union of options for a paginated message.
*/
type PaginatedMessageMessageOptionsUnion = Omit<PaginatedMessageResolvedPage, 'components'> & {
actions?: PaginatedMessageAction[];
};
/**
* Represents the union type of interactions for a paginated message, excluding the ModalSubmitInteraction.
*/
type PaginatedMessageInteractionUnion = Exclude<CollectedInteraction, ModalSubmitInteraction>;
/**
* Represents a union type for components in a paginated message.
* It can be one of the following types:
* - `JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>>`
* - `ActionRowData<ActionRowComponentOptions | MessageActionRowComponentBuilder>`
* - `APIActionRowComponent<APIMessageActionRowComponent>`
*/
type PaginatedMessageComponentUnion = JSONEncodable<APIActionRowComponent<APIMessageActionRowComponent>> | ActionRowData<ActionRowComponentOptions | MessageActionRowComponentBuilder> | APIActionRowComponent<APIMessageActionRowComponent>;
/**
* @internal This is a duplicate of the same interface in `@sapphire/plugin-i18next`
* Duplicated here for the type of the parameters in the functions
*
* Context for {@link InternationalizationHandler.fetchLanguage} functions.
* This context enables implementation of per-guild, per-channel, and per-user localization.
*/
interface PaginatedMessageInternationalizationContext {
/** The {@link Guild} object to fetch the preferred language for, or `null` if the language is to be fetched in a DM. */
guild: Guild | null;
/** The {@link DiscordChannel} object to fetch the preferred language for. */
channel: Message['channel'] | StageChannel | VoiceChannel | null;
/** The user to fetch the preferred language for. */
user: User | null;
/** The {@link Interaction.guildLocale} provided by the Discord API */
interactionGuildLocale?: Interaction['guildLocale'];
/** The {@link Interaction.locale} provided by the Discord API */
interactionLocale?: Interaction['locale'];
}
/**
* Represents the parameters for safely replying to an interaction.
* @template T - The type of message method ('edit', 'reply', or never).
*/
interface SafeReplyToInteractionParameters<T extends 'edit' | 'reply' = never> {
/**
* The message or interaction to reply to.
*/
messageOrInteraction: APIMessage | Message | AnyInteractableInteraction;
/**
* The content to use when editing a reply to an interaction.
*/
interactionEditReplyContent: WebhookMessageEditOptions;
/**
* The content to use when replying to an interaction.
*/
interactionReplyContent: InteractionReplyOptions;
/**
* The content to use when updating a component interaction.
*/
componentUpdateContent: InteractionUpdateOptions;
/**
* The method to use when sending a message.
*/
messageMethod?: T;
/**
* The content to use when sending a message using the 'reply' method.
*/
messageMethodContent?: T extends 'reply' ? MessageReplyOptions : MessageEditOptions;
}
/**
* Represents the possible reasons for stopping a paginated message.
*/
type PaginatedMessageStopReasons = 'time' | 'idle' | 'user' | 'messageDelete' | 'channelDelete' | 'threadDelete' | 'guildDelete' | 'limit' | 'componentLimit' | 'userLimit';
/**
* Represents a resolvable object that can be used to create an embed in Discord.
* It can be either a JSON-encodable object or an APIEmbed object.
*/
type EmbedResolvable = JSONEncodable<APIEmbed> | APIEmbed;
/**
* This is a {@link PaginatedMessage}, a utility to paginate messages (usually embeds).
* You must either use this class directly or extend it.
*
* @remark Please note that for {@link PaginatedMessage} to work in DMs to your client, you need to add the `'CHANNEL'` partial to your `client.options.partials`.
* Message based commands can always be used in DMs, whereas Chat Input interactions can only be used in DMs when they are registered globally.
*
* {@link PaginatedMessage} uses {@linkplain https://discord.js.org/docs/packages/discord.js/main/MessageComponent:TypeAlias MessageComponent} buttons that perform the specified action when clicked.
* You can either use your own actions or the {@link PaginatedMessage.defaultActions}.
* {@link PaginatedMessage.defaultActions} is also static so you can modify these directly.
*
* {@link PaginatedMessage} also uses pages via {@linkplain https://discord.js.org/docs/packages/discord.js/main/Message:Class Messages}.
*
* @example
* ```typescript
* const myPaginatedMessage = new PaginatedMessage();
* // Once you have an instance of PaginatedMessage you can call various methods on it to add pages to it.
* // For more details see each method's documentation.
*
* myPaginatedMessage.addPageEmbed((embed) => {
* embed
* .setColor('#FF0000')
* .setDescription('example description');
*
* return embed;
* });
*
* myPaginatedMessage.addPageBuilder((builder) => {
* const embed = new EmbedBuilder()
* .setColor('#FF0000')
* .setDescription('example description');
*
* return builder
* .setContent('example content')
* .setEmbeds([embed]);
* });
*
* myPaginatedMessage.addPageContent('Example');
*
* myPaginatedMessage.run(message)
* ```
*
* @remark You can also provide a EmbedBuilder template. This will be applied to every page.
* If a page itself has an embed then the two will be merged, with the content of
* the page's embed taking priority over the template.
*
* Furthermore, if the template has a footer then it will be applied _after_ the page index part of the footer
* with a space preceding the template. For example, when setting `- Powered by Sapphire Framework`
* the resulting footer will be `1/2 - Powered by Sapphire Framework`
* @example
* ```typescript
* const myPaginatedMessage = new PaginatedMessage({
* template: new EmbedBuilder().setColor('#FF0000').setFooter('- Powered by Sapphire framework')
* });
* ```
*
* @remark To utilize actions you can implement IPaginatedMessageAction into a class.
* @example
* ```typescript
* class ForwardAction implements IPaginatedMessageAction {
* public id = 'âļī¸';
*
* public run({ handler }) {
* if (handler.index !== handler.pages.length - 1) ++handler.index;
* }
* }
*
* // You can also give the object directly.
*
* const StopAction: IPaginatedMessageAction = {
* customId: 'CustomStopAction',
* run: ({ collector }) => {
* collector.stop();
* }
* }
* ```
*/
declare class PaginatedMessage {
#private;
/**
* The default actions of this handler.
*/
static defaultActions: PaginatedMessageAction[];
/**
* Whether to emit the warning about running a {@link PaginatedMessage} in a DM channel without the client 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.
*
* @remark To overwrite this property change it somewhere in a "setup" file, i.e. where you also call `client.login()` for your client.
* Alternatively, you can also customize it on a per-PaginatedMessage basis by using `paginatedMessageInstance.setEmitPartialDMChannelWarning(newBoolean)`
* @default true
*/
static emitPartialDMChannelWarning: boolean;
/**
* A list of `customId` that are bound to actions that will stop the {@link PaginatedMessage}
* @default ['@sapphire/paginated-messages.stop']
* @remark To overwrite this property change it somewhere in a "setup" file, i.e. where you also call `client.login()` for your client.
* Alternatively, you can also customize it on a per-PaginatedMessage basis by using `paginatedMessageInstance.setStopPaginatedMessageCustomIds(customIds)`
* @example
* ```typescript
* import { PaginatedMessage } from '@sapphire/discord.js-utilities';
*
* PaginatedMessage.stopPaginatedMessageCustomIds = ['my-custom-stop-custom-id'];
* ```
*/
static stopPaginatedMessageCustomIds: string[];
/**
* The reasons sent by {@linkplain https://discord.js.org/docs/packages/discord.js/main/InteractionCollector:Class#end InteractionCollector#end}
* event when the message (or its owner) has been deleted.
*/
static deletionStopReasons: string[];
/**
* 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 ""
* @remark To overwrite this property change it somewhere in a "setup" file, i.e. where you also call `client.login()` for your client.
* @example
* ```typescript
* import { PaginatedMessage } from '@sapphire/discord.js-utilities';
*
* PaginatedMessage.pageIndexPrefix = 'Page';
* // This will make the footer of the embed something like "Page 1/2"
* ```
*/
static pageIndexPrefix: string;
/**
* Custom separator for the page index in the embed footer.
* @default "âĸ"
* @remark To overwrite this property change it somewhere in a "setup" file, i.e. where you also call `client.login()` for your client.
* Alternatively, you can also customize it on a per-PaginatedMessage basis by passing `embedFooterSeparator` in the options of the constructor.
* @example
* ```typescript
* import { PaginatedMessage } from '@sapphire/discord.js-utilities';
*
* PaginatedMessage.embedFooterSeparator = '|';
* // This will make the separator of the embed footer something like "Page 1/2 | Today at 4:20"
* ```
*/
static embedFooterSeparator: string;
/**
* The messages that are currently being handled by a {@link PaginatedMessage}
* The key is the ID of the message that triggered this {@link PaginatedMessage}
*
* This is to ensure that only 1 {@link PaginatedMessage} can run on a specified message at once.
* This is important when having an editable commands solution.
*/
static readonly messages: Map<string, PaginatedMessage>;
/**
* The current {@link InteractionCollector} handlers that are active.
* The key is the ID of of the author who sent the message that triggered this {@link PaginatedMessage}
*
* This is to ensure that any given author can only trigger 1 {@link PaginatedMessage}.
* This is important for performance reasons, and users should not have more than 1 {@link PaginatedMessage} open at once.
*/
static readonly handlers: Map<string, PaginatedMessage>;
/**
* A generator for {@link MessageSelectOption} that will be used to generate the options for the {@link StringSelectMenuBuilder}.
* We do not allow overwriting the {@link MessageSelectOption#value} property with this, as it is vital to how we handle
* select menu interactions.
*
* @param pageIndex The index of the page to add to the {@link StringSelectMenuBuilder}. We will add 1 to this number because our pages are 0 based,
* so this will represent the pages as seen by the user.
* @default
* ```ts
* {
* label: `Page ${pageIndex}`
* }
* ```
* @remark To overwrite this property change it in a "setup" file prior to calling `client.login()` for your client.
*
* @example
* ```typescript
* import { PaginatedMessage } from '@sapphire/discord.js-utilities';
*
* PaginatedMessage.selectMenuOptions = (pageIndex) => ({
* label: `Go to page: ${pageIndex}`,
* description: 'This is a description'
* });
* ```
*/
static selectMenuOptions: PaginatedMessageSelectMenuOptionsFunction;
/**
* A generator for {@link MessageComponentInteraction#reply} that will be called and sent whenever an untargeted user interacts with one of the buttons.
* When modifying this it is recommended that the message is set to be ephemeral so only the user that is pressing the buttons can see them.
* Furthermore, we also recommend setting `allowedMentions: { users: [], roles: [] }`, so you don't have to worry about accidentally pinging anyone.
*
* When setting just a string, we will add `{ ephemeral: true, allowedMentions: { users: [], roles: [] } }` for you.
*
* @param targetUser The {@link User} this {@link PaginatedMessage} was intended for.
* @param interactionUser The {@link User} that actually clicked the button.
* @default
* ```ts
* import { userMention } from 'discord.js';
*
* {
* content: `Please stop interacting with the components on this message. They are only for ${userMention(targetUser.id)}.`,
* ephemeral: true,
* allowedMentions: { users: [], roles: [] }
* }
* ```
* @remark To overwrite this property change it in a "setup" file prior to calling `client.login()` for your client.
*
* @example
* ```typescript
* import { PaginatedMessage } from '@sapphire/discord.js-utilities';
* import { userMention } from 'discord.js';
*
* // We will add ephemeral and no allowed mention for string only overwrites
* PaginatedMessage.wrongUserInteractionReply = (targetUser) =>
* `These buttons are only for ${userMention(targetUser.id)}. Press them as much as you want, but I won't do anything with your clicks.`;
* ```
*
* @example
* ```typescript
* import { PaginatedMessage } from '@sapphire/discord.js-utilities';
* import { userMention } from 'discord.js';
*
* PaginatedMessage.wrongUserInteractionReply = (targetUser) => ({
* content: `These buttons are only for ${userMention(
* targetUser.id
* )}. Press them as much as you want, but I won't do anything with your clicks.`,
* ephemeral: true,
* allowedMentions: { users: [], roles: [] }
* });
* ```
*/
static wrongUserInteractionReply: PaginatedMessageWrongUserInteractionReplyFunction;
/**
* Resolves the template for the PaginatedMessage.
*
* @param template - The template to resolve.
* @returns The resolved template as a BaseMessageOptions object.
*/
private static resolveTemplate;
/**
* The pages to be converted to {@link PaginatedMessage.messages}
*/
pages: PaginatedMessagePage[];
/**
* The response message used to edit on page changes.
*/
response: Message | AnyInteractableInteraction | null;
/**
* The collector used for handling component interactions.
*/
collector: InteractionCollector<PaginatedMessageInteractionUnion> | null;
/**
* The pages which were converted from {@link PaginatedMessage.pages}
*/
messages: (PaginatedMessageResolvedPage | null)[];
/**
* The actions which are to be used.
*/
actions: Map<string, PaginatedMessageAction>;
/**
* The page-specific actions which are to be used.
*/
pageActions: (Map<string, PaginatedMessageAction> | null)[];
/**
* The handler's current page/message index.
*/
index: number;
/**
* 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.
*/
idle: number;
/**
* 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}
*/
template: PaginatedMessageMessageOptionsUnion;
/**
* 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)
*/
pageIndexPrefix: string;
/**
* 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)
*/
embedFooterSeparator: string;
/**
* A list of `customId` that are bound to actions that will stop the {@link PaginatedMessage}
* @default ```PaginatedMessage.stopPaginatedMessageCustomIds``` (static property)
*/
stopPaginatedMessageCustomIds: string[];
/**
* 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)
*/
emitPartialDMChannelWarning: boolean;
/**
* Data for the paginated message.
*/
protected paginatedMessageData: Omit<PaginatedMessageMessageOptionsUnion, 'components'> | null;
/**
* The placeholder for the select menu.
*/
protected selectMenuPlaceholder: string | undefined;
/**
* Tracks whether a warning was already emitted for this {@link PaginatedMessage}
* concerning the maximum amount of pages in the {@link SelectMenu}.
*
* @default false
*/
protected hasEmittedMaxPageWarning: boolean;
/**
* 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
*/
protected hasEmittedPartialDMChannelWarning: boolean;
/**
* 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
*/
protected shouldAddFooterToEmbeds: boolean;
/**
* Function that returns the select menu options for the paginated message.
* @param message The paginated message.
* @returns The select menu options.
*/
protected