Skip to content

Commit

Permalink
fix(computeDifferences): check channel types (#719)
Browse files Browse the repository at this point in the history
  • Loading branch information
vladfrangu committed Jan 19, 2024
1 parent 524fd6a commit 3a1931b
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 13 deletions.
20 changes: 15 additions & 5 deletions src/lib/utils/application-commands/compute-differences/_shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ApplicationCommandOptionType,
ApplicationCommandType,
type APIApplicationCommandChannelOption,
type APIApplicationCommandIntegerOption,
type APIApplicationCommandNumberOption,
type APIApplicationCommandOption,
Expand All @@ -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;
Expand Down
25 changes: 20 additions & 5 deletions src/lib/utils/application-commands/compute-differences/option.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
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';
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';
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
import type { CommandDifference } from '../_shared';

const channelTypeToPrettyName: Record<Exclude<APIApplicationCommandChannelOption['channel_types'], undefined>[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<CommandDifference> {
// 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<APIApplicationCommandChannelOption['channel_types'], undefined>[number];
while ((channelType = existingChannelTypes![index]) !== undefined) {
yield {
key: `${keyPath(index)}.channel_types[${index}]`,
expected: 'no channel type present',
original: getChannelTypePrettyName(channelType)
};

index++;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { APIApplicationCommandNumericTypes, CommandDifference } from '../_shared';
import type { APIApplicationCommandMinAndMaxValueTypes, CommandDifference } from '../_shared';

export function* handleMinMaxValueOptions({
currentIndex,
Expand All @@ -8,8 +8,8 @@ export function* handleMinMaxValueOptions({
}: {
currentIndex: number;
keyPath: (index: number) => string;
expectedOption: APIApplicationCommandNumericTypes;
existingOption: APIApplicationCommandNumericTypes;
expectedOption: APIApplicationCommandMinAndMaxValueTypes;
existingOption: APIApplicationCommandMinAndMaxValueTypes;
}): Generator<CommandDifference> {
// 0. No min_value and now we have min_value
if (existingOption.min_value === undefined && expectedOption.min_value !== undefined) {
Expand Down
73 changes: 73 additions & 0 deletions tests/application-commands/computeDifferences.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof getCommandDifferencesRaw>) {
Expand Down Expand Up @@ -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)'
}
]);
});
});

0 comments on commit 3a1931b

Please sign in to comment.