@minesa-org/mini-interaction 0.3.13 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/builders/FileUploadBuilder.d.ts +3 -23
- package/dist/builders/FileUploadBuilder.js +16 -53
- package/dist/builders/LabelBuilder.d.ts +3 -22
- package/dist/builders/LabelBuilder.js +16 -50
- package/dist/builders/ModalBuilder.d.ts +4 -28
- package/dist/builders/ModalBuilder.js +20 -81
- package/dist/builders/ModalChannelSelectMenuBuilder.d.ts +7 -43
- package/dist/builders/ModalChannelSelectMenuBuilder.js +22 -107
- package/dist/builders/ModalRoleSelectMenuBuilder.d.ts +6 -35
- package/dist/builders/ModalRoleSelectMenuBuilder.js +21 -90
- package/dist/builders/RadioBuilder.d.ts +17 -0
- package/dist/builders/RadioBuilder.js +29 -0
- package/dist/builders/TextInputBuilder.d.ts +3 -32
- package/dist/builders/TextInputBuilder.js +21 -83
- package/dist/builders/__tests__/builders.test.d.ts +1 -0
- package/dist/builders/__tests__/builders.test.js +31 -0
- package/dist/builders/index.d.ts +2 -0
- package/dist/builders/index.js +1 -0
- package/dist/compat/LegacyMiniInteractionAdapter.d.ts +19 -0
- package/dist/compat/LegacyMiniInteractionAdapter.js +23 -0
- package/dist/core/http/DiscordRestClient.d.ts +19 -0
- package/dist/core/http/DiscordRestClient.js +55 -0
- package/dist/core/interactions/InteractionContext.d.ts +25 -0
- package/dist/core/interactions/InteractionContext.js +44 -0
- package/dist/core/interactions/InteractionVerifier.d.ts +8 -0
- package/dist/core/interactions/InteractionVerifier.js +9 -0
- package/dist/core/interactions/__tests__/interaction-context.test.d.ts +1 -0
- package/dist/core/interactions/__tests__/interaction-context.test.js +38 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/router/InteractionRouter.d.ts +12 -0
- package/dist/router/InteractionRouter.js +33 -0
- package/dist/types/discord.d.ts +11 -0
- package/dist/types/discord.js +1 -0
- package/dist/types/radio.d.ts +23 -0
- package/dist/types/radio.js +5 -0
- package/dist/types/validation.d.ts +8 -0
- package/dist/types/validation.js +26 -0
- package/package.json +55 -53
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { SelectMenuDefaultValueType, type APIRoleSelectComponent, type APISelectMenuDefaultValue } from
|
|
2
|
-
import type { JSONEncodable } from
|
|
3
|
-
/** Shape describing initial modal role select menu data accepted by the builder. */
|
|
1
|
+
import { SelectMenuDefaultValueType, type APIRoleSelectComponent, type APISelectMenuDefaultValue } from 'discord-api-types/v10';
|
|
2
|
+
import type { JSONEncodable } from './shared.js';
|
|
4
3
|
export type ModalRoleSelectMenuBuilderData = {
|
|
5
4
|
customId?: string;
|
|
6
5
|
placeholder?: string;
|
|
@@ -10,43 +9,15 @@ export type ModalRoleSelectMenuBuilderData = {
|
|
|
10
9
|
required?: boolean;
|
|
11
10
|
defaultValues?: APISelectMenuDefaultValue<SelectMenuDefaultValueType.Role>[];
|
|
12
11
|
};
|
|
13
|
-
/** Builder for Discord role select menu components in modals. */
|
|
14
12
|
export declare class ModalRoleSelectMenuBuilder implements JSONEncodable<APIRoleSelectComponent> {
|
|
15
|
-
private data;
|
|
16
|
-
/**
|
|
17
|
-
* Creates a new modal role select menu builder with optional seed data.
|
|
18
|
-
*/
|
|
13
|
+
private readonly data;
|
|
19
14
|
constructor(data?: ModalRoleSelectMenuBuilderData);
|
|
20
|
-
/**
|
|
21
|
-
* Sets the unique custom identifier for the select menu interaction.
|
|
22
|
-
*/
|
|
23
15
|
setCustomId(customId: string): this;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
setPlaceholder(placeholder: string | null | undefined): this;
|
|
28
|
-
/**
|
|
29
|
-
* Sets the minimum number of roles that must be selected.
|
|
30
|
-
*/
|
|
31
|
-
setMinValues(minValues: number | null | undefined): this;
|
|
32
|
-
/**
|
|
33
|
-
* Sets the maximum number of roles that can be selected.
|
|
34
|
-
*/
|
|
35
|
-
setMaxValues(maxValues: number | null | undefined): this;
|
|
36
|
-
/**
|
|
37
|
-
* Toggles whether the select menu is disabled.
|
|
38
|
-
*/
|
|
16
|
+
setPlaceholder(placeholder?: string): this;
|
|
17
|
+
setMinValues(minValues?: number): this;
|
|
18
|
+
setMaxValues(maxValues?: number): this;
|
|
39
19
|
setDisabled(disabled: boolean): this;
|
|
40
|
-
/**
|
|
41
|
-
* Marks the select menu as required in the modal.
|
|
42
|
-
*/
|
|
43
20
|
setRequired(required: boolean): this;
|
|
44
|
-
/**
|
|
45
|
-
* Replaces the default role selections displayed when the menu renders.
|
|
46
|
-
*/
|
|
47
21
|
setDefaultValues(defaultValues: Iterable<APISelectMenuDefaultValue<SelectMenuDefaultValueType.Role>>): this;
|
|
48
|
-
/**
|
|
49
|
-
* Serialises the builder into an API compatible role select menu payload.
|
|
50
|
-
*/
|
|
51
22
|
toJSON(): APIRoleSelectComponent;
|
|
52
23
|
}
|
|
@@ -1,98 +1,29 @@
|
|
|
1
|
-
import { ComponentType, SelectMenuDefaultValueType
|
|
2
|
-
|
|
1
|
+
import { ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
|
2
|
+
import { assertDefined, assertRange, assertStringLength, ValidationError } from '../types/validation.js';
|
|
3
3
|
export class ModalRoleSelectMenuBuilder {
|
|
4
4
|
data;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
minValues: data.minValues,
|
|
13
|
-
maxValues: data.maxValues,
|
|
14
|
-
disabled: data.disabled,
|
|
15
|
-
required: data.required,
|
|
16
|
-
defaultValues: data.defaultValues
|
|
17
|
-
? data.defaultValues.map((value) => ({
|
|
18
|
-
...value,
|
|
19
|
-
type: SelectMenuDefaultValueType.Role,
|
|
20
|
-
}))
|
|
21
|
-
: undefined,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Sets the unique custom identifier for the select menu interaction.
|
|
26
|
-
*/
|
|
27
|
-
setCustomId(customId) {
|
|
28
|
-
this.data.customId = customId;
|
|
29
|
-
return this;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Sets or clears the placeholder text displayed when no role is selected.
|
|
33
|
-
*/
|
|
34
|
-
setPlaceholder(placeholder) {
|
|
35
|
-
this.data.placeholder = placeholder ?? undefined;
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Sets the minimum number of roles that must be selected.
|
|
40
|
-
*/
|
|
41
|
-
setMinValues(minValues) {
|
|
42
|
-
this.data.minValues = minValues ?? undefined;
|
|
43
|
-
return this;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Sets the maximum number of roles that can be selected.
|
|
47
|
-
*/
|
|
48
|
-
setMaxValues(maxValues) {
|
|
49
|
-
this.data.maxValues = maxValues ?? undefined;
|
|
50
|
-
return this;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Toggles whether the select menu is disabled.
|
|
54
|
-
*/
|
|
55
|
-
setDisabled(disabled) {
|
|
56
|
-
this.data.disabled = disabled;
|
|
57
|
-
return this;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Marks the select menu as required in the modal.
|
|
61
|
-
*/
|
|
62
|
-
setRequired(required) {
|
|
63
|
-
this.data.required = required;
|
|
64
|
-
return this;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Replaces the default role selections displayed when the menu renders.
|
|
68
|
-
*/
|
|
5
|
+
constructor(data = {}) { this.data = { ...data }; }
|
|
6
|
+
setCustomId(customId) { this.data.customId = customId; return this; }
|
|
7
|
+
setPlaceholder(placeholder) { this.data.placeholder = placeholder; return this; }
|
|
8
|
+
setMinValues(minValues) { this.data.minValues = minValues; return this; }
|
|
9
|
+
setMaxValues(maxValues) { this.data.maxValues = maxValues; return this; }
|
|
10
|
+
setDisabled(disabled) { this.data.disabled = disabled; return this; }
|
|
11
|
+
setRequired(required) { this.data.required = required; return this; }
|
|
69
12
|
setDefaultValues(defaultValues) {
|
|
70
|
-
this.data.defaultValues = Array.from(defaultValues, (
|
|
71
|
-
...value,
|
|
72
|
-
type: SelectMenuDefaultValueType.Role,
|
|
73
|
-
}));
|
|
13
|
+
this.data.defaultValues = Array.from(defaultValues, (v) => ({ ...v, type: SelectMenuDefaultValueType.Role }));
|
|
74
14
|
return this;
|
|
75
15
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Serialises the builder into an API compatible role select menu payload.
|
|
78
|
-
*/
|
|
79
16
|
toJSON() {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
required: this.data.required,
|
|
92
|
-
default_values: this.data.defaultValues?.map((value) => ({
|
|
93
|
-
...value,
|
|
94
|
-
type: SelectMenuDefaultValueType.Role,
|
|
95
|
-
})),
|
|
96
|
-
};
|
|
17
|
+
const customId = assertDefined('ModalRoleSelectMenuBuilder', 'custom_id', this.data.customId);
|
|
18
|
+
assertStringLength('ModalRoleSelectMenuBuilder', 'custom_id', customId, 1, 100);
|
|
19
|
+
if (this.data.placeholder)
|
|
20
|
+
assertStringLength('ModalRoleSelectMenuBuilder', 'placeholder', this.data.placeholder, 1, 150);
|
|
21
|
+
if (this.data.minValues !== undefined)
|
|
22
|
+
assertRange('ModalRoleSelectMenuBuilder', 'min_values', this.data.minValues, 0, 25);
|
|
23
|
+
if (this.data.maxValues !== undefined)
|
|
24
|
+
assertRange('ModalRoleSelectMenuBuilder', 'max_values', this.data.maxValues, 1, 25);
|
|
25
|
+
if (this.data.minValues !== undefined && this.data.maxValues !== undefined && this.data.minValues > this.data.maxValues)
|
|
26
|
+
throw new ValidationError('ModalRoleSelectMenuBuilder', 'min_values', 'cannot exceed max_values');
|
|
27
|
+
return { type: ComponentType.RoleSelect, custom_id: customId, placeholder: this.data.placeholder, min_values: this.data.minValues, max_values: this.data.maxValues, disabled: this.data.disabled, required: this.data.required, default_values: this.data.defaultValues };
|
|
97
28
|
}
|
|
98
29
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JSONEncodable } from './shared.js';
|
|
2
|
+
import { APIRadioComponent, APIRadioOption } from '../types/radio.js';
|
|
3
|
+
export type RadioBuilderData = {
|
|
4
|
+
customId?: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
options?: APIRadioOption[];
|
|
8
|
+
};
|
|
9
|
+
export declare class RadioBuilder implements JSONEncodable<APIRadioComponent> {
|
|
10
|
+
private readonly data;
|
|
11
|
+
constructor(data?: RadioBuilderData);
|
|
12
|
+
setCustomId(customId: string): this;
|
|
13
|
+
setRequired(required: boolean): this;
|
|
14
|
+
setDisabled(disabled: boolean): this;
|
|
15
|
+
addOptions(...options: APIRadioOption[]): this;
|
|
16
|
+
toJSON(): APIRadioComponent;
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RADIO_COMPONENT_TYPE } from '../types/radio.js';
|
|
2
|
+
import { assertDefined, assertStringLength, ValidationError } from '../types/validation.js';
|
|
3
|
+
export class RadioBuilder {
|
|
4
|
+
data;
|
|
5
|
+
constructor(data = {}) { this.data = { ...data, options: data.options ? [...data.options] : [] }; }
|
|
6
|
+
setCustomId(customId) { this.data.customId = customId; return this; }
|
|
7
|
+
setRequired(required) { this.data.required = required; return this; }
|
|
8
|
+
setDisabled(disabled) { this.data.disabled = disabled; return this; }
|
|
9
|
+
addOptions(...options) { this.data.options = [...(this.data.options ?? []), ...options]; return this; }
|
|
10
|
+
toJSON() {
|
|
11
|
+
const customId = assertDefined('RadioBuilder', 'custom_id', this.data.customId);
|
|
12
|
+
assertStringLength('RadioBuilder', 'custom_id', customId, 1, 100);
|
|
13
|
+
const options = [...(this.data.options ?? [])];
|
|
14
|
+
if (options.length === 0 || options.length > 25)
|
|
15
|
+
throw new ValidationError('RadioBuilder', 'options', 'must contain 1-25 options');
|
|
16
|
+
let defaults = 0;
|
|
17
|
+
for (const [index, option] of options.entries()) {
|
|
18
|
+
assertStringLength('RadioBuilder', `options[${index}].label`, option.label, 1, 100);
|
|
19
|
+
assertStringLength('RadioBuilder', `options[${index}].value`, option.value, 1, 100);
|
|
20
|
+
if (option.description)
|
|
21
|
+
assertStringLength('RadioBuilder', `options[${index}].description`, option.description, 1, 100);
|
|
22
|
+
if (option.default)
|
|
23
|
+
defaults += 1;
|
|
24
|
+
}
|
|
25
|
+
if (defaults > 1)
|
|
26
|
+
throw new ValidationError('RadioBuilder', 'options.default', 'radio supports only one default option');
|
|
27
|
+
return { type: RADIO_COMPONENT_TYPE, custom_id: customId, disabled: this.data.disabled, required: this.data.required, options };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { TextInputStyle, type APITextInputComponent } from
|
|
2
|
-
import type
|
|
3
|
-
/** Shape describing initial text input data accepted by the builder. */
|
|
1
|
+
import { TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10';
|
|
2
|
+
import { type JSONEncodable } from './shared.js';
|
|
4
3
|
export type TextInputBuilderData = {
|
|
5
4
|
customId?: string;
|
|
6
5
|
style?: TextInputStyle;
|
|
@@ -10,43 +9,15 @@ export type TextInputBuilderData = {
|
|
|
10
9
|
value?: string;
|
|
11
10
|
placeholder?: string;
|
|
12
11
|
};
|
|
13
|
-
/** Builder for Discord text input components used in modals. */
|
|
14
12
|
export declare class TextInputBuilder implements JSONEncodable<APITextInputComponent> {
|
|
15
|
-
private data;
|
|
16
|
-
/**
|
|
17
|
-
* Creates a new text input builder with optional seed data.
|
|
18
|
-
*/
|
|
13
|
+
private readonly data;
|
|
19
14
|
constructor(data?: TextInputBuilderData);
|
|
20
|
-
/**
|
|
21
|
-
* Sets the custom identifier for this text input.
|
|
22
|
-
*/
|
|
23
15
|
setCustomId(customId: string): this;
|
|
24
|
-
/**
|
|
25
|
-
* Sets the style of the text input (Short or Paragraph).
|
|
26
|
-
*/
|
|
27
16
|
setStyle(style: TextInputStyle): this;
|
|
28
|
-
/**
|
|
29
|
-
* Sets the minimum length of the input text.
|
|
30
|
-
*/
|
|
31
17
|
setMinLength(minLength: number): this;
|
|
32
|
-
/**
|
|
33
|
-
* Sets the maximum length of the input text.
|
|
34
|
-
*/
|
|
35
18
|
setMaxLength(maxLength: number): this;
|
|
36
|
-
/**
|
|
37
|
-
* Sets whether this text input is required.
|
|
38
|
-
*/
|
|
39
19
|
setRequired(required: boolean): this;
|
|
40
|
-
/**
|
|
41
|
-
* Sets the pre-filled value for this text input.
|
|
42
|
-
*/
|
|
43
20
|
setValue(value: string): this;
|
|
44
|
-
/**
|
|
45
|
-
* Sets the placeholder text shown when the input is empty.
|
|
46
|
-
*/
|
|
47
21
|
setPlaceholder(placeholder: string): this;
|
|
48
|
-
/**
|
|
49
|
-
* Serialises the builder into an API compatible text input payload.
|
|
50
|
-
*/
|
|
51
22
|
toJSON(): APITextInputComponent;
|
|
52
23
|
}
|
|
@@ -1,88 +1,26 @@
|
|
|
1
|
-
import { ComponentType, TextInputStyle
|
|
2
|
-
|
|
1
|
+
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
|
2
|
+
import { assertDefined, assertRange, assertStringLength } from '../types/validation.js';
|
|
3
3
|
export class TextInputBuilder {
|
|
4
4
|
data;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
maxLength: data.maxLength,
|
|
14
|
-
required: data.required,
|
|
15
|
-
value: data.value,
|
|
16
|
-
placeholder: data.placeholder,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Sets the custom identifier for this text input.
|
|
21
|
-
*/
|
|
22
|
-
setCustomId(customId) {
|
|
23
|
-
this.data.customId = customId;
|
|
24
|
-
return this;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Sets the style of the text input (Short or Paragraph).
|
|
28
|
-
*/
|
|
29
|
-
setStyle(style) {
|
|
30
|
-
this.data.style = style;
|
|
31
|
-
return this;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Sets the minimum length of the input text.
|
|
35
|
-
*/
|
|
36
|
-
setMinLength(minLength) {
|
|
37
|
-
this.data.minLength = minLength;
|
|
38
|
-
return this;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Sets the maximum length of the input text.
|
|
42
|
-
*/
|
|
43
|
-
setMaxLength(maxLength) {
|
|
44
|
-
this.data.maxLength = maxLength;
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Sets whether this text input is required.
|
|
49
|
-
*/
|
|
50
|
-
setRequired(required) {
|
|
51
|
-
this.data.required = required;
|
|
52
|
-
return this;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Sets the pre-filled value for this text input.
|
|
56
|
-
*/
|
|
57
|
-
setValue(value) {
|
|
58
|
-
this.data.value = value;
|
|
59
|
-
return this;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Sets the placeholder text shown when the input is empty.
|
|
63
|
-
*/
|
|
64
|
-
setPlaceholder(placeholder) {
|
|
65
|
-
this.data.placeholder = placeholder;
|
|
66
|
-
return this;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Serialises the builder into an API compatible text input payload.
|
|
70
|
-
*/
|
|
5
|
+
constructor(data = {}) { this.data = { style: TextInputStyle.Short, ...data }; }
|
|
6
|
+
setCustomId(customId) { this.data.customId = customId; return this; }
|
|
7
|
+
setStyle(style) { this.data.style = style; return this; }
|
|
8
|
+
setMinLength(minLength) { this.data.minLength = minLength; return this; }
|
|
9
|
+
setMaxLength(maxLength) { this.data.maxLength = maxLength; return this; }
|
|
10
|
+
setRequired(required) { this.data.required = required; return this; }
|
|
11
|
+
setValue(value) { this.data.value = value; return this; }
|
|
12
|
+
setPlaceholder(placeholder) { this.data.placeholder = placeholder; return this; }
|
|
71
13
|
toJSON() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
required: this.data.required,
|
|
84
|
-
value: this.data.value,
|
|
85
|
-
placeholder: this.data.placeholder,
|
|
86
|
-
};
|
|
14
|
+
const customId = assertDefined('TextInputBuilder', 'custom_id', this.data.customId);
|
|
15
|
+
assertStringLength('TextInputBuilder', 'custom_id', customId, 1, 100);
|
|
16
|
+
if (this.data.minLength !== undefined)
|
|
17
|
+
assertRange('TextInputBuilder', 'min_length', this.data.minLength, 0, 4000);
|
|
18
|
+
if (this.data.maxLength !== undefined)
|
|
19
|
+
assertRange('TextInputBuilder', 'max_length', this.data.maxLength, 1, 4000);
|
|
20
|
+
if (this.data.placeholder)
|
|
21
|
+
assertStringLength('TextInputBuilder', 'placeholder', this.data.placeholder, 1, 100);
|
|
22
|
+
if (this.data.value)
|
|
23
|
+
assertStringLength('TextInputBuilder', 'value', this.data.value, 0, 4000);
|
|
24
|
+
return { type: ComponentType.TextInput, custom_id: customId, style: this.data.style ?? TextInputStyle.Short, min_length: this.data.minLength, max_length: this.data.maxLength, required: this.data.required, value: this.data.value, placeholder: this.data.placeholder };
|
|
87
25
|
}
|
|
88
26
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { LabelBuilder } from '../LabelBuilder.js';
|
|
4
|
+
import { TextInputBuilder } from '../TextInputBuilder.js';
|
|
5
|
+
import { FileUploadBuilder } from '../FileUploadBuilder.js';
|
|
6
|
+
import { ModalBuilder } from '../ModalBuilder.js';
|
|
7
|
+
import { ModalRoleSelectMenuBuilder } from '../ModalRoleSelectMenuBuilder.js';
|
|
8
|
+
import { RadioBuilder } from '../RadioBuilder.js';
|
|
9
|
+
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
|
10
|
+
test('TextInputBuilder validation and snapshot', () => {
|
|
11
|
+
const json = new TextInputBuilder().setCustomId('name').setStyle(TextInputStyle.Short).setMinLength(1).setMaxLength(20).toJSON();
|
|
12
|
+
assert.equal(json.type, ComponentType.TextInput);
|
|
13
|
+
assert.match(JSON.stringify(json), /"custom_id":"name"/);
|
|
14
|
+
});
|
|
15
|
+
test('LabelBuilder throws descriptive errors', () => {
|
|
16
|
+
assert.throws(() => new LabelBuilder().toJSON(), /\[LabelBuilder\] label/);
|
|
17
|
+
});
|
|
18
|
+
test('FileUploadBuilder min > max fails', () => {
|
|
19
|
+
assert.throws(() => new FileUploadBuilder().setCustomId('upload').setMinValues(3).setMaxValues(1).toJSON(), /min_values/);
|
|
20
|
+
});
|
|
21
|
+
test('ModalBuilder validates nesting', () => {
|
|
22
|
+
const text = new TextInputBuilder().setCustomId('x').toJSON();
|
|
23
|
+
assert.throws(() => new ModalBuilder().setCustomId('id').setTitle('t').addComponents(text).toJSON(), /invalid modal top-level/);
|
|
24
|
+
});
|
|
25
|
+
test('ModalRoleSelectMenuBuilder serializes', () => {
|
|
26
|
+
const json = new ModalRoleSelectMenuBuilder().setCustomId('role').setMinValues(1).setMaxValues(1).toJSON();
|
|
27
|
+
assert.equal(json.type, ComponentType.RoleSelect);
|
|
28
|
+
});
|
|
29
|
+
test('RadioBuilder single default rule', () => {
|
|
30
|
+
assert.throws(() => new RadioBuilder().setCustomId('r').addOptions({ label: 'A', value: 'a', default: true }, { label: 'B', value: 'b', default: true }).toJSON(), /only one default/);
|
|
31
|
+
});
|
package/dist/builders/index.d.ts
CHANGED
|
@@ -36,3 +36,5 @@ export type { EmbedBuilderData } from "./EmbedBuilder.js";
|
|
|
36
36
|
export { ContainerBuilder, SectionBuilder, TextDisplayBuilder, SeparatorBuilder, GalleryBuilder, GalleryItemBuilder, ThumbnailBuilder, } from "./MiniContainerBuilder.js";
|
|
37
37
|
export type { ContainerBuilderData, SectionBuilderData, TextDisplayBuilderData, SeparatorBuilderData, GalleryBuilderData, GalleryItemBuilderData, ThumbnailBuilderData, MiniContainerComponent, MiniSectionComponent, MiniTextDisplayComponent, MiniSeparatorComponent, MiniGalleryComponent, MiniGalleryItemComponent, MiniThumbnailComponent, MiniContentComponent, MiniSectionAccessoryComponent, } from "./MiniContainerBuilder.js";
|
|
38
38
|
export type { JSONEncodable } from "./shared.js";
|
|
39
|
+
export { RadioBuilder } from "./RadioBuilder.js";
|
|
40
|
+
export type { RadioBuilderData } from "./RadioBuilder.js";
|
package/dist/builders/index.js
CHANGED
|
@@ -17,3 +17,4 @@ export { FileUploadBuilder } from "./FileUploadBuilder.js";
|
|
|
17
17
|
export { AutomodRuleBuilder } from "./AutomodRuleBuilder.js";
|
|
18
18
|
export { EmbedBuilder } from "./EmbedBuilder.js";
|
|
19
19
|
export { ContainerBuilder, SectionBuilder, TextDisplayBuilder, SeparatorBuilder, GalleryBuilder, GalleryItemBuilder, ThumbnailBuilder, } from "./MiniContainerBuilder.js";
|
|
20
|
+
export { RadioBuilder } from "./RadioBuilder.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { APIInteractionResponse } from 'discord-api-types/v10';
|
|
2
|
+
import { DiscordRestClient } from '../core/http/DiscordRestClient.js';
|
|
3
|
+
import { InteractionRouter } from '../router/InteractionRouter.js';
|
|
4
|
+
export type LegacyAdapterOptions = {
|
|
5
|
+
publicKey: string;
|
|
6
|
+
applicationId: string;
|
|
7
|
+
token: string;
|
|
8
|
+
};
|
|
9
|
+
export declare class LegacyMiniInteractionAdapter {
|
|
10
|
+
private readonly options;
|
|
11
|
+
readonly router: InteractionRouter;
|
|
12
|
+
readonly rest: DiscordRestClient;
|
|
13
|
+
constructor(options: LegacyAdapterOptions);
|
|
14
|
+
handleRequest(input: {
|
|
15
|
+
body: string;
|
|
16
|
+
signature: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
}): Promise<APIInteractionResponse>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { InteractionType } from 'discord-api-types/v10';
|
|
2
|
+
import { DiscordRestClient } from '../core/http/DiscordRestClient.js';
|
|
3
|
+
import { InteractionContext } from '../core/interactions/InteractionContext.js';
|
|
4
|
+
import { verifyAndParseInteraction } from '../core/interactions/InteractionVerifier.js';
|
|
5
|
+
import { InteractionRouter } from '../router/InteractionRouter.js';
|
|
6
|
+
export class LegacyMiniInteractionAdapter {
|
|
7
|
+
options;
|
|
8
|
+
router = new InteractionRouter();
|
|
9
|
+
rest;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.rest = new DiscordRestClient({ applicationId: options.applicationId, token: options.token });
|
|
13
|
+
}
|
|
14
|
+
async handleRequest(input) {
|
|
15
|
+
const interaction = await verifyAndParseInteraction({ ...input, publicKey: this.options.publicKey });
|
|
16
|
+
if (interaction.type === InteractionType.Ping) {
|
|
17
|
+
return { type: 1 };
|
|
18
|
+
}
|
|
19
|
+
const ctx = new InteractionContext({ interaction, rest: this.rest, autoAck: { enabled: true } });
|
|
20
|
+
const response = await this.router.dispatch(interaction, ctx);
|
|
21
|
+
return response ?? ctx.deferReply();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type FetchLike = typeof fetch;
|
|
2
|
+
export type DiscordRestClientOptions = {
|
|
3
|
+
token: string;
|
|
4
|
+
applicationId: string;
|
|
5
|
+
apiBaseUrl?: string;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
fetchImplementation?: FetchLike;
|
|
8
|
+
};
|
|
9
|
+
export declare class DiscordRestClient {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly fetchImpl;
|
|
12
|
+
private readonly baseUrl;
|
|
13
|
+
private readonly maxRetries;
|
|
14
|
+
constructor(options: DiscordRestClientOptions);
|
|
15
|
+
request<T>(path: string, init?: RequestInit): Promise<T>;
|
|
16
|
+
createFollowup(interactionToken: string, body: unknown): Promise<unknown>;
|
|
17
|
+
editOriginal(interactionToken: string, body: unknown): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
export class DiscordRestClient {
|
|
3
|
+
options;
|
|
4
|
+
fetchImpl;
|
|
5
|
+
baseUrl;
|
|
6
|
+
maxRetries;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.fetchImpl = options.fetchImplementation ?? fetch;
|
|
10
|
+
this.baseUrl = options.apiBaseUrl ?? 'https://discord.com/api/v10';
|
|
11
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
12
|
+
}
|
|
13
|
+
async request(path, init = {}) {
|
|
14
|
+
let lastError;
|
|
15
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
16
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
17
|
+
...init,
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bot ${this.options.token}`,
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
...(init.headers ?? {}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (response.status === 429) {
|
|
25
|
+
const retryAfter = Number(response.headers.get('retry-after') ?? '1');
|
|
26
|
+
await sleep(Math.ceil(retryAfter * 1000));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (response.ok) {
|
|
30
|
+
if (response.status === 204)
|
|
31
|
+
return undefined;
|
|
32
|
+
return (await response.json());
|
|
33
|
+
}
|
|
34
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
35
|
+
await sleep(150 * (attempt + 1));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
lastError = new Error(`[DiscordRestClient] ${init.method ?? 'GET'} ${path} failed: ${response.status}`);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
throw lastError instanceof Error ? lastError : new Error('[DiscordRestClient] unknown request failure');
|
|
42
|
+
}
|
|
43
|
+
createFollowup(interactionToken, body) {
|
|
44
|
+
return this.request(`/webhooks/${this.options.applicationId}/${interactionToken}`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
editOriginal(interactionToken, body) {
|
|
50
|
+
return this.request(`/webhooks/${this.options.applicationId}/${interactionToken}/messages/@original`, {
|
|
51
|
+
method: 'PATCH',
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { APIInteractionResponse, APIInteractionResponseCallbackData, APIModalInteractionResponseCallbackData } from 'discord-api-types/v10';
|
|
2
|
+
import type { ParsedInteraction } from '../../types/discord.js';
|
|
3
|
+
import { DiscordRestClient } from '../http/DiscordRestClient.js';
|
|
4
|
+
export type InteractionContextOptions = {
|
|
5
|
+
interaction: ParsedInteraction;
|
|
6
|
+
rest: DiscordRestClient;
|
|
7
|
+
autoAck?: {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
delayMs?: number;
|
|
10
|
+
};
|
|
11
|
+
onDiagnostic?: (message: string) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare class InteractionContext {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private responded;
|
|
16
|
+
private autoAckTimer?;
|
|
17
|
+
constructor(options: InteractionContextOptions);
|
|
18
|
+
reply(data: APIInteractionResponseCallbackData): APIInteractionResponse;
|
|
19
|
+
deferReply(ephemeral?: boolean): APIInteractionResponse;
|
|
20
|
+
showModal(data: APIModalInteractionResponseCallbackData): APIInteractionResponse;
|
|
21
|
+
editReply(body: unknown): Promise<unknown>;
|
|
22
|
+
followUp(body: unknown): Promise<unknown>;
|
|
23
|
+
get hasResponded(): boolean;
|
|
24
|
+
private clearAutoAck;
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class InteractionContext {
|
|
2
|
+
options;
|
|
3
|
+
responded = false;
|
|
4
|
+
autoAckTimer;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
if (options.autoAck?.enabled) {
|
|
8
|
+
const delay = options.autoAck.delayMs ?? 2000;
|
|
9
|
+
this.autoAckTimer = setTimeout(() => {
|
|
10
|
+
if (!this.responded) {
|
|
11
|
+
options.onDiagnostic?.(`[InteractionContext] auto-ack triggered for ${this.options.interaction.id}`);
|
|
12
|
+
}
|
|
13
|
+
}, delay);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
reply(data) {
|
|
17
|
+
this.responded = true;
|
|
18
|
+
this.clearAutoAck();
|
|
19
|
+
return { type: 4, data };
|
|
20
|
+
}
|
|
21
|
+
deferReply(ephemeral = false) {
|
|
22
|
+
this.responded = true;
|
|
23
|
+
this.clearAutoAck();
|
|
24
|
+
return { type: 5, data: ephemeral ? { flags: 64 } : undefined };
|
|
25
|
+
}
|
|
26
|
+
showModal(data) {
|
|
27
|
+
this.responded = true;
|
|
28
|
+
this.clearAutoAck();
|
|
29
|
+
return { type: 9, data };
|
|
30
|
+
}
|
|
31
|
+
editReply(body) {
|
|
32
|
+
return this.options.rest.editOriginal(this.options.interaction.token, body);
|
|
33
|
+
}
|
|
34
|
+
followUp(body) {
|
|
35
|
+
return this.options.rest.createFollowup(this.options.interaction.token, body);
|
|
36
|
+
}
|
|
37
|
+
get hasResponded() {
|
|
38
|
+
return this.responded;
|
|
39
|
+
}
|
|
40
|
+
clearAutoAck() {
|
|
41
|
+
if (this.autoAckTimer)
|
|
42
|
+
clearTimeout(this.autoAckTimer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { APIInteraction } from 'discord-api-types/v10';
|
|
2
|
+
export type VerifyInteractionRequest = {
|
|
3
|
+
body: string | Uint8Array;
|
|
4
|
+
signature: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
publicKey: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function verifyAndParseInteraction(request: VerifyInteractionRequest): Promise<APIInteraction>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { verifyKey } from 'discord-interactions';
|
|
2
|
+
export async function verifyAndParseInteraction(request) {
|
|
3
|
+
const valid = await verifyKey(request.body, request.signature, request.timestamp, request.publicKey);
|
|
4
|
+
if (!valid) {
|
|
5
|
+
throw new Error('[InteractionVerifier] invalid interaction signature');
|
|
6
|
+
}
|
|
7
|
+
const bodyText = typeof request.body === 'string' ? request.body : Buffer.from(request.body).toString('utf8');
|
|
8
|
+
return JSON.parse(bodyText);
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|