Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(computeDifferences): check channel types #719

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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)'
}
]);
});
});