refactor: builders (#10448)

BREAKING CHANGE: formatters export removed (prev. deprecated)
BREAKING CHANGE: `SelectMenuBuilder` and `SelectMenuOptionBuilder` have been removed (prev. deprecated)
BREAKING CHANGE: `EmbedBuilder` no longer takes camalCase options
BREAKING CHANGE: `ActionRowBuilder` now has specialized `[add/set]X` methods as opposed to the current `[add/set]Components`
BREAKING CHANGE: Removed `equals` methods
BREAKING CHANGE: Sapphire -> zod for validation
BREAKING CHANGE: Removed the ability to pass `null`/`undefined` to clear fields, use `clearX()` instead
BREAKING CHANGE: Renamed all "slash command" symbols to instead use "chat input command"
BREAKING CHANGE: Removed `ContextMenuCommandBuilder` in favor of `MessageCommandBuilder` and `UserCommandBuilder`
BREAKING CHANGE: Removed support for passing the "string key"s of enums
BREAKING CHANGE: Removed `Button` class in favor for specialized classes depending on the style
BREAKING CHANGE: Removed nested `addX` styled-methods in favor of plural `addXs`

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
Co-authored-by: Almeida <github@almeidx.dev>
This commit is contained in:
Denis Cristea
2024-10-01 19:11:56 +03:00
committed by GitHub
parent c633d5c7f6
commit ab32f26cbb
91 changed files with 3772 additions and 3824 deletions

View File

@@ -0,0 +1,83 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
ApplicationIntegrationType,
InteractionContextType,
Permissions,
RESTPostAPIApplicationCommandsJSONBody,
} from 'discord-api-types/v10';
import type { RestOrArray } from '../../util/normalizeArray.js';
import { normalizeArray } from '../../util/normalizeArray.js';
export interface CommandData
extends Partial<
Pick<
RESTPostAPIApplicationCommandsJSONBody,
'contexts' | 'default_member_permissions' | 'integration_types' | 'nsfw'
>
> {}
export abstract class CommandBuilder<Command extends RESTPostAPIApplicationCommandsJSONBody>
implements JSONEncodable<Command>
{
protected declare readonly data: CommandData;
/**
* Sets the contexts of this command.
*
* @param contexts - The contexts
*/
public setContexts(...contexts: RestOrArray<InteractionContextType>) {
this.data.contexts = normalizeArray(contexts);
return this;
}
/**
* Sets the integration types of this command.
*
* @param integrationTypes - The integration types
*/
public setIntegrationTypes(...integrationTypes: RestOrArray<ApplicationIntegrationType>) {
this.data.integration_types = normalizeArray(integrationTypes);
return this;
}
/**
* Sets the default permissions a member should have in order to run the command.
*
* @remarks
* You can set this to `'0'` to disable the command by default.
* @param permissions - The permissions bit field to set
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
*/
public setDefaultMemberPermissions(permissions: Permissions | bigint | number) {
this.data.default_member_permissions = typeof permissions === 'string' ? permissions : permissions.toString();
return this;
}
/**
* Clears the default permissions a member should have in order to run the command.
*/
public clearDefaultMemberPermissions() {
this.data.default_member_permissions = undefined;
return this;
}
/**
* Sets whether this command is NSFW.
*
* @param nsfw - Whether this command is NSFW
*/
public setNSFW(nsfw = true) {
this.data.nsfw = nsfw;
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public abstract toJSON(validationOverride?: boolean): Command;
}

View File

@@ -0,0 +1,64 @@
import type { LocaleString, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
export interface SharedNameData
extends Partial<Pick<RESTPostAPIApplicationCommandsJSONBody, 'name_localizations' | 'name'>> {}
/**
* This mixin holds name and description symbols for chat input commands.
*/
export class SharedName {
protected readonly data: SharedNameData = {};
/**
* Sets the name of this command.
*
* @param name - The name to use
*/
public setName(name: string): this {
this.data.name = name;
return this;
}
/**
* Sets a name localization for this command.
*
* @param locale - The locale to set
* @param localizedName - The localized name for the given `locale`
*/
public setNameLocalization(locale: LocaleString, localizedName: string) {
this.data.name_localizations ??= {};
this.data.name_localizations[locale] = localizedName;
return this;
}
/**
* Clears a name localization for this command.
*
* @param locale - The locale to clear
*/
public clearNameLocalization(locale: LocaleString) {
this.data.name_localizations ??= {};
this.data.name_localizations[locale] = undefined;
return this;
}
/**
* Sets the name localizations for this command.
*
* @param localizedNames - The object of localized names to set
*/
public setNameLocalizations(localizedNames: Partial<Record<LocaleString, string>>) {
this.data.name_localizations = structuredClone(localizedNames);
return this;
}
/**
* Clears all name localizations for this command.
*/
public clearNameLocalizations() {
this.data.name_localizations = undefined;
return this;
}
}

View File

@@ -0,0 +1,67 @@
import type { APIApplicationCommand, LocaleString } from 'discord-api-types/v10';
import type { SharedNameData } from './SharedName.js';
import { SharedName } from './SharedName.js';
export interface SharedNameAndDescriptionData
extends SharedNameData,
Partial<Pick<APIApplicationCommand, 'description_localizations' | 'description'>> {}
/**
* This mixin holds name and description symbols for chat input commands.
*/
export class SharedNameAndDescription extends SharedName {
protected override readonly data: SharedNameAndDescriptionData = {};
/**
* Sets the description of this command.
*
* @param description - The description to use
*/
public setDescription(description: string) {
this.data.description = description;
return this;
}
/**
* Sets a description localization for this command.
*
* @param locale - The locale to set
* @param localizedDescription - The localized description for the given `locale`
*/
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string) {
this.data.description_localizations ??= {};
this.data.description_localizations[locale] = localizedDescription;
return this;
}
/**
* Clears a description localization for this command.
*
* @param locale - The locale to clear
*/
public clearDescriptionLocalization(locale: LocaleString) {
this.data.description_localizations ??= {};
this.data.description_localizations[locale] = undefined;
return this;
}
/**
* Sets the description localizations for this command.
*
* @param localizedDescriptions - The object of localized descriptions to set
*/
public setDescriptionLocalizations(localizedDescriptions: Partial<Record<LocaleString, string>>) {
this.data.description_localizations = structuredClone(localizedDescriptions);
return this;
}
/**
* Clears all description localizations for this command.
*/
public clearDescriptionLocalizations() {
this.data.description_localizations = undefined;
return this;
}
}

View File

@@ -0,0 +1,154 @@
import {
ApplicationIntegrationType,
InteractionContextType,
ApplicationCommandOptionType,
} from 'discord-api-types/v10';
import type { ZodTypeAny } from 'zod';
import { z } from 'zod';
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
import { ApplicationCommandOptionAllowedChannelTypes } from './mixins/ApplicationCommandOptionChannelTypesMixin.js';
const namePredicate = z
.string()
.min(1)
.max(32)
.regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u);
const descriptionPredicate = z.string().min(1).max(100);
const sharedNameAndDescriptionPredicate = z.object({
name: namePredicate,
name_localizations: localeMapPredicate.optional(),
description: descriptionPredicate,
description_localizations: localeMapPredicate.optional(),
});
const numericMixinNumberOptionPredicate = z.object({
max_value: z.number().safe().optional(),
min_value: z.number().safe().optional(),
});
const numericMixinIntegerOptionPredicate = z.object({
max_value: z.number().safe().int().optional(),
min_value: z.number().safe().int().optional(),
});
const channelMixinOptionPredicate = z.object({
channel_types: z
.union(
ApplicationCommandOptionAllowedChannelTypes.map((type) => z.literal(type)) as unknown as [
ZodTypeAny,
ZodTypeAny,
...ZodTypeAny[],
],
)
.array()
.optional(),
});
const autocompleteMixinOptionPredicate = z.object({
autocomplete: z.literal(true),
choices: z.union([z.never(), z.never().array(), z.undefined()]),
});
const choiceValueStringPredicate = z.string().min(1).max(100);
const choiceValueNumberPredicate = z.number().safe();
const choiceBasePredicate = z.object({
name: choiceValueStringPredicate,
name_localizations: localeMapPredicate.optional(),
});
const choiceStringPredicate = choiceBasePredicate.extend({
value: choiceValueStringPredicate,
});
const choiceNumberPredicate = choiceBasePredicate.extend({
value: choiceValueNumberPredicate,
});
const choiceBaseMixinPredicate = z.object({
autocomplete: z.literal(false).optional(),
});
const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({
choices: choiceStringPredicate.array().max(25).optional(),
});
const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({
choices: choiceNumberPredicate.array().max(25).optional(),
});
const basicOptionTypes = [
ApplicationCommandOptionType.Attachment,
ApplicationCommandOptionType.Boolean,
ApplicationCommandOptionType.Channel,
ApplicationCommandOptionType.Integer,
ApplicationCommandOptionType.Mentionable,
ApplicationCommandOptionType.Number,
ApplicationCommandOptionType.Role,
ApplicationCommandOptionType.String,
ApplicationCommandOptionType.User,
] as const;
const basicOptionTypesPredicate = z.union(
basicOptionTypes.map((type) => z.literal(type)) as unknown as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]],
);
export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({
required: z.boolean().optional(),
type: basicOptionTypesPredicate,
});
const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
autocompleteMixinOptionPredicate,
choiceStringMixinPredicate,
]);
const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
autocompleteMixinOptionPredicate,
choiceNumberMixinPredicate,
]);
export const channelOptionPredicate = basicOptionPredicate.merge(channelMixinOptionPredicate);
export const integerOptionPredicate = basicOptionPredicate
.merge(numericMixinIntegerOptionPredicate)
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
export const numberOptionPredicate = basicOptionPredicate
.merge(numericMixinNumberOptionPredicate)
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
export const stringOptionPredicate = basicOptionPredicate
.extend({
max_length: z.number().min(0).max(6_000).optional(),
min_length: z.number().min(1).max(6_000).optional(),
})
.and(autocompleteOrStringChoicesMixinOptionPredicate);
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
contexts: z.array(z.nativeEnum(InteractionContextType)).optional(),
default_member_permissions: memberPermissionsPredicate.optional(),
integration_types: z.array(z.nativeEnum(ApplicationIntegrationType)).optional(),
nsfw: z.boolean().optional(),
});
// Because you can only add options via builders, there's no need to validate whole objects here otherwise
const chatInputCommandOptionsPredicate = z.union([
z.object({ type: basicOptionTypesPredicate }).array(),
z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(),
z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(),
]);
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
options: chatInputCommandOptionsPredicate.optional(),
});
export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
options: z
.array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }))
.min(1)
.max(25),
});
export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.Subcommand),
options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25),
});

View File

@@ -0,0 +1,37 @@
import { ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { isValidationEnabled } from '../../../util/validation.js';
import { CommandBuilder } from '../Command.js';
import { SharedNameAndDescription } from '../SharedNameAndDescription.js';
import { chatInputCommandPredicate } from './Assertions.js';
import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js';
import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js';
/**
* A builder that creates API-compatible JSON data for chat input commands.
*/
export class ChatInputCommandBuilder extends Mixin(
CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>,
SharedChatInputCommandOptions,
SharedNameAndDescription,
SharedChatInputCommandSubcommands,
) {
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public toJSON(validationOverride?: boolean): RESTPostAPIChatInputApplicationCommandsJSONBody {
const { options, ...rest } = this.data;
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
type: ApplicationCommandType.ChatInput,
options: options?.map((option) => option.toJSON(validationOverride)),
};
if (validationOverride ?? isValidationEnabled()) {
chatInputCommandPredicate.parse(data);
}
return data;
}
}

View File

@@ -0,0 +1,111 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
APIApplicationCommandSubcommandOption,
APIApplicationCommandSubcommandGroupOption,
} from 'discord-api-types/v10';
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js';
import { resolveBuilder } from '../../../util/resolveBuilder.js';
import { isValidationEnabled } from '../../../util/validation.js';
import type { SharedNameAndDescriptionData } from '../SharedNameAndDescription.js';
import { SharedNameAndDescription } from '../SharedNameAndDescription.js';
import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js';
import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js';
export interface ChatInputCommandSubcommandGroupData {
options?: ChatInputCommandSubcommandBuilder[];
}
/**
* Represents a folder for subcommands.
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
*/
export class ChatInputCommandSubcommandGroupBuilder
extends SharedNameAndDescription
implements JSONEncodable<APIApplicationCommandSubcommandGroupOption>
{
protected declare readonly data: ChatInputCommandSubcommandGroupData & SharedNameAndDescriptionData;
public get options(): readonly ChatInputCommandSubcommandBuilder[] {
return (this.data.options ??= []);
}
/**
* Adds a new subcommand to this group.
*
* @param input - A function that returns a subcommand builder or an already built builder
*/
public addSubcommands(
...input: RestOrArray<
| ChatInputCommandSubcommandBuilder
| ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder)
>
) {
const normalized = normalizeArray(input);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const result = normalized.map((builder) => resolveBuilder(builder, ChatInputCommandSubcommandBuilder));
this.data.options ??= [];
this.data.options.push(...result);
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandGroupOption {
const { options, ...rest } = this.data;
const data = {
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandGroupOption, 'type'>),
type: ApplicationCommandOptionType.SubcommandGroup as const,
options: options?.map((option) => option.toJSON(validationOverride)) ?? [],
};
if (validationOverride ?? isValidationEnabled()) {
chatInputCommandSubcommandGroupPredicate.parse(data);
}
return data;
}
}
/**
* A builder that creates API-compatible JSON data for chat input command subcommands.
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
*/
export class ChatInputCommandSubcommandBuilder
extends Mixin(SharedNameAndDescription, SharedChatInputCommandOptions)
implements JSONEncodable<APIApplicationCommandSubcommandOption>
{
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandOption {
const { options, ...rest } = this.data;
const data = {
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandOption, 'type'>),
type: ApplicationCommandOptionType.Subcommand as const,
options: options?.map((option) => option.toJSON(validationOverride)) ?? [],
};
if (validationOverride ?? isValidationEnabled()) {
chatInputCommandSubcommandPredicate.parse(data);
}
return data;
}
}

View File

@@ -0,0 +1,47 @@
import type { APIApplicationCommandIntegerOption } from 'discord-api-types/v10';
export interface ApplicationCommandNumericOptionMinMaxValueData
extends Pick<APIApplicationCommandIntegerOption, 'max_value' | 'min_value'> {}
/**
* This mixin holds minimum and maximum symbols used for options.
*/
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
protected declare readonly data: ApplicationCommandNumericOptionMinMaxValueData;
/**
* Sets the maximum number value of this option.
*
* @param max - The maximum value this option can be
*/
public setMaxValue(max: number): this {
this.data.max_value = max;
return this;
}
/**
* Removes the maximum number value of this option.
*/
public clearMaxValue(): this {
this.data.max_value = undefined;
return this;
}
/**
* Sets the minimum number value of this option.
*
* @param min - The minimum value this option can be
*/
public setMinValue(min: number): this {
this.data.min_value = min;
return this;
}
/**
* Removes the minimum number value of this option.
*/
public clearMinValue(): this {
this.data.min_value = undefined;
return this;
}
}

View File

@@ -0,0 +1,52 @@
import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray';
export const ApplicationCommandOptionAllowedChannelTypes = [
ChannelType.GuildText,
ChannelType.GuildVoice,
ChannelType.GuildCategory,
ChannelType.GuildAnnouncement,
ChannelType.AnnouncementThread,
ChannelType.PublicThread,
ChannelType.PrivateThread,
ChannelType.GuildStageVoice,
ChannelType.GuildForum,
ChannelType.GuildMedia,
] as const;
/**
* Allowed channel types used for a channel option.
*/
export type ApplicationCommandOptionAllowedChannelTypes = (typeof ApplicationCommandOptionAllowedChannelTypes)[number];
export interface ApplicationCommandOptionChannelTypesData
extends Pick<APIApplicationCommandChannelOption, 'channel_types'> {}
/**
* This mixin holds channel type symbols used for options.
*/
export class ApplicationCommandOptionChannelTypesMixin {
protected declare readonly data: ApplicationCommandOptionChannelTypesData;
/**
* Adds channel types to this option.
*
* @param channelTypes - The channel types
*/
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
this.data.channel_types ??= [];
this.data.channel_types.push(...normalizeArray(channelTypes));
return this;
}
/**
* Sets the channel types for this option.
*
* @param channelTypes - The channel types
*/
public setChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
this.data.channel_types = normalizeArray(channelTypes);
return this;
}
}

View File

@@ -0,0 +1,29 @@
import type {
APIApplicationCommandIntegerOption,
APIApplicationCommandNumberOption,
APIApplicationCommandStringOption,
} from 'discord-api-types/v10';
export type AutocompletableOptions =
| APIApplicationCommandIntegerOption
| APIApplicationCommandNumberOption
| APIApplicationCommandStringOption;
export interface ApplicationCommandOptionWithAutocompleteData extends Pick<AutocompletableOptions, 'autocomplete'> {}
/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithAutocompleteMixin {
protected declare readonly data: ApplicationCommandOptionWithAutocompleteData;
/**
* Whether this option uses autocomplete.
*
* @param autocomplete - Whether this option should use autocomplete
*/
public setAutocomplete(autocomplete = true): this {
this.data.autocomplete = autocomplete;
return this;
}
}

View File

@@ -0,0 +1,38 @@
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
// Unlike other places, we're not `Pick`ing from discord-api-types. The union includes `[]` and it breaks everything.
export interface ApplicationCommandOptionWithChoicesData {
choices?: APIApplicationCommandOptionChoice<number | string>[];
}
/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithChoicesMixin<ChoiceType extends number | string> {
protected declare readonly data: ApplicationCommandOptionWithChoicesData;
/**
* Adds multiple choices to this option.
*
* @param choices - The choices to add
*/
public addChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
const normalizedChoices = normalizeArray(choices);
this.data.choices ??= [];
this.data.choices.push(...normalizedChoices);
return this;
}
/**
* Sets multiple choices for this option.
*
* @param choices - The choices to set
*/
public setChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
this.data.choices = normalizeArray(choices);
return this;
}
}

View File

@@ -0,0 +1,200 @@
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
import { resolveBuilder } from '../../../../util/resolveBuilder.js';
import type { ApplicationCommandOptionBase } from '../options/ApplicationCommandOptionBase.js';
import { ChatInputCommandAttachmentOption } from '../options/attachment.js';
import { ChatInputCommandBooleanOption } from '../options/boolean.js';
import { ChatInputCommandChannelOption } from '../options/channel.js';
import { ChatInputCommandIntegerOption } from '../options/integer.js';
import { ChatInputCommandMentionableOption } from '../options/mentionable.js';
import { ChatInputCommandNumberOption } from '../options/number.js';
import { ChatInputCommandRoleOption } from '../options/role.js';
import { ChatInputCommandStringOption } from '../options/string.js';
import { ChatInputCommandUserOption } from '../options/user.js';
export interface SharedChatInputCommandOptionsData {
options?: ApplicationCommandOptionBase[];
}
/**
* This mixin holds symbols that can be shared in chat input command options.
*
* @typeParam TypeAfterAddingOptions - The type this class should return after adding an option.
*/
export class SharedChatInputCommandOptions {
protected declare readonly data: SharedChatInputCommandOptionsData;
public get options(): readonly ApplicationCommandOptionBase[] {
return (this.data.options ??= []);
}
/**
* Adds boolean options.
*
* @param options - Options to add
*/
public addBooleanOptions(
...options: RestOrArray<
ChatInputCommandBooleanOption | ((builder: ChatInputCommandBooleanOption) => ChatInputCommandBooleanOption)
>
) {
return this.sharedAddOptions(ChatInputCommandBooleanOption, ...options);
}
/**
* Adds user options.
*
* @param options - Options to add
*/
public addUserOptions(
...options: RestOrArray<
ChatInputCommandUserOption | ((builder: ChatInputCommandUserOption) => ChatInputCommandUserOption)
>
) {
return this.sharedAddOptions(ChatInputCommandUserOption, ...options);
}
/**
* Adds channel options.
*
* @param options - Options to add
*/
public addChannelOptions(
...options: RestOrArray<
ChatInputCommandChannelOption | ((builder: ChatInputCommandChannelOption) => ChatInputCommandChannelOption)
>
) {
return this.sharedAddOptions(ChatInputCommandChannelOption, ...options);
}
/**
* Adds role options.
*
* @param options - Options to add
*/
public addRoleOptions(
...options: RestOrArray<
ChatInputCommandRoleOption | ((builder: ChatInputCommandRoleOption) => ChatInputCommandRoleOption)
>
) {
return this.sharedAddOptions(ChatInputCommandRoleOption, ...options);
}
/**
* Adds attachment options.
*
* @param options - Options to add
*/
public addAttachmentOptions(
...options: RestOrArray<
| ChatInputCommandAttachmentOption
| ((builder: ChatInputCommandAttachmentOption) => ChatInputCommandAttachmentOption)
>
) {
return this.sharedAddOptions(ChatInputCommandAttachmentOption, ...options);
}
/**
* Adds mentionable options.
*
* @param options - Options to add
*/
public addMentionableOptions(
...options: RestOrArray<
| ChatInputCommandMentionableOption
| ((builder: ChatInputCommandMentionableOption) => ChatInputCommandMentionableOption)
>
) {
return this.sharedAddOptions(ChatInputCommandMentionableOption, ...options);
}
/**
* Adds string options.
*
* @param options - Options to add
*/
public addStringOptions(
...options: RestOrArray<
ChatInputCommandStringOption | ((builder: ChatInputCommandStringOption) => ChatInputCommandStringOption)
>
) {
return this.sharedAddOptions(ChatInputCommandStringOption, ...options);
}
/**
* Adds integer options.
*
* @param options - Options to add
*/
public addIntegerOptions(
...options: RestOrArray<
ChatInputCommandIntegerOption | ((builder: ChatInputCommandIntegerOption) => ChatInputCommandIntegerOption)
>
) {
return this.sharedAddOptions(ChatInputCommandIntegerOption, ...options);
}
/**
* Adds number options.
*
* @param options - Options to add
*/
public addNumberOptions(
...options: RestOrArray<
ChatInputCommandNumberOption | ((builder: ChatInputCommandNumberOption) => ChatInputCommandNumberOption)
>
) {
return this.sharedAddOptions(ChatInputCommandNumberOption, ...options);
}
/**
* Removes, replaces, or inserts options for this command.
*
* @remarks
* This method behaves similarly
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
*
* It's useful for modifying and adjusting order of the already-existing options for this command.
* @example
* Remove the first option:
* ```ts
* actionRow.spliceOptions(0, 1);
* ```
* @example
* Remove the first n options:
* ```ts
* const n = 4;
* actionRow.spliceOptions(0, n);
* ```
* @example
* Remove the last option:
* ```ts
* actionRow.spliceOptions(-1, 1);
* ```
* @param index - The index to start at
* @param deleteCount - The number of options to remove
* @param options - The replacing option objects
*/
public spliceOptions(index: number, deleteCount: number, ...options: ApplicationCommandOptionBase[]): this {
this.data.options ??= [];
this.data.options.splice(index, deleteCount, ...options);
return this;
}
/**
* Where the actual adding magic happens. ✨
*
* @internal
*/
private sharedAddOptions<OptionBuilder extends ApplicationCommandOptionBase>(
Instance: new () => OptionBuilder,
...options: RestOrArray<OptionBuilder | ((builder: OptionBuilder) => OptionBuilder)>
): this {
const normalized = normalizeArray(options);
const resolved = normalized.map((option) => resolveBuilder(option, Instance));
this.data.options ??= [];
this.data.options.push(...resolved);
return this;
}
}

View File

@@ -0,0 +1,60 @@
import type { RestOrArray } from '../../../../util/normalizeArray.js';
import { normalizeArray } from '../../../../util/normalizeArray.js';
import { resolveBuilder } from '../../../../util/resolveBuilder.js';
import {
ChatInputCommandSubcommandGroupBuilder,
ChatInputCommandSubcommandBuilder,
} from '../ChatInputCommandSubcommands.js';
export interface SharedChatInputCommandSubcommandsData {
options?: (ChatInputCommandSubcommandBuilder | ChatInputCommandSubcommandGroupBuilder)[];
}
/**
* This mixin holds symbols that can be shared in chat input subcommands.
*
* @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group.
*/
export class SharedChatInputCommandSubcommands {
protected declare readonly data: SharedChatInputCommandSubcommandsData;
/**
* Adds subcommand groups to this command.
*
* @param input - Subcommand groups to add
*/
public addSubcommandGroups(
...input: RestOrArray<
| ChatInputCommandSubcommandGroupBuilder
| ((subcommandGroup: ChatInputCommandSubcommandGroupBuilder) => ChatInputCommandSubcommandGroupBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandGroupBuilder));
this.data.options ??= [];
this.data.options.push(...resolved);
return this;
}
/**
* Adds subcommands to this command.
*
* @param input - Subcommands to add
*/
public addSubcommands(
...input: RestOrArray<
| ChatInputCommandSubcommandBuilder
| ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder)
>
): this {
const normalized = normalizeArray(input);
const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandBuilder));
this.data.options ??= [];
this.data.options.push(...resolved);
return this;
}
}

View File

@@ -0,0 +1,59 @@
import type { JSONEncodable } from '@discordjs/util';
import type {
APIApplicationCommandBasicOption,
APIApplicationCommandOption,
ApplicationCommandOptionType,
} from 'discord-api-types/v10';
import type { z } from 'zod';
import { isValidationEnabled } from '../../../../util/validation.js';
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
import { basicOptionPredicate } from '../Assertions.js';
export interface ApplicationCommandOptionBaseData extends Partial<Pick<APIApplicationCommandOption, 'required'>> {
type: ApplicationCommandOptionType;
}
/**
* The base application command option builder that contains common symbols for application command builders.
*/
export abstract class ApplicationCommandOptionBase
extends SharedNameAndDescription
implements JSONEncodable<APIApplicationCommandBasicOption>
{
protected static readonly predicate: z.ZodTypeAny = basicOptionPredicate;
protected declare readonly data: ApplicationCommandOptionBaseData & SharedNameAndDescriptionData;
public constructor(type: ApplicationCommandOptionType) {
super();
this.data.type = type;
}
/**
* Sets whether this option is required.
*
* @param required - Whether this option should be required
*/
public setRequired(required = true) {
this.data.required = required;
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIApplicationCommandBasicOption {
const clone = structuredClone(this.data);
if (validationOverride ?? isValidationEnabled()) {
(this.constructor as typeof ApplicationCommandOptionBase).predicate.parse(clone);
}
return clone as APIApplicationCommandBasicOption;
}
}

View File

@@ -0,0 +1,11 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command attachment option.
*/
export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Attachment);
}
}

View File

@@ -0,0 +1,11 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command boolean option.
*/
export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Boolean);
}
}

View File

@@ -0,0 +1,19 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { channelOptionPredicate } from '../Assertions.js';
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command channel option.
*/
export class ChatInputCommandChannelOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandOptionChannelTypesMixin,
) {
protected static override readonly predicate = channelOptionPredicate;
public constructor() {
super(ApplicationCommandOptionType.Channel);
}
}

View File

@@ -0,0 +1,23 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { integerOptionPredicate } from '../Assertions.js';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command integer option.
*/
export class ChatInputCommandIntegerOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin<number>,
) {
protected static override readonly predicate = integerOptionPredicate;
public constructor() {
super(ApplicationCommandOptionType.Integer);
}
}

View File

@@ -0,0 +1,11 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command mentionable option.
*/
export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Mentionable);
}
}

View File

@@ -0,0 +1,23 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { numberOptionPredicate } from '../Assertions.js';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command number option.
*/
export class ChatInputCommandNumberOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin<number>,
) {
protected static override readonly predicate = numberOptionPredicate;
public constructor() {
super(ApplicationCommandOptionType.Number);
}
}

View File

@@ -0,0 +1,11 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command role option.
*/
export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.Role);
}
}

View File

@@ -0,0 +1,65 @@
import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { stringOptionPredicate } from '../Assertions.js';
import type { ApplicationCommandOptionWithAutocompleteData } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import type { ApplicationCommandOptionWithChoicesData } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
import type { ApplicationCommandOptionBaseData } from './ApplicationCommandOptionBase.js';
/**
* A chat input command string option.
*/
export class ChatInputCommandStringOption extends Mixin(
ApplicationCommandOptionBase,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin<string>,
) {
protected static override readonly predicate = stringOptionPredicate;
protected declare readonly data: ApplicationCommandOptionBaseData &
ApplicationCommandOptionWithAutocompleteData &
ApplicationCommandOptionWithChoicesData &
Partial<Pick<APIApplicationCommandStringOption, 'max_length' | 'min_length'>>;
public constructor() {
super(ApplicationCommandOptionType.String);
}
/**
* Sets the maximum length of this string option.
*
* @param max - The maximum length this option can be
*/
public setMaxLength(max: number): this {
this.data.max_length = max;
return this;
}
/**
* Clears the maximum length of this string option.
*/
public clearMaxLength(): this {
this.data.max_length = undefined;
return this;
}
/**
* Sets the minimum length of this string option.
*
* @param min - The minimum length this option can be
*/
public setMinLength(min: number): this {
this.data.min_length = min;
return this;
}
/**
* Clears the minimum length of this string option.
*/
public clearMinLength(): this {
this.data.min_length = undefined;
return this;
}
}

View File

@@ -0,0 +1,11 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* A chat input command user option.
*/
export class ChatInputCommandUserOption extends ApplicationCommandOptionBase {
public constructor() {
super(ApplicationCommandOptionType.User);
}
}

View File

@@ -0,0 +1,30 @@
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10';
import { z } from 'zod';
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
const namePredicate = z
.string()
.min(1)
.max(32)
// eslint-disable-next-line prefer-named-capture-group
.regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u);
const contextsPredicate = z.array(z.nativeEnum(InteractionContextType));
const integrationTypesPredicate = z.array(z.nativeEnum(ApplicationIntegrationType));
const baseContextMenuCommandPredicate = z.object({
contexts: contextsPredicate.optional(),
default_member_permissions: memberPermissionsPredicate.optional(),
name: namePredicate,
name_localizations: localeMapPredicate.optional(),
integration_types: integrationTypesPredicate.optional(),
nsfw: z.boolean().optional(),
});
export const userCommandPredicate = baseContextMenuCommandPredicate.extend({
type: z.literal(ApplicationCommandType.User),
});
export const messageCommandPredicate = baseContextMenuCommandPredicate.extend({
type: z.literal(ApplicationCommandType.Message),
});

View File

@@ -0,0 +1,29 @@
import type { ApplicationCommandType, RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { Mixin } from 'ts-mixer';
import { CommandBuilder } from '../Command.js';
import { SharedName } from '../SharedName.js';
/**
* The type a context menu command can be.
*/
export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User;
/**
* A builder that creates API-compatible JSON data for context menu commands.
*/
export abstract class ContextMenuCommandBuilder extends Mixin(
CommandBuilder<RESTPostAPIContextMenuApplicationCommandsJSONBody>,
SharedName,
) {
protected override readonly data: Partial<RESTPostAPIContextMenuApplicationCommandsJSONBody>;
public constructor(data: Partial<RESTPostAPIContextMenuApplicationCommandsJSONBody> = {}) {
super();
this.data = structuredClone(data);
}
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public abstract override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody;
}

View File

@@ -0,0 +1,19 @@
import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { isValidationEnabled } from '../../../util/validation.js';
import { messageCommandPredicate } from './Assertions.js';
import { ContextMenuCommandBuilder } from './ContextMenuCommand.js';
export class MessageContextCommandBuilder extends ContextMenuCommandBuilder {
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody {
const data = { ...structuredClone(this.data), type: ApplicationCommandType.Message };
if (validationOverride ?? isValidationEnabled()) {
messageCommandPredicate.parse(data);
}
return data as RESTPostAPIContextMenuApplicationCommandsJSONBody;
}
}

View File

@@ -0,0 +1,19 @@
import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
import { isValidationEnabled } from '../../../util/validation.js';
import { userCommandPredicate } from './Assertions.js';
import { ContextMenuCommandBuilder } from './ContextMenuCommand.js';
export class UserContextCommandBuilder extends ContextMenuCommandBuilder {
/**
* {@inheritDoc CommandBuilder.toJSON}
*/
public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody {
const data = { ...structuredClone(this.data), type: ApplicationCommandType.User };
if (validationOverride ?? isValidationEnabled()) {
userCommandPredicate.parse(data);
}
return data as RESTPostAPIContextMenuApplicationCommandsJSONBody;
}
}

View File

@@ -1,65 +0,0 @@
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10';
import { isValidationEnabled } from '../../util/validation.js';
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder.js';
const namePredicate = s
.string()
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(32)
// eslint-disable-next-line prefer-named-capture-group
.regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u)
.setValidationEnabled(isValidationEnabled);
const typePredicate = s
.union([s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message)])
.setValidationEnabled(isValidationEnabled);
const booleanPredicate = s.boolean();
export function validateDefaultPermission(value: unknown): asserts value is boolean {
booleanPredicate.parse(value);
}
export function validateName(name: unknown): asserts name is string {
namePredicate.parse(name);
}
export function validateType(type: unknown): asserts type is ContextMenuCommandType {
typePredicate.parse(type);
}
export function validateRequiredParameters(name: string, type: number) {
// Assert name matches all conditions
validateName(name);
// Assert type is valid
validateType(type);
}
const dmPermissionPredicate = s.boolean().nullish();
export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined {
dmPermissionPredicate.parse(value);
}
const memberPermissionPredicate = s
.union([
s.bigint().transform((value) => value.toString()),
s
.number()
.safeInt()
.transform((value) => value.toString()),
s.string().regex(/^\d+$/),
])
.nullish();
export function validateDefaultMemberPermissions(permissions: unknown) {
return memberPermissionPredicate.parse(permissions);
}
export const contextsPredicate = s.array(
s.nativeEnum(InteractionContextType).setValidationEnabled(isValidationEnabled),
);
export const integrationTypesPredicate = s.array(
s.nativeEnum(ApplicationIntegrationType).setValidationEnabled(isValidationEnabled),
);

View File

@@ -1,239 +0,0 @@
import type {
ApplicationCommandType,
ApplicationIntegrationType,
InteractionContextType,
LocaleString,
LocalizationMap,
Permissions,
RESTPostAPIContextMenuApplicationCommandsJSONBody,
} from 'discord-api-types/v10';
import type { RestOrArray } from '../../util/normalizeArray.js';
import { normalizeArray } from '../../util/normalizeArray.js';
import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions.js';
import {
validateRequiredParameters,
validateName,
validateType,
validateDefaultPermission,
validateDefaultMemberPermissions,
validateDMPermission,
contextsPredicate,
integrationTypesPredicate,
} from './Assertions.js';
/**
* The type a context menu command can be.
*/
export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User;
/**
* A builder that creates API-compatible JSON data for context menu commands.
*/
export class ContextMenuCommandBuilder {
/**
* The name of this command.
*/
public readonly name: string = undefined!;
/**
* The name localizations of this command.
*/
public readonly name_localizations?: LocalizationMap;
/**
* The type of this command.
*/
public readonly type: ContextMenuCommandType = undefined!;
/**
* The contexts for this command.
*/
public readonly contexts?: InteractionContextType[];
/**
* Whether this command is enabled by default when the application is added to a guild.
*
* @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead.
*/
public readonly default_permission: boolean | undefined = undefined;
/**
* The set of permissions represented as a bit set for the command.
*/
public readonly default_member_permissions: Permissions | null | undefined = undefined;
/**
* Indicates whether the command is available in direct messages with the application.
*
* @remarks
* By default, commands are visible. This property is only for global commands.
* @deprecated
* Use {@link ContextMenuCommandBuilder.contexts} instead.
*/
public readonly dm_permission: boolean | undefined = undefined;
/**
* The integration types for this command.
*/
public readonly integration_types?: ApplicationIntegrationType[];
/**
* Sets the contexts of this command.
*
* @param contexts - The contexts
*/
public setContexts(...contexts: RestOrArray<InteractionContextType>) {
Reflect.set(this, 'contexts', contextsPredicate.parse(normalizeArray(contexts)));
return this;
}
/**
* Sets integration types of this command.
*
* @param integrationTypes - The integration types
*/
public setIntegrationTypes(...integrationTypes: RestOrArray<ApplicationIntegrationType>) {
Reflect.set(this, 'integration_types', integrationTypesPredicate.parse(normalizeArray(integrationTypes)));
return this;
}
/**
* Sets the name of this command.
*
* @param name - The name to use
*/
public setName(name: string) {
// Assert the name matches the conditions
validateName(name);
Reflect.set(this, 'name', name);
return this;
}
/**
* Sets the type of this command.
*
* @param type - The type to use
*/
public setType(type: ContextMenuCommandType) {
// Assert the type is valid
validateType(type);
Reflect.set(this, 'type', type);
return this;
}
/**
* Sets whether the command is enabled by default when the application is added to a guild.
*
* @remarks
* If set to `false`, you will have to later `PUT` the permissions for this command.
* @param value - Whether to enable this command by default
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead.
*/
public setDefaultPermission(value: boolean) {
// Assert the value matches the conditions
validateDefaultPermission(value);
Reflect.set(this, 'default_permission', value);
return this;
}
/**
* Sets the default permissions a member should have in order to run this command.
*
* @remarks
* You can set this to `'0'` to disable the command by default.
* @param permissions - The permissions bit field to set
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
*/
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
// Assert the value and parse it
const permissionValue = validateDefaultMemberPermissions(permissions);
Reflect.set(this, 'default_member_permissions', permissionValue);
return this;
}
/**
* Sets if the command is available in direct messages with the application.
*
* @remarks
* By default, commands are visible. This method is only for global commands.
* @param enabled - Whether the command should be enabled in direct messages
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated Use {@link ContextMenuCommandBuilder.setContexts} instead.
*/
public setDMPermission(enabled: boolean | null | undefined) {
// Assert the value matches the conditions
validateDMPermission(enabled);
Reflect.set(this, 'dm_permission', enabled);
return this;
}
/**
* Sets a name localization for this command.
*
* @param locale - The locale to set
* @param localizedName - The localized name for the given `locale`
*/
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
if (!this.name_localizations) {
Reflect.set(this, 'name_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedName === null) {
this.name_localizations![parsedLocale] = null;
return this;
}
validateName(localizedName);
this.name_localizations![parsedLocale] = localizedName;
return this;
}
/**
* Sets the name localizations for this command.
*
* @param localizedNames - The object of localized names to set
*/
public setNameLocalizations(localizedNames: LocalizationMap | null) {
if (localizedNames === null) {
Reflect.set(this, 'name_localizations', null);
return this;
}
Reflect.set(this, 'name_localizations', {});
for (const args of Object.entries(localizedNames))
this.setNameLocalization(...(args as [LocaleString, string | null]));
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public toJSON(): RESTPostAPIContextMenuApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.type);
validateLocalizationMap(this.name_localizations);
return { ...this };
}
}

View File

@@ -1,25 +1,21 @@
import { s } from '@sapphire/shapeshift';
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
import { customIdValidator } from '../../components/Assertions.js';
import { isValidationEnabled } from '../../util/validation.js';
import { ComponentType } from 'discord-api-types/v10';
import { z } from 'zod';
import { customIdPredicate } from '../../Assertions.js';
export const titleValidator = s
.string()
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(45)
.setValidationEnabled(isValidationEnabled);
export const componentsValidator = s
.instance(ActionRowBuilder)
.array()
.lengthGreaterThanOrEqual(1)
.setValidationEnabled(isValidationEnabled);
const titlePredicate = z.string().min(1).max(45);
export function validateRequiredParameters(
customId?: string,
title?: string,
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
) {
customIdValidator.parse(customId);
titleValidator.parse(title);
componentsValidator.parse(components);
}
export const modalPredicate = z.object({
title: titlePredicate,
custom_id: customIdPredicate,
components: z
.object({
type: z.literal(ComponentType.ActionRow),
components: z
.object({ type: z.literal(ComponentType.TextInput) })
.array()
.length(1),
})
.array()
.min(1)
.max(5),
});

View File

@@ -6,11 +6,16 @@ import type {
APIModalActionRowComponent,
APIModalInteractionResponseCallbackData,
} from 'discord-api-types/v10';
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
import { customIdValidator } from '../../components/Assertions.js';
import { ActionRowBuilder } from '../../components/ActionRow.js';
import { createComponentBuilder } from '../../components/Components.js';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { titleValidator, validateRequiredParameters } from './Assertions.js';
import { resolveBuilder } from '../../util/resolveBuilder.js';
import { isValidationEnabled } from '../../util/validation.js';
import { modalPredicate } from './Assertions.js';
export interface ModalBuilderData extends Partial<Omit<APIModalInteractionResponseCallbackData, 'components'>> {
components: ActionRowBuilder[];
}
/**
* A builder that creates API-compatible JSON data for modals.
@@ -19,22 +24,25 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
/**
* The API data associated with this modal.
*/
public readonly data: Partial<APIModalInteractionResponseCallbackData>;
private readonly data: ModalBuilderData;
/**
* The components within this modal.
*/
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
public get components(): readonly ActionRowBuilder[] {
return this.data.components;
}
/**
* Creates a new modal from API data.
*
* @param data - The API data to create this modal with
*/
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
this.data = { ...data };
this.components = (components?.map((component) => createComponentBuilder(component)) ??
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
public constructor({ components = [], ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
this.data = {
...structuredClone(data),
components: components.map((component) => createComponentBuilder(component)),
};
}
/**
@@ -43,7 +51,7 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
* @param title - The title to use
*/
public setTitle(title: string) {
this.data.title = titleValidator.parse(title);
this.data.title = title;
return this;
}
@@ -53,49 +61,111 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
this.data.custom_id = customIdValidator.parse(customId);
this.data.custom_id = customId;
return this;
}
/**
* Adds components to this modal.
* Adds action rows to this modal.
*
* @param components - The components to add
*/
public addComponents(
public addActionRows(
...components: RestOrArray<
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIModalActionRowComponent>
| ActionRowBuilder
| APIActionRowComponent<APIModalActionRowComponent>
| ((builder: ActionRowBuilder) => ActionRowBuilder)
>
) {
this.components.push(
...normalizeArray(components).map((component) =>
component instanceof ActionRowBuilder
? component
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
),
);
const normalized = normalizeArray(components);
const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder));
this.data.components.push(...resolved);
return this;
}
/**
* Sets components for this modal.
* Sets the action rows for this modal.
*
* @param components - The components to set
*/
public setComponents(...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder>>) {
this.components.splice(0, this.components.length, ...normalizeArray(components));
public setActionRows(
...components: RestOrArray<
| ActionRowBuilder
| APIActionRowComponent<APIModalActionRowComponent>
| ((builder: ActionRowBuilder) => ActionRowBuilder)
>
) {
const normalized = normalizeArray(components);
this.spliceActionRows(0, this.data.components.length, ...normalized);
return this;
}
/**
* {@inheritDoc ComponentBuilder.toJSON}
* Removes, replaces, or inserts action rows for this modal.
*
* @remarks
* This method behaves similarly
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
* The maximum amount of action rows that can be added is 5.
*
* It's useful for modifying and adjusting order of the already-existing action rows of a modal.
* @example
* Remove the first action row:
* ```ts
* embed.spliceActionRows(0, 1);
* ```
* @example
* Remove the first n action rows:
* ```ts
* const n = 4;
* embed.spliceActionRows(0, n);
* ```
* @example
* Remove the last action row:
* ```ts
* embed.spliceActionRows(-1, 1);
* ```
* @param index - The index to start at
* @param deleteCount - The number of action rows to remove
* @param rows - The replacing action row objects
*/
public toJSON(): APIModalInteractionResponseCallbackData {
validateRequiredParameters(this.data.custom_id, this.data.title, this.components);
public spliceActionRows(
index: number,
deleteCount: number,
...rows: (
| ActionRowBuilder
| APIActionRowComponent<APIModalActionRowComponent>
| ((builder: ActionRowBuilder) => ActionRowBuilder)
)[]
): this {
const resolved = rows.map((row) => resolveBuilder(row, ActionRowBuilder));
this.data.components.splice(index, deleteCount, ...resolved);
return {
...this.data,
components: this.components.map((component) => component.toJSON()),
} as APIModalInteractionResponseCallbackData;
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
*
* @param validationOverride - Force validation to run/not run regardless of your global preference
*/
public toJSON(validationOverride?: boolean): APIModalInteractionResponseCallbackData {
const { components, ...rest } = this.data;
const data = {
...structuredClone(rest),
components: components.map((component) => component.toJSON(validationOverride)),
};
if (validationOverride ?? isValidationEnabled()) {
modalPredicate.parse(data);
}
return data as APIModalInteractionResponseCallbackData;
}
}

View File

@@ -1,123 +0,0 @@
import { s } from '@sapphire/shapeshift';
import {
ApplicationIntegrationType,
InteractionContextType,
Locale,
type APIApplicationCommandOptionChoice,
type LocalizationMap,
} from 'discord-api-types/v10';
import { isValidationEnabled } from '../../util/validation.js';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder.js';
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands.js';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase.js';
const namePredicate = s
.string()
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(32)
.regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u)
.setValidationEnabled(isValidationEnabled);
export function validateName(name: unknown): asserts name is string {
namePredicate.parse(name);
}
const descriptionPredicate = s
.string()
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(100)
.setValidationEnabled(isValidationEnabled);
const localePredicate = s.nativeEnum(Locale);
export function validateDescription(description: unknown): asserts description is string {
descriptionPredicate.parse(description);
}
const maxArrayLengthPredicate = s.unknown().array().lengthLessThanOrEqual(25).setValidationEnabled(isValidationEnabled);
export function validateLocale(locale: unknown) {
return localePredicate.parse(locale);
}
export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] {
maxArrayLengthPredicate.parse(options);
}
export function validateRequiredParameters(
name: string,
description: string,
options: ToAPIApplicationCommandOptions[],
) {
// Assert name matches all conditions
validateName(name);
// Assert description conditions
validateDescription(description);
// Assert options conditions
validateMaxOptionsLength(options);
}
const booleanPredicate = s.boolean();
export function validateDefaultPermission(value: unknown): asserts value is boolean {
booleanPredicate.parse(value);
}
export function validateRequired(required: unknown): asserts required is boolean {
booleanPredicate.parse(required);
}
const choicesLengthPredicate = s.number().lessThanOrEqual(25).setValidationEnabled(isValidationEnabled);
export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
}
export function assertReturnOfBuilder<
ReturnType extends ApplicationCommandOptionBase | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder,
>(input: unknown, ExpectedInstanceOf: new () => ReturnType): asserts input is ReturnType {
s.instance(ExpectedInstanceOf).parse(input);
}
export const localizationMapPredicate = s
.object<LocalizationMap>(Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string().nullish()])))
.strict()
.nullish()
.setValidationEnabled(isValidationEnabled);
export function validateLocalizationMap(value: unknown): asserts value is LocalizationMap {
localizationMapPredicate.parse(value);
}
const dmPermissionPredicate = s.boolean().nullish();
export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined {
dmPermissionPredicate.parse(value);
}
const memberPermissionPredicate = s
.union([
s.bigint().transform((value) => value.toString()),
s
.number()
.safeInt()
.transform((value) => value.toString()),
s.string().regex(/^\d+$/),
])
.nullish();
export function validateDefaultMemberPermissions(permissions: unknown) {
return memberPermissionPredicate.parse(permissions);
}
export function validateNSFW(value: unknown): asserts value is boolean {
booleanPredicate.parse(value);
}
export const contextsPredicate = s.array(
s.nativeEnum(InteractionContextType).setValidationEnabled(isValidationEnabled),
);
export const integrationTypesPredicate = s.array(
s.nativeEnum(ApplicationIntegrationType).setValidationEnabled(isValidationEnabled),
);

View File

@@ -1,110 +0,0 @@
import type {
APIApplicationCommandOption,
ApplicationIntegrationType,
InteractionContextType,
LocalizationMap,
Permissions,
} from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import { SharedNameAndDescription } from './mixins/NameAndDescription.js';
import { SharedSlashCommand } from './mixins/SharedSlashCommand.js';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js';
import { SharedSlashCommandSubcommands } from './mixins/SharedSubcommands.js';
/**
* A builder that creates API-compatible JSON data for slash commands.
*/
@mix(SharedSlashCommandOptions, SharedNameAndDescription, SharedSlashCommandSubcommands, SharedSlashCommand)
export class SlashCommandBuilder {
/**
* The name of this command.
*/
public readonly name: string = undefined!;
/**
* The name localizations of this command.
*/
public readonly name_localizations?: LocalizationMap;
/**
* The description of this command.
*/
public readonly description: string = undefined!;
/**
* The description localizations of this command.
*/
public readonly description_localizations?: LocalizationMap;
/**
* The options of this command.
*/
public readonly options: ToAPIApplicationCommandOptions[] = [];
/**
* The contexts for this command.
*/
public readonly contexts?: InteractionContextType[];
/**
* Whether this command is enabled by default when the application is added to a guild.
*
* @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead.
*/
public readonly default_permission: boolean | undefined = undefined;
/**
* The set of permissions represented as a bit set for the command.
*/
public readonly default_member_permissions: Permissions | null | undefined = undefined;
/**
* Indicates whether the command is available in direct messages with the application.
*
* @remarks
* By default, commands are visible. This property is only for global commands.
* @deprecated
* Use {@link SlashCommandBuilder.contexts} instead.
*/
public readonly dm_permission: boolean | undefined = undefined;
/**
* The integration types for this command.
*/
public readonly integration_types?: ApplicationIntegrationType[];
/**
* Whether this command is NSFW.
*/
public readonly nsfw: boolean | undefined = undefined;
}
export interface SlashCommandBuilder
extends SharedNameAndDescription,
SharedSlashCommandOptions<SlashCommandOptionsOnlyBuilder>,
SharedSlashCommandSubcommands<SlashCommandSubcommandsOnlyBuilder>,
SharedSlashCommand {}
/**
* An interface specifically for slash command subcommands.
*/
export interface SlashCommandSubcommandsOnlyBuilder
extends SharedNameAndDescription,
SharedSlashCommandSubcommands<SlashCommandSubcommandsOnlyBuilder>,
SharedSlashCommand {}
/**
* An interface specifically for slash command options.
*/
export interface SlashCommandOptionsOnlyBuilder
extends SharedNameAndDescription,
SharedSlashCommandOptions<SlashCommandOptionsOnlyBuilder>,
SharedSlashCommand {}
/**
* An interface that ensures the `toJSON()` call will return something
* that can be serialized into API-compatible data.
*/
export interface ToAPIApplicationCommandOptions {
toJSON(): APIApplicationCommandOption;
}

View File

@@ -1,131 +0,0 @@
import {
ApplicationCommandOptionType,
type APIApplicationCommandSubcommandGroupOption,
type APIApplicationCommandSubcommandOption,
} from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions.js';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder.js';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase.js';
import { SharedNameAndDescription } from './mixins/NameAndDescription.js';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js';
/**
* Represents a folder for subcommands.
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
*/
@mix(SharedNameAndDescription)
export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationCommandOptions {
/**
* The name of this subcommand group.
*/
public readonly name: string = undefined!;
/**
* The description of this subcommand group.
*/
public readonly description: string = undefined!;
/**
* The subcommands within this subcommand group.
*/
public readonly options: SlashCommandSubcommandBuilder[] = [];
/**
* Adds a new subcommand to this group.
*
* @param input - A function that returns a subcommand builder or an already built builder
*/
public addSubcommand(
input:
| SlashCommandSubcommandBuilder
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
) {
const { options } = this;
// First, assert options conditions - we cannot have more than 25 options
validateMaxOptionsLength(options);
// Get the final result
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);
// Push it
options.push(result);
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public toJSON(): APIApplicationCommandSubcommandGroupOption {
validateRequiredParameters(this.name, this.description, this.options);
return {
type: ApplicationCommandOptionType.SubcommandGroup,
name: this.name,
name_localizations: this.name_localizations,
description: this.description,
description_localizations: this.description_localizations,
options: this.options.map((option) => option.toJSON()),
};
}
}
export interface SlashCommandSubcommandGroupBuilder extends SharedNameAndDescription {}
/**
* A builder that creates API-compatible JSON data for slash command subcommands.
*
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
*/
@mix(SharedNameAndDescription, SharedSlashCommandOptions)
export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOptions {
/**
* The name of this subcommand.
*/
public readonly name: string = undefined!;
/**
* The description of this subcommand.
*/
public readonly description: string = undefined!;
/**
* The options within this subcommand.
*/
public readonly options: ApplicationCommandOptionBase[] = [];
/**
* Serializes this builder to API-compatible JSON data.
*
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public toJSON(): APIApplicationCommandSubcommandOption {
validateRequiredParameters(this.name, this.description, this.options);
return {
type: ApplicationCommandOptionType.Subcommand,
name: this.name,
name_localizations: this.name_localizations,
description: this.description,
description_localizations: this.description_localizations,
options: this.options.map((option) => option.toJSON()),
};
}
}
export interface SlashCommandSubcommandBuilder
extends SharedNameAndDescription,
SharedSlashCommandOptions<SlashCommandSubcommandBuilder> {}

View File

@@ -1,28 +0,0 @@
/**
* This mixin holds minimum and maximum symbols used for options.
*/
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
/**
* The maximum value of this option.
*/
public readonly max_value?: number;
/**
* The minimum value of this option.
*/
public readonly min_value?: number;
/**
* Sets the maximum number value of this option.
*
* @param max - The maximum value this option can be
*/
public abstract setMaxValue(max: number): this;
/**
* Sets the minimum number value of this option.
*
* @param min - The minimum value this option can be
*/
public abstract setMinValue(min: number): this;
}

View File

@@ -1,57 +0,0 @@
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions.js';
import { SharedNameAndDescription } from './NameAndDescription.js';
/**
* The base application command option builder that contains common symbols for application command builders.
*/
export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
/**
* The type of this option.
*/
public abstract readonly type: ApplicationCommandOptionType;
/**
* Whether this option is required.
*
* @defaultValue `false`
*/
public readonly required: boolean = false;
/**
* Sets whether this option is required.
*
* @param required - Whether this option should be required
*/
public setRequired(required: boolean) {
// Assert that you actually passed a boolean
validateRequired(required);
Reflect.set(this, 'required', required);
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public abstract toJSON(): APIApplicationCommandBasicOption;
/**
* This method runs required validators on this builder.
*/
protected runRequiredValidations() {
validateRequiredParameters(this.name, this.description, []);
// Validate localizations
validateLocalizationMap(this.name_localizations);
validateLocalizationMap(this.description_localizations);
// Assert that you actually passed a boolean
validateRequired(this.required);
}
}

View File

@@ -1,54 +0,0 @@
import { s } from '@sapphire/shapeshift';
import { ChannelType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray';
/**
* The allowed channel types used for a channel option in a slash command builder.
*
* @privateRemarks This can't be dynamic because const enums are erased at runtime.
* @internal
*/
const allowedChannelTypes = [
ChannelType.GuildText,
ChannelType.GuildVoice,
ChannelType.GuildCategory,
ChannelType.GuildAnnouncement,
ChannelType.AnnouncementThread,
ChannelType.PublicThread,
ChannelType.PrivateThread,
ChannelType.GuildStageVoice,
ChannelType.GuildForum,
ChannelType.GuildMedia,
] as const;
/**
* The type of allowed channel types used for a channel option.
*/
export type ApplicationCommandOptionAllowedChannelTypes = (typeof allowedChannelTypes)[number];
const channelTypesPredicate = s.array(s.union(allowedChannelTypes.map((type) => s.literal(type))));
/**
* This mixin holds channel type symbols used for options.
*/
export class ApplicationCommandOptionChannelTypesMixin {
/**
* The channel types of this option.
*/
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
/**
* Adds channel types to this option.
*
* @param channelTypes - The channel types
*/
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
if (this.channel_types === undefined) {
Reflect.set(this, 'channel_types', []);
}
this.channel_types!.push(...channelTypesPredicate.parse(normalizeArray(channelTypes)));
return this;
}
}

View File

@@ -1,39 +0,0 @@
import { s } from '@sapphire/shapeshift';
import type { ApplicationCommandOptionType } from 'discord-api-types/v10';
const booleanPredicate = s.boolean();
/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithAutocompleteMixin {
/**
* Whether this option utilizes autocomplete.
*/
public readonly autocomplete?: boolean;
/**
* The type of this option.
*
* @privateRemarks Since this is present and this is a mixin, this is needed.
*/
public readonly type!: ApplicationCommandOptionType;
/**
* Whether this option uses autocomplete.
*
* @param autocomplete - Whether this option should use autocomplete
*/
public setAutocomplete(autocomplete: boolean): this {
// Assert that you actually passed a boolean
booleanPredicate.parse(autocomplete);
if (autocomplete && 'choices' in this && Array.isArray(this.choices) && this.choices.length > 0) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
Reflect.set(this, 'autocomplete', autocomplete);
return this;
}
}

View File

@@ -1,83 +0,0 @@
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandOptionType, type APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js';
import { localizationMapPredicate, validateChoicesLength } from '../Assertions.js';
const stringPredicate = s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
const numberPredicate = s.number().greaterThan(Number.NEGATIVE_INFINITY).lessThan(Number.POSITIVE_INFINITY);
const choicesPredicate = s
.object({
name: stringPredicate,
name_localizations: localizationMapPredicate,
value: s.union([stringPredicate, numberPredicate]),
})
.array();
/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithChoicesMixin<ChoiceType extends number | string> {
/**
* The choices of this option.
*/
public readonly choices?: APIApplicationCommandOptionChoice<ChoiceType>[];
/**
* The type of this option.
*
* @privateRemarks Since this is present and this is a mixin, this is needed.
*/
public readonly type!: ApplicationCommandOptionType;
/**
* Adds multiple choices to this option.
*
* @param choices - The choices to add
*/
public addChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
const normalizedChoices = normalizeArray(choices);
if (normalizedChoices.length > 0 && 'autocomplete' in this && this.autocomplete) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
choicesPredicate.parse(normalizedChoices);
if (this.choices === undefined) {
Reflect.set(this, 'choices', []);
}
validateChoicesLength(normalizedChoices.length, this.choices);
for (const { name, name_localizations, value } of normalizedChoices) {
// Validate the value
if (this.type === ApplicationCommandOptionType.String) {
stringPredicate.parse(value);
} else {
numberPredicate.parse(value);
}
this.choices!.push({ name, name_localizations, value });
}
return this;
}
/**
* Sets multiple choices for this option.
*
* @param choices - The choices to set
*/
public setChoices<Input extends APIApplicationCommandOptionChoice<ChoiceType>>(...choices: RestOrArray<Input>): this {
const normalizedChoices = normalizeArray(choices);
if (normalizedChoices.length > 0 && 'autocomplete' in this && this.autocomplete) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
choicesPredicate.parse(normalizedChoices);
Reflect.set(this, 'choices', []);
this.addChoices(normalizedChoices);
return this;
}
}

View File

@@ -1,142 +0,0 @@
import type { LocaleString, LocalizationMap } from 'discord-api-types/v10';
import { validateDescription, validateLocale, validateName } from '../Assertions.js';
/**
* This mixin holds name and description symbols for slash commands.
*/
export class SharedNameAndDescription {
/**
* The name of this command.
*/
public readonly name!: string;
/**
* The name localizations of this command.
*/
public readonly name_localizations?: LocalizationMap;
/**
* The description of this command.
*/
public readonly description!: string;
/**
* The description localizations of this command.
*/
public readonly description_localizations?: LocalizationMap;
/**
* Sets the name of this command.
*
* @param name - The name to use
*/
public setName(name: string): this {
// Assert the name matches the conditions
validateName(name);
Reflect.set(this, 'name', name);
return this;
}
/**
* Sets the description of this command.
*
* @param description - The description to use
*/
public setDescription(description: string) {
// Assert the description matches the conditions
validateDescription(description);
Reflect.set(this, 'description', description);
return this;
}
/**
* Sets a name localization for this command.
*
* @param locale - The locale to set
* @param localizedName - The localized name for the given `locale`
*/
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
if (!this.name_localizations) {
Reflect.set(this, 'name_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedName === null) {
this.name_localizations![parsedLocale] = null;
return this;
}
validateName(localizedName);
this.name_localizations![parsedLocale] = localizedName;
return this;
}
/**
* Sets the name localizations for this command.
*
* @param localizedNames - The object of localized names to set
*/
public setNameLocalizations(localizedNames: LocalizationMap | null) {
if (localizedNames === null) {
Reflect.set(this, 'name_localizations', null);
return this;
}
Reflect.set(this, 'name_localizations', {});
for (const args of Object.entries(localizedNames)) {
this.setNameLocalization(...(args as [LocaleString, string | null]));
}
return this;
}
/**
* Sets a description localization for this command.
*
* @param locale - The locale to set
* @param localizedDescription - The localized description for the given locale
*/
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string | null) {
if (!this.description_localizations) {
Reflect.set(this, 'description_localizations', {});
}
const parsedLocale = validateLocale(locale);
if (localizedDescription === null) {
this.description_localizations![parsedLocale] = null;
return this;
}
validateDescription(localizedDescription);
this.description_localizations![parsedLocale] = localizedDescription;
return this;
}
/**
* Sets the description localizations for this command.
*
* @param localizedDescriptions - The object of localized descriptions to set
*/
public setDescriptionLocalizations(localizedDescriptions: LocalizationMap | null) {
if (localizedDescriptions === null) {
Reflect.set(this, 'description_localizations', null);
return this;
}
Reflect.set(this, 'description_localizations', {});
for (const args of Object.entries(localizedDescriptions)) {
this.setDescriptionLocalization(...(args as [LocaleString, string | null]));
}
return this;
}
}

View File

@@ -1,162 +0,0 @@
import {
ApplicationCommandType,
type ApplicationIntegrationType,
type InteractionContextType,
type LocalizationMap,
type Permissions,
type RESTPostAPIChatInputApplicationCommandsJSONBody,
} from 'discord-api-types/v10';
import type { RestOrArray } from '../../../util/normalizeArray.js';
import { normalizeArray } from '../../../util/normalizeArray.js';
import {
contextsPredicate,
integrationTypesPredicate,
validateDMPermission,
validateDefaultMemberPermissions,
validateDefaultPermission,
validateLocalizationMap,
validateNSFW,
validateRequiredParameters,
} from '../Assertions.js';
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder.js';
/**
* This mixin holds symbols that can be shared in slashcommands independent of options or subcommands.
*/
export class SharedSlashCommand {
public readonly name: string = undefined!;
public readonly name_localizations?: LocalizationMap;
public readonly description: string = undefined!;
public readonly description_localizations?: LocalizationMap;
public readonly options: ToAPIApplicationCommandOptions[] = [];
public readonly contexts?: InteractionContextType[];
/**
* @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead.
*/
public readonly default_permission: boolean | undefined = undefined;
public readonly default_member_permissions: Permissions | null | undefined = undefined;
/**
* @deprecated Use {@link SharedSlashCommand.contexts} instead.
*/
public readonly dm_permission: boolean | undefined = undefined;
public readonly integration_types?: ApplicationIntegrationType[];
public readonly nsfw: boolean | undefined = undefined;
/**
* Sets the contexts of this command.
*
* @param contexts - The contexts
*/
public setContexts(...contexts: RestOrArray<InteractionContextType>) {
Reflect.set(this, 'contexts', contextsPredicate.parse(normalizeArray(contexts)));
return this;
}
/**
* Sets the integration types of this command.
*
* @param integrationTypes - The integration types
*/
public setIntegrationTypes(...integrationTypes: RestOrArray<ApplicationIntegrationType>) {
Reflect.set(this, 'integration_types', integrationTypesPredicate.parse(normalizeArray(integrationTypes)));
return this;
}
/**
* Sets whether the command is enabled by default when the application is added to a guild.
*
* @remarks
* If set to `false`, you will have to later `PUT` the permissions for this command.
* @param value - Whether or not to enable this command by default
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead.
*/
public setDefaultPermission(value: boolean) {
// Assert the value matches the conditions
validateDefaultPermission(value);
Reflect.set(this, 'default_permission', value);
return this;
}
/**
* Sets the default permissions a member should have in order to run the command.
*
* @remarks
* You can set this to `'0'` to disable the command by default.
* @param permissions - The permissions bit field to set
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
*/
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
// Assert the value and parse it
const permissionValue = validateDefaultMemberPermissions(permissions);
Reflect.set(this, 'default_member_permissions', permissionValue);
return this;
}
/**
* Sets if the command is available in direct messages with the application.
*
* @remarks
* By default, commands are visible. This method is only for global commands.
* @param enabled - Whether the command should be enabled in direct messages
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated
* Use {@link SharedSlashCommand.setContexts} instead.
*/
public setDMPermission(enabled: boolean | null | undefined) {
// Assert the value matches the conditions
validateDMPermission(enabled);
Reflect.set(this, 'dm_permission', enabled);
return this;
}
/**
* Sets whether this command is NSFW.
*
* @param nsfw - Whether this command is NSFW
*/
public setNSFW(nsfw = true) {
// Assert the value matches the conditions
validateNSFW(nsfw);
Reflect.set(this, 'nsfw', nsfw);
return this;
}
/**
* Serializes this builder to API-compatible JSON data.
*
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.description, this.options);
validateLocalizationMap(this.name_localizations);
validateLocalizationMap(this.description_localizations);
return {
...this,
type: ApplicationCommandType.ChatInput,
options: this.options.map((option) => option.toJSON()),
};
}
}

View File

@@ -1,145 +0,0 @@
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js';
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
import { SlashCommandAttachmentOption } from '../options/attachment.js';
import { SlashCommandBooleanOption } from '../options/boolean.js';
import { SlashCommandChannelOption } from '../options/channel.js';
import { SlashCommandIntegerOption } from '../options/integer.js';
import { SlashCommandMentionableOption } from '../options/mentionable.js';
import { SlashCommandNumberOption } from '../options/number.js';
import { SlashCommandRoleOption } from '../options/role.js';
import { SlashCommandStringOption } from '../options/string.js';
import { SlashCommandUserOption } from '../options/user.js';
import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
/**
* This mixin holds symbols that can be shared in slash command options.
*
* @typeParam TypeAfterAddingOptions - The type this class should return after adding an option.
*/
export class SharedSlashCommandOptions<
TypeAfterAddingOptions extends SharedSlashCommandOptions<TypeAfterAddingOptions>,
> {
public readonly options!: ToAPIApplicationCommandOptions[];
/**
* Adds a boolean option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addBooleanOption(
input: SlashCommandBooleanOption | ((builder: SlashCommandBooleanOption) => SlashCommandBooleanOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandBooleanOption);
}
/**
* Adds a user option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addUserOption(input: SlashCommandUserOption | ((builder: SlashCommandUserOption) => SlashCommandUserOption)) {
return this._sharedAddOptionMethod(input, SlashCommandUserOption);
}
/**
* Adds a channel option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addChannelOption(
input: SlashCommandChannelOption | ((builder: SlashCommandChannelOption) => SlashCommandChannelOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandChannelOption);
}
/**
* Adds a role option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addRoleOption(input: SlashCommandRoleOption | ((builder: SlashCommandRoleOption) => SlashCommandRoleOption)) {
return this._sharedAddOptionMethod(input, SlashCommandRoleOption);
}
/**
* Adds an attachment option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addAttachmentOption(
input: SlashCommandAttachmentOption | ((builder: SlashCommandAttachmentOption) => SlashCommandAttachmentOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandAttachmentOption);
}
/**
* Adds a mentionable option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addMentionableOption(
input: SlashCommandMentionableOption | ((builder: SlashCommandMentionableOption) => SlashCommandMentionableOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandMentionableOption);
}
/**
* Adds a string option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addStringOption(
input: SlashCommandStringOption | ((builder: SlashCommandStringOption) => SlashCommandStringOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandStringOption);
}
/**
* Adds an integer option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addIntegerOption(
input: SlashCommandIntegerOption | ((builder: SlashCommandIntegerOption) => SlashCommandIntegerOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandIntegerOption);
}
/**
* Adds a number option.
*
* @param input - A function that returns an option builder or an already built builder
*/
public addNumberOption(
input: SlashCommandNumberOption | ((builder: SlashCommandNumberOption) => SlashCommandNumberOption),
) {
return this._sharedAddOptionMethod(input, SlashCommandNumberOption);
}
/**
* Where the actual adding magic happens. ✨
*
* @param input - The input. What else?
* @param Instance - The instance of whatever is being added
* @internal
*/
private _sharedAddOptionMethod<OptionBuilder extends ApplicationCommandOptionBase>(
input: OptionBuilder | ((builder: OptionBuilder) => OptionBuilder),
Instance: new () => OptionBuilder,
): TypeAfterAddingOptions {
const { options } = this;
// First, assert options conditions - we cannot have more than 25 options
validateMaxOptionsLength(options);
// Get the final result
const result = typeof input === 'function' ? input(new Instance()) : input;
assertReturnOfBuilder(result, Instance);
// Push it
options.push(result);
return this as unknown as TypeAfterAddingOptions;
}
}

View File

@@ -1,66 +0,0 @@
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js';
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder.js';
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from '../SlashCommandSubcommands.js';
/**
* This mixin holds symbols that can be shared in slash subcommands.
*
* @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group.
*/
export class SharedSlashCommandSubcommands<
TypeAfterAddingSubcommands extends SharedSlashCommandSubcommands<TypeAfterAddingSubcommands>,
> {
public readonly options: ToAPIApplicationCommandOptions[] = [];
/**
* Adds a new subcommand group to this command.
*
* @param input - A function that returns a subcommand group builder or an already built builder
*/
public addSubcommandGroup(
input:
| SlashCommandSubcommandGroupBuilder
| ((subcommandGroup: SlashCommandSubcommandGroupBuilder) => SlashCommandSubcommandGroupBuilder),
): TypeAfterAddingSubcommands {
const { options } = this;
// First, assert options conditions - we cannot have more than 25 options
validateMaxOptionsLength(options);
// Get the final result
const result = typeof input === 'function' ? input(new SlashCommandSubcommandGroupBuilder()) : input;
assertReturnOfBuilder(result, SlashCommandSubcommandGroupBuilder);
// Push it
options.push(result);
return this as unknown as TypeAfterAddingSubcommands;
}
/**
* Adds a new subcommand to this command.
*
* @param input - A function that returns a subcommand builder or an already built builder
*/
public addSubcommand(
input:
| SlashCommandSubcommandBuilder
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
): TypeAfterAddingSubcommands {
const { options } = this;
// First, assert options conditions - we cannot have more than 25 options
validateMaxOptionsLength(options);
// Get the final result
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;
assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);
// Push it
options.push(result);
return this as unknown as TypeAfterAddingSubcommands;
}
}

View File

@@ -1,21 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandAttachmentOption } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
/**
* A slash command attachment option.
*/
export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public override readonly type = ApplicationCommandOptionType.Attachment as const;
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandAttachmentOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -1,21 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandBooleanOption } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
/**
* A slash command boolean option.
*/
export class SlashCommandBooleanOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public readonly type = ApplicationCommandOptionType.Boolean as const;
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandBooleanOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -1,26 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js';
/**
* A slash command channel option.
*/
@mix(ApplicationCommandOptionChannelTypesMixin)
export class SlashCommandChannelOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public override readonly type = ApplicationCommandOptionType.Channel as const;
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandChannelOption {
this.runRequiredValidations();
return { ...this };
}
}
export interface SlashCommandChannelOption extends ApplicationCommandOptionChannelTypesMixin {}

View File

@@ -1,67 +0,0 @@
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandOptionType, type APIApplicationCommandIntegerOption } from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
const numberValidator = s.number().int();
/**
* A slash command integer option.
*/
@mix(
ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin,
)
export class SlashCommandIntegerOption
extends ApplicationCommandOptionBase
implements ApplicationCommandNumericOptionMinMaxValueMixin
{
/**
* The type of this option.
*/
public readonly type = ApplicationCommandOptionType.Integer as const;
/**
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMaxValue}
*/
public setMaxValue(max: number): this {
numberValidator.parse(max);
Reflect.set(this, 'max_value', max);
return this;
}
/**
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMinValue}
*/
public setMinValue(min: number): this {
numberValidator.parse(min);
Reflect.set(this, 'min_value', min);
return this;
}
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandIntegerOption {
this.runRequiredValidations();
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
return { ...this } as APIApplicationCommandIntegerOption;
}
}
export interface SlashCommandIntegerOption
extends ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithChoicesMixin<number>,
ApplicationCommandOptionWithAutocompleteMixin {}

View File

@@ -1,21 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandMentionableOption } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
/**
* A slash command mentionable option.
*/
export class SlashCommandMentionableOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public readonly type = ApplicationCommandOptionType.Mentionable as const;
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandMentionableOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -1,67 +0,0 @@
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandOptionType, type APIApplicationCommandNumberOption } from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
const numberValidator = s.number();
/**
* A slash command number option.
*/
@mix(
ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithAutocompleteMixin,
ApplicationCommandOptionWithChoicesMixin,
)
export class SlashCommandNumberOption
extends ApplicationCommandOptionBase
implements ApplicationCommandNumericOptionMinMaxValueMixin
{
/**
* The type of this option.
*/
public readonly type = ApplicationCommandOptionType.Number as const;
/**
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMaxValue}
*/
public setMaxValue(max: number): this {
numberValidator.parse(max);
Reflect.set(this, 'max_value', max);
return this;
}
/**
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMinValue}
*/
public setMinValue(min: number): this {
numberValidator.parse(min);
Reflect.set(this, 'min_value', min);
return this;
}
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandNumberOption {
this.runRequiredValidations();
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
return { ...this } as APIApplicationCommandNumberOption;
}
}
export interface SlashCommandNumberOption
extends ApplicationCommandNumericOptionMinMaxValueMixin,
ApplicationCommandOptionWithChoicesMixin<number>,
ApplicationCommandOptionWithAutocompleteMixin {}

View File

@@ -1,21 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandRoleOption } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
/**
* A slash command role option.
*/
export class SlashCommandRoleOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public override readonly type = ApplicationCommandOptionType.Role as const;
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandRoleOption {
this.runRequiredValidations();
return { ...this };
}
}

View File

@@ -1,73 +0,0 @@
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
const minLengthValidator = s.number().greaterThanOrEqual(0).lessThanOrEqual(6_000);
const maxLengthValidator = s.number().greaterThanOrEqual(1).lessThanOrEqual(6_000);
/**
* A slash command string option.
*/
@mix(ApplicationCommandOptionWithAutocompleteMixin, ApplicationCommandOptionWithChoicesMixin)
export class SlashCommandStringOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public readonly type = ApplicationCommandOptionType.String as const;
/**
* The maximum length of this option.
*/
public readonly max_length?: number;
/**
* The minimum length of this option.
*/
public readonly min_length?: number;
/**
* Sets the maximum length of this string option.
*
* @param max - The maximum length this option can be
*/
public setMaxLength(max: number): this {
maxLengthValidator.parse(max);
Reflect.set(this, 'max_length', max);
return this;
}
/**
* Sets the minimum length of this string option.
*
* @param min - The minimum length this option can be
*/
public setMinLength(min: number): this {
minLengthValidator.parse(min);
Reflect.set(this, 'min_length', min);
return this;
}
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandStringOption {
this.runRequiredValidations();
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}
return { ...this } as APIApplicationCommandStringOption;
}
}
export interface SlashCommandStringOption
extends ApplicationCommandOptionWithChoicesMixin<string>,
ApplicationCommandOptionWithAutocompleteMixin {}

View File

@@ -1,21 +0,0 @@
import { ApplicationCommandOptionType, type APIApplicationCommandUserOption } from 'discord-api-types/v10';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
/**
* A slash command user option.
*/
export class SlashCommandUserOption extends ApplicationCommandOptionBase {
/**
* The type of this option.
*/
public readonly type = ApplicationCommandOptionType.User as const;
/**
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
*/
public toJSON(): APIApplicationCommandUserOption {
this.runRequiredValidations();
return { ...this };
}
}