mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-15 02:53:31 +01:00
refactor: builders (#10448)
BREAKING CHANGE: formatters export removed (prev. deprecated) BREAKING CHANGE: `SelectMenuBuilder` and `SelectMenuOptionBuilder` have been removed (prev. deprecated) BREAKING CHANGE: `EmbedBuilder` no longer takes camalCase options BREAKING CHANGE: `ActionRowBuilder` now has specialized `[add/set]X` methods as opposed to the current `[add/set]Components` BREAKING CHANGE: Removed `equals` methods BREAKING CHANGE: Sapphire -> zod for validation BREAKING CHANGE: Removed the ability to pass `null`/`undefined` to clear fields, use `clearX()` instead BREAKING CHANGE: Renamed all "slash command" symbols to instead use "chat input command" BREAKING CHANGE: Removed `ContextMenuCommandBuilder` in favor of `MessageCommandBuilder` and `UserCommandBuilder` BREAKING CHANGE: Removed support for passing the "string key"s of enums BREAKING CHANGE: Removed `Button` class in favor for specialized classes depending on the style BREAKING CHANGE: Removed nested `addX` styled-methods in favor of plural `addXs` Co-authored-by: Vlad Frangu <me@vladfrangu.dev> Co-authored-by: Almeida <github@almeidx.dev>
This commit is contained in:
@@ -7,8 +7,8 @@ import {
|
|||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
|
||||||
createComponentBuilder,
|
createComponentBuilder,
|
||||||
|
PrimaryButtonBuilder,
|
||||||
StringSelectMenuBuilder,
|
StringSelectMenuBuilder,
|
||||||
StringSelectMenuOptionBuilder,
|
StringSelectMenuOptionBuilder,
|
||||||
} from '../../src/index.js';
|
} from '../../src/index.js';
|
||||||
@@ -41,21 +41,14 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
|
|||||||
value: 'two',
|
value: 'two',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_values: 10,
|
max_values: 2,
|
||||||
min_values: 12,
|
min_values: 2,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Action Row Components', () => {
|
describe('Action Row Components', () => {
|
||||||
describe('Assertion Tests', () => {
|
describe('Assertion Tests', () => {
|
||||||
test('GIVEN valid components THEN do not throw', () => {
|
|
||||||
expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError();
|
|
||||||
expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError();
|
|
||||||
expect(() => new ActionRowBuilder().addComponents([new ButtonBuilder()])).not.toThrowError();
|
|
||||||
expect(() => new ActionRowBuilder().setComponents([new ButtonBuilder()])).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
test('GIVEN valid JSON input THEN valid JSON output is given', () => {
|
||||||
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
const actionRowData: APIActionRowComponent<APIMessageActionRowComponent> = {
|
||||||
type: ComponentType.ActionRow,
|
type: ComponentType.ActionRow,
|
||||||
@@ -72,22 +65,10 @@ describe('Action Row Components', () => {
|
|||||||
style: ButtonStyle.Link,
|
style: ButtonStyle.Link,
|
||||||
url: 'https://google.com',
|
url: 'https://google.com',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: ComponentType.StringSelect,
|
|
||||||
placeholder: 'test',
|
|
||||||
custom_id: 'test',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: 'option',
|
|
||||||
value: 'option',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
|
expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData);
|
||||||
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
|
||||||
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,24 +101,23 @@ describe('Action Row Components', () => {
|
|||||||
value: 'two',
|
value: 'two',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_values: 10,
|
max_values: 1,
|
||||||
min_values: 12,
|
min_values: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
|
expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData);
|
||||||
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
|
expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData);
|
||||||
expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] });
|
|
||||||
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
|
||||||
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
|
const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123');
|
||||||
const selectMenu = new StringSelectMenuBuilder()
|
const selectMenu = new StringSelectMenuBuilder()
|
||||||
.setCustomId('1234')
|
.setCustomId('1234')
|
||||||
.setMaxValues(10)
|
.setMaxValues(2)
|
||||||
.setMinValues(12)
|
.setMinValues(2)
|
||||||
.setOptions(
|
.setOptions(
|
||||||
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
||||||
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||||
@@ -147,10 +127,39 @@ describe('Action Row Components', () => {
|
|||||||
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);
|
expect(new ActionRowBuilder().addPrimaryButtonComponents(button).toJSON()).toEqual(rowWithButtonData);
|
||||||
expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
expect(new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).toJSON()).toEqual(rowWithSelectMenuData);
|
||||||
expect(new ActionRowBuilder().addComponents([button]).toJSON()).toEqual(rowWithButtonData);
|
expect(new ActionRowBuilder().addPrimaryButtonComponents([button]).toJSON()).toEqual(rowWithButtonData);
|
||||||
expect(new ActionRowBuilder().addComponents([selectMenu]).toJSON()).toEqual(rowWithSelectMenuData);
|
});
|
||||||
|
|
||||||
|
test('GIVEN 2 select menus THEN it throws', () => {
|
||||||
|
const selectMenu = new StringSelectMenuBuilder()
|
||||||
|
.setCustomId('1234')
|
||||||
|
.setOptions(
|
||||||
|
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
||||||
|
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
new ActionRowBuilder()
|
||||||
|
.addStringSelectMenuComponent(selectMenu)
|
||||||
|
.addStringSelectMenuComponent(selectMenu)
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a button and a select menu THEN it throws', () => {
|
||||||
|
const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123');
|
||||||
|
const selectMenu = new StringSelectMenuBuilder()
|
||||||
|
.setCustomId('1234')
|
||||||
|
.setOptions(
|
||||||
|
new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
|
||||||
|
new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).addPrimaryButtonComponents(button).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,45 +5,21 @@ import {
|
|||||||
type APIButtonComponentWithURL,
|
type APIButtonComponentWithURL,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions.js';
|
|
||||||
import { ButtonBuilder } from '../../src/components/button/Button.js';
|
import { ButtonBuilder } from '../../src/components/button/Button.js';
|
||||||
|
import { PrimaryButtonBuilder, PremiumButtonBuilder, LinkButtonBuilder } from '../../src/index.js';
|
||||||
const buttonComponent = () => new ButtonBuilder();
|
|
||||||
|
|
||||||
const longStr =
|
const longStr =
|
||||||
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong';
|
||||||
|
|
||||||
describe('Button Components', () => {
|
describe('Button Components', () => {
|
||||||
describe('Assertion Tests', () => {
|
describe('Assertion Tests', () => {
|
||||||
test('GIVEN valid label THEN validator does not throw', () => {
|
|
||||||
expect(() => buttonLabelValidator.parse('foobar')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid label THEN validator does throw', () => {
|
|
||||||
expect(() => buttonLabelValidator.parse(null)).toThrowError();
|
|
||||||
expect(() => buttonLabelValidator.parse('')).toThrowError();
|
|
||||||
|
|
||||||
expect(() => buttonLabelValidator.parse(longStr)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid style THEN validator does not throw', () => {
|
|
||||||
expect(() => buttonStyleValidator.parse(3)).not.toThrowError();
|
|
||||||
expect(() => buttonStyleValidator.parse(ButtonStyle.Secondary)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid style THEN validator does throw', () => {
|
|
||||||
expect(() => buttonStyleValidator.parse(7)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||||
expect(() =>
|
expect(() => new PrimaryButtonBuilder().setCustomId('custom').setLabel('test')).not.toThrowError();
|
||||||
buttonComponent().setCustomId('custom').setStyle(ButtonStyle.Primary).setLabel('test'),
|
|
||||||
).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const button = buttonComponent()
|
const button = new PrimaryButtonBuilder()
|
||||||
.setCustomId('custom')
|
.setCustomId('custom')
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setLabel('test')
|
||||||
.setDisabled(true)
|
.setDisabled(true)
|
||||||
.setEmoji({ name: 'test' });
|
.setEmoji({ name: 'test' });
|
||||||
|
|
||||||
@@ -51,111 +27,41 @@ describe('Button Components', () => {
|
|||||||
}).not.toThrowError();
|
}).not.toThrowError();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium);
|
const button = new PremiumButtonBuilder().setSKUId('123456789012345678');
|
||||||
button.toJSON();
|
button.toJSON();
|
||||||
}).not.toThrowError();
|
}).not.toThrowError();
|
||||||
|
|
||||||
expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError();
|
expect(() => new LinkButtonBuilder().setURL('https://google.com')).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN invalid fields THEN build does throw', () => {
|
test('GIVEN invalid fields THEN build does throw', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
buttonComponent().setCustomId(longStr);
|
new PrimaryButtonBuilder().setCustomId(longStr).toJSON();
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent()
|
|
||||||
.setCustomId('custom')
|
|
||||||
.setStyle(ButtonStyle.Primary)
|
|
||||||
.setDisabled(true)
|
|
||||||
.setLabel('test')
|
|
||||||
.setURL('https://google.com')
|
|
||||||
.setEmoji({ name: 'test' });
|
|
||||||
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// @ts-expect-error: Invalid emoji
|
// @ts-expect-error: Invalid emoji
|
||||||
const button = buttonComponent().setEmoji('test');
|
const button = new PrimaryButtonBuilder().setEmoji('test');
|
||||||
button.toJSON();
|
button.toJSON();
|
||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const button = buttonComponent().setStyle(ButtonStyle.Primary);
|
const button = new PrimaryButtonBuilder();
|
||||||
button.toJSON();
|
button.toJSON();
|
||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const button = buttonComponent().setStyle(ButtonStyle.Primary).setCustomId('test');
|
const button = new PrimaryButtonBuilder().setCustomId('test');
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent().setStyle(ButtonStyle.Link);
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent().setStyle(ButtonStyle.Primary).setLabel('test').setURL('https://google.com');
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test');
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent().setStyle(ButtonStyle.Primary).setSKUId('123456789012345678');
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent()
|
|
||||||
.setStyle(ButtonStyle.Secondary)
|
|
||||||
.setLabel('button')
|
|
||||||
.setSKUId('123456789012345678');
|
|
||||||
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent()
|
|
||||||
.setStyle(ButtonStyle.Success)
|
|
||||||
.setEmoji({ name: '😇' })
|
|
||||||
.setSKUId('123456789012345678');
|
|
||||||
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent()
|
|
||||||
.setStyle(ButtonStyle.Danger)
|
|
||||||
.setCustomId('test')
|
|
||||||
.setSKUId('123456789012345678');
|
|
||||||
|
|
||||||
button.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const button = buttonComponent()
|
|
||||||
.setStyle(ButtonStyle.Link)
|
|
||||||
.setURL('https://google.com')
|
|
||||||
.setSKUId('123456789012345678');
|
|
||||||
|
|
||||||
button.toJSON();
|
button.toJSON();
|
||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
|
|
||||||
// @ts-expect-error: Invalid style
|
// @ts-expect-error: Invalid style
|
||||||
expect(() => buttonComponent().setStyle(24)).toThrowError();
|
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setStyle(24).toJSON()).toThrowError();
|
||||||
expect(() => buttonComponent().setLabel(longStr)).toThrowError();
|
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setLabel(longStr).toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid parameter for disabled
|
// @ts-expect-error: Invalid parameter for disabled
|
||||||
expect(() => buttonComponent().setDisabled(0)).toThrowError();
|
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setDisabled(0).toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid emoji
|
// @ts-expect-error: Invalid emoji
|
||||||
expect(() => buttonComponent().setEmoji('foo')).toThrowError();
|
expect(() => new PrimaryButtonBuilder().setCustomId('hi').setEmoji('foo').toJSON()).toThrowError();
|
||||||
|
|
||||||
expect(() => buttonComponent().setURL('foobar')).toThrowError();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GiVEN valid input THEN valid JSON outputs are given', () => {
|
test('GiVEN valid input THEN valid JSON outputs are given', () => {
|
||||||
@@ -167,13 +73,12 @@ describe('Button Components', () => {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
|
expect(new PrimaryButtonBuilder(interactionData).toJSON()).toEqual(interactionData);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
buttonComponent()
|
new PrimaryButtonBuilder()
|
||||||
.setCustomId(interactionData.custom_id)
|
.setCustomId(interactionData.custom_id)
|
||||||
.setLabel(interactionData.label!)
|
.setLabel(interactionData.label!)
|
||||||
.setStyle(interactionData.style)
|
|
||||||
.setDisabled(interactionData.disabled)
|
.setDisabled(interactionData.disabled)
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
).toEqual(interactionData);
|
).toEqual(interactionData);
|
||||||
@@ -186,9 +91,7 @@ describe('Button Components', () => {
|
|||||||
url: 'https://google.com',
|
url: 'https://google.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData);
|
expect(new LinkButtonBuilder(linkData).toJSON()).toEqual(linkData);
|
||||||
|
|
||||||
expect(buttonComponent().setLabel(linkData.label!).setDisabled(true).setURL(linkData.url));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import {
|
|||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
|
||||||
createComponentBuilder,
|
createComponentBuilder,
|
||||||
|
CustomIdButtonBuilder,
|
||||||
StringSelectMenuBuilder,
|
StringSelectMenuBuilder,
|
||||||
TextInputBuilder,
|
TextInputBuilder,
|
||||||
} from '../../src/index.js';
|
} from '../../src/index.js';
|
||||||
|
|
||||||
describe('createComponentBuilder', () => {
|
describe('createComponentBuilder', () => {
|
||||||
test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])(
|
test.each([StringSelectMenuBuilder, TextInputBuilder])(
|
||||||
'passing an instance of %j should return itself',
|
'passing an instance of %j should return itself',
|
||||||
(Builder) => {
|
(Builder) => {
|
||||||
const builder = new Builder();
|
const builder = new Builder();
|
||||||
@@ -42,7 +42,7 @@ describe('createComponentBuilder', () => {
|
|||||||
type: ComponentType.Button,
|
type: ComponentType.Button,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder);
|
expect(createComponentBuilder(button)).toBeInstanceOf(CustomIdButtonBuilder);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => {
|
test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, test, expect } from 'vitest';
|
|||||||
import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js';
|
import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js';
|
||||||
|
|
||||||
const selectMenu = () => new StringSelectMenuBuilder();
|
const selectMenu = () => new StringSelectMenuBuilder();
|
||||||
|
const selectMenuWithId = () => new StringSelectMenuBuilder({ custom_id: 'hi' });
|
||||||
const selectMenuOption = () => new StringSelectMenuOptionBuilder();
|
const selectMenuOption = () => new StringSelectMenuOptionBuilder();
|
||||||
|
|
||||||
const longStr = 'a'.repeat(256);
|
const longStr = 'a'.repeat(256);
|
||||||
@@ -16,10 +17,10 @@ const selectMenuOptionData: APISelectMenuOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectMenuDataWithoutOptions = {
|
const selectMenuDataWithoutOptions = {
|
||||||
type: ComponentType.SelectMenu,
|
type: ComponentType.StringSelect,
|
||||||
custom_id: 'test',
|
custom_id: 'test',
|
||||||
max_values: 10,
|
max_values: 1,
|
||||||
min_values: 3,
|
min_values: 1,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
placeholder: 'test',
|
placeholder: 'test',
|
||||||
} as const;
|
} as const;
|
||||||
@@ -109,49 +110,87 @@ describe('Select Menu Components', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
|
test('GIVEN invalid inputs THEN Select Menu does throw', () => {
|
||||||
expect(() => selectMenu().setCustomId(longStr)).toThrowError();
|
expect(() => selectMenu().setCustomId(longStr).toJSON()).toThrowError();
|
||||||
expect(() => selectMenu().setMaxValues(30)).toThrowError();
|
expect(() => selectMenuWithId().setMaxValues(30).toJSON()).toThrowError();
|
||||||
expect(() => selectMenu().setMinValues(-20)).toThrowError();
|
expect(() => selectMenuWithId().setMinValues(-20).toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid disabled value
|
// @ts-expect-error: Invalid disabled value
|
||||||
expect(() => selectMenu().setDisabled(0)).toThrowError();
|
expect(() => selectMenuWithId().setDisabled(0).toJSON()).toThrowError();
|
||||||
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError();
|
expect(() => selectMenuWithId().setPlaceholder(longStr).toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid option
|
// @ts-expect-error: Invalid option
|
||||||
expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError();
|
expect(() => selectMenuWithId().addOptions({ label: 'test' }).toJSON()).toThrowError();
|
||||||
expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError();
|
expect(() => selectMenuWithId().addOptions({ label: longStr, value: 'test' }).toJSON()).toThrowError();
|
||||||
expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError();
|
expect(() => selectMenuWithId().addOptions({ value: longStr, label: 'test' }).toJSON()).toThrowError();
|
||||||
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError();
|
expect(() =>
|
||||||
|
selectMenuWithId().addOptions({ label: 'test', value: 'test', description: longStr }).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Invalid option
|
||||||
|
selectMenuWithId().addOptions({ label: 'test', value: 'test', default: 100 }).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
// @ts-expect-error: Invalid option
|
// @ts-expect-error: Invalid option
|
||||||
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError();
|
expect(() => selectMenuWithId().addOptions({ value: 'test' }).toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid option
|
// @ts-expect-error: Invalid option
|
||||||
expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError();
|
expect(() => selectMenuWithId().addOptions({ default: true }).toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid option
|
expect(() =>
|
||||||
expect(() => selectMenu().addOptions({ default: true })).toThrowError();
|
selectMenuWithId()
|
||||||
// @ts-expect-error: Invalid option
|
// @ts-expect-error: Invalid option
|
||||||
expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError();
|
.addOptions([{ label: 'test' }])
|
||||||
expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError();
|
.toJSON(),
|
||||||
expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError();
|
).toThrowError();
|
||||||
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', description: longStr }])).toThrowError();
|
expect(() =>
|
||||||
// @ts-expect-error: Invalid option
|
selectMenuWithId()
|
||||||
expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', default: 100 }])).toThrowError();
|
.addOptions([{ label: longStr, value: 'test' }])
|
||||||
// @ts-expect-error: Invalid option
|
.toJSON(),
|
||||||
expect(() => selectMenu().addOptions([{ value: 'test' }])).toThrowError();
|
).toThrowError();
|
||||||
// @ts-expect-error: Invalid option
|
expect(() =>
|
||||||
expect(() => selectMenu().addOptions([{ default: true }])).toThrowError();
|
selectMenuWithId()
|
||||||
|
.addOptions([{ value: longStr, label: 'test' }])
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
selectMenuWithId()
|
||||||
|
.addOptions([{ label: 'test', value: 'test', description: longStr }])
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
selectMenuWithId()
|
||||||
|
// @ts-expect-error: Invalid option
|
||||||
|
.addOptions([{ label: 'test', value: 'test', default: 100 }])
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
selectMenuWithId()
|
||||||
|
// @ts-expect-error: Invalid option
|
||||||
|
.addOptions([{ value: 'test' }])
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
selectMenuWithId()
|
||||||
|
// @ts-expect-error: Invalid option
|
||||||
|
.addOptions([{ default: true }])
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
|
||||||
const tooManyOptions = Array.from<APISelectMenuOption>({ length: 26 }).fill({ label: 'test', value: 'test' });
|
const tooManyOptions = Array.from<APISelectMenuOption>({ length: 26 }).fill({ label: 'test', value: 'test' });
|
||||||
|
|
||||||
expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError();
|
expect(() =>
|
||||||
expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError();
|
selectMenu()
|
||||||
|
.setOptions(...tooManyOptions)
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() => selectMenu().setOptions(tooManyOptions).toJSON()).toThrowError();
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
selectMenu()
|
selectMenu()
|
||||||
.addOptions({ label: 'test', value: 'test' })
|
.addOptions({ label: 'test', value: 'test' })
|
||||||
.addOptions(...tooManyOptions),
|
.addOptions(...tooManyOptions)
|
||||||
|
.toJSON(),
|
||||||
).toThrowError();
|
).toThrowError();
|
||||||
expect(() =>
|
expect(() =>
|
||||||
selectMenu()
|
selectMenu()
|
||||||
.addOptions([{ label: 'test', value: 'test' }])
|
.addOptions([{ label: 'test', value: 'test' }])
|
||||||
.addOptions(tooManyOptions),
|
.addOptions(tooManyOptions)
|
||||||
|
.toJSON(),
|
||||||
).toThrowError();
|
).toThrowError();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
@@ -162,7 +201,8 @@ describe('Select Menu Components', () => {
|
|||||||
.setDefault(-1)
|
.setDefault(-1)
|
||||||
// @ts-expect-error: Invalid emoji
|
// @ts-expect-error: Invalid emoji
|
||||||
.setEmoji({ name: 1 })
|
.setEmoji({ name: 1 })
|
||||||
.setDescription(longStr);
|
.setDescription(longStr)
|
||||||
|
.toJSON();
|
||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -212,17 +252,16 @@ describe('Select Menu Components', () => {
|
|||||||
).toStrictEqual([selectMenuOptionData]);
|
).toStrictEqual([selectMenuOptionData]);
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
makeStringSelectMenuWithOptions().spliceOptions(
|
makeStringSelectMenuWithOptions()
|
||||||
0,
|
.spliceOptions(0, 0, ...Array.from({ length: 26 }, () => selectMenuOptionData))
|
||||||
0,
|
.toJSON(),
|
||||||
...Array.from({ length: 26 }, () => selectMenuOptionData),
|
|
||||||
),
|
|
||||||
).toThrowError();
|
).toThrowError();
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
makeStringSelectMenuWithOptions()
|
makeStringSelectMenuWithOptions()
|
||||||
.setOptions(Array.from({ length: 25 }, () => selectMenuOptionData))
|
.setOptions(Array.from({ length: 25 }, () => selectMenuOptionData))
|
||||||
.spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData),
|
.spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData)
|
||||||
|
.toJSON(),
|
||||||
).toThrowError();
|
).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { ComponentType, TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
import { ComponentType, TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import {
|
|
||||||
labelValidator,
|
|
||||||
maxLengthValidator,
|
|
||||||
minLengthValidator,
|
|
||||||
placeholderValidator,
|
|
||||||
valueValidator,
|
|
||||||
textInputStyleValidator,
|
|
||||||
} from '../../src/components/textInput/Assertions.js';
|
|
||||||
import { TextInputBuilder } from '../../src/components/textInput/TextInput.js';
|
import { TextInputBuilder } from '../../src/components/textInput/TextInput.js';
|
||||||
|
|
||||||
const superLongStr = 'a'.repeat(5_000);
|
const superLongStr = 'a'.repeat(5_000);
|
||||||
@@ -16,56 +8,6 @@ const textInputComponent = () => new TextInputBuilder();
|
|||||||
|
|
||||||
describe('Text Input Components', () => {
|
describe('Text Input Components', () => {
|
||||||
describe('Assertion Tests', () => {
|
describe('Assertion Tests', () => {
|
||||||
test('GIVEN valid label THEN validator does not throw', () => {
|
|
||||||
expect(() => labelValidator.parse('foobar')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid label THEN validator does throw', () => {
|
|
||||||
expect(() => labelValidator.parse(24)).toThrowError();
|
|
||||||
expect(() => labelValidator.parse(undefined)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid style THEN validator does not throw', () => {
|
|
||||||
expect(() => textInputStyleValidator.parse(TextInputStyle.Paragraph)).not.toThrowError();
|
|
||||||
expect(() => textInputStyleValidator.parse(TextInputStyle.Short)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid style THEN validator does throw', () => {
|
|
||||||
expect(() => textInputStyleValidator.parse(24)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid min length THEN validator does not throw', () => {
|
|
||||||
expect(() => minLengthValidator.parse(10)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid min length THEN validator does throw', () => {
|
|
||||||
expect(() => minLengthValidator.parse(-1)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid max length THEN validator does not throw', () => {
|
|
||||||
expect(() => maxLengthValidator.parse(10)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid min length THEN validator does throw 2', () => {
|
|
||||||
expect(() => maxLengthValidator.parse(4_001)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid value THEN validator does not throw', () => {
|
|
||||||
expect(() => valueValidator.parse('foobar')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid value THEN validator does throw', () => {
|
|
||||||
expect(() => valueValidator.parse(superLongStr)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid placeholder THEN validator does not throw', () => {
|
|
||||||
expect(() => placeholderValidator.parse('foobar')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid value THEN validator does throw 2', () => {
|
|
||||||
expect(() => placeholderValidator.parse(superLongStr)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
|
textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON();
|
||||||
@@ -84,9 +26,7 @@ describe('Text Input Components', () => {
|
|||||||
}).not.toThrowError();
|
}).not.toThrowError();
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// Issue #8107
|
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON();
|
||||||
// @ts-expect-error: Shapeshift maps the enum key to the value when parsing
|
|
||||||
textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle('Short').toJSON();
|
|
||||||
}).not.toThrowError();
|
}).not.toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,516 @@
|
|||||||
|
import {
|
||||||
|
ApplicationCommandType,
|
||||||
|
ApplicationIntegrationType,
|
||||||
|
ChannelType,
|
||||||
|
InteractionContextType,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import { describe, test, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
ChatInputCommandBooleanOption,
|
||||||
|
ChatInputCommandBuilder,
|
||||||
|
ChatInputCommandChannelOption,
|
||||||
|
ChatInputCommandIntegerOption,
|
||||||
|
ChatInputCommandMentionableOption,
|
||||||
|
ChatInputCommandNumberOption,
|
||||||
|
ChatInputCommandRoleOption,
|
||||||
|
ChatInputCommandAttachmentOption,
|
||||||
|
ChatInputCommandStringOption,
|
||||||
|
ChatInputCommandSubcommandBuilder,
|
||||||
|
ChatInputCommandSubcommandGroupBuilder,
|
||||||
|
ChatInputCommandUserOption,
|
||||||
|
} from '../../../src/index.js';
|
||||||
|
|
||||||
|
const getBuilder = () => new ChatInputCommandBuilder();
|
||||||
|
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
|
||||||
|
const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getIntegerOption = () => new ChatInputCommandIntegerOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getNumberOption = () => new ChatInputCommandNumberOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getBooleanOption = () => new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getUserOption = () => new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getChannelOption = () => new ChatInputCommandChannelOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getRoleOption = () => new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getAttachmentOption = () => new ChatInputCommandAttachmentOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getMentionableOption = () => new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123');
|
||||||
|
const getSubcommandGroup = () =>
|
||||||
|
new ChatInputCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
|
||||||
|
const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
|
||||||
|
|
||||||
|
class Collection {
|
||||||
|
public readonly [Symbol.toStringTag] = 'Map';
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ChatInput Commands', () => {
|
||||||
|
describe('ChatInputCommandBuilder', () => {
|
||||||
|
describe('Builder with no options', () => {
|
||||||
|
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
|
||||||
|
expect(() => getBuilder().toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid builder THEN does not throw error', () => {
|
||||||
|
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Builder with simple options', () => {
|
||||||
|
test('GIVEN valid builder THEN returns type included', () => {
|
||||||
|
expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid builder with options THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getBuilder()
|
||||||
|
.setName('example')
|
||||||
|
.setDescription('Example command')
|
||||||
|
.addBooleanOptions((boolean) =>
|
||||||
|
boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true),
|
||||||
|
)
|
||||||
|
.addChannelOptions((channel) => channel.setName('iscool').setDescription('Are we cool or what?'))
|
||||||
|
.addMentionableOptions((mentionable) =>
|
||||||
|
mentionable.setName('iscool').setDescription('Are we cool or what?'),
|
||||||
|
)
|
||||||
|
.addRoleOptions((role) => role.setName('iscool').setDescription('Are we cool or what?'))
|
||||||
|
.addUserOptions((user) => user.setName('iscool').setDescription('Are we cool or what?'))
|
||||||
|
.addIntegerOptions((integer) =>
|
||||||
|
integer
|
||||||
|
.setName('iscool')
|
||||||
|
.setDescription('Are we cool or what?')
|
||||||
|
.addChoices({ name: 'Very cool', value: 1_000 })
|
||||||
|
.addChoices([{ name: 'Even cooler', value: 2_000 }]),
|
||||||
|
)
|
||||||
|
.addNumberOptions((number) =>
|
||||||
|
number
|
||||||
|
.setName('iscool')
|
||||||
|
.setDescription('Are we cool or what?')
|
||||||
|
.addChoices({ name: 'Very cool', value: 1.5 })
|
||||||
|
.addChoices([{ name: 'Even cooler', value: 2.5 }]),
|
||||||
|
)
|
||||||
|
.addStringOptions((string) =>
|
||||||
|
string
|
||||||
|
.setName('iscool')
|
||||||
|
.setDescription('Are we cool or what?')
|
||||||
|
.addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' })
|
||||||
|
.addChoices([{ name: 'The Whole shebang', value: 'all' }]),
|
||||||
|
)
|
||||||
|
.addIntegerOptions((integer) =>
|
||||||
|
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||||
|
)
|
||||||
|
.addNumberOptions((number) =>
|
||||||
|
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||||
|
)
|
||||||
|
.addStringOptions((string) =>
|
||||||
|
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
getNamedBuilder().addStringOptions(getStringOption().setAutocomplete('not a boolean')).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addStringOptions(
|
||||||
|
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addStringOptions(
|
||||||
|
getStringOption()
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'Fancy Pants', value: 'fp_1' },
|
||||||
|
{ name: 'Fancy Shoes', value: 'fs_1' },
|
||||||
|
{ name: 'The Whole shebang', value: 'all' },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addStringOptions(
|
||||||
|
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addChannelOptions(
|
||||||
|
getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
getNamedBuilder()
|
||||||
|
.addChannelOptions(getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText))
|
||||||
|
.toJSON();
|
||||||
|
}).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Invalid channel type
|
||||||
|
getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100)).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Invalid channel types
|
||||||
|
getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100, 200)).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
|
||||||
|
// @ts-expect-error: Invalid max value
|
||||||
|
expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue('test')).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Invalid max value
|
||||||
|
getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue('test')).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
|
||||||
|
// @ts-expect-error: Invalid min value
|
||||||
|
expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMinValue('test')).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Invalid min value
|
||||||
|
getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue('test')).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1.5)).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1)).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addNumberOptions(getNumberOption().setMinValue(1.5)).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue(1)).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue(1.5)).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN an already built builder THEN does not throw an error', () => {
|
||||||
|
expect(() => getNamedBuilder().addStringOptions(getStringOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addNumberOptions(getNumberOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addUserOptions(getUserOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addChannelOptions(getChannelOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addRoleOptions(getRoleOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addAttachmentOptions(getAttachmentOption()).toJSON()).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addMentionableOptions(getMentionableOption()).toJSON()).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN invalid name THEN throw error', () => {
|
||||||
|
expect(() => getBuilder().setName('TEST_COMMAND').setDescription(':3').toJSON()).toThrowError();
|
||||||
|
expect(() => getBuilder().setName('ĂĂĂĂĂĂ').setDescription(':3').toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid names THEN does not throw error', () => {
|
||||||
|
expect(() => getBuilder().setName('hi_there').setDescription(':3')).not.toThrowError();
|
||||||
|
expect(() => getBuilder().setName('o_comandă').setDescription(':3')).not.toThrowError();
|
||||||
|
expect(() => getBuilder().setName('どうも').setDescription(':3')).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN invalid returns for builder THEN throw error', () => {
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
expect(() => getNamedBuilder().addBooleanOptions(true).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
expect(() => getNamedBuilder().addBooleanOptions(null).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
expect(() => getNamedBuilder().addBooleanOptions(undefined).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
.addBooleanOptions(() => ChatInputCommandStringOption)
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
.addBooleanOptions(() => new Collection())
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addStringOptions(getStringOption().setAutocomplete(true).setChoices()).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addStringOptions(getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }))
|
||||||
|
.toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN an option, THEN setting choices should not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addStringOptions(getStringOption().setChoices({ name: 'owo', value: 'uwu' }))
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid builder with NSFW, THEN does not throw error', () => {
|
||||||
|
expect(() => getNamedBuilder().setName('foo').setDescription('foo').setNSFW(true).toJSON()).not.toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Builder with subcommand (group) options', () => {
|
||||||
|
test('GIVEN builder with subcommand group THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addSubcommandGroups((group) =>
|
||||||
|
group.setName('group').setDescription('Group us together!').addSubcommands(getSubcommand()),
|
||||||
|
)
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN builder with subcommand THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.addSubcommands((subcommand) => subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'))
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN builder with subcommand THEN has regular ChatInput command fields', () => {
|
||||||
|
expect(() =>
|
||||||
|
getBuilder()
|
||||||
|
.setName('name')
|
||||||
|
.setDescription('description')
|
||||||
|
.addSubcommands((option) => option.setName('ye').setDescription('ye'))
|
||||||
|
.addSubcommands((option) => option.setName('no').setDescription('no'))
|
||||||
|
.setDefaultMemberPermissions(1n)
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand())).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
|
||||||
|
expect(() => getNamedBuilder().addSubcommands(getSubcommand()).toJSON()).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addSubcommands(getSubcommand().addBooleanOptions(getBooleanOption())).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
// @ts-expect-error: Checking if check works JS-side too
|
||||||
|
getNamedBuilder().addSubcommands(getSubcommand()).addIntegerOptions(getInteger()).toJSON(),
|
||||||
|
).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => {
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
expect(() => getNamedBuilder().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subcommand group builder', () => {
|
||||||
|
test('GIVEN no valid subcommand THEN throw error', () => {
|
||||||
|
expect(() => getSubcommandGroup().addSubcommands().toJSON()).toThrowError();
|
||||||
|
|
||||||
|
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
||||||
|
expect(() => getSubcommandGroup().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a valid subcommand THEN does not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getSubcommandGroup()
|
||||||
|
.addSubcommands((sub) => sub.setName('sub').setDescription('Testing 123'))
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Subcommand builder', () => {
|
||||||
|
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
|
||||||
|
expect(() => getSubcommand().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ChatInput command localizations', () => {
|
||||||
|
const expectedSingleLocale = { 'en-US': 'foobar' };
|
||||||
|
const expectedMultipleLocales = {
|
||||||
|
...expectedSingleLocale,
|
||||||
|
bg: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('GIVEN valid name localizations THEN does not throw error', () => {
|
||||||
|
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
|
||||||
|
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN invalid name localizations THEN does throw error', () => {
|
||||||
|
// @ts-expect-error: Invalid localization
|
||||||
|
expect(() => getNamedBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError();
|
||||||
|
// @ts-expect-error: Invalid localization
|
||||||
|
expect(() => getNamedBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
||||||
|
expect(getNamedBuilder().setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual(
|
||||||
|
expectedSingleLocale,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getNamedBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON().name_localizations,
|
||||||
|
).toEqual(expectedMultipleLocales);
|
||||||
|
expect(getNamedBuilder().clearNameLocalizations().toJSON().name_localizations).toBeUndefined();
|
||||||
|
expect(getNamedBuilder().clearNameLocalization('en-US').toJSON().name_localizations).toEqual({
|
||||||
|
'en-US': undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid description localizations THEN does not throw error', () => {
|
||||||
|
expect(() => getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON()).not.toThrowError();
|
||||||
|
expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' }).toJSON()).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN invalid description localizations THEN does throw error', () => {
|
||||||
|
// @ts-expect-error: Invalid localization description
|
||||||
|
expect(() => getNamedBuilder().setDescriptionLocalization('en-U', 'foobar').toJSON()).toThrowError();
|
||||||
|
// @ts-expect-error: Invalid localization description
|
||||||
|
expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid description localizations THEN valid data is stored', () => {
|
||||||
|
expect(
|
||||||
|
getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON(false).description_localizations,
|
||||||
|
).toEqual(expectedSingleLocale);
|
||||||
|
expect(
|
||||||
|
getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON(false)
|
||||||
|
.description_localizations,
|
||||||
|
).toEqual(expectedMultipleLocales);
|
||||||
|
expect(
|
||||||
|
getNamedBuilder().clearDescriptionLocalizations().toJSON(false).description_localizations,
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(getNamedBuilder().clearDescriptionLocalization('en-US').toJSON(false).description_localizations).toEqual(
|
||||||
|
{
|
||||||
|
'en-US': undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('permissions', () => {
|
||||||
|
test('GIVEN valid permission string THEN does not throw error', () => {
|
||||||
|
expect(() => getNamedBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid permission bitfield THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().setDefaultMemberPermissions(
|
||||||
|
PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles,
|
||||||
|
),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN null permissions THEN does not throw error', () => {
|
||||||
|
expect(() => getNamedBuilder().clearDefaultMemberPermissions()).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN invalid inputs THEN does throw error', () => {
|
||||||
|
expect(() => getNamedBuilder().setDefaultMemberPermissions('1.1').toJSON()).toThrowError();
|
||||||
|
expect(() => getNamedBuilder().setDefaultMemberPermissions(1.1).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN valid permission with options THEN does not throw error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().addBooleanOptions(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() => getNamedBuilder().addChannelOptions(getChannelOption())).not.toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('contexts', () => {
|
||||||
|
test('GIVEN a builder with valid contexts THEN does not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM).toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
|
||||||
|
// @ts-expect-error: Invalid contexts
|
||||||
|
expect(() => getNamedBuilder().setContexts(999).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
// @ts-expect-error: Invalid contexts
|
||||||
|
expect(() => getNamedBuilder().setContexts([999, 998]).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integration types', () => {
|
||||||
|
test('GIVEN a builder with valid integraton types THEN does not throw an error', () => {
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall])
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
getNamedBuilder()
|
||||||
|
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||||
|
.toJSON(),
|
||||||
|
).not.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
|
||||||
|
// @ts-expect-error: Invalid integration types
|
||||||
|
expect(() => getNamedBuilder().setIntegrationTypes(999).toJSON()).toThrowError();
|
||||||
|
|
||||||
|
// @ts-expect-error: Invalid integration types
|
||||||
|
expect(() => getNamedBuilder().setIntegrationTypes([999, 998]).toJSON()).toThrowError();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,32 +13,32 @@ import {
|
|||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
SlashCommandAttachmentOption,
|
ChatInputCommandAttachmentOption,
|
||||||
SlashCommandBooleanOption,
|
ChatInputCommandBooleanOption,
|
||||||
SlashCommandChannelOption,
|
ChatInputCommandChannelOption,
|
||||||
SlashCommandIntegerOption,
|
ChatInputCommandIntegerOption,
|
||||||
SlashCommandMentionableOption,
|
ChatInputCommandMentionableOption,
|
||||||
SlashCommandNumberOption,
|
ChatInputCommandNumberOption,
|
||||||
SlashCommandRoleOption,
|
ChatInputCommandRoleOption,
|
||||||
SlashCommandStringOption,
|
ChatInputCommandStringOption,
|
||||||
SlashCommandUserOption,
|
ChatInputCommandUserOption,
|
||||||
} from '../../../src/index.js';
|
} from '../../../src/index.js';
|
||||||
|
|
||||||
const getBooleanOption = () =>
|
const getBooleanOption = () =>
|
||||||
new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||||
|
|
||||||
const getChannelOption = () =>
|
const getChannelOption = () =>
|
||||||
new SlashCommandChannelOption()
|
new ChatInputCommandChannelOption()
|
||||||
.setName('owo')
|
.setName('owo')
|
||||||
.setDescription('Testing 123')
|
.setDescription('Testing 123')
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.addChannelTypes(ChannelType.GuildText);
|
.addChannelTypes(ChannelType.GuildText);
|
||||||
|
|
||||||
const getStringOption = () =>
|
const getStringOption = () =>
|
||||||
new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||||
|
|
||||||
const getIntegerOption = () =>
|
const getIntegerOption = () =>
|
||||||
new SlashCommandIntegerOption()
|
new ChatInputCommandIntegerOption()
|
||||||
.setName('owo')
|
.setName('owo')
|
||||||
.setDescription('Testing 123')
|
.setDescription('Testing 123')
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
@@ -46,22 +46,24 @@ const getIntegerOption = () =>
|
|||||||
.setMaxValue(10);
|
.setMaxValue(10);
|
||||||
|
|
||||||
const getNumberOption = () =>
|
const getNumberOption = () =>
|
||||||
new SlashCommandNumberOption()
|
new ChatInputCommandNumberOption()
|
||||||
.setName('owo')
|
.setName('owo')
|
||||||
.setDescription('Testing 123')
|
.setDescription('Testing 123')
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.setMinValue(-1.23)
|
.setMinValue(-1.23)
|
||||||
.setMaxValue(10);
|
.setMaxValue(10);
|
||||||
|
|
||||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
const getUserOption = () =>
|
||||||
|
new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||||
|
|
||||||
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
const getRoleOption = () =>
|
||||||
|
new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||||
|
|
||||||
const getMentionableOption = () =>
|
const getMentionableOption = () =>
|
||||||
new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true);
|
||||||
|
|
||||||
const getAttachmentOption = () =>
|
const getAttachmentOption = () =>
|
||||||
new SlashCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true);
|
new ChatInputCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true);
|
||||||
|
|
||||||
describe('Application Command toJSON() results', () => {
|
describe('Application Command toJSON() results', () => {
|
||||||
test('GIVEN a boolean option THEN calling toJSON should return a valid JSON', () => {
|
test('GIVEN a boolean option THEN calling toJSON should return a valid JSON', () => {
|
||||||
@@ -101,7 +103,6 @@ describe('Application Command toJSON() results', () => {
|
|||||||
max_value: 10,
|
max_value: 10,
|
||||||
min_value: -1,
|
min_value: -1,
|
||||||
autocomplete: true,
|
autocomplete: true,
|
||||||
// TODO
|
|
||||||
choices: [],
|
choices: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,74 +1,31 @@
|
|||||||
import { ApplicationIntegrationType, InteractionContextType, PermissionFlagsBits } from 'discord-api-types/v10';
|
import {
|
||||||
|
ApplicationCommandType,
|
||||||
|
ApplicationIntegrationType,
|
||||||
|
InteractionContextType,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index.js';
|
import { MessageContextCommandBuilder } from '../../src/index.js';
|
||||||
|
|
||||||
const getBuilder = () => new ContextMenuCommandBuilder();
|
const getBuilder = () => new MessageContextCommandBuilder();
|
||||||
|
|
||||||
describe('Context Menu Commands', () => {
|
describe('Context Menu Commands', () => {
|
||||||
describe('Assertions tests', () => {
|
|
||||||
test('GIVEN valid name THEN does not throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateName('ping')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid name THEN throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateName(null)).toThrowError();
|
|
||||||
|
|
||||||
// Too short of a name
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError();
|
|
||||||
|
|
||||||
// Invalid characters used
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError();
|
|
||||||
|
|
||||||
// Too long of a name
|
|
||||||
expect(() =>
|
|
||||||
ContextMenuCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid type THEN does not throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateType(3)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid type THEN throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateType(null)).toThrowError();
|
|
||||||
|
|
||||||
// Out of range
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateType(1)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid required parameters THEN does not throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateRequiredParameters('owo', 2)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid default_permission THEN does not throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid default_permission THEN throw error', () => {
|
|
||||||
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(null)).toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ContextMenuCommandBuilder', () => {
|
describe('ContextMenuCommandBuilder', () => {
|
||||||
describe('Builder tests', () => {
|
describe('Builder tests', () => {
|
||||||
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
|
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
|
||||||
expect(() => getBuilder().toJSON()).toThrowError();
|
expect(() => getBuilder().toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN valid builder THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('example').setType(3).toJSON()).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid name THEN throw error', () => {
|
test('GIVEN invalid name THEN throw error', () => {
|
||||||
expect(() => getBuilder().setName('$$$')).toThrowError();
|
expect(() => getBuilder().setName('$$$').toJSON()).toThrowError();
|
||||||
|
|
||||||
expect(() => getBuilder().setName(' ')).toThrowError();
|
expect(() => getBuilder().setName(' ').toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN valid names THEN does not throw error', () => {
|
test('GIVEN valid names THEN does not throw error', () => {
|
||||||
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
|
expect(() => getBuilder().setName('hi_there').toJSON()).not.toThrowError();
|
||||||
|
|
||||||
expect(() => getBuilder().setName('A COMMAND')).not.toThrowError();
|
expect(() => getBuilder().setName('A COMMAND').toJSON()).not.toThrowError();
|
||||||
|
|
||||||
// Translation: a_command
|
// Translation: a_command
|
||||||
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
|
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
|
||||||
@@ -76,20 +33,6 @@ describe('Context Menu Commands', () => {
|
|||||||
// Translation: thx (according to GTranslate)
|
// Translation: thx (according to GTranslate)
|
||||||
expect(() => getBuilder().setName('どうも')).not.toThrowError();
|
expect(() => getBuilder().setName('どうも')).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN valid types THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setType(2)).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().setType(3)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid builder with dmPermission false THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('foo').setDMPermission(false)).not.toThrowError();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Context menu command localizations', () => {
|
describe('Context menu command localizations', () => {
|
||||||
@@ -106,19 +49,22 @@ describe('Context Menu Commands', () => {
|
|||||||
|
|
||||||
test('GIVEN invalid name localizations THEN does throw error', () => {
|
test('GIVEN invalid name localizations THEN does throw error', () => {
|
||||||
// @ts-expect-error: Invalid localization
|
// @ts-expect-error: Invalid localization
|
||||||
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
|
expect(() => getBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError();
|
||||||
// @ts-expect-error: Invalid localization
|
// @ts-expect-error: Invalid localization
|
||||||
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
||||||
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
|
expect(getBuilder().setName('hi').setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual(
|
||||||
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
|
expectedSingleLocale,
|
||||||
expectedMultipleLocales,
|
|
||||||
);
|
);
|
||||||
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
|
expect(
|
||||||
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
|
getBuilder().setName('hi').setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON()
|
||||||
'en-US': null,
|
.name_localizations,
|
||||||
|
).toEqual(expectedMultipleLocales);
|
||||||
|
expect(getBuilder().setName('hi').clearNameLocalizations().toJSON().name_localizations).toBeUndefined();
|
||||||
|
expect(getBuilder().setName('hi').clearNameLocalization('en-US').toJSON().name_localizations).toEqual({
|
||||||
|
'en-US': undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -134,14 +80,10 @@ describe('Context Menu Commands', () => {
|
|||||||
).not.toThrowError();
|
).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN null permissions THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid inputs THEN does throw error', () => {
|
test('GIVEN invalid inputs THEN does throw error', () => {
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
|
expect(() => getBuilder().setName('hi').setDefaultMemberPermissions('1.1').toJSON()).toThrowError();
|
||||||
|
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
|
expect(() => getBuilder().setName('hi').setDefaultMemberPermissions(1.1).toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,10 +100,10 @@ describe('Context Menu Commands', () => {
|
|||||||
|
|
||||||
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
|
test('GIVEN a builder with invalid contexts THEN does throw an error', () => {
|
||||||
// @ts-expect-error: Invalid contexts
|
// @ts-expect-error: Invalid contexts
|
||||||
expect(() => getBuilder().setContexts(999)).toThrowError();
|
expect(() => getBuilder().setName('hi').setContexts(999).toJSON()).toThrowError();
|
||||||
|
|
||||||
// @ts-expect-error: Invalid contexts
|
// @ts-expect-error: Invalid contexts
|
||||||
expect(() => getBuilder().setContexts([999, 998])).toThrowError();
|
expect(() => getBuilder().setName('hi').setContexts([999, 998]).toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,10 +126,10 @@ describe('Context Menu Commands', () => {
|
|||||||
|
|
||||||
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
|
test('GIVEN a builder with invalid integration types THEN does throw an error', () => {
|
||||||
// @ts-expect-error: Invalid integration types
|
// @ts-expect-error: Invalid integration types
|
||||||
expect(() => getBuilder().setIntegrationTypes(999)).toThrowError();
|
expect(() => getBuilder().setName('hi').setIntegrationTypes(999).toJSON()).toThrowError();
|
||||||
|
|
||||||
// @ts-expect-error: Invalid integration types
|
// @ts-expect-error: Invalid integration types
|
||||||
expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError();
|
expect(() => getBuilder().setName('hi').setIntegrationTypes([999, 998]).toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,593 +0,0 @@
|
|||||||
import {
|
|
||||||
ApplicationCommandType,
|
|
||||||
ApplicationIntegrationType,
|
|
||||||
ChannelType,
|
|
||||||
InteractionContextType,
|
|
||||||
PermissionFlagsBits,
|
|
||||||
type APIApplicationCommandOptionChoice,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
import { describe, test, expect } from 'vitest';
|
|
||||||
import {
|
|
||||||
SlashCommandAssertions,
|
|
||||||
SlashCommandBooleanOption,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
SlashCommandChannelOption,
|
|
||||||
SlashCommandIntegerOption,
|
|
||||||
SlashCommandMentionableOption,
|
|
||||||
SlashCommandNumberOption,
|
|
||||||
SlashCommandRoleOption,
|
|
||||||
SlashCommandAttachmentOption,
|
|
||||||
SlashCommandStringOption,
|
|
||||||
SlashCommandSubcommandBuilder,
|
|
||||||
SlashCommandSubcommandGroupBuilder,
|
|
||||||
SlashCommandUserOption,
|
|
||||||
} from '../../../src/index.js';
|
|
||||||
|
|
||||||
const largeArray = Array.from({ length: 26 }, () => 1 as unknown as APIApplicationCommandOptionChoice);
|
|
||||||
|
|
||||||
const getBuilder = () => new SlashCommandBuilder();
|
|
||||||
const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command');
|
|
||||||
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getIntegerOption = () => new SlashCommandIntegerOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getNumberOption = () => new SlashCommandNumberOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getChannelOption = () => new SlashCommandChannelOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getAttachmentOption = () => new SlashCommandAttachmentOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getMentionableOption = () => new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123');
|
|
||||||
const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123');
|
|
||||||
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
|
|
||||||
|
|
||||||
class Collection {
|
|
||||||
public readonly [Symbol.toStringTag] = 'Map';
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Slash Commands', () => {
|
|
||||||
describe('Assertions tests', () => {
|
|
||||||
test('GIVEN valid name THEN does not throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateName('ping')).not.toThrowError();
|
|
||||||
expect(() => SlashCommandAssertions.validateName('hello-world_command')).not.toThrowError();
|
|
||||||
expect(() => SlashCommandAssertions.validateName('aˇ㐆1٢〣²अก')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid name THEN throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateName(null)).toThrowError();
|
|
||||||
|
|
||||||
// Too short of a name
|
|
||||||
expect(() => SlashCommandAssertions.validateName('')).toThrowError();
|
|
||||||
|
|
||||||
// Invalid characters used
|
|
||||||
expect(() => SlashCommandAssertions.validateName('ABC')).toThrowError();
|
|
||||||
expect(() => SlashCommandAssertions.validateName('ABC123$%^&')).toThrowError();
|
|
||||||
expect(() => SlashCommandAssertions.validateName('help ping')).toThrowError();
|
|
||||||
expect(() => SlashCommandAssertions.validateName('🦦')).toThrowError();
|
|
||||||
|
|
||||||
// Too long of a name
|
|
||||||
expect(() =>
|
|
||||||
SlashCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid description THEN does not throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateDescription('This is an OwO moment fur sure!~')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid description THEN throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateDescription(null)).toThrowError();
|
|
||||||
|
|
||||||
// Too short of a description
|
|
||||||
expect(() => SlashCommandAssertions.validateDescription('')).toThrowError();
|
|
||||||
|
|
||||||
// Too long of a description
|
|
||||||
expect(() =>
|
|
||||||
SlashCommandAssertions.validateDescription(
|
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magnam autem libero expedita vitae accusamus nostrum ipsam tempore repudiandae deserunt ipsum facilis, velit fugiat facere accusantium, explicabo corporis aliquam non quos.',
|
|
||||||
),
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid default_permission THEN does not throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid default_permission THEN throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateDefaultPermission(null)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid array of options or choices THEN does not throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => SlashCommandAssertions.validateChoicesLength(25)).not.toThrowError();
|
|
||||||
expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid options or choices THEN throw error', () => {
|
|
||||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError();
|
|
||||||
|
|
||||||
// Given an array that's too big
|
|
||||||
expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError();
|
|
||||||
|
|
||||||
expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid required parameters THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
SlashCommandAssertions.validateRequiredParameters(
|
|
||||||
'owo',
|
|
||||||
'My fancy command that totally exists, to test assertions',
|
|
||||||
[],
|
|
||||||
),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SlashCommandBuilder', () => {
|
|
||||||
describe('Builder with no options', () => {
|
|
||||||
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
|
|
||||||
expect(() => getBuilder().toJSON()).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid builder THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Builder with simple options', () => {
|
|
||||||
test('GIVEN valid builder THEN returns type included', () => {
|
|
||||||
expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid builder with options THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder()
|
|
||||||
.setName('example')
|
|
||||||
.setDescription('Example command')
|
|
||||||
.setDMPermission(false)
|
|
||||||
.addBooleanOption((boolean) =>
|
|
||||||
boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true),
|
|
||||||
)
|
|
||||||
.addChannelOption((channel) => channel.setName('iscool').setDescription('Are we cool or what?'))
|
|
||||||
.addMentionableOption((mentionable) => mentionable.setName('iscool').setDescription('Are we cool or what?'))
|
|
||||||
.addRoleOption((role) => role.setName('iscool').setDescription('Are we cool or what?'))
|
|
||||||
.addUserOption((user) => user.setName('iscool').setDescription('Are we cool or what?'))
|
|
||||||
.addIntegerOption((integer) =>
|
|
||||||
integer
|
|
||||||
.setName('iscool')
|
|
||||||
.setDescription('Are we cool or what?')
|
|
||||||
.addChoices({ name: 'Very cool', value: 1_000 })
|
|
||||||
.addChoices([{ name: 'Even cooler', value: 2_000 }]),
|
|
||||||
)
|
|
||||||
.addNumberOption((number) =>
|
|
||||||
number
|
|
||||||
.setName('iscool')
|
|
||||||
.setDescription('Are we cool or what?')
|
|
||||||
.addChoices({ name: 'Very cool', value: 1.5 })
|
|
||||||
.addChoices([{ name: 'Even cooler', value: 2.5 }]),
|
|
||||||
)
|
|
||||||
.addStringOption((string) =>
|
|
||||||
string
|
|
||||||
.setName('iscool')
|
|
||||||
.setDescription('Are we cool or what?')
|
|
||||||
.addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' })
|
|
||||||
.addChoices([{ name: 'The Whole shebang', value: 'all' }]),
|
|
||||||
)
|
|
||||||
.addIntegerOption((integer) =>
|
|
||||||
integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
|
||||||
)
|
|
||||||
.addNumberOption((number) =>
|
|
||||||
number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
|
||||||
)
|
|
||||||
.addStringOption((string) =>
|
|
||||||
string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true),
|
|
||||||
)
|
|
||||||
.toJSON(),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => {
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addStringOption(getStringOption().setAutocomplete('not a boolean'))).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addStringOption(
|
|
||||||
getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }),
|
|
||||||
),
|
|
||||||
).toThrowError();
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addStringOption(
|
|
||||||
getStringOption()
|
|
||||||
.setAutocomplete(true)
|
|
||||||
.addChoices(
|
|
||||||
{ name: 'Fancy Pants', value: 'fp_1' },
|
|
||||||
{ name: 'Fancy Shoes', value: 'fs_1' },
|
|
||||||
{ name: 'The Whole shebang', value: 'all' },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).toThrowError();
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addStringOption(
|
|
||||||
getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true),
|
|
||||||
),
|
|
||||||
).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const option = getStringOption();
|
|
||||||
Reflect.set(option, 'autocomplete', true);
|
|
||||||
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
|
|
||||||
return option.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const option = getNumberOption();
|
|
||||||
Reflect.set(option, 'autocomplete', true);
|
|
||||||
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
|
|
||||||
return option.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
const option = getIntegerOption();
|
|
||||||
Reflect.set(option, 'autocomplete', true);
|
|
||||||
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
|
|
||||||
return option.toJSON();
|
|
||||||
}).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addChannelOption(
|
|
||||||
getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]),
|
|
||||||
),
|
|
||||||
).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
getBuilder().addChannelOption(
|
|
||||||
getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText),
|
|
||||||
);
|
|
||||||
}).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => {
|
|
||||||
// @ts-expect-error: Invalid channel type
|
|
||||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100))).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Invalid channel types
|
|
||||||
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
|
|
||||||
// @ts-expect-error: Invalid max value
|
|
||||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue('test'))).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Invalid max value
|
|
||||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue('test'))).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Invalid min value
|
|
||||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue('test'))).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Invalid min value
|
|
||||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue('test'))).toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5))).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => {
|
|
||||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1))).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue(1.5))).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue(1))).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue(1.5))).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN an already built builder THEN does not throw an error', () => {
|
|
||||||
expect(() => getBuilder().addStringOption(getStringOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addIntegerOption(getIntegerOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addNumberOption(getNumberOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addBooleanOption(getBooleanOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addUserOption(getUserOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addChannelOption(getChannelOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().addMentionableOption(getMentionableOption())).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN no valid return for an addOption method THEN throw error', () => {
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption()).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption(getRoleOption())).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid name THEN throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('TEST_COMMAND')).toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().setName('ĂĂĂĂĂĂ')).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid names THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
|
|
||||||
|
|
||||||
// Translation: a_command
|
|
||||||
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
|
|
||||||
|
|
||||||
// Translation: thx (according to GTranslate)
|
|
||||||
expect(() => getBuilder().setName('どうも')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid returns for builder THEN throw error', () => {
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption(true)).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption(null)).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption(undefined)).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption(() => SlashCommandStringOption)).toThrowError();
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addBooleanOption(() => new Collection())).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addStringOption(
|
|
||||||
getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }),
|
|
||||||
),
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN an option, THEN setting choices should not throw an error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid builder with NSFW, THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setName('foo').setDescription('foo').setNSFW(true)).not.toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Builder with subcommand (group) options', () => {
|
|
||||||
test('GIVEN builder with subcommand group THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getNamedBuilder().addSubcommandGroup((group) => group.setName('group').setDescription('Group us together!')),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN builder with subcommand THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getNamedBuilder().addSubcommand((subcommand) =>
|
|
||||||
subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'),
|
|
||||||
),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN builder with subcommand THEN has regular slash command fields', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder()
|
|
||||||
.setName('name')
|
|
||||||
.setDescription('description')
|
|
||||||
.addSubcommand((option) => option.setName('ye').setDescription('ye'))
|
|
||||||
.addSubcommand((option) => option.setName('no').setDescription('no'))
|
|
||||||
.setDMPermission(false)
|
|
||||||
.setDefaultMemberPermissions(1n),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN builder with already built subcommand group THEN does not throw error', () => {
|
|
||||||
expect(() => getNamedBuilder().addSubcommandGroup(getSubcommandGroup())).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN builder with already built subcommand THEN does not throw error', () => {
|
|
||||||
expect(() => getNamedBuilder().addSubcommand(getSubcommand())).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN builder with already built subcommand with options THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getNamedBuilder().addSubcommand(getSubcommand().addBooleanOption(getBooleanOption())),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
// @ts-expect-error: Checking if check works JS-side too
|
|
||||||
getNamedBuilder().addSubcommand(getSubcommand()).addInteger(getInteger()),
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => {
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addSubcommandGroup()).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addSubcommand()).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getBuilder().addSubcommand(getSubcommandGroup())).toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Subcommand group builder', () => {
|
|
||||||
test('GIVEN no valid subcommand THEN throw error', () => {
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getSubcommandGroup().addSubcommand()).toThrowError();
|
|
||||||
|
|
||||||
// @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error
|
|
||||||
expect(() => getSubcommandGroup().addSubcommand(getSubcommandGroup())).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN a valid subcommand THEN does not throw an error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getSubcommandGroup()
|
|
||||||
.addSubcommand((sub) => sub.setName('sub').setDescription('Testing 123'))
|
|
||||||
.toJSON(),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Subcommand builder', () => {
|
|
||||||
test('GIVEN a valid subcommand with options THEN does not throw error', () => {
|
|
||||||
expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Slash command localizations', () => {
|
|
||||||
const expectedSingleLocale = { 'en-US': 'foobar' };
|
|
||||||
const expectedMultipleLocales = {
|
|
||||||
...expectedSingleLocale,
|
|
||||||
bg: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
test('GIVEN valid name localizations THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError();
|
|
||||||
expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid name localizations THEN does throw error', () => {
|
|
||||||
// @ts-expect-error: Invalid localization
|
|
||||||
expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError();
|
|
||||||
// @ts-expect-error: Invalid localization
|
|
||||||
expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid name localizations THEN valid data is stored', () => {
|
|
||||||
expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale);
|
|
||||||
expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual(
|
|
||||||
expectedMultipleLocales,
|
|
||||||
);
|
|
||||||
expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull();
|
|
||||||
expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({
|
|
||||||
'en-US': null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid description localizations THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError();
|
|
||||||
expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid description localizations THEN does throw error', () => {
|
|
||||||
// @ts-expect-error: Invalid localization description
|
|
||||||
expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError();
|
|
||||||
// @ts-expect-error: Invalid localization description
|
|
||||||
expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid description localizations THEN valid data is stored', () => {
|
|
||||||
expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual(
|
|
||||||
expectedSingleLocale,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations,
|
|
||||||
).toEqual(expectedMultipleLocales);
|
|
||||||
expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull();
|
|
||||||
expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({
|
|
||||||
'en-US': null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('permissions', () => {
|
|
||||||
test('GIVEN valid permission string THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid permission bitfield THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN null permissions THEN does not throw error', () => {
|
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid inputs THEN does throw error', () => {
|
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError();
|
|
||||||
|
|
||||||
expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid permission with options THEN does not throw error', () => {
|
|
||||||
expect(() =>
|
|
||||||
getBuilder().addBooleanOption(getBooleanOption()).setDefaultMemberPermissions('1'),
|
|
||||||
).not.toThrowError();
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,71 +1,21 @@
|
|||||||
import {
|
import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
|
||||||
ComponentType,
|
|
||||||
TextInputStyle,
|
|
||||||
type APIModalInteractionResponseCallbackData,
|
|
||||||
type APITextInputComponent,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
import {
|
import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js';
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonBuilder,
|
|
||||||
ModalBuilder,
|
|
||||||
TextInputBuilder,
|
|
||||||
type ModalActionRowComponentBuilder,
|
|
||||||
} from '../../src/index.js';
|
|
||||||
import {
|
|
||||||
componentsValidator,
|
|
||||||
titleValidator,
|
|
||||||
validateRequiredParameters,
|
|
||||||
} from '../../src/interactions/modals/Assertions.js';
|
|
||||||
|
|
||||||
const modal = () => new ModalBuilder();
|
const modal = () => new ModalBuilder();
|
||||||
|
const textInput = () =>
|
||||||
|
new ActionRowBuilder().addTextInputComponent(
|
||||||
|
new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short),
|
||||||
|
);
|
||||||
|
|
||||||
describe('Modals', () => {
|
describe('Modals', () => {
|
||||||
describe('Assertion Tests', () => {
|
|
||||||
test('GIVEN valid title THEN validator does not throw', () => {
|
|
||||||
expect(() => titleValidator.parse('foobar')).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid title THEN validator does throw', () => {
|
|
||||||
expect(() => titleValidator.parse(42)).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid components THEN validator does not throw', () => {
|
|
||||||
expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid components THEN validator does throw', () => {
|
|
||||||
expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid required parameters THEN validator does not throw', () => {
|
|
||||||
expect(() =>
|
|
||||||
validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN invalid required parameters THEN validator does throw', () => {
|
|
||||||
expect(() =>
|
|
||||||
// @ts-expect-error: Missing required parameter
|
|
||||||
validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]),
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN valid fields THEN builder does not throw', () => {
|
test('GIVEN valid fields THEN builder does not throw', () => {
|
||||||
expect(() =>
|
expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError();
|
||||||
modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()),
|
expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError();
|
||||||
).not.toThrowError();
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
// @ts-expect-error: You can pass a TextInputBuilder and it will add it to an action row
|
|
||||||
modal().setTitle('test').setCustomId('foobar').addComponents(new TextInputBuilder()),
|
|
||||||
).not.toThrowError();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN invalid fields THEN builder does throw', () => {
|
test('GIVEN invalid fields THEN builder does throw', () => {
|
||||||
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
|
expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError();
|
||||||
|
|
||||||
// @ts-expect-error: CustomId is invalid
|
// @ts-expect-error: CustomId is invalid
|
||||||
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
|
expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
@@ -106,68 +56,17 @@ describe('Modals', () => {
|
|||||||
modal()
|
modal()
|
||||||
.setTitle(modalData.title)
|
.setTitle(modalData.title)
|
||||||
.setCustomId('custom id')
|
.setCustomId('custom id')
|
||||||
.setComponents(
|
.setActionRows(
|
||||||
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
new ActionRowBuilder().addTextInputComponent(
|
||||||
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.addComponents([
|
.addActionRows([
|
||||||
new ActionRowBuilder<ModalActionRowComponentBuilder>().addComponents(
|
new ActionRowBuilder().addTextInputComponent(
|
||||||
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.toJSON(),
|
.toJSON(),
|
||||||
).toEqual(modalData);
|
).toEqual(modalData);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('equals()', () => {
|
|
||||||
const textInput1 = new TextInputBuilder()
|
|
||||||
.setCustomId('custom id')
|
|
||||||
.setLabel('label')
|
|
||||||
.setStyle(TextInputStyle.Paragraph);
|
|
||||||
|
|
||||||
const textInput2: APITextInputComponent = {
|
|
||||||
type: ComponentType.TextInput,
|
|
||||||
custom_id: 'custom id',
|
|
||||||
label: 'label',
|
|
||||||
style: TextInputStyle.Paragraph,
|
|
||||||
};
|
|
||||||
|
|
||||||
test('GIVEN equal builders THEN returns true', () => {
|
|
||||||
const equalTextInput = new TextInputBuilder()
|
|
||||||
.setCustomId('custom id')
|
|
||||||
.setLabel('label')
|
|
||||||
.setStyle(TextInputStyle.Paragraph);
|
|
||||||
|
|
||||||
expect(textInput1.equals(equalTextInput)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN the same builder THEN returns true', () => {
|
|
||||||
expect(textInput1.equals(textInput1)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN equal builder and data THEN returns true', () => {
|
|
||||||
expect(textInput1.equals(textInput2)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN different builders THEN returns false', () => {
|
|
||||||
const diffTextInput = new TextInputBuilder()
|
|
||||||
.setCustomId('custom id')
|
|
||||||
.setLabel('label 2')
|
|
||||||
.setStyle(TextInputStyle.Paragraph);
|
|
||||||
|
|
||||||
expect(textInput1.equals(diffTextInput)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GIVEN different text input builder and data THEN returns false', () => {
|
|
||||||
const diffTextInputData: APITextInputComponent = {
|
|
||||||
type: ComponentType.TextInput,
|
|
||||||
custom_id: 'custom id',
|
|
||||||
label: 'label 2',
|
|
||||||
style: TextInputStyle.Short,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(textInput1.equals(diffTextInputData)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { EmbedBuilder, embedLength } from '../../src/index.js';
|
|||||||
|
|
||||||
const alpha = 'abcdefghijklmnopqrstuvwxyz';
|
const alpha = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
const dummy = {
|
||||||
|
title: 'ooooo aaaaa uuuuuu aaaa',
|
||||||
|
};
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
author: undefined,
|
||||||
|
fields: [],
|
||||||
|
footer: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
describe('Embed', () => {
|
describe('Embed', () => {
|
||||||
describe('Embed getters', () => {
|
describe('Embed getters', () => {
|
||||||
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
|
test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => {
|
||||||
@@ -14,127 +24,136 @@ describe('Embed', () => {
|
|||||||
footer: { text: alpha },
|
footer: { text: alpha },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(embedLength(embed.data)).toEqual(alpha.length * 6);
|
expect(embedLength(embed.toJSON())).toEqual(alpha.length * 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
|
test('GIVEN an embed with zero characters THEN returns amount of characters', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(embedLength(embed.data)).toEqual(0);
|
expect(embedLength(embed.toJSON(false))).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Embed title', () => {
|
describe('Embed title', () => {
|
||||||
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ title: 'foo' });
|
const embed = new EmbedBuilder({ title: 'foo' });
|
||||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.setTitle('foo');
|
embed.setTitle('foo');
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ title: 'foo' });
|
expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ title: 'foo' });
|
const embed = new EmbedBuilder({ title: 'foo', description: ':3' });
|
||||||
embed.setTitle(null);
|
embed.clearTitle();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ title: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, description: ':3', title: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with an invalid title THEN throws error', () => {
|
test('GIVEN an embed with an invalid title THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setTitle('a'.repeat(257))).toThrowError();
|
embed.setTitle('a'.repeat(257));
|
||||||
|
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Embed description', () => {
|
describe('Embed description', () => {
|
||||||
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ description: 'foo' });
|
const embed = new EmbedBuilder({ description: 'foo' });
|
||||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.setDescription('foo');
|
embed.setDescription('foo');
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ description: 'foo' });
|
expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ description: 'foo' });
|
const embed = new EmbedBuilder({ description: 'foo', ...dummy });
|
||||||
embed.setDescription(null);
|
embed.clearDescription();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ description: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, description: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with an invalid description THEN throws error', () => {
|
test('GIVEN an embed with an invalid description THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setDescription('a'.repeat(4_097))).toThrowError();
|
embed.setDescription('a'.repeat(4_097));
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Embed URL', () => {
|
describe('Embed URL', () => {
|
||||||
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org/' });
|
const embed = new EmbedBuilder({ url: 'https://discord.js.org/', ...dummy });
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
|
...dummy,
|
||||||
url: 'https://discord.js.org/',
|
url: 'https://discord.js.org/',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder(dummy);
|
||||||
embed.setURL('https://discord.js.org/');
|
embed.setURL('https://discord.js.org/');
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
|
...dummy,
|
||||||
url: 'https://discord.js.org/',
|
url: 'https://discord.js.org/',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ url: 'https://discord.js.org' });
|
const embed = new EmbedBuilder({ url: 'https://discord.js.org', ...dummy });
|
||||||
embed.setURL(null);
|
embed.clearURL();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ url: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, url: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
|
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setURL(input)).toThrowError();
|
embed.setURL(input);
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Embed Color', () => {
|
describe('Embed Color', () => {
|
||||||
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ color: 0xff0000 });
|
const embed = new EmbedBuilder({ color: 0xff0000, ...dummy });
|
||||||
expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => {
|
||||||
expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 });
|
expect(new EmbedBuilder(dummy).setColor(0xff0000).toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 });
|
||||||
expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ color: 0xff0000 });
|
const embed = new EmbedBuilder({ ...dummy, color: 0xff0000 });
|
||||||
embed.setColor(null);
|
embed.clearColor();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ color: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with an invalid color THEN throws error', () => {
|
test('GIVEN an embed with an invalid color THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
// @ts-expect-error: Invalid color
|
// @ts-expect-error: Invalid color
|
||||||
expect(() => embed.setColor('RED')).toThrowError();
|
embed.setColor('RED');
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
|
|
||||||
// @ts-expect-error: Invalid color
|
// @ts-expect-error: Invalid color
|
||||||
expect(() => embed.setColor([42, 36])).toThrowError();
|
embed.setColor([42, 36]);
|
||||||
expect(() => embed.setColor([42, 36, 1_000])).toThrowError();
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,98 +161,92 @@ describe('Embed', () => {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
|
const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy });
|
||||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder(dummy);
|
||||||
embed.setTimestamp(now);
|
embed.setTimestamp(now);
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder(dummy);
|
||||||
embed.setTimestamp(now.getTime());
|
embed.setTimestamp(now.getTime());
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder(dummy);
|
||||||
embed.setTimestamp();
|
embed.setTimestamp();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: embed.toJSON().timestamp });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ timestamp: now.toISOString() });
|
const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy });
|
||||||
embed.setTimestamp(null);
|
embed.clearTimestamp();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ timestamp: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: undefined });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Embed Thumbnail', () => {
|
describe('Embed Thumbnail', () => {
|
||||||
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.setThumbnail('https://discord.js.org/static/logo.svg');
|
embed.setThumbnail('https://discord.js.org/static/logo.svg');
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||||
thumbnail: { url: 'https://discord.js.org/static/logo.svg' },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } });
|
const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, ...dummy });
|
||||||
embed.setThumbnail(null);
|
embed.clearThumbnail();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, thumbnail: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
|
test('GIVEN an embed with an invalid thumbnail THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setThumbnail('owo')).toThrowError();
|
embed.setThumbnail('owo');
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Embed Image', () => {
|
describe('Embed Image', () => {
|
||||||
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.setImage('https://discord.js.org/static/logo.svg');
|
embed.setImage('https://discord.js.org/static/logo.svg');
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } });
|
||||||
image: { url: 'https://discord.js.org/static/logo.svg' },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } });
|
const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' }, ...dummy });
|
||||||
embed.setImage(null);
|
embed.clearImage();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ image: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, image: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with an invalid image THEN throws error', () => {
|
test('GIVEN an embed with an invalid image THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setImage('owo')).toThrowError();
|
embed.setImage('owo');
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -243,19 +256,19 @@ describe('Embed', () => {
|
|||||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||||
});
|
});
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.setAuthor({
|
embed.setAuthor((author) =>
|
||||||
name: 'Wumpus',
|
author.setName('Wumpus').setIconURL('https://discord.js.org/static/logo.svg').setURL('https://discord.js.org'),
|
||||||
iconURL: 'https://discord.js.org/static/logo.svg',
|
);
|
||||||
url: 'https://discord.js.org',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -263,16 +276,18 @@ describe('Embed', () => {
|
|||||||
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({
|
const embed = new EmbedBuilder({
|
||||||
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' },
|
||||||
|
...dummy,
|
||||||
});
|
});
|
||||||
embed.setAuthor(null);
|
embed.clearAuthor();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ author: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, author: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with an invalid author name THEN throws error', () => {
|
test('GIVEN an embed with an invalid author name THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError();
|
embed.setAuthor({ name: 'a'.repeat(257) });
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,32 +297,36 @@ describe('Embed', () => {
|
|||||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||||
});
|
});
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' });
|
embed.setFooter({ text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' });
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
|
test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder({
|
const embed = new EmbedBuilder({
|
||||||
|
...dummy,
|
||||||
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' },
|
||||||
});
|
});
|
||||||
embed.setFooter(null);
|
embed.clearFooter();
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({ footer: undefined });
|
expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, footer: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed with invalid footer text THEN throws error', () => {
|
test('GIVEN an embed with invalid footer text THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.setFooter({ text: 'a'.repeat(2_049) })).toThrowError();
|
embed.setFooter({ text: 'a'.repeat(2_049) });
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,9 +335,7 @@ describe('Embed', () => {
|
|||||||
const embed = new EmbedBuilder({
|
const embed = new EmbedBuilder({
|
||||||
fields: [{ name: 'foo', value: 'bar' }],
|
fields: [{ name: 'foo', value: 'bar' }],
|
||||||
});
|
});
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'bar' }] });
|
||||||
fields: [{ name: 'foo', value: 'bar' }],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => {
|
||||||
@@ -327,6 +344,7 @@ describe('Embed', () => {
|
|||||||
embed.addFields([{ name: 'foo', value: 'bar' }]);
|
embed.addFields([{ name: 'foo', value: 'bar' }]);
|
||||||
|
|
||||||
expect(embed.toJSON()).toStrictEqual({
|
expect(embed.toJSON()).toStrictEqual({
|
||||||
|
...base,
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'foo', value: 'bar' },
|
{ name: 'foo', value: 'bar' },
|
||||||
{ name: 'foo', value: 'bar' },
|
{ name: 'foo', value: 'bar' },
|
||||||
@@ -338,56 +356,51 @@ describe('Embed', () => {
|
|||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
|
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });
|
||||||
|
|
||||||
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
|
expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'baz' }] });
|
||||||
fields: [{ name: 'foo', value: 'baz' }],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => {
|
test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
|
|
||||||
expect(() =>
|
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))),
|
expect(() => embed.toJSON()).not.toThrowError();
|
||||||
).not.toThrowError();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
|
test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
|
|
||||||
expect(() =>
|
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))),
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
).toThrowError();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
|
test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() =>
|
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
|
expect(() => embed.toJSON()).not.toThrowError();
|
||||||
).not.toThrowError();
|
|
||||||
expect(() =>
|
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))),
|
expect(() => embed.toJSON()).not.toThrowError();
|
||||||
).not.toThrowError();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
|
test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() =>
|
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
).toThrowError();
|
|
||||||
expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError();
|
embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GIVEN invalid field amount THEN throws error', () => {
|
describe('GIVEN invalid field amount THEN throws error', () => {
|
||||||
test('1', () => {
|
test('1', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() =>
|
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })));
|
||||||
embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))),
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
).toThrowError();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -395,7 +408,8 @@ describe('Embed', () => {
|
|||||||
test('2', () => {
|
test('2', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError();
|
embed.addFields({ name: '', value: 'bar' });
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -403,7 +417,8 @@ describe('Embed', () => {
|
|||||||
test('3', () => {
|
test('3', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError();
|
embed.addFields({ name: 'a'.repeat(257), value: 'bar' });
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -411,7 +426,8 @@ describe('Embed', () => {
|
|||||||
test('4', () => {
|
test('4', () => {
|
||||||
const embed = new EmbedBuilder();
|
const embed = new EmbedBuilder();
|
||||||
|
|
||||||
expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError();
|
embed.addFields({ name: '', value: 'a'.repeat(1_025) });
|
||||||
|
expect(() => embed.toJSON()).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { expectTypeOf } from 'vitest';
|
import { expectTypeOf } from 'vitest';
|
||||||
import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandSubcommandBuilder } from '../src/index.js';
|
import {
|
||||||
|
ChatInputCommandBuilder,
|
||||||
|
ChatInputCommandStringOption,
|
||||||
|
ChatInputCommandSubcommandBuilder,
|
||||||
|
} from '../src/index.js';
|
||||||
|
|
||||||
const getBuilder = () => new SlashCommandBuilder();
|
const getBuilder = () => new ChatInputCommandBuilder();
|
||||||
const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123');
|
const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123');
|
||||||
const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
|
const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123');
|
||||||
|
|
||||||
type BuilderPropsOnly<Type = SlashCommandBuilder> = Pick<
|
type BuilderPropsOnly<Type = ChatInputCommandBuilder> = Pick<
|
||||||
Type,
|
Type,
|
||||||
keyof {
|
keyof {
|
||||||
[Key in keyof Type as Type[Key] extends (...args: any) => any ? never : Key]: any;
|
[Key in keyof Type as Type[Key] extends (...args: any) => any ? never : Key]: any;
|
||||||
|
|||||||
@@ -65,19 +65,17 @@
|
|||||||
"homepage": "https://discord.js.org",
|
"homepage": "https://discord.js.org",
|
||||||
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
"funding": "https://github.com/discordjs/discord.js?sponsor",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/formatters": "workspace:^",
|
|
||||||
"@discordjs/util": "workspace:^",
|
"@discordjs/util": "workspace:^",
|
||||||
"@sapphire/shapeshift": "^4.0.0",
|
|
||||||
"discord-api-types": "^0.37.101",
|
"discord-api-types": "^0.37.101",
|
||||||
"fast-deep-equal": "^3.1.3",
|
|
||||||
"ts-mixer": "^6.0.4",
|
"ts-mixer": "^6.0.4",
|
||||||
"tslib": "^2.6.3"
|
"tslib": "^2.6.3",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@discordjs/api-extractor": "workspace:^",
|
"@discordjs/api-extractor": "workspace:^",
|
||||||
"@discordjs/scripts": "workspace:^",
|
"@discordjs/scripts": "workspace:^",
|
||||||
"@favware/cliff-jumper": "^4.1.0",
|
"@favware/cliff-jumper": "^4.1.0",
|
||||||
"@types/node": "^16.18.105",
|
"@types/node": "^18.19.44",
|
||||||
"@vitest/coverage-v8": "^2.0.5",
|
"@vitest/coverage-v8": "^2.0.5",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild-plugin-version-injector": "^1.2.1",
|
"esbuild-plugin-version-injector": "^1.2.1",
|
||||||
|
|||||||
20
packages/builders/src/Assertions.ts
Normal file
20
packages/builders/src/Assertions.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Locale } from 'discord-api-types/v10';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const customIdPredicate = z.string().min(1).max(100);
|
||||||
|
|
||||||
|
export const memberPermissionsPredicate = z.coerce.bigint();
|
||||||
|
|
||||||
|
export const localeMapPredicate = z
|
||||||
|
.object(
|
||||||
|
Object.fromEntries(Object.values(Locale).map((loc) => [loc, z.string().optional()])) as Record<
|
||||||
|
Locale,
|
||||||
|
z.ZodOptional<z.ZodString>
|
||||||
|
>,
|
||||||
|
)
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const refineURLPredicate = (allowedProtocols: string[]) => (value: string) => {
|
||||||
|
const url = new URL(value);
|
||||||
|
return allowedProtocols.includes(url.protocol);
|
||||||
|
};
|
||||||
@@ -1,68 +1,60 @@
|
|||||||
/* eslint-disable jsdoc/check-param-names */
|
/* eslint-disable jsdoc/check-param-names */
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
type APIActionRowComponent,
|
APITextInputComponent,
|
||||||
ComponentType,
|
APIActionRowComponent,
|
||||||
type APIMessageActionRowComponent,
|
APIActionRowComponentTypes,
|
||||||
type APIModalActionRowComponent,
|
APIChannelSelectComponent,
|
||||||
type APIActionRowComponentTypes,
|
APIMentionableSelectComponent,
|
||||||
|
APIRoleSelectComponent,
|
||||||
|
APIStringSelectComponent,
|
||||||
|
APIUserSelectComponent,
|
||||||
|
APIButtonComponentWithCustomId,
|
||||||
|
APIButtonComponentWithSKUId,
|
||||||
|
APIButtonComponentWithURL,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
|
||||||
|
import { resolveBuilder } from '../util/resolveBuilder.js';
|
||||||
|
import { isValidationEnabled } from '../util/validation.js';
|
||||||
|
import { actionRowPredicate } from './Assertions.js';
|
||||||
import { ComponentBuilder } from './Component.js';
|
import { ComponentBuilder } from './Component.js';
|
||||||
|
import type { AnyActionRowComponentBuilder } from './Components.js';
|
||||||
import { createComponentBuilder } from './Components.js';
|
import { createComponentBuilder } from './Components.js';
|
||||||
import type { ButtonBuilder } from './button/Button.js';
|
import {
|
||||||
import type { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
DangerButtonBuilder,
|
||||||
import type { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
PrimaryButtonBuilder,
|
||||||
import type { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
SecondaryButtonBuilder,
|
||||||
import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
SuccessButtonBuilder,
|
||||||
import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
} from './button/CustomIdButton.js';
|
||||||
import type { TextInputBuilder } from './textInput/TextInput.js';
|
import { LinkButtonBuilder } from './button/LinkButton.js';
|
||||||
|
import { PremiumButtonBuilder } from './button/PremiumButton.js';
|
||||||
|
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||||
|
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||||
|
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||||
|
import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
||||||
|
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||||
|
import { TextInputBuilder } from './textInput/TextInput.js';
|
||||||
|
|
||||||
/**
|
export interface ActionRowBuilderData
|
||||||
* The builders that may be used for messages.
|
extends Partial<Omit<APIActionRowComponent<APIActionRowComponentTypes>, 'components'>> {
|
||||||
*/
|
components: AnyActionRowComponentBuilder[];
|
||||||
export type MessageComponentBuilder =
|
}
|
||||||
| ActionRowBuilder<MessageActionRowComponentBuilder>
|
|
||||||
| MessageActionRowComponentBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The builders that may be used for modals.
|
|
||||||
*/
|
|
||||||
export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The builders that may be used within an action row for messages.
|
|
||||||
*/
|
|
||||||
export type MessageActionRowComponentBuilder =
|
|
||||||
| ButtonBuilder
|
|
||||||
| ChannelSelectMenuBuilder
|
|
||||||
| MentionableSelectMenuBuilder
|
|
||||||
| RoleSelectMenuBuilder
|
|
||||||
| StringSelectMenuBuilder
|
|
||||||
| UserSelectMenuBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The builders that may be used within an action row for modals.
|
|
||||||
*/
|
|
||||||
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Any builder.
|
|
||||||
*/
|
|
||||||
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for action rows.
|
* A builder that creates API-compatible JSON data for action rows.
|
||||||
*
|
*
|
||||||
* @typeParam ComponentType - The types of components this action row holds
|
* @typeParam ComponentType - The types of components this action row holds
|
||||||
*/
|
*/
|
||||||
export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends ComponentBuilder<
|
export class ActionRowBuilder extends ComponentBuilder<APIActionRowComponent<APIActionRowComponentTypes>> {
|
||||||
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
|
private readonly data: ActionRowBuilderData;
|
||||||
> {
|
|
||||||
/**
|
/**
|
||||||
* The components within this action row.
|
* The components within this action row.
|
||||||
*/
|
*/
|
||||||
public readonly components: ComponentType[];
|
public get components(): readonly AnyActionRowComponentBuilder[] {
|
||||||
|
return this.data.components;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new action row from API data.
|
* Creates a new action row from API data.
|
||||||
@@ -98,38 +90,256 @@ export class ActionRowBuilder<ComponentType extends AnyComponentBuilder> extends
|
|||||||
* .addComponents(button2, button3);
|
* .addComponents(button2, button3);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor({ components, ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
public constructor({ components = [], ...data }: Partial<APIActionRowComponent<APIActionRowComponentTypes>> = {}) {
|
||||||
super({ type: ComponentType.ActionRow, ...data });
|
super();
|
||||||
this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[];
|
this.data = {
|
||||||
|
...structuredClone(data),
|
||||||
|
type: ComponentType.ActionRow,
|
||||||
|
components: components.map((component) => createComponentBuilder(component)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds components to this action row.
|
* Adds primary button components to this action row.
|
||||||
*
|
*
|
||||||
* @param components - The components to add
|
* @param input - The buttons to add
|
||||||
*/
|
*/
|
||||||
public addComponents(...components: RestOrArray<ComponentType>) {
|
public addPrimaryButtonComponents(
|
||||||
this.components.push(...normalizeArray(components));
|
...input: RestOrArray<
|
||||||
|
APIButtonComponentWithCustomId | PrimaryButtonBuilder | ((builder: PrimaryButtonBuilder) => PrimaryButtonBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((component) => resolveBuilder(component, PrimaryButtonBuilder));
|
||||||
|
|
||||||
|
this.data.components.push(...resolved);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets components for this action row.
|
* Adds secondary button components to this action row.
|
||||||
*
|
*
|
||||||
* @param components - The components to set
|
* @param input - The buttons to add
|
||||||
*/
|
*/
|
||||||
public setComponents(...components: RestOrArray<ComponentType>) {
|
public addSecondaryButtonComponents(
|
||||||
this.components.splice(0, this.components.length, ...normalizeArray(components));
|
...input: RestOrArray<
|
||||||
|
| APIButtonComponentWithCustomId
|
||||||
|
| SecondaryButtonBuilder
|
||||||
|
| ((builder: SecondaryButtonBuilder) => SecondaryButtonBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((component) => resolveBuilder(component, SecondaryButtonBuilder));
|
||||||
|
|
||||||
|
this.data.components.push(...resolved);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds success button components to this action row.
|
||||||
|
*
|
||||||
|
* @param input - The buttons to add
|
||||||
|
*/
|
||||||
|
public addSuccessButtonComponents(
|
||||||
|
...input: RestOrArray<
|
||||||
|
APIButtonComponentWithCustomId | SuccessButtonBuilder | ((builder: SuccessButtonBuilder) => SuccessButtonBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((component) => resolveBuilder(component, SuccessButtonBuilder));
|
||||||
|
|
||||||
|
this.data.components.push(...resolved);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds danger button components to this action row.
|
||||||
|
*/
|
||||||
|
public addDangerButtonComponents(
|
||||||
|
...input: RestOrArray<
|
||||||
|
APIButtonComponentWithCustomId | DangerButtonBuilder | ((builder: DangerButtonBuilder) => DangerButtonBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((component) => resolveBuilder(component, DangerButtonBuilder));
|
||||||
|
|
||||||
|
this.data.components.push(...resolved);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generically add any type of component to this action row, only takes in an instance of a component builder.
|
||||||
|
*/
|
||||||
|
public addComponents(...input: RestOrArray<AnyActionRowComponentBuilder>): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
this.data.components.push(...normalized);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds SKU id button components to this action row.
|
||||||
|
*
|
||||||
|
* @param input - The buttons to add
|
||||||
|
*/
|
||||||
|
public addPremiumButtonComponents(
|
||||||
|
...input: RestOrArray<
|
||||||
|
APIButtonComponentWithSKUId | PremiumButtonBuilder | ((builder: PremiumButtonBuilder) => PremiumButtonBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((component) => resolveBuilder(component, PremiumButtonBuilder));
|
||||||
|
|
||||||
|
this.data.components.push(...resolved);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds URL button components to this action row.
|
||||||
|
*
|
||||||
|
* @param input - The buttons to add
|
||||||
|
*/
|
||||||
|
public addLinkButtonComponents(
|
||||||
|
...input: RestOrArray<
|
||||||
|
APIButtonComponentWithURL | LinkButtonBuilder | ((builder: LinkButtonBuilder) => LinkButtonBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((component) => resolveBuilder(component, LinkButtonBuilder));
|
||||||
|
|
||||||
|
this.data.components.push(...resolved);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a channel select menu component to this action row.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a component builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addChannelSelectMenuComponent(
|
||||||
|
input:
|
||||||
|
| APIChannelSelectComponent
|
||||||
|
| ChannelSelectMenuBuilder
|
||||||
|
| ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.components.push(resolveBuilder(input, ChannelSelectMenuBuilder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a mentionable select menu component to this action row.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a component builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addMentionableSelectMenuComponent(
|
||||||
|
input:
|
||||||
|
| APIMentionableSelectComponent
|
||||||
|
| MentionableSelectMenuBuilder
|
||||||
|
| ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.components.push(resolveBuilder(input, MentionableSelectMenuBuilder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a role select menu component to this action row.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a component builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addRoleSelectMenuComponent(
|
||||||
|
input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.components.push(resolveBuilder(input, RoleSelectMenuBuilder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a string select menu component to this action row.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a component builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addStringSelectMenuComponent(
|
||||||
|
input:
|
||||||
|
| APIStringSelectComponent
|
||||||
|
| StringSelectMenuBuilder
|
||||||
|
| ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.components.push(resolveBuilder(input, StringSelectMenuBuilder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a user select menu component to this action row.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a component builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addUserSelectMenuComponent(
|
||||||
|
input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.components.push(resolveBuilder(input, UserSelectMenuBuilder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a text input component to this action row.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a component builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addTextInputComponent(
|
||||||
|
input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.components.push(resolveBuilder(input, TextInputBuilder));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes, replaces, or inserts components for this action row.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This method behaves similarly
|
||||||
|
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||||
|
*
|
||||||
|
* It's useful for modifying and adjusting order of the already-existing components of an action row.
|
||||||
|
* @example
|
||||||
|
* Remove the first component:
|
||||||
|
* ```ts
|
||||||
|
* actionRow.spliceComponents(0, 1);
|
||||||
|
* ```
|
||||||
|
* @example
|
||||||
|
* Remove the first n components:
|
||||||
|
* ```ts
|
||||||
|
* const n = 4;
|
||||||
|
* actionRow.spliceComponents(0, n);
|
||||||
|
* ```
|
||||||
|
* @example
|
||||||
|
* Remove the last component:
|
||||||
|
* ```ts
|
||||||
|
* actionRow.spliceComponents(-1, 1);
|
||||||
|
* ```
|
||||||
|
* @param index - The index to start at
|
||||||
|
* @param deleteCount - The number of components to remove
|
||||||
|
* @param components - The replacing component objects
|
||||||
|
*/
|
||||||
|
public spliceComponents(index: number, deleteCount: number, ...components: AnyActionRowComponentBuilder[]): this {
|
||||||
|
this.data.components.splice(index, deleteCount, ...components);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc ComponentBuilder.toJSON}
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
*/
|
*/
|
||||||
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>> {
|
public override toJSON(validationOverride?: boolean): APIActionRowComponent<APIActionRowComponentTypes> {
|
||||||
return {
|
const { components, ...rest } = this.data;
|
||||||
...this.data,
|
|
||||||
components: this.components.map((component) => component.toJSON()),
|
const data = {
|
||||||
} as APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
|
...structuredClone(rest),
|
||||||
|
components: components.map((component) => component.toJSON(validationOverride)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
actionRowPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as APIActionRowComponent<APIActionRowComponentTypes>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,127 +1,168 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||||
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
|
import { z } from 'zod';
|
||||||
import { isValidationEnabled } from '../util/validation.js';
|
import { customIdPredicate, refineURLPredicate } from '../Assertions.js';
|
||||||
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
|
|
||||||
|
|
||||||
export const customIdValidator = s
|
const labelPredicate = z.string().min(1).max(80);
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(100)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const emojiValidator = s
|
export const emojiPredicate = z
|
||||||
.object({
|
.object({
|
||||||
id: s.string(),
|
id: z.string().optional(),
|
||||||
name: s.string(),
|
name: z.string().min(2).max(32).optional(),
|
||||||
animated: s.boolean(),
|
animated: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.partial()
|
|
||||||
.strict()
|
.strict()
|
||||||
.setValidationEnabled(isValidationEnabled);
|
.refine((data) => data.id !== undefined || data.name !== undefined, {
|
||||||
|
message: "Either 'id' or 'name' must be provided",
|
||||||
|
});
|
||||||
|
|
||||||
export const disabledValidator = s.boolean();
|
const buttonPredicateBase = z.object({
|
||||||
|
type: z.literal(ComponentType.Button),
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const buttonLabelValidator = s
|
const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
|
||||||
.string()
|
custom_id: customIdPredicate,
|
||||||
.lengthGreaterThanOrEqual(1)
|
emoji: emojiPredicate.optional(),
|
||||||
.lengthLessThanOrEqual(80)
|
label: labelPredicate,
|
||||||
.setValidationEnabled(isValidationEnabled);
|
});
|
||||||
|
|
||||||
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
|
const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }).strict();
|
||||||
|
const buttonSecondaryPredicate = buttonCustomIdPredicateBase
|
||||||
|
.extend({ style: z.literal(ButtonStyle.Secondary) })
|
||||||
|
.strict();
|
||||||
|
const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }).strict();
|
||||||
|
const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }).strict();
|
||||||
|
|
||||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled);
|
const buttonLinkPredicate = buttonPredicateBase
|
||||||
export const minMaxValidator = s
|
.extend({
|
||||||
.number()
|
style: z.literal(ButtonStyle.Link),
|
||||||
.int()
|
url: z
|
||||||
.greaterThanOrEqual(0)
|
.string()
|
||||||
.lessThanOrEqual(25)
|
.url()
|
||||||
.setValidationEnabled(isValidationEnabled);
|
.refine(refineURLPredicate(['http:', 'https:', 'discord:'])),
|
||||||
|
emoji: emojiPredicate.optional(),
|
||||||
export const labelValueDescriptionValidator = s
|
label: labelPredicate,
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(100)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const jsonOptionValidator = s
|
|
||||||
.object({
|
|
||||||
label: labelValueDescriptionValidator,
|
|
||||||
value: labelValueDescriptionValidator,
|
|
||||||
description: labelValueDescriptionValidator.optional(),
|
|
||||||
emoji: emojiValidator.optional(),
|
|
||||||
default: s.boolean().optional(),
|
|
||||||
})
|
})
|
||||||
.setValidationEnabled(isValidationEnabled);
|
.strict();
|
||||||
|
|
||||||
export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
|
const buttonPremiumPredicate = buttonPredicateBase
|
||||||
|
.extend({
|
||||||
export const optionsValidator = optionValidator
|
style: z.literal(ButtonStyle.Premium),
|
||||||
.array()
|
sku_id: z.string(),
|
||||||
.lengthGreaterThanOrEqual(0)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
export const optionsLengthValidator = s
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.greaterThanOrEqual(0)
|
|
||||||
.lessThanOrEqual(25)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
|
|
||||||
customIdValidator.parse(customId);
|
|
||||||
optionsValidator.parse(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultValidator = s.boolean();
|
|
||||||
|
|
||||||
export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
|
|
||||||
labelValueDescriptionValidator.parse(label);
|
|
||||||
labelValueDescriptionValidator.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const channelTypesValidator = s.nativeEnum(ChannelType).array().setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const urlValidator = s
|
|
||||||
.string()
|
|
||||||
.url({
|
|
||||||
allowedProtocols: ['http:', 'https:', 'discord:'],
|
|
||||||
})
|
})
|
||||||
.setValidationEnabled(isValidationEnabled);
|
.strict();
|
||||||
|
|
||||||
export function validateRequiredButtonParameters(
|
export const buttonPredicate = z.discriminatedUnion('style', [
|
||||||
style?: ButtonStyle,
|
buttonLinkPredicate,
|
||||||
label?: string,
|
buttonPrimaryPredicate,
|
||||||
emoji?: APIMessageComponentEmoji,
|
buttonSecondaryPredicate,
|
||||||
customId?: string,
|
buttonSuccessPredicate,
|
||||||
skuId?: string,
|
buttonDangerPredicate,
|
||||||
url?: string,
|
buttonPremiumPredicate,
|
||||||
) {
|
]);
|
||||||
if (style === ButtonStyle.Premium) {
|
|
||||||
if (!skuId) {
|
const selectMenuBasePredicate = z.object({
|
||||||
throw new RangeError('Premium buttons must have an SKU id.');
|
placeholder: z.string().max(150).optional(),
|
||||||
|
min_values: z.number().min(0).max(25).optional(),
|
||||||
|
max_values: z.number().min(0).max(25).optional(),
|
||||||
|
custom_id: customIdPredicate,
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
|
||||||
|
type: z.literal(ComponentType.ChannelSelect),
|
||||||
|
channel_types: z.nativeEnum(ChannelType).array().optional(),
|
||||||
|
default_values: z
|
||||||
|
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
|
||||||
|
.array()
|
||||||
|
.max(25)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
|
||||||
|
type: z.literal(ComponentType.MentionableSelect),
|
||||||
|
default_values: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.max(25)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
|
||||||
|
type: z.literal(ComponentType.RoleSelect),
|
||||||
|
default_values: z
|
||||||
|
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
|
||||||
|
.array()
|
||||||
|
.max(25)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectMenuStringOptionPredicate = z.object({
|
||||||
|
label: labelPredicate,
|
||||||
|
value: z.string().min(1).max(100),
|
||||||
|
description: z.string().min(1).max(100).optional(),
|
||||||
|
emoji: emojiPredicate.optional(),
|
||||||
|
default: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||||
|
.extend({
|
||||||
|
type: z.literal(ComponentType.StringSelect),
|
||||||
|
options: selectMenuStringOptionPredicate.array().min(1).max(25),
|
||||||
|
})
|
||||||
|
.superRefine((menu, ctx) => {
|
||||||
|
const addIssue = (name: string, minimum: number) =>
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'too_small',
|
||||||
|
message: `The number of options must be greater than or equal to ${name}`,
|
||||||
|
inclusive: true,
|
||||||
|
minimum,
|
||||||
|
type: 'number',
|
||||||
|
path: ['options'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (menu.max_values !== undefined && menu.options.length < menu.max_values) {
|
||||||
|
addIssue('max_values', menu.max_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customId || label || url || emoji) {
|
if (menu.min_values !== undefined && menu.options.length < menu.min_values) {
|
||||||
throw new RangeError('Premium buttons cannot have a custom id, label, URL, or emoji.');
|
addIssue('min_values', menu.min_values);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (skuId) {
|
|
||||||
throw new RangeError('Non-premium buttons must not have an SKU id.');
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (url && customId) {
|
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
|
||||||
throw new RangeError('URL and custom id are mutually exclusive.');
|
type: z.literal(ComponentType.UserSelect),
|
||||||
}
|
default_values: z
|
||||||
|
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
|
||||||
|
.array()
|
||||||
|
.max(25)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
if (!label && !emoji) {
|
export const actionRowPredicate = z.object({
|
||||||
throw new RangeError('Non-premium buttons must have a label and/or an emoji.');
|
type: z.literal(ComponentType.ActionRow),
|
||||||
}
|
components: z.union([
|
||||||
|
z
|
||||||
if (style === ButtonStyle.Link) {
|
.object({ type: z.literal(ComponentType.Button) })
|
||||||
if (!url) {
|
.array()
|
||||||
throw new RangeError('Link buttons must have a URL.');
|
.min(1)
|
||||||
}
|
.max(5),
|
||||||
} else if (url) {
|
z
|
||||||
throw new RangeError('Non-premium and non-link buttons cannot have a URL.');
|
.object({
|
||||||
}
|
type: z.union([
|
||||||
}
|
z.literal(ComponentType.ChannelSelect),
|
||||||
}
|
z.literal(ComponentType.MentionableSelect),
|
||||||
|
z.literal(ComponentType.RoleSelect),
|
||||||
|
z.literal(ComponentType.StringSelect),
|
||||||
|
z.literal(ComponentType.UserSelect),
|
||||||
|
// And this!
|
||||||
|
z.literal(ComponentType.TextInput),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.length(1),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import type { JSONEncodable } from '@discordjs/util';
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type {
|
import type { APIActionRowComponent, APIActionRowComponentTypes } from 'discord-api-types/v10';
|
||||||
APIActionRowComponent,
|
|
||||||
APIActionRowComponentTypes,
|
|
||||||
APIBaseComponent,
|
|
||||||
ComponentType,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any action row component data represented as an object.
|
* Any action row component data represented as an object.
|
||||||
@@ -14,32 +9,15 @@ export type AnyAPIActionRowComponent = APIActionRowComponent<APIActionRowCompone
|
|||||||
/**
|
/**
|
||||||
* The base component builder that contains common symbols for all sorts of components.
|
* The base component builder that contains common symbols for all sorts of components.
|
||||||
*
|
*
|
||||||
* @typeParam DataType - The type of internal API data that is stored within the component
|
* @typeParam Component - The type of API data that is stored within the builder
|
||||||
*/
|
*/
|
||||||
export abstract class ComponentBuilder<
|
export abstract class ComponentBuilder<Component extends AnyAPIActionRowComponent> implements JSONEncodable<Component> {
|
||||||
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
|
|
||||||
> implements JSONEncodable<AnyAPIActionRowComponent>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The API data associated with this component.
|
|
||||||
*/
|
|
||||||
public readonly data: Partial<DataType>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes this builder to API-compatible JSON data.
|
* Serializes this builder to API-compatible JSON data.
|
||||||
*
|
*
|
||||||
* @remarks
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
* This method runs validations on the data before serializing it.
|
|
||||||
* As such, it may throw an error if the data is invalid.
|
|
||||||
*/
|
|
||||||
public abstract toJSON(): AnyAPIActionRowComponent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new kind of component.
|
|
||||||
*
|
*
|
||||||
* @param data - The data to construct a component out of
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
*/
|
*/
|
||||||
public constructor(data: Partial<DataType>) {
|
public abstract toJSON(validationOverride?: boolean): Component;
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10';
|
import type { APIButtonComponent, APIMessageComponent, APIModalComponent } from 'discord-api-types/v10';
|
||||||
import {
|
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||||
ActionRowBuilder,
|
import { ActionRowBuilder } from './ActionRow.js';
|
||||||
type AnyComponentBuilder,
|
import type { AnyAPIActionRowComponent } from './Component.js';
|
||||||
type MessageComponentBuilder,
|
|
||||||
type ModalComponentBuilder,
|
|
||||||
} from './ActionRow.js';
|
|
||||||
import { ComponentBuilder } from './Component.js';
|
import { ComponentBuilder } from './Component.js';
|
||||||
import { ButtonBuilder } from './button/Button.js';
|
import type { BaseButtonBuilder } from './button/Button.js';
|
||||||
|
import {
|
||||||
|
DangerButtonBuilder,
|
||||||
|
PrimaryButtonBuilder,
|
||||||
|
SecondaryButtonBuilder,
|
||||||
|
SuccessButtonBuilder,
|
||||||
|
} from './button/CustomIdButton.js';
|
||||||
|
import { LinkButtonBuilder } from './button/LinkButton.js';
|
||||||
|
import { PremiumButtonBuilder } from './button/PremiumButton.js';
|
||||||
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js';
|
||||||
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js';
|
||||||
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js';
|
||||||
@@ -14,6 +19,48 @@ import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js';
|
|||||||
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js';
|
||||||
import { TextInputBuilder } from './textInput/TextInput.js';
|
import { TextInputBuilder } from './textInput/TextInput.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The builders that may be used for messages.
|
||||||
|
*/
|
||||||
|
export type MessageComponentBuilder = ActionRowBuilder | MessageActionRowComponentBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The builders that may be used for modals.
|
||||||
|
*/
|
||||||
|
export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any button builder
|
||||||
|
*/
|
||||||
|
export type ButtonBuilder =
|
||||||
|
| DangerButtonBuilder
|
||||||
|
| LinkButtonBuilder
|
||||||
|
| PremiumButtonBuilder
|
||||||
|
| PrimaryButtonBuilder
|
||||||
|
| SecondaryButtonBuilder
|
||||||
|
| SuccessButtonBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The builders that may be used within an action row for messages.
|
||||||
|
*/
|
||||||
|
export type MessageActionRowComponentBuilder =
|
||||||
|
| ButtonBuilder
|
||||||
|
| ChannelSelectMenuBuilder
|
||||||
|
| MentionableSelectMenuBuilder
|
||||||
|
| RoleSelectMenuBuilder
|
||||||
|
| StringSelectMenuBuilder
|
||||||
|
| UserSelectMenuBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The builders that may be used within an action row for modals.
|
||||||
|
*/
|
||||||
|
export type ModalActionRowComponentBuilder = TextInputBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any action row component builder.
|
||||||
|
*/
|
||||||
|
export type AnyActionRowComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Components here are mapped to their respective builder.
|
* Components here are mapped to their respective builder.
|
||||||
*/
|
*/
|
||||||
@@ -21,9 +68,9 @@ export interface MappedComponentTypes {
|
|||||||
/**
|
/**
|
||||||
* The action row component type is associated with an {@link ActionRowBuilder}.
|
* The action row component type is associated with an {@link ActionRowBuilder}.
|
||||||
*/
|
*/
|
||||||
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
|
[ComponentType.ActionRow]: ActionRowBuilder;
|
||||||
/**
|
/**
|
||||||
* The button component type is associated with a {@link ButtonBuilder}.
|
* The button component type is associated with a {@link BaseButtonBuilder}.
|
||||||
*/
|
*/
|
||||||
[ComponentType.Button]: ButtonBuilder;
|
[ComponentType.Button]: ButtonBuilder;
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +122,7 @@ export function createComponentBuilder<ComponentBuilder extends MessageComponent
|
|||||||
|
|
||||||
export function createComponentBuilder(
|
export function createComponentBuilder(
|
||||||
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
data: APIMessageComponent | APIModalComponent | MessageComponentBuilder,
|
||||||
): ComponentBuilder {
|
): ComponentBuilder<AnyAPIActionRowComponent> {
|
||||||
if (data instanceof ComponentBuilder) {
|
if (data instanceof ComponentBuilder) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -84,7 +131,7 @@ export function createComponentBuilder(
|
|||||||
case ComponentType.ActionRow:
|
case ComponentType.ActionRow:
|
||||||
return new ActionRowBuilder(data);
|
return new ActionRowBuilder(data);
|
||||||
case ComponentType.Button:
|
case ComponentType.Button:
|
||||||
return new ButtonBuilder(data);
|
return createButtonBuilder(data);
|
||||||
case ComponentType.StringSelect:
|
case ComponentType.StringSelect:
|
||||||
return new StringSelectMenuBuilder(data);
|
return new StringSelectMenuBuilder(data);
|
||||||
case ComponentType.TextInput:
|
case ComponentType.TextInput:
|
||||||
@@ -102,3 +149,23 @@ export function createComponentBuilder(
|
|||||||
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
throw new Error(`Cannot properly serialize component type: ${data.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createButtonBuilder(data: APIButtonComponent): ButtonBuilder {
|
||||||
|
switch (data.style) {
|
||||||
|
case ButtonStyle.Primary:
|
||||||
|
return new PrimaryButtonBuilder(data);
|
||||||
|
case ButtonStyle.Secondary:
|
||||||
|
return new SecondaryButtonBuilder(data);
|
||||||
|
case ButtonStyle.Success:
|
||||||
|
return new SuccessButtonBuilder(data);
|
||||||
|
case ButtonStyle.Danger:
|
||||||
|
return new DangerButtonBuilder(data);
|
||||||
|
case ButtonStyle.Link:
|
||||||
|
return new LinkButtonBuilder(data);
|
||||||
|
case ButtonStyle.Premium:
|
||||||
|
return new PremiumButtonBuilder(data);
|
||||||
|
default:
|
||||||
|
// @ts-expect-error This case can still occur if we get a newer unsupported button style
|
||||||
|
throw new Error(`Cannot properly serialize button with style: ${data.style}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,115 +1,13 @@
|
|||||||
import {
|
import type { APIButtonComponent } from 'discord-api-types/v10';
|
||||||
ComponentType,
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
type APIButtonComponent,
|
import { buttonPredicate } from '../Assertions.js';
|
||||||
type APIButtonComponentWithCustomId,
|
|
||||||
type APIButtonComponentWithSKUId,
|
|
||||||
type APIButtonComponentWithURL,
|
|
||||||
type APIMessageComponentEmoji,
|
|
||||||
type ButtonStyle,
|
|
||||||
type Snowflake,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
import {
|
|
||||||
buttonLabelValidator,
|
|
||||||
buttonStyleValidator,
|
|
||||||
customIdValidator,
|
|
||||||
disabledValidator,
|
|
||||||
emojiValidator,
|
|
||||||
urlValidator,
|
|
||||||
validateRequiredButtonParameters,
|
|
||||||
} from '../Assertions.js';
|
|
||||||
import { ComponentBuilder } from '../Component.js';
|
import { ComponentBuilder } from '../Component.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for buttons.
|
* A builder that creates API-compatible JSON data for buttons.
|
||||||
*/
|
*/
|
||||||
export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
|
export abstract class BaseButtonBuilder<ButtonData extends APIButtonComponent> extends ComponentBuilder<ButtonData> {
|
||||||
/**
|
protected declare readonly data: Partial<ButtonData>;
|
||||||
* Creates a new button from API data.
|
|
||||||
*
|
|
||||||
* @param data - The API data to create this button with
|
|
||||||
* @example
|
|
||||||
* Creating a button from an API data object:
|
|
||||||
* ```ts
|
|
||||||
* const button = new ButtonBuilder({
|
|
||||||
* custom_id: 'a cool button',
|
|
||||||
* style: ButtonStyle.Primary,
|
|
||||||
* label: 'Click Me',
|
|
||||||
* emoji: {
|
|
||||||
* name: 'smile',
|
|
||||||
* id: '123456789012345678',
|
|
||||||
* },
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
* @example
|
|
||||||
* Creating a button using setters and API data:
|
|
||||||
* ```ts
|
|
||||||
* const button = new ButtonBuilder({
|
|
||||||
* style: ButtonStyle.Secondary,
|
|
||||||
* label: 'Click Me',
|
|
||||||
* })
|
|
||||||
* .setEmoji({ name: '🙂' })
|
|
||||||
* .setCustomId('another cool button');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public constructor(data?: Partial<APIButtonComponent>) {
|
|
||||||
super({ type: ComponentType.Button, ...data });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the style of this button.
|
|
||||||
*
|
|
||||||
* @param style - The style to use
|
|
||||||
*/
|
|
||||||
public setStyle(style: ButtonStyle) {
|
|
||||||
this.data.style = buttonStyleValidator.parse(style);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the URL for this button.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method is only available to buttons using the `Link` button style.
|
|
||||||
* Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`.
|
|
||||||
* @param url - The URL to use
|
|
||||||
*/
|
|
||||||
public setURL(url: string) {
|
|
||||||
(this.data as APIButtonComponentWithURL).url = urlValidator.parse(url);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the custom id for this button.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method is only applicable to buttons that are not using the `Link` button style.
|
|
||||||
* @param customId - The custom id to use
|
|
||||||
*/
|
|
||||||
public setCustomId(customId: string) {
|
|
||||||
(this.data as APIButtonComponentWithCustomId).custom_id = customIdValidator.parse(customId);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the SKU id that represents a purchasable SKU for this button.
|
|
||||||
*
|
|
||||||
* @remarks Only available when using premium-style buttons.
|
|
||||||
* @param skuId - The SKU id to use
|
|
||||||
*/
|
|
||||||
public setSKUId(skuId: Snowflake) {
|
|
||||||
(this.data as APIButtonComponentWithSKUId).sku_id = skuId;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the emoji to display on this button.
|
|
||||||
*
|
|
||||||
* @param emoji - The emoji to use
|
|
||||||
*/
|
|
||||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
|
||||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji = emojiValidator.parse(emoji);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether this button is disabled.
|
* Sets whether this button is disabled.
|
||||||
@@ -117,35 +15,20 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
|
|||||||
* @param disabled - Whether to disable this button
|
* @param disabled - Whether to disable this button
|
||||||
*/
|
*/
|
||||||
public setDisabled(disabled = true) {
|
public setDisabled(disabled = true) {
|
||||||
this.data.disabled = disabledValidator.parse(disabled);
|
this.data.disabled = disabled;
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the label for this button.
|
|
||||||
*
|
|
||||||
* @param label - The label to use
|
|
||||||
*/
|
|
||||||
public setLabel(label: string) {
|
|
||||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label = buttonLabelValidator.parse(label);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc ComponentBuilder.toJSON}
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
*/
|
*/
|
||||||
public toJSON(): APIButtonComponent {
|
public override toJSON(validationOverride?: boolean): ButtonData {
|
||||||
validateRequiredButtonParameters(
|
const clone = structuredClone(this.data);
|
||||||
this.data.style,
|
|
||||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label,
|
|
||||||
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji,
|
|
||||||
(this.data as APIButtonComponentWithCustomId).custom_id,
|
|
||||||
(this.data as APIButtonComponentWithSKUId).sku_id,
|
|
||||||
(this.data as APIButtonComponentWithURL).url,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
...this.data,
|
buttonPredicate.parse(clone);
|
||||||
} as APIButtonComponent;
|
}
|
||||||
|
|
||||||
|
return clone as ButtonData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
69
packages/builders/src/components/button/CustomIdButton.ts
Normal file
69
packages/builders/src/components/button/CustomIdButton.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { ButtonStyle, ComponentType, type APIButtonComponentWithCustomId } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { BaseButtonBuilder } from './Button.js';
|
||||||
|
import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js';
|
||||||
|
|
||||||
|
export type CustomIdButtonStyle = APIButtonComponentWithCustomId['style'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for buttons with custom IDs.
|
||||||
|
*/
|
||||||
|
export abstract class CustomIdButtonBuilder extends Mixin(
|
||||||
|
BaseButtonBuilder<APIButtonComponentWithCustomId>,
|
||||||
|
EmojiOrLabelButtonMixin,
|
||||||
|
) {
|
||||||
|
protected override readonly data: Partial<APIButtonComponentWithCustomId>;
|
||||||
|
|
||||||
|
protected constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||||
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.Button };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the custom id for this button.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This method is only applicable to buttons that are not using the `Link` button style.
|
||||||
|
* @param customId - The custom id to use
|
||||||
|
*/
|
||||||
|
public setCustomId(customId: string) {
|
||||||
|
this.data.custom_id = customId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the primary style).
|
||||||
|
*/
|
||||||
|
export class PrimaryButtonBuilder extends CustomIdButtonBuilder {
|
||||||
|
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||||
|
super({ ...data, style: ButtonStyle.Primary });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the secondary style).
|
||||||
|
*/
|
||||||
|
export class SecondaryButtonBuilder extends CustomIdButtonBuilder {
|
||||||
|
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||||
|
super({ ...data, style: ButtonStyle.Secondary });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the success style).
|
||||||
|
*/
|
||||||
|
export class SuccessButtonBuilder extends CustomIdButtonBuilder {
|
||||||
|
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||||
|
super({ ...data, style: ButtonStyle.Success });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for buttons with custom IDs (using the danger style).
|
||||||
|
*/
|
||||||
|
export class DangerButtonBuilder extends CustomIdButtonBuilder {
|
||||||
|
public constructor(data: Partial<APIButtonComponentWithCustomId> = {}) {
|
||||||
|
super({ ...data, style: ButtonStyle.Danger });
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/builders/src/components/button/LinkButton.ts
Normal file
34
packages/builders/src/components/button/LinkButton.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
ButtonStyle,
|
||||||
|
ComponentType,
|
||||||
|
type APIButtonComponent,
|
||||||
|
type APIButtonComponentWithURL,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { BaseButtonBuilder } from './Button.js';
|
||||||
|
import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for buttons with links.
|
||||||
|
*/
|
||||||
|
export class LinkButtonBuilder extends Mixin(BaseButtonBuilder<APIButtonComponentWithURL>, EmojiOrLabelButtonMixin) {
|
||||||
|
protected override readonly data: Partial<APIButtonComponentWithURL>;
|
||||||
|
|
||||||
|
public constructor(data: Partial<APIButtonComponent> = {}) {
|
||||||
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Link };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the URL for this button.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This method is only available to buttons using the `Link` button style.
|
||||||
|
* Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`.
|
||||||
|
* @param url - The URL to use
|
||||||
|
*/
|
||||||
|
public setURL(url: string) {
|
||||||
|
this.data.url = url;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/builders/src/components/button/PremiumButton.ts
Normal file
26
packages/builders/src/components/button/PremiumButton.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { APIButtonComponentWithSKUId, Snowflake } from 'discord-api-types/v10';
|
||||||
|
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
|
||||||
|
import { BaseButtonBuilder } from './Button.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for premium buttons.
|
||||||
|
*/
|
||||||
|
export class PremiumButtonBuilder extends BaseButtonBuilder<APIButtonComponentWithSKUId> {
|
||||||
|
protected override readonly data: Partial<APIButtonComponentWithSKUId>;
|
||||||
|
|
||||||
|
public constructor(data: Partial<APIButtonComponentWithSKUId> = {}) {
|
||||||
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Premium };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the SKU id that represents a purchasable SKU for this button.
|
||||||
|
*
|
||||||
|
* @remarks Only available when using premium-style buttons.
|
||||||
|
* @param skuId - The SKU id to use
|
||||||
|
*/
|
||||||
|
public setSKUId(skuId: Snowflake) {
|
||||||
|
this.data.sku_id = skuId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { APIButtonComponent, APIButtonComponentWithSKUId, APIMessageComponentEmoji } from 'discord-api-types/v10';
|
||||||
|
|
||||||
|
export interface EmojiOrLabelButtonData
|
||||||
|
extends Pick<Exclude<APIButtonComponent, APIButtonComponentWithSKUId>, 'emoji' | 'label'> {}
|
||||||
|
|
||||||
|
export class EmojiOrLabelButtonMixin {
|
||||||
|
protected declare readonly data: EmojiOrLabelButtonData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the emoji to display on this button.
|
||||||
|
*
|
||||||
|
* @param emoji - The emoji to use
|
||||||
|
*/
|
||||||
|
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||||
|
this.data.emoji = emoji;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the emoji on this button.
|
||||||
|
*/
|
||||||
|
public clearEmoji() {
|
||||||
|
this.data.emoji = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label for this button.
|
||||||
|
*
|
||||||
|
* @param label - The label to use
|
||||||
|
*/
|
||||||
|
public setLabel(label: string) {
|
||||||
|
this.data.label = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the label on this button.
|
||||||
|
*/
|
||||||
|
public clearLabel() {
|
||||||
|
this.data.label = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APISelectMenuComponent } from 'discord-api-types/v10';
|
import type { APISelectMenuComponent } from 'discord-api-types/v10';
|
||||||
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
|
|
||||||
import { ComponentBuilder } from '../Component.js';
|
import { ComponentBuilder } from '../Component.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,16 +7,29 @@ import { ComponentBuilder } from '../Component.js';
|
|||||||
*
|
*
|
||||||
* @typeParam SelectMenuType - The type of select menu this would be instantiated for.
|
* @typeParam SelectMenuType - The type of select menu this would be instantiated for.
|
||||||
*/
|
*/
|
||||||
export abstract class BaseSelectMenuBuilder<
|
export abstract class BaseSelectMenuBuilder<Data extends APISelectMenuComponent>
|
||||||
SelectMenuType extends APISelectMenuComponent,
|
extends ComponentBuilder<Data>
|
||||||
> extends ComponentBuilder<SelectMenuType> {
|
implements JSONEncodable<APISelectMenuComponent>
|
||||||
|
{
|
||||||
|
protected abstract readonly data: Partial<
|
||||||
|
Pick<Data, 'custom_id' | 'disabled' | 'max_values' | 'min_values' | 'placeholder'>
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the placeholder for this select menu.
|
* Sets the placeholder for this select menu.
|
||||||
*
|
*
|
||||||
* @param placeholder - The placeholder to use
|
* @param placeholder - The placeholder to use
|
||||||
*/
|
*/
|
||||||
public setPlaceholder(placeholder: string) {
|
public setPlaceholder(placeholder: string) {
|
||||||
this.data.placeholder = placeholderValidator.parse(placeholder);
|
this.data.placeholder = placeholder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the placeholder for this select menu.
|
||||||
|
*/
|
||||||
|
public clearPlaceholder() {
|
||||||
|
this.data.placeholder = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +39,7 @@ export abstract class BaseSelectMenuBuilder<
|
|||||||
* @param minValues - The minimum values that must be selected
|
* @param minValues - The minimum values that must be selected
|
||||||
*/
|
*/
|
||||||
public setMinValues(minValues: number) {
|
public setMinValues(minValues: number) {
|
||||||
this.data.min_values = minMaxValidator.parse(minValues);
|
this.data.min_values = minValues;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +49,7 @@ export abstract class BaseSelectMenuBuilder<
|
|||||||
* @param maxValues - The maximum values that must be selected
|
* @param maxValues - The maximum values that must be selected
|
||||||
*/
|
*/
|
||||||
public setMaxValues(maxValues: number) {
|
public setMaxValues(maxValues: number) {
|
||||||
this.data.max_values = minMaxValidator.parse(maxValues);
|
this.data.max_values = maxValues;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +59,7 @@ export abstract class BaseSelectMenuBuilder<
|
|||||||
* @param customId - The custom id to use
|
* @param customId - The custom id to use
|
||||||
*/
|
*/
|
||||||
public setCustomId(customId: string) {
|
public setCustomId(customId: string) {
|
||||||
this.data.custom_id = customIdValidator.parse(customId);
|
this.data.custom_id = customId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,17 +69,7 @@ export abstract class BaseSelectMenuBuilder<
|
|||||||
* @param disabled - Whether this select menu is disabled
|
* @param disabled - Whether this select menu is disabled
|
||||||
*/
|
*/
|
||||||
public setDisabled(disabled = true) {
|
public setDisabled(disabled = true) {
|
||||||
this.data.disabled = disabledValidator.parse(disabled);
|
this.data.disabled = disabled;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ComponentBuilder.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): SelectMenuType {
|
|
||||||
customIdValidator.parse(this.data.custom_id);
|
|
||||||
return {
|
|
||||||
...this.data,
|
|
||||||
} as SelectMenuType;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import {
|
|||||||
SelectMenuDefaultValueType,
|
SelectMenuDefaultValueType,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||||
import { channelTypesValidator, customIdValidator, optionsLengthValidator } from '../Assertions.js';
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { selectMenuChannelPredicate } from '../Assertions.js';
|
||||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for channel select menus.
|
* A builder that creates API-compatible JSON data for channel select menus.
|
||||||
*/
|
*/
|
||||||
export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSelectComponent> {
|
export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSelectComponent> {
|
||||||
|
protected override readonly data: Partial<APIChannelSelectComponent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new select menu from API data.
|
* Creates a new select menu from API data.
|
||||||
*
|
*
|
||||||
@@ -36,8 +39,9 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
|||||||
* .setMinValues(2);
|
* .setMinValues(2);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(data?: Partial<APIChannelSelectComponent>) {
|
public constructor(data: Partial<APIChannelSelectComponent> = {}) {
|
||||||
super({ ...data, type: ComponentType.ChannelSelect });
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.ChannelSelect };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +52,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
|||||||
public addChannelTypes(...types: RestOrArray<ChannelType>) {
|
public addChannelTypes(...types: RestOrArray<ChannelType>) {
|
||||||
const normalizedTypes = normalizeArray(types);
|
const normalizedTypes = normalizeArray(types);
|
||||||
this.data.channel_types ??= [];
|
this.data.channel_types ??= [];
|
||||||
this.data.channel_types.push(...channelTypesValidator.parse(normalizedTypes));
|
this.data.channel_types.push(...normalizedTypes);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
|||||||
public setChannelTypes(...types: RestOrArray<ChannelType>) {
|
public setChannelTypes(...types: RestOrArray<ChannelType>) {
|
||||||
const normalizedTypes = normalizeArray(types);
|
const normalizedTypes = normalizeArray(types);
|
||||||
this.data.channel_types ??= [];
|
this.data.channel_types ??= [];
|
||||||
this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(normalizedTypes));
|
this.data.channel_types.splice(0, this.data.channel_types.length, ...normalizedTypes);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +75,6 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
|||||||
*/
|
*/
|
||||||
public addDefaultChannels(...channels: RestOrArray<Snowflake>) {
|
public addDefaultChannels(...channels: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(channels);
|
const normalizedValues = normalizeArray(channels);
|
||||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
|
||||||
this.data.default_values ??= [];
|
this.data.default_values ??= [];
|
||||||
|
|
||||||
this.data.default_values.push(
|
this.data.default_values.push(
|
||||||
@@ -91,7 +94,6 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
|||||||
*/
|
*/
|
||||||
public setDefaultChannels(...channels: RestOrArray<Snowflake>) {
|
public setDefaultChannels(...channels: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(channels);
|
const normalizedValues = normalizeArray(channels);
|
||||||
optionsLengthValidator.parse(normalizedValues.length);
|
|
||||||
|
|
||||||
this.data.default_values = normalizedValues.map((id) => ({
|
this.data.default_values = normalizedValues.map((id) => ({
|
||||||
id,
|
id,
|
||||||
@@ -102,13 +104,15 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
*/
|
*/
|
||||||
public override toJSON(): APIChannelSelectComponent {
|
public override toJSON(validationOverride?: boolean): APIChannelSelectComponent {
|
||||||
customIdValidator.parse(this.data.custom_id);
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
return {
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
...this.data,
|
selectMenuChannelPredicate.parse(clone);
|
||||||
} as APIChannelSelectComponent;
|
}
|
||||||
|
|
||||||
|
return clone as APIChannelSelectComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import {
|
|||||||
SelectMenuDefaultValueType,
|
SelectMenuDefaultValueType,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||||
import { optionsLengthValidator } from '../Assertions.js';
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { selectMenuMentionablePredicate } from '../Assertions.js';
|
||||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for mentionable select menus.
|
* A builder that creates API-compatible JSON data for mentionable select menus.
|
||||||
*/
|
*/
|
||||||
export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMentionableSelectComponent> {
|
export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMentionableSelectComponent> {
|
||||||
|
protected override readonly data: Partial<APIMentionableSelectComponent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new select menu from API data.
|
* Creates a new select menu from API data.
|
||||||
*
|
*
|
||||||
@@ -35,8 +38,9 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
|||||||
* .setMinValues(1);
|
* .setMinValues(1);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(data?: Partial<APIMentionableSelectComponent>) {
|
public constructor(data: Partial<APIMentionableSelectComponent> = {}) {
|
||||||
super({ ...data, type: ComponentType.MentionableSelect });
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.MentionableSelect };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +50,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
|||||||
*/
|
*/
|
||||||
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(roles);
|
const normalizedValues = normalizeArray(roles);
|
||||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
|
||||||
this.data.default_values ??= [];
|
this.data.default_values ??= [];
|
||||||
|
|
||||||
this.data.default_values.push(
|
this.data.default_values.push(
|
||||||
@@ -66,7 +69,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
|||||||
*/
|
*/
|
||||||
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
|
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(users);
|
const normalizedValues = normalizeArray(users);
|
||||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
|
||||||
this.data.default_values ??= [];
|
this.data.default_values ??= [];
|
||||||
|
|
||||||
this.data.default_values.push(
|
this.data.default_values.push(
|
||||||
@@ -91,7 +93,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
|||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
const normalizedValues = normalizeArray(values);
|
const normalizedValues = normalizeArray(values);
|
||||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
|
||||||
this.data.default_values ??= [];
|
this.data.default_values ??= [];
|
||||||
this.data.default_values.push(...normalizedValues);
|
this.data.default_values.push(...normalizedValues);
|
||||||
return this;
|
return this;
|
||||||
@@ -109,8 +110,20 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
|
|||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
const normalizedValues = normalizeArray(values);
|
const normalizedValues = normalizeArray(values);
|
||||||
optionsLengthValidator.parse(normalizedValues.length);
|
|
||||||
this.data.default_values = normalizedValues;
|
this.data.default_values = normalizedValues;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON(validationOverride?: boolean): APIMentionableSelectComponent {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
selectMenuMentionablePredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIMentionableSelectComponent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import {
|
|||||||
SelectMenuDefaultValueType,
|
SelectMenuDefaultValueType,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||||
import { optionsLengthValidator } from '../Assertions.js';
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { selectMenuRolePredicate } from '../Assertions.js';
|
||||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for role select menus.
|
* A builder that creates API-compatible JSON data for role select menus.
|
||||||
*/
|
*/
|
||||||
export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectComponent> {
|
export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectComponent> {
|
||||||
|
protected override readonly data: Partial<APIRoleSelectComponent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new select menu from API data.
|
* Creates a new select menu from API data.
|
||||||
*
|
*
|
||||||
@@ -34,8 +37,9 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
|||||||
* .setMinValues(1);
|
* .setMinValues(1);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(data?: Partial<APIRoleSelectComponent>) {
|
public constructor(data: Partial<APIRoleSelectComponent> = {}) {
|
||||||
super({ ...data, type: ComponentType.RoleSelect });
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.RoleSelect };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +49,6 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
|||||||
*/
|
*/
|
||||||
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(roles);
|
const normalizedValues = normalizeArray(roles);
|
||||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
|
||||||
this.data.default_values ??= [];
|
this.data.default_values ??= [];
|
||||||
|
|
||||||
this.data.default_values.push(
|
this.data.default_values.push(
|
||||||
@@ -65,7 +68,6 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
|||||||
*/
|
*/
|
||||||
public setDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
public setDefaultRoles(...roles: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(roles);
|
const normalizedValues = normalizeArray(roles);
|
||||||
optionsLengthValidator.parse(normalizedValues.length);
|
|
||||||
|
|
||||||
this.data.default_values = normalizedValues.map((id) => ({
|
this.data.default_values = normalizedValues.map((id) => ({
|
||||||
id,
|
id,
|
||||||
@@ -74,4 +76,17 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON(validationOverride?: boolean): APIRoleSelectComponent {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
selectMenuRolePredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIRoleSelectComponent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
|
/* eslint-disable jsdoc/check-param-names */
|
||||||
|
|
||||||
import { ComponentType } from 'discord-api-types/v10';
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
import type { APIStringSelectComponent, APISelectMenuOption } from 'discord-api-types/v10';
|
import type { APIStringSelectComponent, APISelectMenuOption } from 'discord-api-types/v10';
|
||||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||||
import { jsonOptionValidator, optionsLengthValidator, validateRequiredSelectMenuParameters } from '../Assertions.js';
|
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||||
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { selectMenuStringPredicate } from '../Assertions.js';
|
||||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||||
import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js';
|
import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js';
|
||||||
|
|
||||||
|
export interface StringSelectMenuData extends Partial<Omit<APIStringSelectComponent, 'options'>> {
|
||||||
|
options: StringSelectMenuOptionBuilder[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for string select menus.
|
* A builder that creates API-compatible JSON data for string select menus.
|
||||||
*/
|
*/
|
||||||
export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSelectComponent> {
|
export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSelectComponent> {
|
||||||
|
protected override readonly data: StringSelectMenuData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options within this select menu.
|
* The options for this select menu.
|
||||||
*/
|
*/
|
||||||
public readonly options: StringSelectMenuOptionBuilder[];
|
public get options(): readonly StringSelectMenuOptionBuilder[] {
|
||||||
|
return this.data.options;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new select menu from API data.
|
* Creates a new select menu from API data.
|
||||||
@@ -45,10 +57,13 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
|||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(data?: Partial<APIStringSelectComponent>) {
|
public constructor({ options = [], ...data }: Partial<APIStringSelectComponent> = {}) {
|
||||||
const { options, ...initData } = data ?? {};
|
super();
|
||||||
super({ ...initData, type: ComponentType.StringSelect });
|
this.data = {
|
||||||
this.options = options?.map((option: APISelectMenuOption) => new StringSelectMenuOptionBuilder(option)) ?? [];
|
...structuredClone(data),
|
||||||
|
options: options.map((option) => new StringSelectMenuOptionBuilder(option)),
|
||||||
|
type: ComponentType.StringSelect,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,16 +71,18 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
|||||||
*
|
*
|
||||||
* @param options - The options to add
|
* @param options - The options to add
|
||||||
*/
|
*/
|
||||||
public addOptions(...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
|
public addOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
| APISelectMenuOption
|
||||||
|
| StringSelectMenuOptionBuilder
|
||||||
|
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
|
||||||
|
>
|
||||||
|
) {
|
||||||
const normalizedOptions = normalizeArray(options);
|
const normalizedOptions = normalizeArray(options);
|
||||||
optionsLengthValidator.parse(this.options.length + normalizedOptions.length);
|
const resolved = normalizedOptions.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder));
|
||||||
this.options.push(
|
|
||||||
...normalizedOptions.map((normalizedOption) =>
|
this.data.options.push(...resolved);
|
||||||
normalizedOption instanceof StringSelectMenuOptionBuilder
|
|
||||||
? normalizedOption
|
|
||||||
: new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +91,14 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
|||||||
*
|
*
|
||||||
* @param options - The options to set
|
* @param options - The options to set
|
||||||
*/
|
*/
|
||||||
public setOptions(...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
|
public setOptions(
|
||||||
return this.spliceOptions(0, this.options.length, ...options);
|
...options: RestOrArray<
|
||||||
|
| APISelectMenuOption
|
||||||
|
| StringSelectMenuOptionBuilder
|
||||||
|
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.spliceOptions(0, this.options.length, ...normalizeArray(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,36 +131,35 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder<APIStringSele
|
|||||||
public spliceOptions(
|
public spliceOptions(
|
||||||
index: number,
|
index: number,
|
||||||
deleteCount: number,
|
deleteCount: number,
|
||||||
...options: RestOrArray<APISelectMenuOption | StringSelectMenuOptionBuilder>
|
...options: (
|
||||||
|
| APISelectMenuOption
|
||||||
|
| StringSelectMenuOptionBuilder
|
||||||
|
| ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder)
|
||||||
|
)[]
|
||||||
) {
|
) {
|
||||||
const normalizedOptions = normalizeArray(options);
|
const resolved = options.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder));
|
||||||
|
|
||||||
const clone = [...this.options];
|
this.data.options ??= [];
|
||||||
|
this.data.options.splice(index, deleteCount, ...resolved);
|
||||||
|
|
||||||
clone.splice(
|
|
||||||
index,
|
|
||||||
deleteCount,
|
|
||||||
...normalizedOptions.map((normalizedOption) =>
|
|
||||||
normalizedOption instanceof StringSelectMenuOptionBuilder
|
|
||||||
? normalizedOption
|
|
||||||
: new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
optionsLengthValidator.parse(clone.length);
|
|
||||||
this.options.splice(0, this.options.length, ...clone);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
*/
|
*/
|
||||||
public override toJSON(): APIStringSelectComponent {
|
public override toJSON(validationOverride?: boolean): APIStringSelectComponent {
|
||||||
validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
|
const { options, ...rest } = this.data;
|
||||||
|
const data = {
|
||||||
|
...(structuredClone(rest) as APIStringSelectComponent),
|
||||||
|
// selectMenuStringPredicate covers the validation of options
|
||||||
|
options: options.map((option) => option.toJSON(false)),
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
...this.data,
|
selectMenuStringPredicate.parse(data);
|
||||||
options: this.options.map((option) => option.toJSON()),
|
}
|
||||||
} as APIStringSelectComponent;
|
|
||||||
|
return data as APIStringSelectComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import type { JSONEncodable } from '@discordjs/util';
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
|
||||||
import {
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
defaultValidator,
|
import { selectMenuStringOptionPredicate } from '../Assertions.js';
|
||||||
emojiValidator,
|
|
||||||
labelValueDescriptionValidator,
|
|
||||||
validateRequiredSelectMenuOptionParameters,
|
|
||||||
} from '../Assertions.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for string select menu options.
|
* A builder that creates API-compatible JSON data for string select menu options.
|
||||||
*/
|
*/
|
||||||
export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
|
export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
|
||||||
|
private readonly data: Partial<APISelectMenuOption>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new string select menu option from API data.
|
* Creates a new string select menu option from API data.
|
||||||
*
|
*
|
||||||
@@ -33,7 +31,9 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
|||||||
* .setLabel('woah');
|
* .setLabel('woah');
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(public data: Partial<APISelectMenuOption> = {}) {}
|
public constructor(data: Partial<APISelectMenuOption> = {}) {
|
||||||
|
this.data = structuredClone(data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the label for this option.
|
* Sets the label for this option.
|
||||||
@@ -41,7 +41,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
|||||||
* @param label - The label to use
|
* @param label - The label to use
|
||||||
*/
|
*/
|
||||||
public setLabel(label: string) {
|
public setLabel(label: string) {
|
||||||
this.data.label = labelValueDescriptionValidator.parse(label);
|
this.data.label = label;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
|||||||
* @param value - The value to use
|
* @param value - The value to use
|
||||||
*/
|
*/
|
||||||
public setValue(value: string) {
|
public setValue(value: string) {
|
||||||
this.data.value = labelValueDescriptionValidator.parse(value);
|
this.data.value = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,15 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
|||||||
* @param description - The description to use
|
* @param description - The description to use
|
||||||
*/
|
*/
|
||||||
public setDescription(description: string) {
|
public setDescription(description: string) {
|
||||||
this.data.description = labelValueDescriptionValidator.parse(description);
|
this.data.description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the description for this option.
|
||||||
|
*/
|
||||||
|
public clearDescription() {
|
||||||
|
this.data.description = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +79,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
|||||||
* @param isDefault - Whether this option is selected by default
|
* @param isDefault - Whether this option is selected by default
|
||||||
*/
|
*/
|
||||||
public setDefault(isDefault = true) {
|
public setDefault(isDefault = true) {
|
||||||
this.data.default = defaultValidator.parse(isDefault);
|
this.data.default = isDefault;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,18 +89,28 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMen
|
|||||||
* @param emoji - The emoji to use
|
* @param emoji - The emoji to use
|
||||||
*/
|
*/
|
||||||
public setEmoji(emoji: APIMessageComponentEmoji) {
|
public setEmoji(emoji: APIMessageComponentEmoji) {
|
||||||
this.data.emoji = emojiValidator.parse(emoji);
|
this.data.emoji = emoji;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
|
* Clears the emoji for this option.
|
||||||
*/
|
*/
|
||||||
public toJSON(): APISelectMenuOption {
|
public clearEmoji() {
|
||||||
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
|
this.data.emoji = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
/**
|
||||||
...this.data,
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
} as APISelectMenuOption;
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APISelectMenuOption {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
selectMenuStringOptionPredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APISelectMenuOption;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import {
|
|||||||
SelectMenuDefaultValueType,
|
SelectMenuDefaultValueType,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
|
||||||
import { optionsLengthValidator } from '../Assertions.js';
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { selectMenuUserPredicate } from '../Assertions.js';
|
||||||
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for user select menus.
|
* A builder that creates API-compatible JSON data for user select menus.
|
||||||
*/
|
*/
|
||||||
export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectComponent> {
|
export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectComponent> {
|
||||||
|
protected override readonly data: Partial<APIUserSelectComponent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new select menu from API data.
|
* Creates a new select menu from API data.
|
||||||
*
|
*
|
||||||
@@ -34,8 +37,9 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
|||||||
* .setMinValues(1);
|
* .setMinValues(1);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(data?: Partial<APIUserSelectComponent>) {
|
public constructor(data: Partial<APIUserSelectComponent> = {}) {
|
||||||
super({ ...data, type: ComponentType.UserSelect });
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.UserSelect };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,9 +49,8 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
|||||||
*/
|
*/
|
||||||
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
|
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(users);
|
const normalizedValues = normalizeArray(users);
|
||||||
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
|
|
||||||
this.data.default_values ??= [];
|
|
||||||
|
|
||||||
|
this.data.default_values ??= [];
|
||||||
this.data.default_values.push(
|
this.data.default_values.push(
|
||||||
...normalizedValues.map((id) => ({
|
...normalizedValues.map((id) => ({
|
||||||
id,
|
id,
|
||||||
@@ -65,7 +68,6 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
|||||||
*/
|
*/
|
||||||
public setDefaultUsers(...users: RestOrArray<Snowflake>) {
|
public setDefaultUsers(...users: RestOrArray<Snowflake>) {
|
||||||
const normalizedValues = normalizeArray(users);
|
const normalizedValues = normalizeArray(users);
|
||||||
optionsLengthValidator.parse(normalizedValues.length);
|
|
||||||
|
|
||||||
this.data.default_values = normalizedValues.map((id) => ({
|
this.data.default_values = normalizedValues.map((id) => ({
|
||||||
id,
|
id,
|
||||||
@@ -74,4 +76,17 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectCo
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON(validationOverride?: boolean): APIUserSelectComponent {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
selectMenuUserPredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIUserSelectComponent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,15 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||||
import { TextInputStyle } from 'discord-api-types/v10';
|
import { z } from 'zod';
|
||||||
import { isValidationEnabled } from '../../util/validation.js';
|
import { customIdPredicate } from '../../Assertions.js';
|
||||||
import { customIdValidator } from '../Assertions.js';
|
|
||||||
|
|
||||||
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
|
export const textInputPredicate = z.object({
|
||||||
export const minLengthValidator = s
|
type: z.literal(ComponentType.TextInput),
|
||||||
.number()
|
custom_id: customIdPredicate,
|
||||||
.int()
|
label: z.string().min(1).max(45),
|
||||||
.greaterThanOrEqual(0)
|
style: z.nativeEnum(TextInputStyle),
|
||||||
.lessThanOrEqual(4_000)
|
min_length: z.number().min(0).max(4_000).optional(),
|
||||||
.setValidationEnabled(isValidationEnabled);
|
max_length: z.number().min(1).max(4_000).optional(),
|
||||||
export const maxLengthValidator = s
|
placeholder: z.string().max(100).optional(),
|
||||||
.number()
|
value: z.string().max(4_000).optional(),
|
||||||
.int()
|
required: z.boolean().optional(),
|
||||||
.greaterThanOrEqual(1)
|
});
|
||||||
.lessThanOrEqual(4_000)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
export const requiredValidator = s.boolean();
|
|
||||||
export const valueValidator = s.string().lengthLessThanOrEqual(4_000).setValidationEnabled(isValidationEnabled);
|
|
||||||
export const placeholderValidator = s.string().lengthLessThanOrEqual(100).setValidationEnabled(isValidationEnabled);
|
|
||||||
export const labelValidator = s
|
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(45)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
|
|
||||||
customIdValidator.parse(customId);
|
|
||||||
textInputStyleValidator.parse(style);
|
|
||||||
labelValidator.parse(label);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
import { isJSONEncodable, type Equatable, type JSONEncodable } from '@discordjs/util';
|
|
||||||
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
import { ComponentType, type TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
||||||
import isEqual from 'fast-deep-equal';
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
import { customIdValidator } from '../Assertions.js';
|
|
||||||
import { ComponentBuilder } from '../Component.js';
|
import { ComponentBuilder } from '../Component.js';
|
||||||
import {
|
import { textInputPredicate } from './Assertions.js';
|
||||||
maxLengthValidator,
|
|
||||||
minLengthValidator,
|
|
||||||
placeholderValidator,
|
|
||||||
requiredValidator,
|
|
||||||
valueValidator,
|
|
||||||
validateRequiredParameters,
|
|
||||||
labelValidator,
|
|
||||||
textInputStyleValidator,
|
|
||||||
} from './Assertions.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for text inputs.
|
* A builder that creates API-compatible JSON data for text inputs.
|
||||||
*/
|
*/
|
||||||
export class TextInputBuilder
|
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
|
||||||
extends ComponentBuilder<APITextInputComponent>
|
private readonly data: Partial<APITextInputComponent>;
|
||||||
implements Equatable<APITextInputComponent | JSONEncodable<APITextInputComponent>>
|
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Creates a new text input from API data.
|
* Creates a new text input from API data.
|
||||||
*
|
*
|
||||||
@@ -44,8 +32,9 @@ export class TextInputBuilder
|
|||||||
* .setStyle(TextInputStyle.Paragraph);
|
* .setStyle(TextInputStyle.Paragraph);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
|
public constructor(data: Partial<APITextInputComponent> = {}) {
|
||||||
super({ type: ComponentType.TextInput, ...data });
|
super();
|
||||||
|
this.data = { ...structuredClone(data), type: ComponentType.TextInput };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +43,7 @@ export class TextInputBuilder
|
|||||||
* @param customId - The custom id to use
|
* @param customId - The custom id to use
|
||||||
*/
|
*/
|
||||||
public setCustomId(customId: string) {
|
public setCustomId(customId: string) {
|
||||||
this.data.custom_id = customIdValidator.parse(customId);
|
this.data.custom_id = customId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +53,7 @@ export class TextInputBuilder
|
|||||||
* @param label - The label to use
|
* @param label - The label to use
|
||||||
*/
|
*/
|
||||||
public setLabel(label: string) {
|
public setLabel(label: string) {
|
||||||
this.data.label = labelValidator.parse(label);
|
this.data.label = label;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +63,7 @@ export class TextInputBuilder
|
|||||||
* @param style - The style to use
|
* @param style - The style to use
|
||||||
*/
|
*/
|
||||||
public setStyle(style: TextInputStyle) {
|
public setStyle(style: TextInputStyle) {
|
||||||
this.data.style = textInputStyleValidator.parse(style);
|
this.data.style = style;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +73,15 @@ export class TextInputBuilder
|
|||||||
* @param minLength - The minimum length of text for this text input
|
* @param minLength - The minimum length of text for this text input
|
||||||
*/
|
*/
|
||||||
public setMinLength(minLength: number) {
|
public setMinLength(minLength: number) {
|
||||||
this.data.min_length = minLengthValidator.parse(minLength);
|
this.data.min_length = minLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the minimum length of text for this text input.
|
||||||
|
*/
|
||||||
|
public clearMinLength() {
|
||||||
|
this.data.min_length = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +91,15 @@ export class TextInputBuilder
|
|||||||
* @param maxLength - The maximum length of text for this text input
|
* @param maxLength - The maximum length of text for this text input
|
||||||
*/
|
*/
|
||||||
public setMaxLength(maxLength: number) {
|
public setMaxLength(maxLength: number) {
|
||||||
this.data.max_length = maxLengthValidator.parse(maxLength);
|
this.data.max_length = maxLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the maximum length of text for this text input.
|
||||||
|
*/
|
||||||
|
public clearMaxLength() {
|
||||||
|
this.data.max_length = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +109,15 @@ export class TextInputBuilder
|
|||||||
* @param placeholder - The placeholder to use
|
* @param placeholder - The placeholder to use
|
||||||
*/
|
*/
|
||||||
public setPlaceholder(placeholder: string) {
|
public setPlaceholder(placeholder: string) {
|
||||||
this.data.placeholder = placeholderValidator.parse(placeholder);
|
this.data.placeholder = placeholder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the placeholder for this text input.
|
||||||
|
*/
|
||||||
|
public clearPlaceholder() {
|
||||||
|
this.data.placeholder = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +127,15 @@ export class TextInputBuilder
|
|||||||
* @param value - The value to use
|
* @param value - The value to use
|
||||||
*/
|
*/
|
||||||
public setValue(value: string) {
|
public setValue(value: string) {
|
||||||
this.data.value = valueValidator.parse(value);
|
this.data.value = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the value for this text input.
|
||||||
|
*/
|
||||||
|
public clearValue() {
|
||||||
|
this.data.value = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,29 +145,20 @@ export class TextInputBuilder
|
|||||||
* @param required - Whether this text input is required
|
* @param required - Whether this text input is required
|
||||||
*/
|
*/
|
||||||
public setRequired(required = true) {
|
public setRequired(required = true) {
|
||||||
this.data.required = requiredValidator.parse(required);
|
this.data.required = required;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc ComponentBuilder.toJSON}
|
* {@inheritDoc ComponentBuilder.toJSON}
|
||||||
*/
|
*/
|
||||||
public toJSON(): APITextInputComponent {
|
public toJSON(validationOverride?: boolean): APITextInputComponent {
|
||||||
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
return {
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
...this.data,
|
textInputPredicate.parse(clone);
|
||||||
} as APITextInputComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this is equal to another structure.
|
|
||||||
*/
|
|
||||||
public equals(other: APITextInputComponent | JSONEncodable<APITextInputComponent>): boolean {
|
|
||||||
if (isJSONEncodable(other)) {
|
|
||||||
return isEqual(other.toJSON(), this.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isEqual(other, this.data);
|
return clone as APITextInputComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,71 @@
|
|||||||
export * as EmbedAssertions from './messages/embed/Assertions.js';
|
export * from './components/button/mixins/EmojiOrLabelButtonMixin.js';
|
||||||
export * from './messages/embed/Embed.js';
|
|
||||||
// TODO: Consider removing this dep in the next major version
|
|
||||||
export * from '@discordjs/formatters';
|
|
||||||
|
|
||||||
export * as ComponentAssertions from './components/Assertions.js';
|
|
||||||
export * from './components/ActionRow.js';
|
|
||||||
export * from './components/button/Button.js';
|
export * from './components/button/Button.js';
|
||||||
export * from './components/Component.js';
|
export * from './components/button/CustomIdButton.js';
|
||||||
export * from './components/Components.js';
|
export * from './components/button/LinkButton.js';
|
||||||
export * from './components/textInput/TextInput.js';
|
export * from './components/button/PremiumButton.js';
|
||||||
export * as TextInputAssertions from './components/textInput/Assertions.js';
|
|
||||||
export * from './interactions/modals/Modal.js';
|
|
||||||
export * as ModalAssertions from './interactions/modals/Assertions.js';
|
|
||||||
|
|
||||||
export * from './components/selectMenu/BaseSelectMenu.js';
|
export * from './components/selectMenu/BaseSelectMenu.js';
|
||||||
export * from './components/selectMenu/ChannelSelectMenu.js';
|
export * from './components/selectMenu/ChannelSelectMenu.js';
|
||||||
export * from './components/selectMenu/MentionableSelectMenu.js';
|
export * from './components/selectMenu/MentionableSelectMenu.js';
|
||||||
export * from './components/selectMenu/RoleSelectMenu.js';
|
export * from './components/selectMenu/RoleSelectMenu.js';
|
||||||
export * from './components/selectMenu/StringSelectMenu.js';
|
export * from './components/selectMenu/StringSelectMenu.js';
|
||||||
// TODO: Remove those aliases in v2
|
|
||||||
export {
|
|
||||||
/**
|
|
||||||
* @deprecated Will be removed in the next major version, use {@link StringSelectMenuBuilder} instead.
|
|
||||||
*/
|
|
||||||
StringSelectMenuBuilder as SelectMenuBuilder,
|
|
||||||
} from './components/selectMenu/StringSelectMenu.js';
|
|
||||||
export {
|
|
||||||
/**
|
|
||||||
* @deprecated Will be removed in the next major version, use {@link StringSelectMenuOptionBuilder} instead.
|
|
||||||
*/
|
|
||||||
StringSelectMenuOptionBuilder as SelectMenuOptionBuilder,
|
|
||||||
} from './components/selectMenu/StringSelectMenuOption.js';
|
|
||||||
export * from './components/selectMenu/StringSelectMenuOption.js';
|
export * from './components/selectMenu/StringSelectMenuOption.js';
|
||||||
export * from './components/selectMenu/UserSelectMenu.js';
|
export * from './components/selectMenu/UserSelectMenu.js';
|
||||||
|
|
||||||
export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js';
|
export * from './components/textInput/TextInput.js';
|
||||||
export * from './interactions/slashCommands/SlashCommandBuilder.js';
|
export * from './components/textInput/Assertions.js';
|
||||||
export * from './interactions/slashCommands/SlashCommandSubcommands.js';
|
|
||||||
export * from './interactions/slashCommands/options/boolean.js';
|
|
||||||
export * from './interactions/slashCommands/options/channel.js';
|
|
||||||
export * from './interactions/slashCommands/options/integer.js';
|
|
||||||
export * from './interactions/slashCommands/options/mentionable.js';
|
|
||||||
export * from './interactions/slashCommands/options/number.js';
|
|
||||||
export * from './interactions/slashCommands/options/role.js';
|
|
||||||
export * from './interactions/slashCommands/options/attachment.js';
|
|
||||||
export * from './interactions/slashCommands/options/string.js';
|
|
||||||
export * from './interactions/slashCommands/options/user.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/NameAndDescription.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/SharedSubcommands.js';
|
|
||||||
export * from './interactions/slashCommands/mixins/SharedSlashCommand.js';
|
|
||||||
|
|
||||||
export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions.js';
|
export * from './components/ActionRow.js';
|
||||||
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder.js';
|
export * from './components/Assertions.js';
|
||||||
|
export * from './components/Component.js';
|
||||||
|
export * from './components/Components.js';
|
||||||
|
|
||||||
|
export * from './interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
||||||
|
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
||||||
|
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
||||||
|
export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
||||||
|
export * from './interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.js';
|
||||||
|
export * from './interactions/commands/chatInput/mixins/SharedSubcommands.js';
|
||||||
|
|
||||||
|
export * from './interactions/commands/chatInput/options/ApplicationCommandOptionBase.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/boolean.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/channel.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/integer.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/mentionable.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/number.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/role.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/attachment.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/string.js';
|
||||||
|
export * from './interactions/commands/chatInput/options/user.js';
|
||||||
|
|
||||||
|
export * from './interactions/commands/chatInput/Assertions.js';
|
||||||
|
export * from './interactions/commands/chatInput/ChatInputCommand.js';
|
||||||
|
export * from './interactions/commands/chatInput/ChatInputCommandSubcommands.js';
|
||||||
|
|
||||||
|
export * from './interactions/commands/contextMenu/Assertions.js';
|
||||||
|
export * from './interactions/commands/contextMenu/ContextMenuCommand.js';
|
||||||
|
export * from './interactions/commands/contextMenu/MessageCommand.js';
|
||||||
|
export * from './interactions/commands/contextMenu/UserCommand.js';
|
||||||
|
|
||||||
|
export * from './interactions/commands/Command.js';
|
||||||
|
export * from './interactions/commands/SharedName.js';
|
||||||
|
export * from './interactions/commands/SharedNameAndDescription.js';
|
||||||
|
|
||||||
|
export * from './interactions/modals/Assertions.js';
|
||||||
|
export * from './interactions/modals/Modal.js';
|
||||||
|
|
||||||
|
export * from './messages/embed/Assertions.js';
|
||||||
|
export * from './messages/embed/Embed.js';
|
||||||
|
export * from './messages/embed/EmbedAuthor.js';
|
||||||
|
export * from './messages/embed/EmbedField.js';
|
||||||
|
export * from './messages/embed/EmbedFooter.js';
|
||||||
|
|
||||||
export * from './util/componentUtil.js';
|
export * from './util/componentUtil.js';
|
||||||
export * from './util/normalizeArray.js';
|
export * from './util/normalizeArray.js';
|
||||||
export * from './util/validation.js';
|
export * from './util/validation.js';
|
||||||
|
|
||||||
|
export * from './Assertions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version
|
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version
|
||||||
* that you are currently using.
|
* that you are currently using.
|
||||||
|
|||||||
83
packages/builders/src/interactions/commands/Command.ts
Normal file
83
packages/builders/src/interactions/commands/Command.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
|
import type {
|
||||||
|
ApplicationIntegrationType,
|
||||||
|
InteractionContextType,
|
||||||
|
Permissions,
|
||||||
|
RESTPostAPIApplicationCommandsJSONBody,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import type { RestOrArray } from '../../util/normalizeArray.js';
|
||||||
|
import { normalizeArray } from '../../util/normalizeArray.js';
|
||||||
|
|
||||||
|
export interface CommandData
|
||||||
|
extends Partial<
|
||||||
|
Pick<
|
||||||
|
RESTPostAPIApplicationCommandsJSONBody,
|
||||||
|
'contexts' | 'default_member_permissions' | 'integration_types' | 'nsfw'
|
||||||
|
>
|
||||||
|
> {}
|
||||||
|
|
||||||
|
export abstract class CommandBuilder<Command extends RESTPostAPIApplicationCommandsJSONBody>
|
||||||
|
implements JSONEncodable<Command>
|
||||||
|
{
|
||||||
|
protected declare readonly data: CommandData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the contexts of this command.
|
||||||
|
*
|
||||||
|
* @param contexts - The contexts
|
||||||
|
*/
|
||||||
|
public setContexts(...contexts: RestOrArray<InteractionContextType>) {
|
||||||
|
this.data.contexts = normalizeArray(contexts);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the integration types of this command.
|
||||||
|
*
|
||||||
|
* @param integrationTypes - The integration types
|
||||||
|
*/
|
||||||
|
public setIntegrationTypes(...integrationTypes: RestOrArray<ApplicationIntegrationType>) {
|
||||||
|
this.data.integration_types = normalizeArray(integrationTypes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default permissions a member should have in order to run the command.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* You can set this to `'0'` to disable the command by default.
|
||||||
|
* @param permissions - The permissions bit field to set
|
||||||
|
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
||||||
|
*/
|
||||||
|
public setDefaultMemberPermissions(permissions: Permissions | bigint | number) {
|
||||||
|
this.data.default_member_permissions = typeof permissions === 'string' ? permissions : permissions.toString();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the default permissions a member should have in order to run the command.
|
||||||
|
*/
|
||||||
|
public clearDefaultMemberPermissions() {
|
||||||
|
this.data.default_member_permissions = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether this command is NSFW.
|
||||||
|
*
|
||||||
|
* @param nsfw - Whether this command is NSFW
|
||||||
|
*/
|
||||||
|
public setNSFW(nsfw = true) {
|
||||||
|
this.data.nsfw = nsfw;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public abstract toJSON(validationOverride?: boolean): Command;
|
||||||
|
}
|
||||||
64
packages/builders/src/interactions/commands/SharedName.ts
Normal file
64
packages/builders/src/interactions/commands/SharedName.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { LocaleString, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||||
|
|
||||||
|
export interface SharedNameData
|
||||||
|
extends Partial<Pick<RESTPostAPIApplicationCommandsJSONBody, 'name_localizations' | 'name'>> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds name and description symbols for chat input commands.
|
||||||
|
*/
|
||||||
|
export class SharedName {
|
||||||
|
protected readonly data: SharedNameData = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of this command.
|
||||||
|
*
|
||||||
|
* @param name - The name to use
|
||||||
|
*/
|
||||||
|
public setName(name: string): this {
|
||||||
|
this.data.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a name localization for this command.
|
||||||
|
*
|
||||||
|
* @param locale - The locale to set
|
||||||
|
* @param localizedName - The localized name for the given `locale`
|
||||||
|
*/
|
||||||
|
public setNameLocalization(locale: LocaleString, localizedName: string) {
|
||||||
|
this.data.name_localizations ??= {};
|
||||||
|
this.data.name_localizations[locale] = localizedName;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears a name localization for this command.
|
||||||
|
*
|
||||||
|
* @param locale - The locale to clear
|
||||||
|
*/
|
||||||
|
public clearNameLocalization(locale: LocaleString) {
|
||||||
|
this.data.name_localizations ??= {};
|
||||||
|
this.data.name_localizations[locale] = undefined;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name localizations for this command.
|
||||||
|
*
|
||||||
|
* @param localizedNames - The object of localized names to set
|
||||||
|
*/
|
||||||
|
public setNameLocalizations(localizedNames: Partial<Record<LocaleString, string>>) {
|
||||||
|
this.data.name_localizations = structuredClone(localizedNames);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all name localizations for this command.
|
||||||
|
*/
|
||||||
|
public clearNameLocalizations() {
|
||||||
|
this.data.name_localizations = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type { APIApplicationCommand, LocaleString } from 'discord-api-types/v10';
|
||||||
|
import type { SharedNameData } from './SharedName.js';
|
||||||
|
import { SharedName } from './SharedName.js';
|
||||||
|
|
||||||
|
export interface SharedNameAndDescriptionData
|
||||||
|
extends SharedNameData,
|
||||||
|
Partial<Pick<APIApplicationCommand, 'description_localizations' | 'description'>> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds name and description symbols for chat input commands.
|
||||||
|
*/
|
||||||
|
export class SharedNameAndDescription extends SharedName {
|
||||||
|
protected override readonly data: SharedNameAndDescriptionData = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the description of this command.
|
||||||
|
*
|
||||||
|
* @param description - The description to use
|
||||||
|
*/
|
||||||
|
public setDescription(description: string) {
|
||||||
|
this.data.description = description;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a description localization for this command.
|
||||||
|
*
|
||||||
|
* @param locale - The locale to set
|
||||||
|
* @param localizedDescription - The localized description for the given `locale`
|
||||||
|
*/
|
||||||
|
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string) {
|
||||||
|
this.data.description_localizations ??= {};
|
||||||
|
this.data.description_localizations[locale] = localizedDescription;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears a description localization for this command.
|
||||||
|
*
|
||||||
|
* @param locale - The locale to clear
|
||||||
|
*/
|
||||||
|
public clearDescriptionLocalization(locale: LocaleString) {
|
||||||
|
this.data.description_localizations ??= {};
|
||||||
|
this.data.description_localizations[locale] = undefined;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the description localizations for this command.
|
||||||
|
*
|
||||||
|
* @param localizedDescriptions - The object of localized descriptions to set
|
||||||
|
*/
|
||||||
|
public setDescriptionLocalizations(localizedDescriptions: Partial<Record<LocaleString, string>>) {
|
||||||
|
this.data.description_localizations = structuredClone(localizedDescriptions);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all description localizations for this command.
|
||||||
|
*/
|
||||||
|
public clearDescriptionLocalizations() {
|
||||||
|
this.data.description_localizations = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
ApplicationIntegrationType,
|
||||||
|
InteractionContextType,
|
||||||
|
ApplicationCommandOptionType,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import type { ZodTypeAny } from 'zod';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
|
||||||
|
import { ApplicationCommandOptionAllowedChannelTypes } from './mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
||||||
|
|
||||||
|
const namePredicate = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(32)
|
||||||
|
.regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u);
|
||||||
|
|
||||||
|
const descriptionPredicate = z.string().min(1).max(100);
|
||||||
|
|
||||||
|
const sharedNameAndDescriptionPredicate = z.object({
|
||||||
|
name: namePredicate,
|
||||||
|
name_localizations: localeMapPredicate.optional(),
|
||||||
|
description: descriptionPredicate,
|
||||||
|
description_localizations: localeMapPredicate.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const numericMixinNumberOptionPredicate = z.object({
|
||||||
|
max_value: z.number().safe().optional(),
|
||||||
|
min_value: z.number().safe().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const numericMixinIntegerOptionPredicate = z.object({
|
||||||
|
max_value: z.number().safe().int().optional(),
|
||||||
|
min_value: z.number().safe().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelMixinOptionPredicate = z.object({
|
||||||
|
channel_types: z
|
||||||
|
.union(
|
||||||
|
ApplicationCommandOptionAllowedChannelTypes.map((type) => z.literal(type)) as unknown as [
|
||||||
|
ZodTypeAny,
|
||||||
|
ZodTypeAny,
|
||||||
|
...ZodTypeAny[],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const autocompleteMixinOptionPredicate = z.object({
|
||||||
|
autocomplete: z.literal(true),
|
||||||
|
choices: z.union([z.never(), z.never().array(), z.undefined()]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const choiceValueStringPredicate = z.string().min(1).max(100);
|
||||||
|
const choiceValueNumberPredicate = z.number().safe();
|
||||||
|
const choiceBasePredicate = z.object({
|
||||||
|
name: choiceValueStringPredicate,
|
||||||
|
name_localizations: localeMapPredicate.optional(),
|
||||||
|
});
|
||||||
|
const choiceStringPredicate = choiceBasePredicate.extend({
|
||||||
|
value: choiceValueStringPredicate,
|
||||||
|
});
|
||||||
|
const choiceNumberPredicate = choiceBasePredicate.extend({
|
||||||
|
value: choiceValueNumberPredicate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const choiceBaseMixinPredicate = z.object({
|
||||||
|
autocomplete: z.literal(false).optional(),
|
||||||
|
});
|
||||||
|
const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({
|
||||||
|
choices: choiceStringPredicate.array().max(25).optional(),
|
||||||
|
});
|
||||||
|
const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({
|
||||||
|
choices: choiceNumberPredicate.array().max(25).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const basicOptionTypes = [
|
||||||
|
ApplicationCommandOptionType.Attachment,
|
||||||
|
ApplicationCommandOptionType.Boolean,
|
||||||
|
ApplicationCommandOptionType.Channel,
|
||||||
|
ApplicationCommandOptionType.Integer,
|
||||||
|
ApplicationCommandOptionType.Mentionable,
|
||||||
|
ApplicationCommandOptionType.Number,
|
||||||
|
ApplicationCommandOptionType.Role,
|
||||||
|
ApplicationCommandOptionType.String,
|
||||||
|
ApplicationCommandOptionType.User,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const basicOptionTypesPredicate = z.union(
|
||||||
|
basicOptionTypes.map((type) => z.literal(type)) as unknown as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({
|
||||||
|
required: z.boolean().optional(),
|
||||||
|
type: basicOptionTypesPredicate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
|
||||||
|
autocompleteMixinOptionPredicate,
|
||||||
|
choiceStringMixinPredicate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
|
||||||
|
autocompleteMixinOptionPredicate,
|
||||||
|
choiceNumberMixinPredicate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const channelOptionPredicate = basicOptionPredicate.merge(channelMixinOptionPredicate);
|
||||||
|
|
||||||
|
export const integerOptionPredicate = basicOptionPredicate
|
||||||
|
.merge(numericMixinIntegerOptionPredicate)
|
||||||
|
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
||||||
|
|
||||||
|
export const numberOptionPredicate = basicOptionPredicate
|
||||||
|
.merge(numericMixinNumberOptionPredicate)
|
||||||
|
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
||||||
|
|
||||||
|
export const stringOptionPredicate = basicOptionPredicate
|
||||||
|
.extend({
|
||||||
|
max_length: z.number().min(0).max(6_000).optional(),
|
||||||
|
min_length: z.number().min(1).max(6_000).optional(),
|
||||||
|
})
|
||||||
|
.and(autocompleteOrStringChoicesMixinOptionPredicate);
|
||||||
|
|
||||||
|
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
|
||||||
|
contexts: z.array(z.nativeEnum(InteractionContextType)).optional(),
|
||||||
|
default_member_permissions: memberPermissionsPredicate.optional(),
|
||||||
|
integration_types: z.array(z.nativeEnum(ApplicationIntegrationType)).optional(),
|
||||||
|
nsfw: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Because you can only add options via builders, there's no need to validate whole objects here otherwise
|
||||||
|
const chatInputCommandOptionsPredicate = z.union([
|
||||||
|
z.object({ type: basicOptionTypesPredicate }).array(),
|
||||||
|
z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(),
|
||||||
|
z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
|
||||||
|
options: chatInputCommandOptionsPredicate.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
|
||||||
|
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
|
||||||
|
options: z
|
||||||
|
.array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }))
|
||||||
|
.min(1)
|
||||||
|
.max(25),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
|
||||||
|
type: z.literal(ApplicationCommandOptionType.Subcommand),
|
||||||
|
options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25),
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { isValidationEnabled } from '../../../util/validation.js';
|
||||||
|
import { CommandBuilder } from '../Command.js';
|
||||||
|
import { SharedNameAndDescription } from '../SharedNameAndDescription.js';
|
||||||
|
import { chatInputCommandPredicate } from './Assertions.js';
|
||||||
|
import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js';
|
||||||
|
import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for chat input commands.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandBuilder extends Mixin(
|
||||||
|
CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>,
|
||||||
|
SharedChatInputCommandOptions,
|
||||||
|
SharedNameAndDescription,
|
||||||
|
SharedChatInputCommandSubcommands,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* {@inheritDoc CommandBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): RESTPostAPIChatInputApplicationCommandsJSONBody {
|
||||||
|
const { options, ...rest } = this.data;
|
||||||
|
|
||||||
|
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
|
||||||
|
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
|
||||||
|
type: ApplicationCommandType.ChatInput,
|
||||||
|
options: options?.map((option) => option.toJSON(validationOverride)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
chatInputCommandPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
|
import type {
|
||||||
|
APIApplicationCommandSubcommandOption,
|
||||||
|
APIApplicationCommandSubcommandGroupOption,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js';
|
||||||
|
import { resolveBuilder } from '../../../util/resolveBuilder.js';
|
||||||
|
import { isValidationEnabled } from '../../../util/validation.js';
|
||||||
|
import type { SharedNameAndDescriptionData } from '../SharedNameAndDescription.js';
|
||||||
|
import { SharedNameAndDescription } from '../SharedNameAndDescription.js';
|
||||||
|
import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js';
|
||||||
|
import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js';
|
||||||
|
|
||||||
|
export interface ChatInputCommandSubcommandGroupData {
|
||||||
|
options?: ChatInputCommandSubcommandBuilder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a folder for subcommands.
|
||||||
|
*
|
||||||
|
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandSubcommandGroupBuilder
|
||||||
|
extends SharedNameAndDescription
|
||||||
|
implements JSONEncodable<APIApplicationCommandSubcommandGroupOption>
|
||||||
|
{
|
||||||
|
protected declare readonly data: ChatInputCommandSubcommandGroupData & SharedNameAndDescriptionData;
|
||||||
|
|
||||||
|
public get options(): readonly ChatInputCommandSubcommandBuilder[] {
|
||||||
|
return (this.data.options ??= []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new subcommand to this group.
|
||||||
|
*
|
||||||
|
* @param input - A function that returns a subcommand builder or an already built builder
|
||||||
|
*/
|
||||||
|
public addSubcommands(
|
||||||
|
...input: RestOrArray<
|
||||||
|
| ChatInputCommandSubcommandBuilder
|
||||||
|
| ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||||
|
const result = normalized.map((builder) => resolveBuilder(builder, ChatInputCommandSubcommandBuilder));
|
||||||
|
|
||||||
|
this.data.options ??= [];
|
||||||
|
this.data.options.push(...result);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandGroupOption {
|
||||||
|
const { options, ...rest } = this.data;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandGroupOption, 'type'>),
|
||||||
|
type: ApplicationCommandOptionType.SubcommandGroup as const,
|
||||||
|
options: options?.map((option) => option.toJSON(validationOverride)) ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
chatInputCommandSubcommandGroupPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for chat input command subcommands.
|
||||||
|
*
|
||||||
|
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandSubcommandBuilder
|
||||||
|
extends Mixin(SharedNameAndDescription, SharedChatInputCommandOptions)
|
||||||
|
implements JSONEncodable<APIApplicationCommandSubcommandOption>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandOption {
|
||||||
|
const { options, ...rest } = this.data;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandOption, 'type'>),
|
||||||
|
type: ApplicationCommandOptionType.Subcommand as const,
|
||||||
|
options: options?.map((option) => option.toJSON(validationOverride)) ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
chatInputCommandSubcommandPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { APIApplicationCommandIntegerOption } from 'discord-api-types/v10';
|
||||||
|
|
||||||
|
export interface ApplicationCommandNumericOptionMinMaxValueData
|
||||||
|
extends Pick<APIApplicationCommandIntegerOption, 'max_value' | 'min_value'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds minimum and maximum symbols used for options.
|
||||||
|
*/
|
||||||
|
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
|
||||||
|
protected declare readonly data: ApplicationCommandNumericOptionMinMaxValueData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number value of this option.
|
||||||
|
*
|
||||||
|
* @param max - The maximum value this option can be
|
||||||
|
*/
|
||||||
|
public setMaxValue(max: number): this {
|
||||||
|
this.data.max_value = max;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the maximum number value of this option.
|
||||||
|
*/
|
||||||
|
public clearMaxValue(): this {
|
||||||
|
this.data.max_value = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the minimum number value of this option.
|
||||||
|
*
|
||||||
|
* @param min - The minimum value this option can be
|
||||||
|
*/
|
||||||
|
public setMinValue(min: number): this {
|
||||||
|
this.data.min_value = min;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the minimum number value of this option.
|
||||||
|
*/
|
||||||
|
public clearMinValue(): this {
|
||||||
|
this.data.min_value = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
|
||||||
|
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray';
|
||||||
|
|
||||||
|
export const ApplicationCommandOptionAllowedChannelTypes = [
|
||||||
|
ChannelType.GuildText,
|
||||||
|
ChannelType.GuildVoice,
|
||||||
|
ChannelType.GuildCategory,
|
||||||
|
ChannelType.GuildAnnouncement,
|
||||||
|
ChannelType.AnnouncementThread,
|
||||||
|
ChannelType.PublicThread,
|
||||||
|
ChannelType.PrivateThread,
|
||||||
|
ChannelType.GuildStageVoice,
|
||||||
|
ChannelType.GuildForum,
|
||||||
|
ChannelType.GuildMedia,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed channel types used for a channel option.
|
||||||
|
*/
|
||||||
|
export type ApplicationCommandOptionAllowedChannelTypes = (typeof ApplicationCommandOptionAllowedChannelTypes)[number];
|
||||||
|
|
||||||
|
export interface ApplicationCommandOptionChannelTypesData
|
||||||
|
extends Pick<APIApplicationCommandChannelOption, 'channel_types'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds channel type symbols used for options.
|
||||||
|
*/
|
||||||
|
export class ApplicationCommandOptionChannelTypesMixin {
|
||||||
|
protected declare readonly data: ApplicationCommandOptionChannelTypesData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds channel types to this option.
|
||||||
|
*
|
||||||
|
* @param channelTypes - The channel types
|
||||||
|
*/
|
||||||
|
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
|
||||||
|
this.data.channel_types ??= [];
|
||||||
|
this.data.channel_types.push(...normalizeArray(channelTypes));
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the channel types for this option.
|
||||||
|
*
|
||||||
|
* @param channelTypes - The channel types
|
||||||
|
*/
|
||||||
|
public setChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
|
||||||
|
this.data.channel_types = normalizeArray(channelTypes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type {
|
||||||
|
APIApplicationCommandIntegerOption,
|
||||||
|
APIApplicationCommandNumberOption,
|
||||||
|
APIApplicationCommandStringOption,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
|
||||||
|
export type AutocompletableOptions =
|
||||||
|
| APIApplicationCommandIntegerOption
|
||||||
|
| APIApplicationCommandNumberOption
|
||||||
|
| APIApplicationCommandStringOption;
|
||||||
|
|
||||||
|
export interface ApplicationCommandOptionWithAutocompleteData extends Pick<AutocompletableOptions, 'autocomplete'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds choices and autocomplete symbols used for options.
|
||||||
|
*/
|
||||||
|
export class ApplicationCommandOptionWithAutocompleteMixin {
|
||||||
|
protected declare readonly data: ApplicationCommandOptionWithAutocompleteData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this option uses autocomplete.
|
||||||
|
*
|
||||||
|
* @param autocomplete - Whether this option should use autocomplete
|
||||||
|
*/
|
||||||
|
public setAutocomplete(autocomplete = true): this {
|
||||||
|
this.data.autocomplete = autocomplete;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
|
||||||
|
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
|
||||||
|
|
||||||
|
// Unlike other places, we're not `Pick`ing from discord-api-types. The union includes `[]` and it breaks everything.
|
||||||
|
export interface ApplicationCommandOptionWithChoicesData {
|
||||||
|
choices?: APIApplicationCommandOptionChoice<number | string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds choices and autocomplete symbols used for options.
|
||||||
|
*/
|
||||||
|
export class ApplicationCommandOptionWithChoicesMixin<ChoiceType extends number | string> {
|
||||||
|
protected declare readonly data: ApplicationCommandOptionWithChoicesData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple choices to this option.
|
||||||
|
*
|
||||||
|
* @param choices - The choices to add
|
||||||
|
*/
|
||||||
|
public addChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
|
||||||
|
const normalizedChoices = normalizeArray(choices);
|
||||||
|
|
||||||
|
this.data.choices ??= [];
|
||||||
|
this.data.choices.push(...normalizedChoices);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets multiple choices for this option.
|
||||||
|
*
|
||||||
|
* @param choices - The choices to set
|
||||||
|
*/
|
||||||
|
public setChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
|
||||||
|
this.data.choices = normalizeArray(choices);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
|
||||||
|
import { resolveBuilder } from '../../../../util/resolveBuilder.js';
|
||||||
|
import type { ApplicationCommandOptionBase } from '../options/ApplicationCommandOptionBase.js';
|
||||||
|
import { ChatInputCommandAttachmentOption } from '../options/attachment.js';
|
||||||
|
import { ChatInputCommandBooleanOption } from '../options/boolean.js';
|
||||||
|
import { ChatInputCommandChannelOption } from '../options/channel.js';
|
||||||
|
import { ChatInputCommandIntegerOption } from '../options/integer.js';
|
||||||
|
import { ChatInputCommandMentionableOption } from '../options/mentionable.js';
|
||||||
|
import { ChatInputCommandNumberOption } from '../options/number.js';
|
||||||
|
import { ChatInputCommandRoleOption } from '../options/role.js';
|
||||||
|
import { ChatInputCommandStringOption } from '../options/string.js';
|
||||||
|
import { ChatInputCommandUserOption } from '../options/user.js';
|
||||||
|
|
||||||
|
export interface SharedChatInputCommandOptionsData {
|
||||||
|
options?: ApplicationCommandOptionBase[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds symbols that can be shared in chat input command options.
|
||||||
|
*
|
||||||
|
* @typeParam TypeAfterAddingOptions - The type this class should return after adding an option.
|
||||||
|
*/
|
||||||
|
export class SharedChatInputCommandOptions {
|
||||||
|
protected declare readonly data: SharedChatInputCommandOptionsData;
|
||||||
|
|
||||||
|
public get options(): readonly ApplicationCommandOptionBase[] {
|
||||||
|
return (this.data.options ??= []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds boolean options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addBooleanOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandBooleanOption | ((builder: ChatInputCommandBooleanOption) => ChatInputCommandBooleanOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandBooleanOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds user options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addUserOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandUserOption | ((builder: ChatInputCommandUserOption) => ChatInputCommandUserOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandUserOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds channel options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addChannelOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandChannelOption | ((builder: ChatInputCommandChannelOption) => ChatInputCommandChannelOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandChannelOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds role options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addRoleOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandRoleOption | ((builder: ChatInputCommandRoleOption) => ChatInputCommandRoleOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandRoleOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds attachment options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addAttachmentOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
| ChatInputCommandAttachmentOption
|
||||||
|
| ((builder: ChatInputCommandAttachmentOption) => ChatInputCommandAttachmentOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandAttachmentOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds mentionable options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addMentionableOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
| ChatInputCommandMentionableOption
|
||||||
|
| ((builder: ChatInputCommandMentionableOption) => ChatInputCommandMentionableOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandMentionableOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds string options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addStringOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandStringOption | ((builder: ChatInputCommandStringOption) => ChatInputCommandStringOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandStringOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds integer options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addIntegerOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandIntegerOption | ((builder: ChatInputCommandIntegerOption) => ChatInputCommandIntegerOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandIntegerOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds number options.
|
||||||
|
*
|
||||||
|
* @param options - Options to add
|
||||||
|
*/
|
||||||
|
public addNumberOptions(
|
||||||
|
...options: RestOrArray<
|
||||||
|
ChatInputCommandNumberOption | ((builder: ChatInputCommandNumberOption) => ChatInputCommandNumberOption)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
return this.sharedAddOptions(ChatInputCommandNumberOption, ...options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes, replaces, or inserts options for this command.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This method behaves similarly
|
||||||
|
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||||
|
*
|
||||||
|
* It's useful for modifying and adjusting order of the already-existing options for this command.
|
||||||
|
* @example
|
||||||
|
* Remove the first option:
|
||||||
|
* ```ts
|
||||||
|
* actionRow.spliceOptions(0, 1);
|
||||||
|
* ```
|
||||||
|
* @example
|
||||||
|
* Remove the first n options:
|
||||||
|
* ```ts
|
||||||
|
* const n = 4;
|
||||||
|
* actionRow.spliceOptions(0, n);
|
||||||
|
* ```
|
||||||
|
* @example
|
||||||
|
* Remove the last option:
|
||||||
|
* ```ts
|
||||||
|
* actionRow.spliceOptions(-1, 1);
|
||||||
|
* ```
|
||||||
|
* @param index - The index to start at
|
||||||
|
* @param deleteCount - The number of options to remove
|
||||||
|
* @param options - The replacing option objects
|
||||||
|
*/
|
||||||
|
public spliceOptions(index: number, deleteCount: number, ...options: ApplicationCommandOptionBase[]): this {
|
||||||
|
this.data.options ??= [];
|
||||||
|
this.data.options.splice(index, deleteCount, ...options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the actual adding magic happens. ✨
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private sharedAddOptions<OptionBuilder extends ApplicationCommandOptionBase>(
|
||||||
|
Instance: new () => OptionBuilder,
|
||||||
|
...options: RestOrArray<OptionBuilder | ((builder: OptionBuilder) => OptionBuilder)>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(options);
|
||||||
|
const resolved = normalized.map((option) => resolveBuilder(option, Instance));
|
||||||
|
|
||||||
|
this.data.options ??= [];
|
||||||
|
this.data.options.push(...resolved);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { RestOrArray } from '../../../../util/normalizeArray.js';
|
||||||
|
import { normalizeArray } from '../../../../util/normalizeArray.js';
|
||||||
|
import { resolveBuilder } from '../../../../util/resolveBuilder.js';
|
||||||
|
import {
|
||||||
|
ChatInputCommandSubcommandGroupBuilder,
|
||||||
|
ChatInputCommandSubcommandBuilder,
|
||||||
|
} from '../ChatInputCommandSubcommands.js';
|
||||||
|
|
||||||
|
export interface SharedChatInputCommandSubcommandsData {
|
||||||
|
options?: (ChatInputCommandSubcommandBuilder | ChatInputCommandSubcommandGroupBuilder)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin holds symbols that can be shared in chat input subcommands.
|
||||||
|
*
|
||||||
|
* @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group.
|
||||||
|
*/
|
||||||
|
export class SharedChatInputCommandSubcommands {
|
||||||
|
protected declare readonly data: SharedChatInputCommandSubcommandsData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds subcommand groups to this command.
|
||||||
|
*
|
||||||
|
* @param input - Subcommand groups to add
|
||||||
|
*/
|
||||||
|
public addSubcommandGroups(
|
||||||
|
...input: RestOrArray<
|
||||||
|
| ChatInputCommandSubcommandGroupBuilder
|
||||||
|
| ((subcommandGroup: ChatInputCommandSubcommandGroupBuilder) => ChatInputCommandSubcommandGroupBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandGroupBuilder));
|
||||||
|
|
||||||
|
this.data.options ??= [];
|
||||||
|
this.data.options.push(...resolved);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds subcommands to this command.
|
||||||
|
*
|
||||||
|
* @param input - Subcommands to add
|
||||||
|
*/
|
||||||
|
public addSubcommands(
|
||||||
|
...input: RestOrArray<
|
||||||
|
| ChatInputCommandSubcommandBuilder
|
||||||
|
| ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder)
|
||||||
|
>
|
||||||
|
): this {
|
||||||
|
const normalized = normalizeArray(input);
|
||||||
|
const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandBuilder));
|
||||||
|
|
||||||
|
this.data.options ??= [];
|
||||||
|
this.data.options.push(...resolved);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
|
import type {
|
||||||
|
APIApplicationCommandBasicOption,
|
||||||
|
APIApplicationCommandOption,
|
||||||
|
ApplicationCommandOptionType,
|
||||||
|
} from 'discord-api-types/v10';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
import { isValidationEnabled } from '../../../../util/validation.js';
|
||||||
|
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
|
||||||
|
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
|
||||||
|
import { basicOptionPredicate } from '../Assertions.js';
|
||||||
|
|
||||||
|
export interface ApplicationCommandOptionBaseData extends Partial<Pick<APIApplicationCommandOption, 'required'>> {
|
||||||
|
type: ApplicationCommandOptionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base application command option builder that contains common symbols for application command builders.
|
||||||
|
*/
|
||||||
|
export abstract class ApplicationCommandOptionBase
|
||||||
|
extends SharedNameAndDescription
|
||||||
|
implements JSONEncodable<APIApplicationCommandBasicOption>
|
||||||
|
{
|
||||||
|
protected static readonly predicate: z.ZodTypeAny = basicOptionPredicate;
|
||||||
|
|
||||||
|
protected declare readonly data: ApplicationCommandOptionBaseData & SharedNameAndDescriptionData;
|
||||||
|
|
||||||
|
public constructor(type: ApplicationCommandOptionType) {
|
||||||
|
super();
|
||||||
|
this.data.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether this option is required.
|
||||||
|
*
|
||||||
|
* @param required - Whether this option should be required
|
||||||
|
*/
|
||||||
|
public setRequired(required = true) {
|
||||||
|
this.data.required = required;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIApplicationCommandBasicOption {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
(this.constructor as typeof ApplicationCommandOptionBase).predicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIApplicationCommandBasicOption;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command attachment option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase {
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command boolean option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase {
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { channelOptionPredicate } from '../Assertions.js';
|
||||||
|
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command channel option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandChannelOption extends Mixin(
|
||||||
|
ApplicationCommandOptionBase,
|
||||||
|
ApplicationCommandOptionChannelTypesMixin,
|
||||||
|
) {
|
||||||
|
protected static override readonly predicate = channelOptionPredicate;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { integerOptionPredicate } from '../Assertions.js';
|
||||||
|
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
||||||
|
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
||||||
|
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command integer option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandIntegerOption extends Mixin(
|
||||||
|
ApplicationCommandOptionBase,
|
||||||
|
ApplicationCommandNumericOptionMinMaxValueMixin,
|
||||||
|
ApplicationCommandOptionWithAutocompleteMixin,
|
||||||
|
ApplicationCommandOptionWithChoicesMixin<number>,
|
||||||
|
) {
|
||||||
|
protected static override readonly predicate = integerOptionPredicate;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Integer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command mentionable option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase {
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Mentionable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { numberOptionPredicate } from '../Assertions.js';
|
||||||
|
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
||||||
|
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
||||||
|
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command number option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandNumberOption extends Mixin(
|
||||||
|
ApplicationCommandOptionBase,
|
||||||
|
ApplicationCommandNumericOptionMinMaxValueMixin,
|
||||||
|
ApplicationCommandOptionWithAutocompleteMixin,
|
||||||
|
ApplicationCommandOptionWithChoicesMixin<number>,
|
||||||
|
) {
|
||||||
|
protected static override readonly predicate = numberOptionPredicate;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Number);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command role option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase {
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.Role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { stringOptionPredicate } from '../Assertions.js';
|
||||||
|
import type { ApplicationCommandOptionWithAutocompleteData } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
||||||
|
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
||||||
|
import type { ApplicationCommandOptionWithChoicesData } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
||||||
|
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
import type { ApplicationCommandOptionBaseData } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command string option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandStringOption extends Mixin(
|
||||||
|
ApplicationCommandOptionBase,
|
||||||
|
ApplicationCommandOptionWithAutocompleteMixin,
|
||||||
|
ApplicationCommandOptionWithChoicesMixin<string>,
|
||||||
|
) {
|
||||||
|
protected static override readonly predicate = stringOptionPredicate;
|
||||||
|
|
||||||
|
protected declare readonly data: ApplicationCommandOptionBaseData &
|
||||||
|
ApplicationCommandOptionWithAutocompleteData &
|
||||||
|
ApplicationCommandOptionWithChoicesData &
|
||||||
|
Partial<Pick<APIApplicationCommandStringOption, 'max_length' | 'min_length'>>;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum length of this string option.
|
||||||
|
*
|
||||||
|
* @param max - The maximum length this option can be
|
||||||
|
*/
|
||||||
|
public setMaxLength(max: number): this {
|
||||||
|
this.data.max_length = max;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the maximum length of this string option.
|
||||||
|
*/
|
||||||
|
public clearMaxLength(): this {
|
||||||
|
this.data.max_length = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the minimum length of this string option.
|
||||||
|
*
|
||||||
|
* @param min - The minimum length this option can be
|
||||||
|
*/
|
||||||
|
public setMinLength(min: number): this {
|
||||||
|
this.data.min_length = min;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the minimum length of this string option.
|
||||||
|
*/
|
||||||
|
public clearMinLength(): this {
|
||||||
|
this.data.min_length = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
||||||
|
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chat input command user option.
|
||||||
|
*/
|
||||||
|
export class ChatInputCommandUserOption extends ApplicationCommandOptionBase {
|
||||||
|
public constructor() {
|
||||||
|
super(ApplicationCommandOptionType.User);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
|
||||||
|
|
||||||
|
const namePredicate = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(32)
|
||||||
|
// eslint-disable-next-line prefer-named-capture-group
|
||||||
|
.regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u);
|
||||||
|
|
||||||
|
const contextsPredicate = z.array(z.nativeEnum(InteractionContextType));
|
||||||
|
const integrationTypesPredicate = z.array(z.nativeEnum(ApplicationIntegrationType));
|
||||||
|
|
||||||
|
const baseContextMenuCommandPredicate = z.object({
|
||||||
|
contexts: contextsPredicate.optional(),
|
||||||
|
default_member_permissions: memberPermissionsPredicate.optional(),
|
||||||
|
name: namePredicate,
|
||||||
|
name_localizations: localeMapPredicate.optional(),
|
||||||
|
integration_types: integrationTypesPredicate.optional(),
|
||||||
|
nsfw: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const userCommandPredicate = baseContextMenuCommandPredicate.extend({
|
||||||
|
type: z.literal(ApplicationCommandType.User),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const messageCommandPredicate = baseContextMenuCommandPredicate.extend({
|
||||||
|
type: z.literal(ApplicationCommandType.Message),
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ApplicationCommandType, RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||||
|
import { Mixin } from 'ts-mixer';
|
||||||
|
import { CommandBuilder } from '../Command.js';
|
||||||
|
import { SharedName } from '../SharedName.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type a context menu command can be.
|
||||||
|
*/
|
||||||
|
export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for context menu commands.
|
||||||
|
*/
|
||||||
|
export abstract class ContextMenuCommandBuilder extends Mixin(
|
||||||
|
CommandBuilder<RESTPostAPIContextMenuApplicationCommandsJSONBody>,
|
||||||
|
SharedName,
|
||||||
|
) {
|
||||||
|
protected override readonly data: Partial<RESTPostAPIContextMenuApplicationCommandsJSONBody>;
|
||||||
|
|
||||||
|
public constructor(data: Partial<RESTPostAPIContextMenuApplicationCommandsJSONBody> = {}) {
|
||||||
|
super();
|
||||||
|
this.data = structuredClone(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc CommandBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public abstract override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||||
|
import { isValidationEnabled } from '../../../util/validation.js';
|
||||||
|
import { messageCommandPredicate } from './Assertions.js';
|
||||||
|
import { ContextMenuCommandBuilder } from './ContextMenuCommand.js';
|
||||||
|
|
||||||
|
export class MessageContextCommandBuilder extends ContextMenuCommandBuilder {
|
||||||
|
/**
|
||||||
|
* {@inheritDoc CommandBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody {
|
||||||
|
const data = { ...structuredClone(this.data), type: ApplicationCommandType.Message };
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
messageCommandPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as RESTPostAPIContextMenuApplicationCommandsJSONBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10';
|
||||||
|
import { isValidationEnabled } from '../../../util/validation.js';
|
||||||
|
import { userCommandPredicate } from './Assertions.js';
|
||||||
|
import { ContextMenuCommandBuilder } from './ContextMenuCommand.js';
|
||||||
|
|
||||||
|
export class UserContextCommandBuilder extends ContextMenuCommandBuilder {
|
||||||
|
/**
|
||||||
|
* {@inheritDoc CommandBuilder.toJSON}
|
||||||
|
*/
|
||||||
|
public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody {
|
||||||
|
const data = { ...structuredClone(this.data), type: ApplicationCommandType.User };
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
userCommandPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as RESTPostAPIContextMenuApplicationCommandsJSONBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10';
|
|
||||||
import { isValidationEnabled } from '../../util/validation.js';
|
|
||||||
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder.js';
|
|
||||||
|
|
||||||
const namePredicate = s
|
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(32)
|
|
||||||
// eslint-disable-next-line prefer-named-capture-group
|
|
||||||
.regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
const typePredicate = s
|
|
||||||
.union([s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message)])
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
const booleanPredicate = s.boolean();
|
|
||||||
|
|
||||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
|
||||||
booleanPredicate.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateName(name: unknown): asserts name is string {
|
|
||||||
namePredicate.parse(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateType(type: unknown): asserts type is ContextMenuCommandType {
|
|
||||||
typePredicate.parse(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateRequiredParameters(name: string, type: number) {
|
|
||||||
// Assert name matches all conditions
|
|
||||||
validateName(name);
|
|
||||||
|
|
||||||
// Assert type is valid
|
|
||||||
validateType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmPermissionPredicate = s.boolean().nullish();
|
|
||||||
|
|
||||||
export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined {
|
|
||||||
dmPermissionPredicate.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberPermissionPredicate = s
|
|
||||||
.union([
|
|
||||||
s.bigint().transform((value) => value.toString()),
|
|
||||||
s
|
|
||||||
.number()
|
|
||||||
.safeInt()
|
|
||||||
.transform((value) => value.toString()),
|
|
||||||
s.string().regex(/^\d+$/),
|
|
||||||
])
|
|
||||||
.nullish();
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
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,
|
|
||||||
validateName,
|
|
||||||
validateType,
|
|
||||||
validateDefaultPermission,
|
|
||||||
validateDefaultMemberPermissions,
|
|
||||||
validateDMPermission,
|
|
||||||
contextsPredicate,
|
|
||||||
integrationTypesPredicate,
|
|
||||||
} from './Assertions.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type a context menu command can be.
|
|
||||||
*/
|
|
||||||
export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A builder that creates API-compatible JSON data for context menu commands.
|
|
||||||
*/
|
|
||||||
export class ContextMenuCommandBuilder {
|
|
||||||
/**
|
|
||||||
* The name of this command.
|
|
||||||
*/
|
|
||||||
public readonly name: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name localizations of this command.
|
|
||||||
*/
|
|
||||||
public readonly name_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of this command.
|
|
||||||
*/
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead.
|
|
||||||
*/
|
|
||||||
public readonly default_permission: boolean | undefined = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The set of permissions represented as a bit set for the command.
|
|
||||||
*/
|
|
||||||
public readonly default_member_permissions: Permissions | null | undefined = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the command is available in direct messages with the application.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* By default, commands are visible. This property is only for global commands.
|
|
||||||
* @deprecated
|
|
||||||
* Use {@link ContextMenuCommandBuilder.contexts} instead.
|
|
||||||
*/
|
|
||||||
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<InteractionContextType>) {
|
|
||||||
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<ApplicationIntegrationType>) {
|
|
||||||
Reflect.set(this, 'integration_types', integrationTypesPredicate.parse(normalizeArray(integrationTypes)));
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the name of this command.
|
|
||||||
*
|
|
||||||
* @param name - The name to use
|
|
||||||
*/
|
|
||||||
public setName(name: string) {
|
|
||||||
// Assert the name matches the conditions
|
|
||||||
validateName(name);
|
|
||||||
|
|
||||||
Reflect.set(this, 'name', name);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the type of this command.
|
|
||||||
*
|
|
||||||
* @param type - The type to use
|
|
||||||
*/
|
|
||||||
public setType(type: ContextMenuCommandType) {
|
|
||||||
// Assert the type is valid
|
|
||||||
validateType(type);
|
|
||||||
|
|
||||||
Reflect.set(this, 'type', type);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the command is enabled by default when the application is added to a guild.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* If set to `false`, you will have to later `PUT` the permissions for this command.
|
|
||||||
* @param value - Whether to enable this command by default
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
|
||||||
* @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead.
|
|
||||||
*/
|
|
||||||
public setDefaultPermission(value: boolean) {
|
|
||||||
// Assert the value matches the conditions
|
|
||||||
validateDefaultPermission(value);
|
|
||||||
|
|
||||||
Reflect.set(this, 'default_permission', value);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the default permissions a member should have in order to run this command.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* You can set this to `'0'` to disable the command by default.
|
|
||||||
* @param permissions - The permissions bit field to set
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
|
||||||
*/
|
|
||||||
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
|
|
||||||
// Assert the value and parse it
|
|
||||||
const permissionValue = validateDefaultMemberPermissions(permissions);
|
|
||||||
|
|
||||||
Reflect.set(this, 'default_member_permissions', permissionValue);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets if the command is available in direct messages with the application.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* By default, commands are visible. This method is only for global commands.
|
|
||||||
* @param enabled - Whether the command should be enabled in direct messages
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
|
||||||
* @deprecated Use {@link ContextMenuCommandBuilder.setContexts} instead.
|
|
||||||
*/
|
|
||||||
public setDMPermission(enabled: boolean | null | undefined) {
|
|
||||||
// Assert the value matches the conditions
|
|
||||||
validateDMPermission(enabled);
|
|
||||||
|
|
||||||
Reflect.set(this, 'dm_permission', enabled);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a name localization for this command.
|
|
||||||
*
|
|
||||||
* @param locale - The locale to set
|
|
||||||
* @param localizedName - The localized name for the given `locale`
|
|
||||||
*/
|
|
||||||
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
|
|
||||||
if (!this.name_localizations) {
|
|
||||||
Reflect.set(this, 'name_localizations', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedLocale = validateLocale(locale);
|
|
||||||
|
|
||||||
if (localizedName === null) {
|
|
||||||
this.name_localizations![parsedLocale] = null;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateName(localizedName);
|
|
||||||
|
|
||||||
this.name_localizations![parsedLocale] = localizedName;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the name localizations for this command.
|
|
||||||
*
|
|
||||||
* @param localizedNames - The object of localized names to set
|
|
||||||
*/
|
|
||||||
public setNameLocalizations(localizedNames: LocalizationMap | null) {
|
|
||||||
if (localizedNames === null) {
|
|
||||||
Reflect.set(this, 'name_localizations', null);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Reflect.set(this, 'name_localizations', {});
|
|
||||||
|
|
||||||
for (const args of Object.entries(localizedNames))
|
|
||||||
this.setNameLocalization(...(args as [LocaleString, string | null]));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes this builder to API-compatible JSON data.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method runs validations on the data before serializing it.
|
|
||||||
* As such, it may throw an error if the data is invalid.
|
|
||||||
*/
|
|
||||||
public toJSON(): RESTPostAPIContextMenuApplicationCommandsJSONBody {
|
|
||||||
validateRequiredParameters(this.name, this.type);
|
|
||||||
|
|
||||||
validateLocalizationMap(this.name_localizations);
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,21 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
import { ComponentType } from 'discord-api-types/v10';
|
||||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
import { z } from 'zod';
|
||||||
import { customIdValidator } from '../../components/Assertions.js';
|
import { customIdPredicate } from '../../Assertions.js';
|
||||||
import { isValidationEnabled } from '../../util/validation.js';
|
|
||||||
|
|
||||||
export const titleValidator = s
|
const titlePredicate = z.string().min(1).max(45);
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(45)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
export const componentsValidator = s
|
|
||||||
.instance(ActionRowBuilder)
|
|
||||||
.array()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export function validateRequiredParameters(
|
export const modalPredicate = z.object({
|
||||||
customId?: string,
|
title: titlePredicate,
|
||||||
title?: string,
|
custom_id: customIdPredicate,
|
||||||
components?: ActionRowBuilder<ModalActionRowComponentBuilder>[],
|
components: z
|
||||||
) {
|
.object({
|
||||||
customIdValidator.parse(customId);
|
type: z.literal(ComponentType.ActionRow),
|
||||||
titleValidator.parse(title);
|
components: z
|
||||||
componentsValidator.parse(components);
|
.object({ type: z.literal(ComponentType.TextInput) })
|
||||||
}
|
.array()
|
||||||
|
.length(1),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.max(5),
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ import type {
|
|||||||
APIModalActionRowComponent,
|
APIModalActionRowComponent,
|
||||||
APIModalInteractionResponseCallbackData,
|
APIModalInteractionResponseCallbackData,
|
||||||
} from 'discord-api-types/v10';
|
} from 'discord-api-types/v10';
|
||||||
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js';
|
import { ActionRowBuilder } from '../../components/ActionRow.js';
|
||||||
import { customIdValidator } from '../../components/Assertions.js';
|
|
||||||
import { createComponentBuilder } from '../../components/Components.js';
|
import { createComponentBuilder } from '../../components/Components.js';
|
||||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
||||||
import { titleValidator, validateRequiredParameters } from './Assertions.js';
|
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||||
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { modalPredicate } from './Assertions.js';
|
||||||
|
|
||||||
|
export interface ModalBuilderData extends Partial<Omit<APIModalInteractionResponseCallbackData, 'components'>> {
|
||||||
|
components: ActionRowBuilder[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for modals.
|
* A builder that creates API-compatible JSON data for modals.
|
||||||
@@ -19,22 +24,25 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
|||||||
/**
|
/**
|
||||||
* The API data associated with this modal.
|
* The API data associated with this modal.
|
||||||
*/
|
*/
|
||||||
public readonly data: Partial<APIModalInteractionResponseCallbackData>;
|
private readonly data: ModalBuilderData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The components within this modal.
|
* The components within this modal.
|
||||||
*/
|
*/
|
||||||
public readonly components: ActionRowBuilder<ModalActionRowComponentBuilder>[] = [];
|
public get components(): readonly ActionRowBuilder[] {
|
||||||
|
return this.data.components;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new modal from API data.
|
* Creates a new modal from API data.
|
||||||
*
|
*
|
||||||
* @param data - The API data to create this modal with
|
* @param data - The API data to create this modal with
|
||||||
*/
|
*/
|
||||||
public constructor({ components, ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
public constructor({ components = [], ...data }: Partial<APIModalInteractionResponseCallbackData> = {}) {
|
||||||
this.data = { ...data };
|
this.data = {
|
||||||
this.components = (components?.map((component) => createComponentBuilder(component)) ??
|
...structuredClone(data),
|
||||||
[]) as ActionRowBuilder<ModalActionRowComponentBuilder>[];
|
components: components.map((component) => createComponentBuilder(component)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +51,7 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
|||||||
* @param title - The title to use
|
* @param title - The title to use
|
||||||
*/
|
*/
|
||||||
public setTitle(title: string) {
|
public setTitle(title: string) {
|
||||||
this.data.title = titleValidator.parse(title);
|
this.data.title = title;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,49 +61,111 @@ export class ModalBuilder implements JSONEncodable<APIModalInteractionResponseCa
|
|||||||
* @param customId - The custom id to use
|
* @param customId - The custom id to use
|
||||||
*/
|
*/
|
||||||
public setCustomId(customId: string) {
|
public setCustomId(customId: string) {
|
||||||
this.data.custom_id = customIdValidator.parse(customId);
|
this.data.custom_id = customId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds components to this modal.
|
* Adds action rows to this modal.
|
||||||
*
|
*
|
||||||
* @param components - The components to add
|
* @param components - The components to add
|
||||||
*/
|
*/
|
||||||
public addComponents(
|
public addActionRows(
|
||||||
...components: RestOrArray<
|
...components: RestOrArray<
|
||||||
ActionRowBuilder<ModalActionRowComponentBuilder> | APIActionRowComponent<APIModalActionRowComponent>
|
| ActionRowBuilder
|
||||||
|
| APIActionRowComponent<APIModalActionRowComponent>
|
||||||
|
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
this.components.push(
|
const normalized = normalizeArray(components);
|
||||||
...normalizeArray(components).map((component) =>
|
const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder));
|
||||||
component instanceof ActionRowBuilder
|
|
||||||
? component
|
this.data.components.push(...resolved);
|
||||||
: new ActionRowBuilder<ModalActionRowComponentBuilder>(component),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets components for this modal.
|
* Sets the action rows for this modal.
|
||||||
*
|
*
|
||||||
* @param components - The components to set
|
* @param components - The components to set
|
||||||
*/
|
*/
|
||||||
public setComponents(...components: RestOrArray<ActionRowBuilder<ModalActionRowComponentBuilder>>) {
|
public setActionRows(
|
||||||
this.components.splice(0, this.components.length, ...normalizeArray(components));
|
...components: RestOrArray<
|
||||||
|
| ActionRowBuilder
|
||||||
|
| APIActionRowComponent<APIModalActionRowComponent>
|
||||||
|
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const normalized = normalizeArray(components);
|
||||||
|
this.spliceActionRows(0, this.data.components.length, ...normalized);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc ComponentBuilder.toJSON}
|
* Removes, replaces, or inserts action rows for this modal.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This method behaves similarly
|
||||||
|
* to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}.
|
||||||
|
* The maximum amount of action rows that can be added is 5.
|
||||||
|
*
|
||||||
|
* It's useful for modifying and adjusting order of the already-existing action rows of a modal.
|
||||||
|
* @example
|
||||||
|
* Remove the first action row:
|
||||||
|
* ```ts
|
||||||
|
* embed.spliceActionRows(0, 1);
|
||||||
|
* ```
|
||||||
|
* @example
|
||||||
|
* Remove the first n action rows:
|
||||||
|
* ```ts
|
||||||
|
* const n = 4;
|
||||||
|
* embed.spliceActionRows(0, n);
|
||||||
|
* ```
|
||||||
|
* @example
|
||||||
|
* Remove the last action row:
|
||||||
|
* ```ts
|
||||||
|
* embed.spliceActionRows(-1, 1);
|
||||||
|
* ```
|
||||||
|
* @param index - The index to start at
|
||||||
|
* @param deleteCount - The number of action rows to remove
|
||||||
|
* @param rows - The replacing action row objects
|
||||||
*/
|
*/
|
||||||
public toJSON(): APIModalInteractionResponseCallbackData {
|
public spliceActionRows(
|
||||||
validateRequiredParameters(this.data.custom_id, this.data.title, this.components);
|
index: number,
|
||||||
|
deleteCount: number,
|
||||||
|
...rows: (
|
||||||
|
| ActionRowBuilder
|
||||||
|
| APIActionRowComponent<APIModalActionRowComponent>
|
||||||
|
| ((builder: ActionRowBuilder) => ActionRowBuilder)
|
||||||
|
)[]
|
||||||
|
): this {
|
||||||
|
const resolved = rows.map((row) => resolveBuilder(row, ActionRowBuilder));
|
||||||
|
this.data.components.splice(index, deleteCount, ...resolved);
|
||||||
|
|
||||||
return {
|
return this;
|
||||||
...this.data,
|
}
|
||||||
components: this.components.map((component) => component.toJSON()),
|
|
||||||
} as APIModalInteractionResponseCallbackData;
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIModalInteractionResponseCallbackData {
|
||||||
|
const { components, ...rest } = this.data;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...structuredClone(rest),
|
||||||
|
components: components.map((component) => component.toJSON(validationOverride)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
modalPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as APIModalInteractionResponseCallbackData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
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';
|
|
||||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
const namePredicate = s
|
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(32)
|
|
||||||
.regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export function validateName(name: unknown): asserts name is string {
|
|
||||||
namePredicate.parse(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptionPredicate = s
|
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(100)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
const localePredicate = s.nativeEnum(Locale);
|
|
||||||
|
|
||||||
export function validateDescription(description: unknown): asserts description is string {
|
|
||||||
descriptionPredicate.parse(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxArrayLengthPredicate = s.unknown().array().lengthLessThanOrEqual(25).setValidationEnabled(isValidationEnabled);
|
|
||||||
export function validateLocale(locale: unknown) {
|
|
||||||
return localePredicate.parse(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] {
|
|
||||||
maxArrayLengthPredicate.parse(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateRequiredParameters(
|
|
||||||
name: string,
|
|
||||||
description: string,
|
|
||||||
options: ToAPIApplicationCommandOptions[],
|
|
||||||
) {
|
|
||||||
// Assert name matches all conditions
|
|
||||||
validateName(name);
|
|
||||||
|
|
||||||
// Assert description conditions
|
|
||||||
validateDescription(description);
|
|
||||||
|
|
||||||
// Assert options conditions
|
|
||||||
validateMaxOptionsLength(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
const booleanPredicate = s.boolean();
|
|
||||||
|
|
||||||
export function validateDefaultPermission(value: unknown): asserts value is boolean {
|
|
||||||
booleanPredicate.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateRequired(required: unknown): asserts required is boolean {
|
|
||||||
booleanPredicate.parse(required);
|
|
||||||
}
|
|
||||||
|
|
||||||
const choicesLengthPredicate = s.number().lessThanOrEqual(25).setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
|
|
||||||
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function assertReturnOfBuilder<
|
|
||||||
ReturnType extends ApplicationCommandOptionBase | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder,
|
|
||||||
>(input: unknown, ExpectedInstanceOf: new () => ReturnType): asserts input is ReturnType {
|
|
||||||
s.instance(ExpectedInstanceOf).parse(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const localizationMapPredicate = s
|
|
||||||
.object<LocalizationMap>(Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string().nullish()])))
|
|
||||||
.strict()
|
|
||||||
.nullish()
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export function validateLocalizationMap(value: unknown): asserts value is LocalizationMap {
|
|
||||||
localizationMapPredicate.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dmPermissionPredicate = s.boolean().nullish();
|
|
||||||
|
|
||||||
export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined {
|
|
||||||
dmPermissionPredicate.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberPermissionPredicate = s
|
|
||||||
.union([
|
|
||||||
s.bigint().transform((value) => value.toString()),
|
|
||||||
s
|
|
||||||
.number()
|
|
||||||
.safeInt()
|
|
||||||
.transform((value) => value.toString()),
|
|
||||||
s.string().regex(/^\d+$/),
|
|
||||||
])
|
|
||||||
.nullish();
|
|
||||||
|
|
||||||
export function validateDefaultMemberPermissions(permissions: unknown) {
|
|
||||||
return memberPermissionPredicate.parse(permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
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';
|
|
||||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js';
|
|
||||||
import { SharedSlashCommandSubcommands } from './mixins/SharedSubcommands.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A builder that creates API-compatible JSON data for slash commands.
|
|
||||||
*/
|
|
||||||
@mix(SharedSlashCommandOptions, SharedNameAndDescription, SharedSlashCommandSubcommands, SharedSlashCommand)
|
|
||||||
export class SlashCommandBuilder {
|
|
||||||
/**
|
|
||||||
* The name of this command.
|
|
||||||
*/
|
|
||||||
public readonly name: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name localizations of this command.
|
|
||||||
*/
|
|
||||||
public readonly name_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of this command.
|
|
||||||
*/
|
|
||||||
public readonly description: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description localizations of this command.
|
|
||||||
*/
|
|
||||||
public readonly description_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The options of this command.
|
|
||||||
*/
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead.
|
|
||||||
*/
|
|
||||||
public readonly default_permission: boolean | undefined = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The set of permissions represented as a bit set for the command.
|
|
||||||
*/
|
|
||||||
public readonly default_member_permissions: Permissions | null | undefined = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the command is available in direct messages with the application.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* By default, commands are visible. This property is only for global commands.
|
|
||||||
* @deprecated
|
|
||||||
* Use {@link SlashCommandBuilder.contexts} instead.
|
|
||||||
*/
|
|
||||||
public readonly dm_permission: boolean | undefined = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The integration types for this command.
|
|
||||||
*/
|
|
||||||
public readonly integration_types?: ApplicationIntegrationType[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this command is NSFW.
|
|
||||||
*/
|
|
||||||
public readonly nsfw: boolean | undefined = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandBuilder
|
|
||||||
extends SharedNameAndDescription,
|
|
||||||
SharedSlashCommandOptions<SlashCommandOptionsOnlyBuilder>,
|
|
||||||
SharedSlashCommandSubcommands<SlashCommandSubcommandsOnlyBuilder>,
|
|
||||||
SharedSlashCommand {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface specifically for slash command subcommands.
|
|
||||||
*/
|
|
||||||
export interface SlashCommandSubcommandsOnlyBuilder
|
|
||||||
extends SharedNameAndDescription,
|
|
||||||
SharedSlashCommandSubcommands<SlashCommandSubcommandsOnlyBuilder>,
|
|
||||||
SharedSlashCommand {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface specifically for slash command options.
|
|
||||||
*/
|
|
||||||
export interface SlashCommandOptionsOnlyBuilder
|
|
||||||
extends SharedNameAndDescription,
|
|
||||||
SharedSlashCommandOptions<SlashCommandOptionsOnlyBuilder>,
|
|
||||||
SharedSlashCommand {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface that ensures the `toJSON()` call will return something
|
|
||||||
* that can be serialized into API-compatible data.
|
|
||||||
*/
|
|
||||||
export interface ToAPIApplicationCommandOptions {
|
|
||||||
toJSON(): APIApplicationCommandOption;
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import {
|
|
||||||
ApplicationCommandOptionType,
|
|
||||||
type APIApplicationCommandSubcommandGroupOption,
|
|
||||||
type APIApplicationCommandSubcommandOption,
|
|
||||||
} from 'discord-api-types/v10';
|
|
||||||
import { mix } from 'ts-mixer';
|
|
||||||
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions.js';
|
|
||||||
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder.js';
|
|
||||||
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase.js';
|
|
||||||
import { SharedNameAndDescription } from './mixins/NameAndDescription.js';
|
|
||||||
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a folder for subcommands.
|
|
||||||
*
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
|
|
||||||
*/
|
|
||||||
@mix(SharedNameAndDescription)
|
|
||||||
export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationCommandOptions {
|
|
||||||
/**
|
|
||||||
* The name of this subcommand group.
|
|
||||||
*/
|
|
||||||
public readonly name: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of this subcommand group.
|
|
||||||
*/
|
|
||||||
public readonly description: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The subcommands within this subcommand group.
|
|
||||||
*/
|
|
||||||
public readonly options: SlashCommandSubcommandBuilder[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new subcommand to this group.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns a subcommand builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addSubcommand(
|
|
||||||
input:
|
|
||||||
| SlashCommandSubcommandBuilder
|
|
||||||
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
|
|
||||||
) {
|
|
||||||
const { options } = this;
|
|
||||||
|
|
||||||
// First, assert options conditions - we cannot have more than 25 options
|
|
||||||
validateMaxOptionsLength(options);
|
|
||||||
|
|
||||||
// Get the final result
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
||||||
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
||||||
assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);
|
|
||||||
|
|
||||||
// Push it
|
|
||||||
options.push(result);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes this builder to API-compatible JSON data.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method runs validations on the data before serializing it.
|
|
||||||
* As such, it may throw an error if the data is invalid.
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandSubcommandGroupOption {
|
|
||||||
validateRequiredParameters(this.name, this.description, this.options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.name_localizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.description_localizations,
|
|
||||||
options: this.options.map((option) => option.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandSubcommandGroupBuilder extends SharedNameAndDescription {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A builder that creates API-compatible JSON data for slash command subcommands.
|
|
||||||
*
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups}
|
|
||||||
*/
|
|
||||||
@mix(SharedNameAndDescription, SharedSlashCommandOptions)
|
|
||||||
export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOptions {
|
|
||||||
/**
|
|
||||||
* The name of this subcommand.
|
|
||||||
*/
|
|
||||||
public readonly name: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of this subcommand.
|
|
||||||
*/
|
|
||||||
public readonly description: string = undefined!;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The options within this subcommand.
|
|
||||||
*/
|
|
||||||
public readonly options: ApplicationCommandOptionBase[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes this builder to API-compatible JSON data.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method runs validations on the data before serializing it.
|
|
||||||
* As such, it may throw an error if the data is invalid.
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandSubcommandOption {
|
|
||||||
validateRequiredParameters(this.name, this.description, this.options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: ApplicationCommandOptionType.Subcommand,
|
|
||||||
name: this.name,
|
|
||||||
name_localizations: this.name_localizations,
|
|
||||||
description: this.description,
|
|
||||||
description_localizations: this.description_localizations,
|
|
||||||
options: this.options.map((option) => option.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandSubcommandBuilder
|
|
||||||
extends SharedNameAndDescription,
|
|
||||||
SharedSlashCommandOptions<SlashCommandSubcommandBuilder> {}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* This mixin holds minimum and maximum symbols used for options.
|
|
||||||
*/
|
|
||||||
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
|
|
||||||
/**
|
|
||||||
* The maximum value of this option.
|
|
||||||
*/
|
|
||||||
public readonly max_value?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The minimum value of this option.
|
|
||||||
*/
|
|
||||||
public readonly min_value?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the maximum number value of this option.
|
|
||||||
*
|
|
||||||
* @param max - The maximum value this option can be
|
|
||||||
*/
|
|
||||||
public abstract setMaxValue(max: number): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the minimum number value of this option.
|
|
||||||
*
|
|
||||||
* @param min - The minimum value this option can be
|
|
||||||
*/
|
|
||||||
public abstract setMinValue(min: number): this;
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
|
|
||||||
import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions.js';
|
|
||||||
import { SharedNameAndDescription } from './NameAndDescription.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base application command option builder that contains common symbols for application command builders.
|
|
||||||
*/
|
|
||||||
export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public abstract readonly type: ApplicationCommandOptionType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this option is required.
|
|
||||||
*
|
|
||||||
* @defaultValue `false`
|
|
||||||
*/
|
|
||||||
public readonly required: boolean = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether this option is required.
|
|
||||||
*
|
|
||||||
* @param required - Whether this option should be required
|
|
||||||
*/
|
|
||||||
public setRequired(required: boolean) {
|
|
||||||
// Assert that you actually passed a boolean
|
|
||||||
validateRequired(required);
|
|
||||||
|
|
||||||
Reflect.set(this, 'required', required);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes this builder to API-compatible JSON data.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method runs validations on the data before serializing it.
|
|
||||||
* As such, it may throw an error if the data is invalid.
|
|
||||||
*/
|
|
||||||
public abstract toJSON(): APIApplicationCommandBasicOption;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method runs required validators on this builder.
|
|
||||||
*/
|
|
||||||
protected runRequiredValidations() {
|
|
||||||
validateRequiredParameters(this.name, this.description, []);
|
|
||||||
|
|
||||||
// Validate localizations
|
|
||||||
validateLocalizationMap(this.name_localizations);
|
|
||||||
validateLocalizationMap(this.description_localizations);
|
|
||||||
|
|
||||||
// Assert that you actually passed a boolean
|
|
||||||
validateRequired(this.required);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import { ChannelType } from 'discord-api-types/v10';
|
|
||||||
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The allowed channel types used for a channel option in a slash command builder.
|
|
||||||
*
|
|
||||||
* @privateRemarks This can't be dynamic because const enums are erased at runtime.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
const allowedChannelTypes = [
|
|
||||||
ChannelType.GuildText,
|
|
||||||
ChannelType.GuildVoice,
|
|
||||||
ChannelType.GuildCategory,
|
|
||||||
ChannelType.GuildAnnouncement,
|
|
||||||
ChannelType.AnnouncementThread,
|
|
||||||
ChannelType.PublicThread,
|
|
||||||
ChannelType.PrivateThread,
|
|
||||||
ChannelType.GuildStageVoice,
|
|
||||||
ChannelType.GuildForum,
|
|
||||||
ChannelType.GuildMedia,
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of allowed channel types used for a channel option.
|
|
||||||
*/
|
|
||||||
export type ApplicationCommandOptionAllowedChannelTypes = (typeof allowedChannelTypes)[number];
|
|
||||||
|
|
||||||
const channelTypesPredicate = s.array(s.union(allowedChannelTypes.map((type) => s.literal(type))));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds channel type symbols used for options.
|
|
||||||
*/
|
|
||||||
export class ApplicationCommandOptionChannelTypesMixin {
|
|
||||||
/**
|
|
||||||
* The channel types of this option.
|
|
||||||
*/
|
|
||||||
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds channel types to this option.
|
|
||||||
*
|
|
||||||
* @param channelTypes - The channel types
|
|
||||||
*/
|
|
||||||
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
|
|
||||||
if (this.channel_types === undefined) {
|
|
||||||
Reflect.set(this, 'channel_types', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.channel_types!.push(...channelTypesPredicate.parse(normalizeArray(channelTypes)));
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import type { ApplicationCommandOptionType } from 'discord-api-types/v10';
|
|
||||||
|
|
||||||
const booleanPredicate = s.boolean();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds choices and autocomplete symbols used for options.
|
|
||||||
*/
|
|
||||||
export class ApplicationCommandOptionWithAutocompleteMixin {
|
|
||||||
/**
|
|
||||||
* Whether this option utilizes autocomplete.
|
|
||||||
*/
|
|
||||||
public readonly autocomplete?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*
|
|
||||||
* @privateRemarks Since this is present and this is a mixin, this is needed.
|
|
||||||
*/
|
|
||||||
public readonly type!: ApplicationCommandOptionType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this option uses autocomplete.
|
|
||||||
*
|
|
||||||
* @param autocomplete - Whether this option should use autocomplete
|
|
||||||
*/
|
|
||||||
public setAutocomplete(autocomplete: boolean): this {
|
|
||||||
// Assert that you actually passed a boolean
|
|
||||||
booleanPredicate.parse(autocomplete);
|
|
||||||
|
|
||||||
if (autocomplete && 'choices' in this && Array.isArray(this.choices) && this.choices.length > 0) {
|
|
||||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
|
||||||
}
|
|
||||||
|
|
||||||
Reflect.set(this, 'autocomplete', autocomplete);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import { ApplicationCommandOptionType, type APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
|
|
||||||
import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js';
|
|
||||||
import { localizationMapPredicate, validateChoicesLength } from '../Assertions.js';
|
|
||||||
|
|
||||||
const stringPredicate = s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100);
|
|
||||||
const numberPredicate = s.number().greaterThan(Number.NEGATIVE_INFINITY).lessThan(Number.POSITIVE_INFINITY);
|
|
||||||
const choicesPredicate = s
|
|
||||||
.object({
|
|
||||||
name: stringPredicate,
|
|
||||||
name_localizations: localizationMapPredicate,
|
|
||||||
value: s.union([stringPredicate, numberPredicate]),
|
|
||||||
})
|
|
||||||
.array();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds choices and autocomplete symbols used for options.
|
|
||||||
*/
|
|
||||||
export class ApplicationCommandOptionWithChoicesMixin<ChoiceType extends number | string> {
|
|
||||||
/**
|
|
||||||
* The choices of this option.
|
|
||||||
*/
|
|
||||||
public readonly choices?: APIApplicationCommandOptionChoice<ChoiceType>[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*
|
|
||||||
* @privateRemarks Since this is present and this is a mixin, this is needed.
|
|
||||||
*/
|
|
||||||
public readonly type!: ApplicationCommandOptionType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds multiple choices to this option.
|
|
||||||
*
|
|
||||||
* @param choices - The choices to add
|
|
||||||
*/
|
|
||||||
public addChoices(...choices: RestOrArray<APIApplicationCommandOptionChoice<ChoiceType>>): this {
|
|
||||||
const normalizedChoices = normalizeArray(choices);
|
|
||||||
if (normalizedChoices.length > 0 && 'autocomplete' in this && this.autocomplete) {
|
|
||||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
|
||||||
}
|
|
||||||
|
|
||||||
choicesPredicate.parse(normalizedChoices);
|
|
||||||
|
|
||||||
if (this.choices === undefined) {
|
|
||||||
Reflect.set(this, 'choices', []);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateChoicesLength(normalizedChoices.length, this.choices);
|
|
||||||
|
|
||||||
for (const { name, name_localizations, value } of normalizedChoices) {
|
|
||||||
// Validate the value
|
|
||||||
if (this.type === ApplicationCommandOptionType.String) {
|
|
||||||
stringPredicate.parse(value);
|
|
||||||
} else {
|
|
||||||
numberPredicate.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.choices!.push({ name, name_localizations, value });
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets multiple choices for this option.
|
|
||||||
*
|
|
||||||
* @param choices - The choices to set
|
|
||||||
*/
|
|
||||||
public setChoices<Input extends APIApplicationCommandOptionChoice<ChoiceType>>(...choices: RestOrArray<Input>): this {
|
|
||||||
const normalizedChoices = normalizeArray(choices);
|
|
||||||
if (normalizedChoices.length > 0 && 'autocomplete' in this && this.autocomplete) {
|
|
||||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
|
||||||
}
|
|
||||||
|
|
||||||
choicesPredicate.parse(normalizedChoices);
|
|
||||||
|
|
||||||
Reflect.set(this, 'choices', []);
|
|
||||||
this.addChoices(normalizedChoices);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import type { LocaleString, LocalizationMap } from 'discord-api-types/v10';
|
|
||||||
import { validateDescription, validateLocale, validateName } from '../Assertions.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds name and description symbols for slash commands.
|
|
||||||
*/
|
|
||||||
export class SharedNameAndDescription {
|
|
||||||
/**
|
|
||||||
* The name of this command.
|
|
||||||
*/
|
|
||||||
public readonly name!: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name localizations of this command.
|
|
||||||
*/
|
|
||||||
public readonly name_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description of this command.
|
|
||||||
*/
|
|
||||||
public readonly description!: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The description localizations of this command.
|
|
||||||
*/
|
|
||||||
public readonly description_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the name of this command.
|
|
||||||
*
|
|
||||||
* @param name - The name to use
|
|
||||||
*/
|
|
||||||
public setName(name: string): this {
|
|
||||||
// Assert the name matches the conditions
|
|
||||||
validateName(name);
|
|
||||||
|
|
||||||
Reflect.set(this, 'name', name);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the description of this command.
|
|
||||||
*
|
|
||||||
* @param description - The description to use
|
|
||||||
*/
|
|
||||||
public setDescription(description: string) {
|
|
||||||
// Assert the description matches the conditions
|
|
||||||
validateDescription(description);
|
|
||||||
|
|
||||||
Reflect.set(this, 'description', description);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a name localization for this command.
|
|
||||||
*
|
|
||||||
* @param locale - The locale to set
|
|
||||||
* @param localizedName - The localized name for the given `locale`
|
|
||||||
*/
|
|
||||||
public setNameLocalization(locale: LocaleString, localizedName: string | null) {
|
|
||||||
if (!this.name_localizations) {
|
|
||||||
Reflect.set(this, 'name_localizations', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedLocale = validateLocale(locale);
|
|
||||||
|
|
||||||
if (localizedName === null) {
|
|
||||||
this.name_localizations![parsedLocale] = null;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateName(localizedName);
|
|
||||||
|
|
||||||
this.name_localizations![parsedLocale] = localizedName;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the name localizations for this command.
|
|
||||||
*
|
|
||||||
* @param localizedNames - The object of localized names to set
|
|
||||||
*/
|
|
||||||
public setNameLocalizations(localizedNames: LocalizationMap | null) {
|
|
||||||
if (localizedNames === null) {
|
|
||||||
Reflect.set(this, 'name_localizations', null);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Reflect.set(this, 'name_localizations', {});
|
|
||||||
|
|
||||||
for (const args of Object.entries(localizedNames)) {
|
|
||||||
this.setNameLocalization(...(args as [LocaleString, string | null]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a description localization for this command.
|
|
||||||
*
|
|
||||||
* @param locale - The locale to set
|
|
||||||
* @param localizedDescription - The localized description for the given locale
|
|
||||||
*/
|
|
||||||
public setDescriptionLocalization(locale: LocaleString, localizedDescription: string | null) {
|
|
||||||
if (!this.description_localizations) {
|
|
||||||
Reflect.set(this, 'description_localizations', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedLocale = validateLocale(locale);
|
|
||||||
|
|
||||||
if (localizedDescription === null) {
|
|
||||||
this.description_localizations![parsedLocale] = null;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateDescription(localizedDescription);
|
|
||||||
|
|
||||||
this.description_localizations![parsedLocale] = localizedDescription;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the description localizations for this command.
|
|
||||||
*
|
|
||||||
* @param localizedDescriptions - The object of localized descriptions to set
|
|
||||||
*/
|
|
||||||
public setDescriptionLocalizations(localizedDescriptions: LocalizationMap | null) {
|
|
||||||
if (localizedDescriptions === null) {
|
|
||||||
Reflect.set(this, 'description_localizations', null);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Reflect.set(this, 'description_localizations', {});
|
|
||||||
for (const args of Object.entries(localizedDescriptions)) {
|
|
||||||
this.setDescriptionLocalization(...(args as [LocaleString, string | null]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
import {
|
|
||||||
ApplicationCommandType,
|
|
||||||
type ApplicationIntegrationType,
|
|
||||||
type InteractionContextType,
|
|
||||||
type LocalizationMap,
|
|
||||||
type Permissions,
|
|
||||||
type 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,
|
|
||||||
validateLocalizationMap,
|
|
||||||
validateNSFW,
|
|
||||||
validateRequiredParameters,
|
|
||||||
} from '../Assertions.js';
|
|
||||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds symbols that can be shared in slashcommands independent of options or subcommands.
|
|
||||||
*/
|
|
||||||
export class SharedSlashCommand {
|
|
||||||
public readonly name: string = undefined!;
|
|
||||||
|
|
||||||
public readonly name_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
public readonly description: string = undefined!;
|
|
||||||
|
|
||||||
public readonly description_localizations?: LocalizationMap;
|
|
||||||
|
|
||||||
public readonly options: ToAPIApplicationCommandOptions[] = [];
|
|
||||||
|
|
||||||
public readonly contexts?: InteractionContextType[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead.
|
|
||||||
*/
|
|
||||||
public readonly default_permission: boolean | undefined = undefined;
|
|
||||||
|
|
||||||
public readonly default_member_permissions: Permissions | null | undefined = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use {@link SharedSlashCommand.contexts} instead.
|
|
||||||
*/
|
|
||||||
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<InteractionContextType>) {
|
|
||||||
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<ApplicationIntegrationType>) {
|
|
||||||
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.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* If set to `false`, you will have to later `PUT` the permissions for this command.
|
|
||||||
* @param value - Whether or not to enable this command by default
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
|
||||||
* @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead.
|
|
||||||
*/
|
|
||||||
public setDefaultPermission(value: boolean) {
|
|
||||||
// Assert the value matches the conditions
|
|
||||||
validateDefaultPermission(value);
|
|
||||||
|
|
||||||
Reflect.set(this, 'default_permission', value);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the default permissions a member should have in order to run the command.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* You can set this to `'0'` to disable the command by default.
|
|
||||||
* @param permissions - The permissions bit field to set
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
|
||||||
*/
|
|
||||||
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
|
|
||||||
// Assert the value and parse it
|
|
||||||
const permissionValue = validateDefaultMemberPermissions(permissions);
|
|
||||||
|
|
||||||
Reflect.set(this, 'default_member_permissions', permissionValue);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets if the command is available in direct messages with the application.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* By default, commands are visible. This method is only for global commands.
|
|
||||||
* @param enabled - Whether the command should be enabled in direct messages
|
|
||||||
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
|
|
||||||
* @deprecated
|
|
||||||
* Use {@link SharedSlashCommand.setContexts} instead.
|
|
||||||
*/
|
|
||||||
public setDMPermission(enabled: boolean | null | undefined) {
|
|
||||||
// Assert the value matches the conditions
|
|
||||||
validateDMPermission(enabled);
|
|
||||||
|
|
||||||
Reflect.set(this, 'dm_permission', enabled);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether this command is NSFW.
|
|
||||||
*
|
|
||||||
* @param nsfw - Whether this command is NSFW
|
|
||||||
*/
|
|
||||||
public setNSFW(nsfw = true) {
|
|
||||||
// Assert the value matches the conditions
|
|
||||||
validateNSFW(nsfw);
|
|
||||||
Reflect.set(this, 'nsfw', nsfw);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializes this builder to API-compatible JSON data.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method runs validations on the data before serializing it.
|
|
||||||
* As such, it may throw an error if the data is invalid.
|
|
||||||
*/
|
|
||||||
public toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody {
|
|
||||||
validateRequiredParameters(this.name, this.description, this.options);
|
|
||||||
|
|
||||||
validateLocalizationMap(this.name_localizations);
|
|
||||||
validateLocalizationMap(this.description_localizations);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...this,
|
|
||||||
type: ApplicationCommandType.ChatInput,
|
|
||||||
options: this.options.map((option) => option.toJSON()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js';
|
|
||||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder';
|
|
||||||
import { SlashCommandAttachmentOption } from '../options/attachment.js';
|
|
||||||
import { SlashCommandBooleanOption } from '../options/boolean.js';
|
|
||||||
import { SlashCommandChannelOption } from '../options/channel.js';
|
|
||||||
import { SlashCommandIntegerOption } from '../options/integer.js';
|
|
||||||
import { SlashCommandMentionableOption } from '../options/mentionable.js';
|
|
||||||
import { SlashCommandNumberOption } from '../options/number.js';
|
|
||||||
import { SlashCommandRoleOption } from '../options/role.js';
|
|
||||||
import { SlashCommandStringOption } from '../options/string.js';
|
|
||||||
import { SlashCommandUserOption } from '../options/user.js';
|
|
||||||
import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds symbols that can be shared in slash command options.
|
|
||||||
*
|
|
||||||
* @typeParam TypeAfterAddingOptions - The type this class should return after adding an option.
|
|
||||||
*/
|
|
||||||
export class SharedSlashCommandOptions<
|
|
||||||
TypeAfterAddingOptions extends SharedSlashCommandOptions<TypeAfterAddingOptions>,
|
|
||||||
> {
|
|
||||||
public readonly options!: ToAPIApplicationCommandOptions[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a boolean option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addBooleanOption(
|
|
||||||
input: SlashCommandBooleanOption | ((builder: SlashCommandBooleanOption) => SlashCommandBooleanOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandBooleanOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a user option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addUserOption(input: SlashCommandUserOption | ((builder: SlashCommandUserOption) => SlashCommandUserOption)) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandUserOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a channel option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addChannelOption(
|
|
||||||
input: SlashCommandChannelOption | ((builder: SlashCommandChannelOption) => SlashCommandChannelOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandChannelOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a role option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addRoleOption(input: SlashCommandRoleOption | ((builder: SlashCommandRoleOption) => SlashCommandRoleOption)) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandRoleOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an attachment option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addAttachmentOption(
|
|
||||||
input: SlashCommandAttachmentOption | ((builder: SlashCommandAttachmentOption) => SlashCommandAttachmentOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandAttachmentOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a mentionable option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addMentionableOption(
|
|
||||||
input: SlashCommandMentionableOption | ((builder: SlashCommandMentionableOption) => SlashCommandMentionableOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandMentionableOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a string option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addStringOption(
|
|
||||||
input: SlashCommandStringOption | ((builder: SlashCommandStringOption) => SlashCommandStringOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandStringOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an integer option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addIntegerOption(
|
|
||||||
input: SlashCommandIntegerOption | ((builder: SlashCommandIntegerOption) => SlashCommandIntegerOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandIntegerOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a number option.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns an option builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addNumberOption(
|
|
||||||
input: SlashCommandNumberOption | ((builder: SlashCommandNumberOption) => SlashCommandNumberOption),
|
|
||||||
) {
|
|
||||||
return this._sharedAddOptionMethod(input, SlashCommandNumberOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Where the actual adding magic happens. ✨
|
|
||||||
*
|
|
||||||
* @param input - The input. What else?
|
|
||||||
* @param Instance - The instance of whatever is being added
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
private _sharedAddOptionMethod<OptionBuilder extends ApplicationCommandOptionBase>(
|
|
||||||
input: OptionBuilder | ((builder: OptionBuilder) => OptionBuilder),
|
|
||||||
Instance: new () => OptionBuilder,
|
|
||||||
): TypeAfterAddingOptions {
|
|
||||||
const { options } = this;
|
|
||||||
|
|
||||||
// First, assert options conditions - we cannot have more than 25 options
|
|
||||||
validateMaxOptionsLength(options);
|
|
||||||
|
|
||||||
// Get the final result
|
|
||||||
const result = typeof input === 'function' ? input(new Instance()) : input;
|
|
||||||
|
|
||||||
assertReturnOfBuilder(result, Instance);
|
|
||||||
|
|
||||||
// Push it
|
|
||||||
options.push(result);
|
|
||||||
|
|
||||||
return this as unknown as TypeAfterAddingOptions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js';
|
|
||||||
import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder.js';
|
|
||||||
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from '../SlashCommandSubcommands.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This mixin holds symbols that can be shared in slash subcommands.
|
|
||||||
*
|
|
||||||
* @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group.
|
|
||||||
*/
|
|
||||||
export class SharedSlashCommandSubcommands<
|
|
||||||
TypeAfterAddingSubcommands extends SharedSlashCommandSubcommands<TypeAfterAddingSubcommands>,
|
|
||||||
> {
|
|
||||||
public readonly options: ToAPIApplicationCommandOptions[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new subcommand group to this command.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns a subcommand group builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addSubcommandGroup(
|
|
||||||
input:
|
|
||||||
| SlashCommandSubcommandGroupBuilder
|
|
||||||
| ((subcommandGroup: SlashCommandSubcommandGroupBuilder) => SlashCommandSubcommandGroupBuilder),
|
|
||||||
): TypeAfterAddingSubcommands {
|
|
||||||
const { options } = this;
|
|
||||||
|
|
||||||
// First, assert options conditions - we cannot have more than 25 options
|
|
||||||
validateMaxOptionsLength(options);
|
|
||||||
|
|
||||||
// Get the final result
|
|
||||||
const result = typeof input === 'function' ? input(new SlashCommandSubcommandGroupBuilder()) : input;
|
|
||||||
|
|
||||||
assertReturnOfBuilder(result, SlashCommandSubcommandGroupBuilder);
|
|
||||||
|
|
||||||
// Push it
|
|
||||||
options.push(result);
|
|
||||||
|
|
||||||
return this as unknown as TypeAfterAddingSubcommands;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new subcommand to this command.
|
|
||||||
*
|
|
||||||
* @param input - A function that returns a subcommand builder or an already built builder
|
|
||||||
*/
|
|
||||||
public addSubcommand(
|
|
||||||
input:
|
|
||||||
| SlashCommandSubcommandBuilder
|
|
||||||
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
|
|
||||||
): TypeAfterAddingSubcommands {
|
|
||||||
const { options } = this;
|
|
||||||
|
|
||||||
// First, assert options conditions - we cannot have more than 25 options
|
|
||||||
validateMaxOptionsLength(options);
|
|
||||||
|
|
||||||
// Get the final result
|
|
||||||
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;
|
|
||||||
|
|
||||||
assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);
|
|
||||||
|
|
||||||
// Push it
|
|
||||||
options.push(result);
|
|
||||||
|
|
||||||
return this as unknown as TypeAfterAddingSubcommands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType, type APIApplicationCommandAttachmentOption } from 'discord-api-types/v10';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command attachment option.
|
|
||||||
*/
|
|
||||||
export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public override readonly type = ApplicationCommandOptionType.Attachment as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandAttachmentOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType, type APIApplicationCommandBooleanOption } from 'discord-api-types/v10';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command boolean option.
|
|
||||||
*/
|
|
||||||
export class SlashCommandBooleanOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public readonly type = ApplicationCommandOptionType.Boolean as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandBooleanOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10';
|
|
||||||
import { mix } from 'ts-mixer';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command channel option.
|
|
||||||
*/
|
|
||||||
@mix(ApplicationCommandOptionChannelTypesMixin)
|
|
||||||
export class SlashCommandChannelOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public override readonly type = ApplicationCommandOptionType.Channel as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandChannelOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandChannelOption extends ApplicationCommandOptionChannelTypesMixin {}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import { ApplicationCommandOptionType, type APIApplicationCommandIntegerOption } from 'discord-api-types/v10';
|
|
||||||
import { mix } from 'ts-mixer';
|
|
||||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
|
||||||
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
|
||||||
|
|
||||||
const numberValidator = s.number().int();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command integer option.
|
|
||||||
*/
|
|
||||||
@mix(
|
|
||||||
ApplicationCommandNumericOptionMinMaxValueMixin,
|
|
||||||
ApplicationCommandOptionWithAutocompleteMixin,
|
|
||||||
ApplicationCommandOptionWithChoicesMixin,
|
|
||||||
)
|
|
||||||
export class SlashCommandIntegerOption
|
|
||||||
extends ApplicationCommandOptionBase
|
|
||||||
implements ApplicationCommandNumericOptionMinMaxValueMixin
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public readonly type = ApplicationCommandOptionType.Integer as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMaxValue}
|
|
||||||
*/
|
|
||||||
public setMaxValue(max: number): this {
|
|
||||||
numberValidator.parse(max);
|
|
||||||
|
|
||||||
Reflect.set(this, 'max_value', max);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMinValue}
|
|
||||||
*/
|
|
||||||
public setMinValue(min: number): this {
|
|
||||||
numberValidator.parse(min);
|
|
||||||
|
|
||||||
Reflect.set(this, 'min_value', min);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandIntegerOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
|
||||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...this } as APIApplicationCommandIntegerOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandIntegerOption
|
|
||||||
extends ApplicationCommandNumericOptionMinMaxValueMixin,
|
|
||||||
ApplicationCommandOptionWithChoicesMixin<number>,
|
|
||||||
ApplicationCommandOptionWithAutocompleteMixin {}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType, type APIApplicationCommandMentionableOption } from 'discord-api-types/v10';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command mentionable option.
|
|
||||||
*/
|
|
||||||
export class SlashCommandMentionableOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public readonly type = ApplicationCommandOptionType.Mentionable as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandMentionableOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import { ApplicationCommandOptionType, type APIApplicationCommandNumberOption } from 'discord-api-types/v10';
|
|
||||||
import { mix } from 'ts-mixer';
|
|
||||||
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
|
||||||
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
|
||||||
|
|
||||||
const numberValidator = s.number();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command number option.
|
|
||||||
*/
|
|
||||||
@mix(
|
|
||||||
ApplicationCommandNumericOptionMinMaxValueMixin,
|
|
||||||
ApplicationCommandOptionWithAutocompleteMixin,
|
|
||||||
ApplicationCommandOptionWithChoicesMixin,
|
|
||||||
)
|
|
||||||
export class SlashCommandNumberOption
|
|
||||||
extends ApplicationCommandOptionBase
|
|
||||||
implements ApplicationCommandNumericOptionMinMaxValueMixin
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public readonly type = ApplicationCommandOptionType.Number as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMaxValue}
|
|
||||||
*/
|
|
||||||
public setMaxValue(max: number): this {
|
|
||||||
numberValidator.parse(max);
|
|
||||||
|
|
||||||
Reflect.set(this, 'max_value', max);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMinValue}
|
|
||||||
*/
|
|
||||||
public setMinValue(min: number): this {
|
|
||||||
numberValidator.parse(min);
|
|
||||||
|
|
||||||
Reflect.set(this, 'min_value', min);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandNumberOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
|
||||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...this } as APIApplicationCommandNumberOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandNumberOption
|
|
||||||
extends ApplicationCommandNumericOptionMinMaxValueMixin,
|
|
||||||
ApplicationCommandOptionWithChoicesMixin<number>,
|
|
||||||
ApplicationCommandOptionWithAutocompleteMixin {}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType, type APIApplicationCommandRoleOption } from 'discord-api-types/v10';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command role option.
|
|
||||||
*/
|
|
||||||
export class SlashCommandRoleOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public override readonly type = ApplicationCommandOptionType.Role as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandRoleOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
|
||||||
import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10';
|
|
||||||
import { mix } from 'ts-mixer';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
|
|
||||||
import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js';
|
|
||||||
|
|
||||||
const minLengthValidator = s.number().greaterThanOrEqual(0).lessThanOrEqual(6_000);
|
|
||||||
const maxLengthValidator = s.number().greaterThanOrEqual(1).lessThanOrEqual(6_000);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command string option.
|
|
||||||
*/
|
|
||||||
@mix(ApplicationCommandOptionWithAutocompleteMixin, ApplicationCommandOptionWithChoicesMixin)
|
|
||||||
export class SlashCommandStringOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public readonly type = ApplicationCommandOptionType.String as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum length of this option.
|
|
||||||
*/
|
|
||||||
public readonly max_length?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The minimum length of this option.
|
|
||||||
*/
|
|
||||||
public readonly min_length?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the maximum length of this string option.
|
|
||||||
*
|
|
||||||
* @param max - The maximum length this option can be
|
|
||||||
*/
|
|
||||||
public setMaxLength(max: number): this {
|
|
||||||
maxLengthValidator.parse(max);
|
|
||||||
|
|
||||||
Reflect.set(this, 'max_length', max);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the minimum length of this string option.
|
|
||||||
*
|
|
||||||
* @param min - The minimum length this option can be
|
|
||||||
*/
|
|
||||||
public setMinLength(min: number): this {
|
|
||||||
minLengthValidator.parse(min);
|
|
||||||
|
|
||||||
Reflect.set(this, 'min_length', min);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandStringOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
|
|
||||||
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...this } as APIApplicationCommandStringOption;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SlashCommandStringOption
|
|
||||||
extends ApplicationCommandOptionWithChoicesMixin<string>,
|
|
||||||
ApplicationCommandOptionWithAutocompleteMixin {}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType, type APIApplicationCommandUserOption } from 'discord-api-types/v10';
|
|
||||||
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slash command user option.
|
|
||||||
*/
|
|
||||||
export class SlashCommandUserOption extends ApplicationCommandOptionBase {
|
|
||||||
/**
|
|
||||||
* The type of this option.
|
|
||||||
*/
|
|
||||||
public readonly type = ApplicationCommandOptionType.User as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc ApplicationCommandOptionBase.toJSON}
|
|
||||||
*/
|
|
||||||
public toJSON(): APIApplicationCommandUserOption {
|
|
||||||
this.runRequiredValidations();
|
|
||||||
|
|
||||||
return { ...this };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +1,70 @@
|
|||||||
import { s } from '@sapphire/shapeshift';
|
import { z } from 'zod';
|
||||||
import type { APIEmbedField } from 'discord-api-types/v10';
|
import { refineURLPredicate } from '../../Assertions.js';
|
||||||
import { isValidationEnabled } from '../../util/validation.js';
|
import { embedLength } from '../../util/componentUtil.js';
|
||||||
|
|
||||||
export const fieldNamePredicate = s
|
const namePredicate = z.string().min(1).max(256);
|
||||||
|
|
||||||
|
const iconURLPredicate = z
|
||||||
.string()
|
.string()
|
||||||
.lengthGreaterThanOrEqual(1)
|
.url()
|
||||||
.lengthLessThanOrEqual(256)
|
.refine(refineURLPredicate(['http:', 'https:', 'attachment:']), {
|
||||||
.setValidationEnabled(isValidationEnabled);
|
message: 'Invalid protocol for icon URL. Must be http:, https:, or attachment:',
|
||||||
|
});
|
||||||
|
|
||||||
export const fieldValuePredicate = s
|
const URLPredicate = z
|
||||||
.string()
|
.string()
|
||||||
.lengthGreaterThanOrEqual(1)
|
.url()
|
||||||
.lengthLessThanOrEqual(1_024)
|
.refine(refineURLPredicate(['http:', 'https:']), { message: 'Invalid protocol for URL. Must be http: or https:' });
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const fieldInlinePredicate = s.boolean().optional();
|
export const embedFieldPredicate = z.object({
|
||||||
|
name: namePredicate,
|
||||||
|
value: z.string().min(1).max(1_024),
|
||||||
|
inline: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const embedFieldPredicate = s
|
export const embedAuthorPredicate = z.object({
|
||||||
|
name: namePredicate,
|
||||||
|
icon_url: iconURLPredicate.optional(),
|
||||||
|
url: URLPredicate.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const embedFooterPredicate = z.object({
|
||||||
|
text: z.string().min(1).max(2_048),
|
||||||
|
icon_url: iconURLPredicate.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const embedPredicate = z
|
||||||
.object({
|
.object({
|
||||||
name: fieldNamePredicate,
|
title: namePredicate.optional(),
|
||||||
value: fieldValuePredicate,
|
description: z.string().min(1).max(4_096).optional(),
|
||||||
inline: fieldInlinePredicate,
|
url: URLPredicate.optional(),
|
||||||
|
timestamp: z.string().optional(),
|
||||||
|
color: z.number().int().min(0).max(0xffffff).optional(),
|
||||||
|
footer: embedFooterPredicate.optional(),
|
||||||
|
image: z.object({ url: URLPredicate }).optional(),
|
||||||
|
thumbnail: z.object({ url: URLPredicate }).optional(),
|
||||||
|
author: embedAuthorPredicate.optional(),
|
||||||
|
fields: z.array(embedFieldPredicate).max(25).optional(),
|
||||||
})
|
})
|
||||||
.setValidationEnabled(isValidationEnabled);
|
.refine(
|
||||||
|
(embed) => {
|
||||||
export const embedFieldsArrayPredicate = embedFieldPredicate.array().setValidationEnabled(isValidationEnabled);
|
return (
|
||||||
|
embed.title !== undefined ||
|
||||||
export const fieldLengthPredicate = s.number().lessThanOrEqual(25).setValidationEnabled(isValidationEnabled);
|
embed.description !== undefined ||
|
||||||
|
(embed.fields !== undefined && embed.fields.length > 0) ||
|
||||||
export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void {
|
embed.footer !== undefined ||
|
||||||
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
|
embed.author !== undefined ||
|
||||||
}
|
embed.image !== undefined ||
|
||||||
|
embed.thumbnail !== undefined
|
||||||
export const authorNamePredicate = fieldNamePredicate.nullable().setValidationEnabled(isValidationEnabled);
|
);
|
||||||
|
},
|
||||||
export const imageURLPredicate = s
|
{
|
||||||
.string()
|
message: 'Embed must have at least a title, description, a field, a footer, an author, an image, OR a thumbnail.',
|
||||||
.url({
|
},
|
||||||
allowedProtocols: ['http:', 'https:', 'attachment:'],
|
)
|
||||||
})
|
.refine(
|
||||||
.nullish()
|
(embed) => {
|
||||||
.setValidationEnabled(isValidationEnabled);
|
return embedLength(embed) <= 6_000;
|
||||||
|
},
|
||||||
export const urlPredicate = s
|
{ message: 'Embeds must not exceed 6000 characters in total.' },
|
||||||
.string()
|
);
|
||||||
.url({
|
|
||||||
allowedProtocols: ['http:', 'https:'],
|
|
||||||
})
|
|
||||||
.nullish()
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const embedAuthorPredicate = s
|
|
||||||
.object({
|
|
||||||
name: authorNamePredicate,
|
|
||||||
iconURL: imageURLPredicate,
|
|
||||||
url: urlPredicate,
|
|
||||||
})
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const RGBPredicate = s
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.greaterThanOrEqual(0)
|
|
||||||
.lessThanOrEqual(255)
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
export const colorPredicate = s
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.greaterThanOrEqual(0)
|
|
||||||
.lessThanOrEqual(0xffffff)
|
|
||||||
.or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate]))
|
|
||||||
.nullable()
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const descriptionPredicate = s
|
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(4_096)
|
|
||||||
.nullable()
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const footerTextPredicate = s
|
|
||||||
.string()
|
|
||||||
.lengthGreaterThanOrEqual(1)
|
|
||||||
.lengthLessThanOrEqual(2_048)
|
|
||||||
.nullable()
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const embedFooterPredicate = s
|
|
||||||
.object({
|
|
||||||
text: footerTextPredicate,
|
|
||||||
iconURL: imageURLPredicate,
|
|
||||||
})
|
|
||||||
.setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const timestampPredicate = s.union([s.number(), s.date()]).nullable().setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|
||||||
export const titlePredicate = fieldNamePredicate.nullable().setValidationEnabled(isValidationEnabled);
|
|
||||||
|
|||||||
@@ -1,77 +1,38 @@
|
|||||||
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10';
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
|
import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter } from 'discord-api-types/v10';
|
||||||
import {
|
import type { RestOrArray } from '../../util/normalizeArray.js';
|
||||||
colorPredicate,
|
import { normalizeArray } from '../../util/normalizeArray.js';
|
||||||
descriptionPredicate,
|
import { resolveBuilder } from '../../util/resolveBuilder.js';
|
||||||
embedAuthorPredicate,
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
embedFieldsArrayPredicate,
|
import { embedPredicate } from './Assertions.js';
|
||||||
embedFooterPredicate,
|
import { EmbedAuthorBuilder } from './EmbedAuthor.js';
|
||||||
imageURLPredicate,
|
import { EmbedFieldBuilder } from './EmbedField.js';
|
||||||
timestampPredicate,
|
import { EmbedFooterBuilder } from './EmbedFooter.js';
|
||||||
titlePredicate,
|
|
||||||
urlPredicate,
|
|
||||||
validateFieldLength,
|
|
||||||
} from './Assertions.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tuple satisfying the RGB color model.
|
* Data stored in the process of constructing an embed.
|
||||||
*
|
|
||||||
* @see {@link https://developer.mozilla.org/docs/Glossary/RGB}
|
|
||||||
*/
|
*/
|
||||||
export type RGBTuple = [red: number, green: number, blue: number];
|
export interface EmbedBuilderData extends Omit<APIEmbed, 'author' | 'fields' | 'footer'> {
|
||||||
|
author?: EmbedAuthorBuilder;
|
||||||
/**
|
fields: EmbedFieldBuilder[];
|
||||||
* The base icon data typically used in payloads.
|
footer?: EmbedFooterBuilder;
|
||||||
*/
|
|
||||||
export interface IconData {
|
|
||||||
/**
|
|
||||||
* The URL of the icon.
|
|
||||||
*/
|
|
||||||
iconURL?: string;
|
|
||||||
/**
|
|
||||||
* The proxy URL of the icon.
|
|
||||||
*/
|
|
||||||
proxyIconURL?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the author data of an embed.
|
|
||||||
*/
|
|
||||||
export interface EmbedAuthorData extends IconData, Omit<APIEmbedAuthor, 'icon_url' | 'proxy_icon_url'> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the author options of an embed.
|
|
||||||
*/
|
|
||||||
export interface EmbedAuthorOptions extends Omit<EmbedAuthorData, 'proxyIconURL'> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the footer data of an embed.
|
|
||||||
*/
|
|
||||||
export interface EmbedFooterData extends IconData, Omit<APIEmbedFooter, 'icon_url' | 'proxy_icon_url'> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the footer options of an embed.
|
|
||||||
*/
|
|
||||||
export interface EmbedFooterOptions extends Omit<EmbedFooterData, 'proxyIconURL'> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the image data of an embed.
|
|
||||||
*/
|
|
||||||
export interface EmbedImageData extends Omit<APIEmbedImage, 'proxy_url'> {
|
|
||||||
/**
|
|
||||||
* The proxy URL for the image.
|
|
||||||
*/
|
|
||||||
proxyURL?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder that creates API-compatible JSON data for embeds.
|
* A builder that creates API-compatible JSON data for embeds.
|
||||||
*/
|
*/
|
||||||
export class EmbedBuilder {
|
export class EmbedBuilder implements JSONEncodable<APIEmbed> {
|
||||||
/**
|
/**
|
||||||
* The API data associated with this embed.
|
* The API data associated with this embed.
|
||||||
*/
|
*/
|
||||||
public readonly data: APIEmbed;
|
private readonly data: EmbedBuilderData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the fields of this embed.
|
||||||
|
*/
|
||||||
|
public get fields(): readonly EmbedFieldBuilder[] {
|
||||||
|
return this.data.fields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new embed from API data.
|
* Creates a new embed from API data.
|
||||||
@@ -79,8 +40,12 @@ export class EmbedBuilder {
|
|||||||
* @param data - The API data to create this embed with
|
* @param data - The API data to create this embed with
|
||||||
*/
|
*/
|
||||||
public constructor(data: APIEmbed = {}) {
|
public constructor(data: APIEmbed = {}) {
|
||||||
this.data = { ...data };
|
this.data = {
|
||||||
if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString();
|
...structuredClone(data),
|
||||||
|
author: data.author && new EmbedAuthorBuilder(data.author),
|
||||||
|
fields: data.fields?.map((field) => new EmbedFieldBuilder(field)) ?? [],
|
||||||
|
footer: data.footer && new EmbedFooterBuilder(data.footer),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,16 +72,13 @@ export class EmbedBuilder {
|
|||||||
* ```
|
* ```
|
||||||
* @param fields - The fields to add
|
* @param fields - The fields to add
|
||||||
*/
|
*/
|
||||||
public addFields(...fields: RestOrArray<APIEmbedField>): this {
|
public addFields(
|
||||||
|
...fields: RestOrArray<APIEmbedField | EmbedFieldBuilder | ((builder: EmbedFieldBuilder) => EmbedFieldBuilder)>
|
||||||
|
): this {
|
||||||
const normalizedFields = normalizeArray(fields);
|
const normalizedFields = normalizeArray(fields);
|
||||||
// Ensure adding these fields won't exceed the 25 field limit
|
const resolved = normalizedFields.map((field) => resolveBuilder(field, EmbedFieldBuilder));
|
||||||
validateFieldLength(normalizedFields.length, this.data.fields);
|
|
||||||
|
|
||||||
// Data assertions
|
this.data.fields.push(...resolved);
|
||||||
embedFieldsArrayPredicate.parse(normalizedFields);
|
|
||||||
|
|
||||||
if (this.data.fields) this.data.fields.push(...normalizedFields);
|
|
||||||
else this.data.fields = normalizedFields;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,14 +111,14 @@ export class EmbedBuilder {
|
|||||||
* @param deleteCount - The number of fields to remove
|
* @param deleteCount - The number of fields to remove
|
||||||
* @param fields - The replacing field objects
|
* @param fields - The replacing field objects
|
||||||
*/
|
*/
|
||||||
public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this {
|
public spliceFields(
|
||||||
// Ensure adding these fields won't exceed the 25 field limit
|
index: number,
|
||||||
validateFieldLength(fields.length - deleteCount, this.data.fields);
|
deleteCount: number,
|
||||||
|
...fields: (APIEmbedField | EmbedFieldBuilder | ((builder: EmbedFieldBuilder) => EmbedFieldBuilder))[]
|
||||||
|
): this {
|
||||||
|
const resolved = fields.map((field) => resolveBuilder(field, EmbedFieldBuilder));
|
||||||
|
this.data.fields.splice(index, deleteCount, ...resolved);
|
||||||
|
|
||||||
// Data assertions
|
|
||||||
embedFieldsArrayPredicate.parse(fields);
|
|
||||||
if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields);
|
|
||||||
else this.data.fields = fields;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +132,10 @@ export class EmbedBuilder {
|
|||||||
* You can set a maximum of 25 fields.
|
* You can set a maximum of 25 fields.
|
||||||
* @param fields - The fields to set
|
* @param fields - The fields to set
|
||||||
*/
|
*/
|
||||||
public setFields(...fields: RestOrArray<APIEmbedField>): this {
|
public setFields(
|
||||||
this.spliceFields(0, this.data.fields?.length ?? 0, ...normalizeArray(fields));
|
...fields: RestOrArray<APIEmbedField | EmbedFieldBuilder | ((builder: EmbedFieldBuilder) => EmbedFieldBuilder)>
|
||||||
|
): this {
|
||||||
|
this.spliceFields(0, this.data.fields.length, ...normalizeArray(fields));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,17 +144,28 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param options - The options to use
|
* @param options - The options to use
|
||||||
*/
|
*/
|
||||||
|
public setAuthor(
|
||||||
|
options: APIEmbedAuthor | EmbedAuthorBuilder | ((builder: EmbedAuthorBuilder) => EmbedAuthorBuilder),
|
||||||
|
): this {
|
||||||
|
this.data.author = resolveBuilder(options, EmbedAuthorBuilder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public setAuthor(options: EmbedAuthorOptions | null): this {
|
/**
|
||||||
if (options === null) {
|
* Updates the author of this embed (and creates it if it doesn't exist).
|
||||||
this.data.author = undefined;
|
*
|
||||||
return this;
|
* @param updater - The function to update the author with
|
||||||
}
|
*/
|
||||||
|
public updateAuthor(updater: (builder: EmbedAuthorBuilder) => void) {
|
||||||
|
updater((this.data.author ??= new EmbedAuthorBuilder()));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
// Data assertions
|
/**
|
||||||
embedAuthorPredicate.parse(options);
|
* Clears the author of this embed.
|
||||||
|
*/
|
||||||
this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL };
|
public clearAuthor(): this {
|
||||||
|
this.data.author = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,17 +174,16 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param color - The color to use
|
* @param color - The color to use
|
||||||
*/
|
*/
|
||||||
public setColor(color: RGBTuple | number | null): this {
|
public setColor(color: number): this {
|
||||||
// Data assertions
|
this.data.color = color;
|
||||||
colorPredicate.parse(color);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(color)) {
|
/**
|
||||||
const [red, green, blue] = color;
|
* Clears the color of this embed.
|
||||||
this.data.color = (red << 16) + (green << 8) + blue;
|
*/
|
||||||
return this;
|
public clearColor(): this {
|
||||||
}
|
this.data.color = undefined;
|
||||||
|
|
||||||
this.data.color = color ?? undefined;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,11 +192,16 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param description - The description to use
|
* @param description - The description to use
|
||||||
*/
|
*/
|
||||||
public setDescription(description: string | null): this {
|
public setDescription(description: string): this {
|
||||||
// Data assertions
|
this.data.description = description;
|
||||||
descriptionPredicate.parse(description);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.description = description ?? undefined;
|
/**
|
||||||
|
* Clears the description of this embed.
|
||||||
|
*/
|
||||||
|
public clearDescription(): this {
|
||||||
|
this.data.description = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,16 +210,28 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param options - The footer to use
|
* @param options - The footer to use
|
||||||
*/
|
*/
|
||||||
public setFooter(options: EmbedFooterOptions | null): this {
|
public setFooter(
|
||||||
if (options === null) {
|
options: APIEmbedFooter | EmbedFooterBuilder | ((builder: EmbedFooterBuilder) => EmbedFooterBuilder),
|
||||||
this.data.footer = undefined;
|
): this {
|
||||||
return this;
|
this.data.footer = resolveBuilder(options, EmbedFooterBuilder);
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
// Data assertions
|
/**
|
||||||
embedFooterPredicate.parse(options);
|
* Updates the footer of this embed (and creates it if it doesn't exist).
|
||||||
|
*
|
||||||
|
* @param updater - The function to update the footer with
|
||||||
|
*/
|
||||||
|
public updateFooter(updater: (builder: EmbedFooterBuilder) => void) {
|
||||||
|
updater((this.data.footer ??= new EmbedFooterBuilder()));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.footer = { text: options.text, icon_url: options.iconURL };
|
/**
|
||||||
|
* Clears the footer of this embed.
|
||||||
|
*/
|
||||||
|
public clearFooter(): this {
|
||||||
|
this.data.footer = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,11 +240,16 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param url - The image URL to use
|
* @param url - The image URL to use
|
||||||
*/
|
*/
|
||||||
public setImage(url: string | null): this {
|
public setImage(url: string): this {
|
||||||
// Data assertions
|
this.data.image = { url };
|
||||||
imageURLPredicate.parse(url);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.image = url ? { url } : undefined;
|
/**
|
||||||
|
* Clears the image of this embed.
|
||||||
|
*/
|
||||||
|
public clearImage(): this {
|
||||||
|
this.data.image = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,11 +258,16 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param url - The thumbnail URL to use
|
* @param url - The thumbnail URL to use
|
||||||
*/
|
*/
|
||||||
public setThumbnail(url: string | null): this {
|
public setThumbnail(url: string): this {
|
||||||
// Data assertions
|
this.data.thumbnail = { url };
|
||||||
imageURLPredicate.parse(url);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.thumbnail = url ? { url } : undefined;
|
/**
|
||||||
|
* Clears the thumbnail of this embed.
|
||||||
|
*/
|
||||||
|
public clearThumbnail(): this {
|
||||||
|
this.data.thumbnail = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,11 +276,16 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param timestamp - The timestamp or date to use
|
* @param timestamp - The timestamp or date to use
|
||||||
*/
|
*/
|
||||||
public setTimestamp(timestamp: Date | number | null = Date.now()): this {
|
public setTimestamp(timestamp: Date | number | string = Date.now()): this {
|
||||||
// Data assertions
|
this.data.timestamp = new Date(timestamp).toISOString();
|
||||||
timestampPredicate.parse(timestamp);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined;
|
/**
|
||||||
|
* Clears the timestamp of this embed.
|
||||||
|
*/
|
||||||
|
public clearTimestamp(): this {
|
||||||
|
this.data.timestamp = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,11 +294,16 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param title - The title to use
|
* @param title - The title to use
|
||||||
*/
|
*/
|
||||||
public setTitle(title: string | null): this {
|
public setTitle(title: string): this {
|
||||||
// Data assertions
|
this.data.title = title;
|
||||||
titlePredicate.parse(title);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.title = title ?? undefined;
|
/**
|
||||||
|
* Clears the title of this embed.
|
||||||
|
*/
|
||||||
|
public clearTitle(): this {
|
||||||
|
this.data.title = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,22 +312,41 @@ export class EmbedBuilder {
|
|||||||
*
|
*
|
||||||
* @param url - The URL to use
|
* @param url - The URL to use
|
||||||
*/
|
*/
|
||||||
public setURL(url: string | null): this {
|
public setURL(url: string): this {
|
||||||
// Data assertions
|
this.data.url = url;
|
||||||
urlPredicate.parse(url);
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
this.data.url = url ?? undefined;
|
/**
|
||||||
|
* Clears the URL of this embed.
|
||||||
|
*/
|
||||||
|
public clearURL(): this {
|
||||||
|
this.data.url = undefined;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes this builder to API-compatible JSON data.
|
* Serializes this builder to API-compatible JSON data.
|
||||||
*
|
*
|
||||||
* @remarks
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
* This method runs validations on the data before serializing it.
|
*
|
||||||
* As such, it may throw an error if the data is invalid.
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
*/
|
*/
|
||||||
public toJSON(): APIEmbed {
|
public toJSON(validationOverride?: boolean): APIEmbed {
|
||||||
return { ...this.data };
|
const { author, fields, footer, ...rest } = this.data;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...structuredClone(rest),
|
||||||
|
// Disable validation because the embedPredicate below will validate those as well
|
||||||
|
author: this.data.author?.toJSON(false),
|
||||||
|
fields: this.data.fields?.map((field) => field.toJSON(false)),
|
||||||
|
footer: this.data.footer?.toJSON(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
embedPredicate.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
packages/builders/src/messages/embed/EmbedAuthor.ts
Normal file
82
packages/builders/src/messages/embed/EmbedAuthor.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { APIEmbedAuthor } from 'discord-api-types/v10';
|
||||||
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { embedAuthorPredicate } from './Assertions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for the embed author.
|
||||||
|
*/
|
||||||
|
export class EmbedAuthorBuilder {
|
||||||
|
private readonly data: Partial<APIEmbedAuthor>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new embed author from API data.
|
||||||
|
*
|
||||||
|
* @param data - The API data to use
|
||||||
|
*/
|
||||||
|
public constructor(data?: Partial<APIEmbedAuthor>) {
|
||||||
|
this.data = structuredClone(data) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name for this embed author.
|
||||||
|
*
|
||||||
|
* @param name - The name to use
|
||||||
|
*/
|
||||||
|
public setName(name: string): this {
|
||||||
|
this.data.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the URL for this embed author.
|
||||||
|
*
|
||||||
|
* @param url - The url to use
|
||||||
|
*/
|
||||||
|
public setURL(url: string): this {
|
||||||
|
this.data.url = url;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the URL for this embed author.
|
||||||
|
*/
|
||||||
|
public clearURL(): this {
|
||||||
|
this.data.url = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the icon URL for this embed author.
|
||||||
|
*
|
||||||
|
* @param iconURL - The icon URL to use
|
||||||
|
*/
|
||||||
|
public setIconURL(iconURL: string): this {
|
||||||
|
this.data.icon_url = iconURL;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the icon URL for this embed author.
|
||||||
|
*/
|
||||||
|
public clearIconURL(): this {
|
||||||
|
this.data.icon_url = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIEmbedAuthor {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
embedAuthorPredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIEmbedAuthor;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/builders/src/messages/embed/EmbedField.ts
Normal file
66
packages/builders/src/messages/embed/EmbedField.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { APIEmbedField } from 'discord-api-types/v10';
|
||||||
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { embedFieldPredicate } from './Assertions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for embed fields.
|
||||||
|
*/
|
||||||
|
export class EmbedFieldBuilder {
|
||||||
|
private readonly data: Partial<APIEmbedField>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new embed field from API data.
|
||||||
|
*
|
||||||
|
* @param data - The API data to use
|
||||||
|
*/
|
||||||
|
public constructor(data?: Partial<APIEmbedField>) {
|
||||||
|
this.data = structuredClone(data) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name for this embed field.
|
||||||
|
*
|
||||||
|
* @param name - The name to use
|
||||||
|
*/
|
||||||
|
public setName(name: string): this {
|
||||||
|
this.data.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value for this embed field.
|
||||||
|
*
|
||||||
|
* @param value - The value to use
|
||||||
|
*/
|
||||||
|
public setValue(value: string): this {
|
||||||
|
this.data.value = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether this field should display inline.
|
||||||
|
*
|
||||||
|
* @param inline - Whether this field should display inline
|
||||||
|
*/
|
||||||
|
public setInline(inline = true): this {
|
||||||
|
this.data.inline = inline;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIEmbedField {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
embedFieldPredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIEmbedField;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
packages/builders/src/messages/embed/EmbedFooter.ts
Normal file
64
packages/builders/src/messages/embed/EmbedFooter.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { APIEmbedFooter } from 'discord-api-types/v10';
|
||||||
|
import { isValidationEnabled } from '../../util/validation.js';
|
||||||
|
import { embedFooterPredicate } from './Assertions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder that creates API-compatible JSON data for the embed footer.
|
||||||
|
*/
|
||||||
|
export class EmbedFooterBuilder {
|
||||||
|
private readonly data: Partial<APIEmbedFooter>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new embed footer from API data.
|
||||||
|
*
|
||||||
|
* @param data - The API data to use
|
||||||
|
*/
|
||||||
|
public constructor(data?: Partial<APIEmbedFooter>) {
|
||||||
|
this.data = structuredClone(data) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the text for this embed footer.
|
||||||
|
*
|
||||||
|
* @param text - The text to use
|
||||||
|
*/
|
||||||
|
public setText(text: string): this {
|
||||||
|
this.data.text = text;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the url for this embed footer.
|
||||||
|
*
|
||||||
|
* @param url - The url to use
|
||||||
|
*/
|
||||||
|
public setIconURL(url: string): this {
|
||||||
|
this.data.icon_url = url;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the icon URL for this embed footer.
|
||||||
|
*/
|
||||||
|
public clearIconURL(): this {
|
||||||
|
this.data.icon_url = undefined;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this builder to API-compatible JSON data.
|
||||||
|
*
|
||||||
|
* Note that by disabling validation, there is no guarantee that the resulting object will be valid.
|
||||||
|
*
|
||||||
|
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||||
|
*/
|
||||||
|
public toJSON(validationOverride?: boolean): APIEmbedFooter {
|
||||||
|
const clone = structuredClone(this.data);
|
||||||
|
|
||||||
|
if (validationOverride ?? isValidationEnabled()) {
|
||||||
|
embedFooterPredicate.parse(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as APIEmbedFooter;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/builders/src/util/resolveBuilder.ts
Normal file
40
packages/builders/src/util/resolveBuilder.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { JSONEncodable } from '@discordjs/util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @privateRemarks
|
||||||
|
* This is a type-guard util, because if you were to in-line `builder instanceof Constructor` in the `resolveBuilder`
|
||||||
|
* function, TS doesn't narrow out the type `Builder`, causing a type error on the last return statement.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
function isBuilder<Builder extends JSONEncodable<any>>(
|
||||||
|
builder: unknown,
|
||||||
|
Constructor: new () => Builder,
|
||||||
|
): builder is Builder {
|
||||||
|
return builder instanceof Constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Resolves" a builder from the 3 ways it can be input:
|
||||||
|
* 1. A clean instance
|
||||||
|
* 2. A data object that can be used to construct the builder
|
||||||
|
* 3. A function that takes a builder and returns a builder e.g. `builder => builder.setFoo('bar')`
|
||||||
|
*
|
||||||
|
* @typeParam Builder - The builder type
|
||||||
|
* @typeParam BuilderData - The data object that can be used to construct the builder
|
||||||
|
* @param builder - The user input, as described in the function description
|
||||||
|
* @param Constructor - The constructor of the builder
|
||||||
|
*/
|
||||||
|
export function resolveBuilder<Builder extends JSONEncodable<any>, BuilderData extends Record<PropertyKey, any>>(
|
||||||
|
builder: Builder | BuilderData | ((builder: Builder) => Builder),
|
||||||
|
Constructor: new (data?: BuilderData) => Builder,
|
||||||
|
): Builder {
|
||||||
|
if (isBuilder(builder, Constructor)) {
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof builder === 'function') {
|
||||||
|
return builder(new Constructor());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Constructor(builder);
|
||||||
|
}
|
||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -670,27 +670,21 @@ importers:
|
|||||||
|
|
||||||
packages/builders:
|
packages/builders:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@discordjs/formatters':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../formatters
|
|
||||||
'@discordjs/util':
|
'@discordjs/util':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../util
|
version: link:../util
|
||||||
'@sapphire/shapeshift':
|
|
||||||
specifier: ^4.0.0
|
|
||||||
version: 4.0.0
|
|
||||||
discord-api-types:
|
discord-api-types:
|
||||||
specifier: ^0.37.101
|
specifier: ^0.37.101
|
||||||
version: 0.37.101
|
version: 0.37.101
|
||||||
fast-deep-equal:
|
|
||||||
specifier: ^3.1.3
|
|
||||||
version: 3.1.3
|
|
||||||
ts-mixer:
|
ts-mixer:
|
||||||
specifier: ^6.0.4
|
specifier: ^6.0.4
|
||||||
version: 6.0.4
|
version: 6.0.4
|
||||||
tslib:
|
tslib:
|
||||||
specifier: ^2.6.3
|
specifier: ^2.6.3
|
||||||
version: 2.6.3
|
version: 2.6.3
|
||||||
|
zod:
|
||||||
|
specifier: ^3.23.8
|
||||||
|
version: 3.23.8
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@discordjs/api-extractor':
|
'@discordjs/api-extractor':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
@@ -702,11 +696,11 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^16.18.105
|
specifier: ^18.19.44
|
||||||
version: 16.18.105
|
version: 18.19.45
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^2.0.5
|
specifier: ^2.0.5
|
||||||
version: 2.0.5(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@16.18.105)(happy-dom@14.12.3)(terser@5.31.6))
|
version: 2.0.5(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))
|
||||||
cross-env:
|
cross-env:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
@@ -727,7 +721,7 @@ importers:
|
|||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
tsup:
|
tsup:
|
||||||
specifier: ^8.2.4
|
specifier: ^8.2.4
|
||||||
version: 8.2.4(@microsoft/api-extractor@7.43.0(@types/node@16.18.105))(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0)
|
version: 8.2.4(@microsoft/api-extractor@7.43.0(@types/node@18.19.45))(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0)
|
||||||
turbo:
|
turbo:
|
||||||
specifier: ^2.0.14
|
specifier: ^2.0.14
|
||||||
version: 2.0.14
|
version: 2.0.14
|
||||||
@@ -736,7 +730,7 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^2.0.5
|
specifier: ^2.0.5
|
||||||
version: 2.0.5(@edge-runtime/vm@3.2.0)(@types/node@16.18.105)(happy-dom@14.12.3)(terser@5.31.6)
|
version: 2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)
|
||||||
|
|
||||||
packages/collection:
|
packages/collection:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user