diff --git a/src/lib/utils/application-commands/compute-differences/_shared.ts b/src/lib/utils/application-commands/compute-differences/_shared.ts index 19c1e0af8..891496ade 100644 --- a/src/lib/utils/application-commands/compute-differences/_shared.ts +++ b/src/lib/utils/application-commands/compute-differences/_shared.ts @@ -1,6 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, + type APIApplicationCommandChannelOption, type APIApplicationCommandIntegerOption, type APIApplicationCommandNumberOption, type APIApplicationCommandOption, @@ -27,23 +28,32 @@ export const contextMenuTypes = [ApplicationCommandType.Message, ApplicationComm export const subcommandTypes = [ApplicationCommandOptionType.SubcommandGroup, ApplicationCommandOptionType.Subcommand]; export type APIApplicationCommandSubcommandTypes = APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption; -export type APIApplicationCommandNumericTypes = APIApplicationCommandIntegerOption | APIApplicationCommandNumberOption; -export type APIApplicationCommandChoosableAndAutocompletableTypes = APIApplicationCommandNumericTypes | APIApplicationCommandStringOption; +export type APIApplicationCommandMinAndMaxValueTypes = APIApplicationCommandIntegerOption | APIApplicationCommandNumberOption; +export type APIApplicationCommandChoosableAndAutocompletableTypes = APIApplicationCommandMinAndMaxValueTypes | APIApplicationCommandStringOption; +export type APIApplicationCommandMinMaxLengthTypes = APIApplicationCommandStringOption; -export function hasMinMaxValueSupport(option: APIApplicationCommandOption): option is APIApplicationCommandNumericTypes { +export function hasMinMaxValueSupport(option: APIApplicationCommandOption): option is APIApplicationCommandMinAndMaxValueTypes { return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number].includes(option.type); } export function hasChoicesAndAutocompleteSupport( option: APIApplicationCommandOption ): option is APIApplicationCommandChoosableAndAutocompletableTypes { - return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number, ApplicationCommandOptionType.String].includes(option.type); + return [ + ApplicationCommandOptionType.Integer, // + ApplicationCommandOptionType.Number, + ApplicationCommandOptionType.String + ].includes(option.type); } -export function hasMinMaxLengthSupport(option: APIApplicationCommandOption): option is APIApplicationCommandStringOption { +export function hasMinMaxLengthSupport(option: APIApplicationCommandOption): option is APIApplicationCommandMinMaxLengthTypes { return option.type === ApplicationCommandOptionType.String; } +export function hasChannelTypesSupport(option: APIApplicationCommandOption): option is APIApplicationCommandChannelOption { + return option.type === ApplicationCommandOptionType.Channel; +} + export interface CommandDifference { key: string; expected: string; diff --git a/src/lib/utils/application-commands/compute-differences/option.ts b/src/lib/utils/application-commands/compute-differences/option.ts index 33c381cc3..65d734332 100644 --- a/src/lib/utils/application-commands/compute-differences/option.ts +++ b/src/lib/utils/application-commands/compute-differences/option.ts @@ -1,17 +1,19 @@ import { ApplicationCommandOptionType, type APIApplicationCommandBasicOption, - type APIApplicationCommandOption, - type APIApplicationCommandStringOption + type APIApplicationCommandChannelOption, + type APIApplicationCommandOption } from 'discord-api-types/v10'; import { + hasChannelTypesSupport, hasChoicesAndAutocompleteSupport, hasMinMaxLengthSupport, hasMinMaxValueSupport, optionTypeToPrettyName, subcommandTypes, type APIApplicationCommandChoosableAndAutocompletableTypes, - type APIApplicationCommandNumericTypes, + type APIApplicationCommandMinAndMaxValueTypes, + type APIApplicationCommandMinMaxLengthTypes, type APIApplicationCommandSubcommandTypes, type CommandDifference } from './_shared'; @@ -19,6 +21,7 @@ import { checkDescription } from './description'; import { checkLocalizations } from './localizations'; import { checkName } from './name'; import { handleAutocomplete } from './option/autocomplete'; +import { checkChannelTypes } from './option/channelTypes'; import { handleMinMaxLengthOptions } from './option/minMaxLength'; import { handleMinMaxValueOptions } from './option/minMaxValue'; import { checkOptionRequired } from './option/required'; @@ -133,7 +136,7 @@ export function* reportOptionDifferences({ if (hasMinMaxValueSupport(option)) { // Check min and max_value - const existingCasted = existingOption as APIApplicationCommandNumericTypes; + const existingCasted = existingOption as APIApplicationCommandMinAndMaxValueTypes; yield* handleMinMaxValueOptions({ currentIndex, @@ -156,7 +159,7 @@ export function* reportOptionDifferences({ if (hasMinMaxLengthSupport(option)) { // Check min and max_value - const existingCasted = existingOption as APIApplicationCommandStringOption; + const existingCasted = existingOption as APIApplicationCommandMinMaxLengthTypes; yield* handleMinMaxLengthOptions({ currentIndex, @@ -165,6 +168,18 @@ export function* reportOptionDifferences({ keyPath }); } + + if (hasChannelTypesSupport(option)) { + // Check channel_types + const existingCasted = existingOption as APIApplicationCommandChannelOption; + + yield* checkChannelTypes({ + currentIndex, + existingChannelTypes: existingCasted.channel_types, + keyPath, + newChannelTypes: option.channel_types + }); + } } function* handleSubcommandOptions({ diff --git a/src/lib/utils/application-commands/compute-differences/option/channelTypes.ts b/src/lib/utils/application-commands/compute-differences/option/channelTypes.ts new file mode 100644 index 000000000..3228ec88c --- /dev/null +++ b/src/lib/utils/application-commands/compute-differences/option/channelTypes.ts @@ -0,0 +1,80 @@ +import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10'; +import type { CommandDifference } from '../_shared'; + +const channelTypeToPrettyName: Record[number], string> = { + [ChannelType.GuildText]: 'text channel (type 0)', + [ChannelType.GuildVoice]: 'voice channel (type 2)', + [ChannelType.GuildCategory]: 'guild category (type 4)', + [ChannelType.GuildAnnouncement]: 'guild announcement channel (type 5)', + [ChannelType.AnnouncementThread]: 'guild announcement thread (type 10)', + [ChannelType.PublicThread]: 'guild public thread (type 11)', + [ChannelType.PrivateThread]: 'guild private thread (type 12)', + [ChannelType.GuildStageVoice]: 'guild stage voice channel (type 13)', + [ChannelType.GuildDirectory]: 'guild directory (type 14)', + [ChannelType.GuildForum]: 'guild forum (type 15)', + [ChannelType.GuildMedia]: 'guild media channel (type 16)' +}; + +const unknownChannelType = (type: number): string => `unknown channel type (${type}); please contact Sapphire developers about this!`; + +function getChannelTypePrettyName(type: keyof typeof channelTypeToPrettyName): string { + return channelTypeToPrettyName[type] ?? unknownChannelType(type); +} + +export function* checkChannelTypes({ + existingChannelTypes, + newChannelTypes, + currentIndex, + keyPath +}: { + currentIndex: number; + keyPath: (index: number) => string; + existingChannelTypes?: APIApplicationCommandChannelOption['channel_types']; + newChannelTypes?: APIApplicationCommandChannelOption['channel_types']; +}): Generator { + // 0. No existing channel types and now we have channel types + if (!existingChannelTypes?.length && newChannelTypes?.length) { + yield { + key: `${keyPath(currentIndex)}.channel_types`, + original: 'no channel types present', + expected: 'channel types present' + }; + } + // 1. Existing channel types and now we have no channel types + else if (existingChannelTypes?.length && !newChannelTypes?.length) { + yield { + key: `${keyPath(currentIndex)}.channel_types`, + original: 'channel types present', + expected: 'no channel types present' + }; + } + // 2. Iterate over each channel type if we have any and see what's different + else if (newChannelTypes?.length) { + let index = 0; + for (const channelType of newChannelTypes) { + const currentIndex = index++; + const existingChannelType = existingChannelTypes![currentIndex]; + if (channelType !== existingChannelType) { + yield { + key: `${keyPath(currentIndex)}.channel_types[${currentIndex}]`, + original: existingChannelType === undefined ? 'no channel type present' : getChannelTypePrettyName(existingChannelType), + expected: getChannelTypePrettyName(channelType) + }; + } + } + + // If we went through less channel types than we previously had, report that + if (index < existingChannelTypes!.length) { + let channelType: Exclude[number]; + while ((channelType = existingChannelTypes![index]) !== undefined) { + yield { + key: `${keyPath(index)}.channel_types[${index}]`, + expected: 'no channel type present', + original: getChannelTypePrettyName(channelType) + }; + + index++; + } + } + } +} diff --git a/src/lib/utils/application-commands/compute-differences/option/minMaxValue.ts b/src/lib/utils/application-commands/compute-differences/option/minMaxValue.ts index fdb90eaa3..b74b14292 100644 --- a/src/lib/utils/application-commands/compute-differences/option/minMaxValue.ts +++ b/src/lib/utils/application-commands/compute-differences/option/minMaxValue.ts @@ -1,4 +1,4 @@ -import type { APIApplicationCommandNumericTypes, CommandDifference } from '../_shared'; +import type { APIApplicationCommandMinAndMaxValueTypes, CommandDifference } from '../_shared'; export function* handleMinMaxValueOptions({ currentIndex, @@ -8,8 +8,8 @@ export function* handleMinMaxValueOptions({ }: { currentIndex: number; keyPath: (index: number) => string; - expectedOption: APIApplicationCommandNumericTypes; - existingOption: APIApplicationCommandNumericTypes; + expectedOption: APIApplicationCommandMinAndMaxValueTypes; + existingOption: APIApplicationCommandMinAndMaxValueTypes; }): Generator { // 0. No min_value and now we have min_value if (existingOption.min_value === undefined && expectedOption.min_value !== undefined) { diff --git a/tests/application-commands/computeDifferences.test.ts b/tests/application-commands/computeDifferences.test.ts index c8bad0258..07ddf28fc 100644 --- a/tests/application-commands/computeDifferences.test.ts +++ b/tests/application-commands/computeDifferences.test.ts @@ -1,4 +1,5 @@ import { ApplicationCommandOptionType, ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10'; +import { ChannelType } from 'discord.js'; import { getCommandDifferences as getCommandDifferencesRaw } from '../../src/lib/utils/application-commands/computeDifferences'; function getCommandDifferences(...args: Parameters) { @@ -1651,4 +1652,76 @@ describe('Compute differences for provided application commands', () => { expect(getCommandDifferences(command1, command2, true)).toEqual([]); }); + + // Channel types + test('GIVEN a command WHEN a channel option has no channel_types defined and a command with a channel option with channel_types defined THEN return the differences', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + description: 'description 1', + name: 'test', + options: [ + { + type: ApplicationCommandOptionType.Channel, + description: 'description 1', + name: 'option1' + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + name: 'test', + description: 'description 1', + options: [ + { + type: ApplicationCommandOptionType.Channel, + description: 'description 1', + name: 'option1', + channel_types: [ChannelType.GuildAnnouncement] + } + ] + }; + + expect(getCommandDifferences(command1, command2, false)).toEqual([ + { + key: 'options[0].channel_types', + expected: 'channel types present', + original: 'no channel types present' + } + ]); + }); + + test('GIVEN a command WHEN a channel option has one type of channel and a command with a channel option has a different channel type defined THEN return the differences', () => { + const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = { + name: 'test', + description: 'description 1', + options: [ + { + type: ApplicationCommandOptionType.Channel, + description: 'description 1', + name: 'option1', + channel_types: [ChannelType.GuildAnnouncement] + } + ] + }; + + const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = { + name: 'test', + description: 'description 1', + options: [ + { + type: ApplicationCommandOptionType.Channel, + description: 'description 1', + name: 'option1', + channel_types: [ChannelType.GuildText] + } + ] + }; + + expect(getCommandDifferences(command1, command2, false)).toEqual([ + { + key: 'options[0].channel_types[0]', + expected: 'text channel (type 0)', + original: 'guild announcement channel (type 5)' + } + ]); + }); });