Skip to content

Commit 338d262

Browse files
committed
fix(computeDifferences): check channel types
1 parent 524fd6a commit 338d262

File tree

5 files changed

+191
-13
lines changed

5 files changed

+191
-13
lines changed

src/lib/utils/application-commands/compute-differences/_shared.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ApplicationCommandOptionType,
33
ApplicationCommandType,
4+
type APIApplicationCommandChannelOption,
45
type APIApplicationCommandIntegerOption,
56
type APIApplicationCommandNumberOption,
67
type APIApplicationCommandOption,
@@ -27,23 +28,32 @@ export const contextMenuTypes = [ApplicationCommandType.Message, ApplicationComm
2728
export const subcommandTypes = [ApplicationCommandOptionType.SubcommandGroup, ApplicationCommandOptionType.Subcommand];
2829

2930
export type APIApplicationCommandSubcommandTypes = APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption;
30-
export type APIApplicationCommandNumericTypes = APIApplicationCommandIntegerOption | APIApplicationCommandNumberOption;
31-
export type APIApplicationCommandChoosableAndAutocompletableTypes = APIApplicationCommandNumericTypes | APIApplicationCommandStringOption;
31+
export type APIApplicationCommandMinAndMaxValueTypes = APIApplicationCommandIntegerOption | APIApplicationCommandNumberOption;
32+
export type APIApplicationCommandChoosableAndAutocompletableTypes = APIApplicationCommandMinAndMaxValueTypes | APIApplicationCommandStringOption;
33+
export type APIApplicationCommandMinMaxLengthTypes = APIApplicationCommandStringOption;
3234

33-
export function hasMinMaxValueSupport(option: APIApplicationCommandOption): option is APIApplicationCommandNumericTypes {
35+
export function hasMinMaxValueSupport(option: APIApplicationCommandOption): option is APIApplicationCommandMinAndMaxValueTypes {
3436
return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number].includes(option.type);
3537
}
3638

3739
export function hasChoicesAndAutocompleteSupport(
3840
option: APIApplicationCommandOption
3941
): option is APIApplicationCommandChoosableAndAutocompletableTypes {
40-
return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number, ApplicationCommandOptionType.String].includes(option.type);
42+
return [
43+
ApplicationCommandOptionType.Integer, //
44+
ApplicationCommandOptionType.Number,
45+
ApplicationCommandOptionType.String
46+
].includes(option.type);
4147
}
4248

43-
export function hasMinMaxLengthSupport(option: APIApplicationCommandOption): option is APIApplicationCommandStringOption {
49+
export function hasMinMaxLengthSupport(option: APIApplicationCommandOption): option is APIApplicationCommandMinMaxLengthTypes {
4450
return option.type === ApplicationCommandOptionType.String;
4551
}
4652

53+
export function hasChannelTypesSupport(option: APIApplicationCommandOption): option is APIApplicationCommandChannelOption {
54+
return option.type === ApplicationCommandOptionType.Channel;
55+
}
56+
4757
export interface CommandDifference {
4858
key: string;
4959
expected: string;

src/lib/utils/application-commands/compute-differences/option.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import {
22
ApplicationCommandOptionType,
33
type APIApplicationCommandBasicOption,
4-
type APIApplicationCommandOption,
5-
type APIApplicationCommandStringOption
4+
type APIApplicationCommandChannelOption,
5+
type APIApplicationCommandOption
66
} from 'discord-api-types/v10';
77
import {
8+
hasChannelTypesSupport,
89
hasChoicesAndAutocompleteSupport,
910
hasMinMaxLengthSupport,
1011
hasMinMaxValueSupport,
1112
optionTypeToPrettyName,
1213
subcommandTypes,
1314
type APIApplicationCommandChoosableAndAutocompletableTypes,
14-
type APIApplicationCommandNumericTypes,
15+
type APIApplicationCommandMinAndMaxValueTypes,
16+
type APIApplicationCommandMinMaxLengthTypes,
1517
type APIApplicationCommandSubcommandTypes,
1618
type CommandDifference
1719
} from './_shared';
1820
import { checkDescription } from './description';
1921
import { checkLocalizations } from './localizations';
2022
import { checkName } from './name';
2123
import { handleAutocomplete } from './option/autocomplete';
24+
import { checkChannelTypes } from './option/channelTypes';
2225
import { handleMinMaxLengthOptions } from './option/minMaxLength';
2326
import { handleMinMaxValueOptions } from './option/minMaxValue';
2427
import { checkOptionRequired } from './option/required';
@@ -133,7 +136,7 @@ export function* reportOptionDifferences({
133136

134137
if (hasMinMaxValueSupport(option)) {
135138
// Check min and max_value
136-
const existingCasted = existingOption as APIApplicationCommandNumericTypes;
139+
const existingCasted = existingOption as APIApplicationCommandMinAndMaxValueTypes;
137140

138141
yield* handleMinMaxValueOptions({
139142
currentIndex,
@@ -156,7 +159,7 @@ export function* reportOptionDifferences({
156159

157160
if (hasMinMaxLengthSupport(option)) {
158161
// Check min and max_value
159-
const existingCasted = existingOption as APIApplicationCommandStringOption;
162+
const existingCasted = existingOption as APIApplicationCommandMinMaxLengthTypes;
160163

161164
yield* handleMinMaxLengthOptions({
162165
currentIndex,
@@ -165,6 +168,18 @@ export function* reportOptionDifferences({
165168
keyPath
166169
});
167170
}
171+
172+
if (hasChannelTypesSupport(option)) {
173+
// Check channel_types
174+
const existingCasted = existingOption as APIApplicationCommandChannelOption;
175+
176+
yield* checkChannelTypes({
177+
currentIndex,
178+
existingChannelTypes: existingCasted.channel_types,
179+
keyPath,
180+
newChannelTypes: option.channel_types
181+
});
182+
}
168183
}
169184

170185
function* handleSubcommandOptions({
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
2+
import type { CommandDifference } from '../_shared';
3+
4+
const channelTypeToPrettyName: Record<Exclude<APIApplicationCommandChannelOption['channel_types'], undefined>[number], string> = {
5+
[ChannelType.GuildText]: 'text channel (type 0)',
6+
[ChannelType.GuildVoice]: 'voice channel (type 2)',
7+
[ChannelType.GuildCategory]: 'guild category (type 4)',
8+
[ChannelType.GuildAnnouncement]: 'guild announcement channel (type 5)',
9+
[ChannelType.AnnouncementThread]: 'guild announcement thread (type 10)',
10+
[ChannelType.PublicThread]: 'guild public thread (type 11)',
11+
[ChannelType.PrivateThread]: 'guild private thread (type 12)',
12+
[ChannelType.GuildStageVoice]: 'guild stage voice channel (type 13)',
13+
[ChannelType.GuildDirectory]: 'guild directory (type 14)',
14+
[ChannelType.GuildForum]: 'guild forum (type 15)',
15+
[ChannelType.GuildMedia]: 'guild media channel (type 16)'
16+
};
17+
18+
const unknownChannelType = (type: number): string => `unknown channel type (${type}); please contact Sapphire developers about this!`;
19+
20+
function getChannelTypePrettyName(type: keyof typeof channelTypeToPrettyName): string {
21+
return channelTypeToPrettyName[type] ?? unknownChannelType(type);
22+
}
23+
24+
export function* checkChannelTypes({
25+
existingChannelTypes,
26+
newChannelTypes,
27+
currentIndex,
28+
keyPath
29+
}: {
30+
currentIndex: number;
31+
keyPath: (index: number) => string;
32+
existingChannelTypes?: APIApplicationCommandChannelOption['channel_types'];
33+
newChannelTypes?: APIApplicationCommandChannelOption['channel_types'];
34+
}): Generator<CommandDifference> {
35+
// 0. No existing channel types and now we have channel types
36+
if (!existingChannelTypes?.length && newChannelTypes?.length) {
37+
yield {
38+
key: `${keyPath(currentIndex)}.channel_types`,
39+
original: 'no channel types present',
40+
expected: 'channel types present'
41+
};
42+
}
43+
// 1. Existing channel types and now we have no channel types
44+
else if (existingChannelTypes?.length && !newChannelTypes?.length) {
45+
yield {
46+
key: `${keyPath(currentIndex)}.channel_types`,
47+
original: 'channel types present',
48+
expected: 'no channel types present'
49+
};
50+
}
51+
// 2. Iterate over each channel type if we have any and see what's different
52+
else if (newChannelTypes?.length) {
53+
let index = 0;
54+
for (const channelType of newChannelTypes) {
55+
const currentIndex = index++;
56+
const existingChannelType = existingChannelTypes![currentIndex];
57+
if (channelType !== existingChannelType) {
58+
yield {
59+
key: `${keyPath(currentIndex)}.channel_types[${currentIndex}]`,
60+
original: existingChannelType === undefined ? 'no channel type present' : getChannelTypePrettyName(existingChannelType),
61+
expected: getChannelTypePrettyName(channelType)
62+
};
63+
}
64+
}
65+
66+
// If we went through less channel types than we previously had, report that
67+
if (index < existingChannelTypes!.length) {
68+
let channelType: Exclude<APIApplicationCommandChannelOption['channel_types'], undefined>[number];
69+
while ((channelType = existingChannelTypes![index]) !== undefined) {
70+
yield {
71+
key: `${keyPath(index)}.channel_types[${index}]`,
72+
expected: 'no channel type present',
73+
original: getChannelTypePrettyName(channelType)
74+
};
75+
76+
index++;
77+
}
78+
}
79+
}
80+
}

src/lib/utils/application-commands/compute-differences/option/minMaxValue.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { APIApplicationCommandNumericTypes, CommandDifference } from '../_shared';
1+
import type { APIApplicationCommandMinAndMaxValueTypes, CommandDifference } from '../_shared';
22

33
export function* handleMinMaxValueOptions({
44
currentIndex,
@@ -8,8 +8,8 @@ export function* handleMinMaxValueOptions({
88
}: {
99
currentIndex: number;
1010
keyPath: (index: number) => string;
11-
expectedOption: APIApplicationCommandNumericTypes;
12-
existingOption: APIApplicationCommandNumericTypes;
11+
expectedOption: APIApplicationCommandMinAndMaxValueTypes;
12+
existingOption: APIApplicationCommandMinAndMaxValueTypes;
1313
}): Generator<CommandDifference> {
1414
// 0. No min_value and now we have min_value
1515
if (existingOption.min_value === undefined && expectedOption.min_value !== undefined) {

tests/application-commands/computeDifferences.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApplicationCommandOptionType, ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10';
2+
import { ChannelType } from 'discord.js';
23
import { getCommandDifferences as getCommandDifferencesRaw } from '../../src/lib/utils/application-commands/computeDifferences';
34

45
function getCommandDifferences(...args: Parameters<typeof getCommandDifferencesRaw>) {
@@ -1651,4 +1652,76 @@ describe('Compute differences for provided application commands', () => {
16511652

16521653
expect(getCommandDifferences(command1, command2, true)).toEqual([]);
16531654
});
1655+
1656+
// Channel types
1657+
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', () => {
1658+
const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = {
1659+
description: 'description 1',
1660+
name: 'test',
1661+
options: [
1662+
{
1663+
type: ApplicationCommandOptionType.Channel,
1664+
description: 'description 1',
1665+
name: 'option1'
1666+
}
1667+
]
1668+
};
1669+
1670+
const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = {
1671+
name: 'test',
1672+
description: 'description 1',
1673+
options: [
1674+
{
1675+
type: ApplicationCommandOptionType.Channel,
1676+
description: 'description 1',
1677+
name: 'option1',
1678+
channel_types: [ChannelType.GuildAnnouncement]
1679+
}
1680+
]
1681+
};
1682+
1683+
expect(getCommandDifferences(command1, command2, false)).toEqual([
1684+
{
1685+
key: 'options[0].channel_types',
1686+
expected: 'channel types present',
1687+
original: 'no channel types present'
1688+
}
1689+
]);
1690+
});
1691+
1692+
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', () => {
1693+
const command1: RESTPostAPIChatInputApplicationCommandsJSONBody = {
1694+
name: 'test',
1695+
description: 'description 1',
1696+
options: [
1697+
{
1698+
type: ApplicationCommandOptionType.Channel,
1699+
description: 'description 1',
1700+
name: 'option1',
1701+
channel_types: [ChannelType.GuildAnnouncement]
1702+
}
1703+
]
1704+
};
1705+
1706+
const command2: RESTPostAPIChatInputApplicationCommandsJSONBody = {
1707+
name: 'test',
1708+
description: 'description 1',
1709+
options: [
1710+
{
1711+
type: ApplicationCommandOptionType.Channel,
1712+
description: 'description 1',
1713+
name: 'option1',
1714+
channel_types: [ChannelType.GuildText]
1715+
}
1716+
]
1717+
};
1718+
1719+
expect(getCommandDifferences(command1, command2, false)).toEqual([
1720+
{
1721+
key: 'options[0].channel_types[0]',
1722+
expected: 'text channel (type 0)',
1723+
original: 'guild announcement channel (type 5)'
1724+
}
1725+
]);
1726+
});
16541727
});

0 commit comments

Comments
 (0)