From 95fae306062d3d09344c8ef617e375e58a5a7ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=86MB=C3=98?= <69138346+TAEMBO@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:56:47 -0700 Subject: [PATCH] feat: add user-installable apps support (#10348) * feat(SlashCommandBuilder): `addContexts()` and `addIntegrationTypes()` * Add methods to ContextMenuCommandbuilder * Fix JSDoc * Use `setX` over `addX` * Fix tests --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- .../interactions/ContextMenuCommands.test.ts | 48 ++++++++++++++++- .../SlashCommands/SlashCommands.test.ts | 54 ++++++++++++++++++- .../contextMenuCommands/Assertions.ts | 10 +++- .../ContextMenuCommandBuilder.ts | 38 +++++++++++++ .../interactions/slashCommands/Assertions.ts | 16 +++++- .../slashCommands/SlashCommandBuilder.ts | 18 ++++++- .../mixins/SharedSlashCommand.ts | 32 +++++++++++ 7 files changed, 211 insertions(+), 5 deletions(-) diff --git a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts index 99716c886..ecec8c1b0 100644 --- a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts +++ b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts @@ -1,4 +1,4 @@ -import { PermissionFlagsBits } from 'discord-api-types/v10'; +import { ApplicationIntegrationType, InteractionContextType, PermissionFlagsBits } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index.js'; @@ -144,5 +144,51 @@ describe('Context Menu Commands', () => { expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError(); }); }); + + describe('contexts', () => { + test('GIVEN a builder with valid contexts THEN does not throw an error', () => { + expect(() => + getBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]), + ).not.toThrowError(); + + expect(() => + getBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid contexts THEN does throw an error', () => { + // @ts-expect-error: Invalid contexts + expect(() => getBuilder().setContexts(999)).toThrowError(); + + // @ts-expect-error: Invalid contexts + expect(() => getBuilder().setContexts([999, 998])).toThrowError(); + }); + }); + + describe('integration types', () => { + test('GIVEN a builder with valid integraton types THEN does not throw an error', () => { + expect(() => + getBuilder().setIntegrationTypes([ + ApplicationIntegrationType.GuildInstall, + ApplicationIntegrationType.UserInstall, + ]), + ).not.toThrowError(); + + expect(() => + getBuilder().setIntegrationTypes( + ApplicationIntegrationType.GuildInstall, + ApplicationIntegrationType.UserInstall, + ), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid integration types THEN does throw an error', () => { + // @ts-expect-error: Invalid integration types + expect(() => getBuilder().setIntegrationTypes(999)).toThrowError(); + + // @ts-expect-error: Invalid integration types + expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError(); + }); + }); }); }); diff --git a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts index 85e887818..acfb6fdc2 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts +++ b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts @@ -1,4 +1,10 @@ -import { ChannelType, PermissionFlagsBits, type APIApplicationCommandOptionChoice } from 'discord-api-types/v10'; +import { + ApplicationIntegrationType, + ChannelType, + InteractionContextType, + PermissionFlagsBits, + type APIApplicationCommandOptionChoice, +} from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { SlashCommandAssertions, @@ -532,5 +538,51 @@ describe('Slash Commands', () => { expect(() => getBuilder().addChannelOption(getChannelOption()).setDMPermission(false)).not.toThrowError(); }); }); + + describe('contexts', () => { + test('GIVEN a builder with valid contexts THEN does not throw an error', () => { + expect(() => + getBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]), + ).not.toThrowError(); + + expect(() => + getBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid contexts THEN does throw an error', () => { + // @ts-expect-error: Invalid contexts + expect(() => getBuilder().setContexts(999)).toThrowError(); + + // @ts-expect-error: Invalid contexts + expect(() => getBuilder().setContexts([999, 998])).toThrowError(); + }); + }); + + describe('integration types', () => { + test('GIVEN a builder with valid integraton types THEN does not throw an error', () => { + expect(() => + getBuilder().setIntegrationTypes([ + ApplicationIntegrationType.GuildInstall, + ApplicationIntegrationType.UserInstall, + ]), + ).not.toThrowError(); + + expect(() => + getBuilder().setIntegrationTypes( + ApplicationIntegrationType.GuildInstall, + ApplicationIntegrationType.UserInstall, + ), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid integration types THEN does throw an error', () => { + // @ts-expect-error: Invalid integration types + expect(() => getBuilder().setIntegrationTypes(999)).toThrowError(); + + // @ts-expect-error: Invalid integration types + expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError(); + }); + }); }); }); diff --git a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts index feb97af4f..ac76a25d7 100644 --- a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts +++ b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts @@ -1,5 +1,5 @@ import { s } from '@sapphire/shapeshift'; -import { ApplicationCommandType } from 'discord-api-types/v10'; +import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10'; import { isValidationEnabled } from '../../util/validation.js'; import type { ContextMenuCommandType } from './ContextMenuCommandBuilder.js'; @@ -49,3 +49,11 @@ const memberPermissionPredicate = s.union( 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), +); diff --git a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts index a4cb2be1d..1c11391c8 100644 --- a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts +++ b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts @@ -1,10 +1,14 @@ 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, @@ -13,6 +17,8 @@ import { validateDefaultPermission, validateDefaultMemberPermissions, validateDMPermission, + contextsPredicate, + integrationTypesPredicate, } from './Assertions.js'; /** @@ -39,6 +45,11 @@ export class ContextMenuCommandBuilder { */ 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. * @@ -59,6 +70,33 @@ export class ContextMenuCommandBuilder { */ 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) { + 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) { + Reflect.set(this, 'integration_types', integrationTypesPredicate.parse(normalizeArray(integrationTypes))); + + return this; + } + /** * Sets the name of this command. * diff --git a/packages/builders/src/interactions/slashCommands/Assertions.ts b/packages/builders/src/interactions/slashCommands/Assertions.ts index f98e5be3a..1d9868e5b 100644 --- a/packages/builders/src/interactions/slashCommands/Assertions.ts +++ b/packages/builders/src/interactions/slashCommands/Assertions.ts @@ -1,5 +1,11 @@ import { s } from '@sapphire/shapeshift'; -import { Locale, type APIApplicationCommandOptionChoice, type LocalizationMap } from 'discord-api-types/v10'; +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'; @@ -98,3 +104,11 @@ export function validateDefaultMemberPermissions(permissions: unknown) { 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), +); diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts index 4c23a770d..9f94c88ab 100644 --- a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts +++ b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts @@ -1,4 +1,10 @@ -import type { APIApplicationCommandOption, LocalizationMap, Permissions } from 'discord-api-types/v10'; +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'; @@ -35,6 +41,11 @@ export class SlashCommandBuilder { */ 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. * @@ -55,6 +66,11 @@ export class SlashCommandBuilder { */ public readonly dm_permission: boolean | undefined = undefined; + /** + * The integration types for this command. + */ + public readonly integration_types?: ApplicationIntegrationType[]; + /** * Whether this command is NSFW. */ diff --git a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts index 9e178f82f..5d39cfc60 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts @@ -1,9 +1,15 @@ import type { + ApplicationIntegrationType, + InteractionContextType, LocalizationMap, Permissions, 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, @@ -27,6 +33,8 @@ export class SharedSlashCommand { public readonly options: ToAPIApplicationCommandOptions[] = []; + public readonly contexts?: InteractionContextType[]; + /** * @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead. */ @@ -36,8 +44,32 @@ export class SharedSlashCommand { 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) { + 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) { + 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. *