UNPKG

@sapphire/discord.js-utilities

Version:

Discord.js specific utilities for your JavaScript/TypeScript bots

1,078 lines (1,065 loc) â€ĸ 112 kB
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