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. * diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index 76443f134..909ac6213 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -128,8 +128,9 @@ class GuildMemberManager extends CachedManager { resolvedOptions.roles = resolvedRoles; } const data = await this.client.rest.put(Routes.guildMember(this.guild.id, userId), { body: resolvedOptions }); - // Data is an empty Uint8Array if the member is already part of the guild. - return data instanceof Uint8Array + + // Data is an empty array buffer if the member is already part of the guild. + return data instanceof ArrayBuffer ? options.fetchWhenExisting === false ? null : this.fetch(userId) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index ae236f758..5545e4f31 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -4069,7 +4069,7 @@ export class ApplicationCommandManager< id: Snowflake, options: FetchApplicationCommandOptions & { guildId: Snowflake }, ): Promise; - public fetch(options: FetchApplicationCommandOptions): Promise>; + public fetch(options: FetchApplicationCommandOptions): Promise>; public fetch(id: Snowflake, options?: FetchApplicationCommandOptions): Promise; public fetch( id?: Snowflake,