Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
"typescript": "^4.5.4"
},
"dependencies": {
"discord-api-types": "^0.27.0"
"discord-api-types": "^0.37.28"
}
}
79 changes: 69 additions & 10 deletions src/commands/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
APIRole,
APIUser,
ChannelType,
LocalizationMap,
} from "discord-api-types/v9";
import { ApplicationCommandOptionType } from "../api";
import { Awaitable, ValueOf } from "../helpers";
Expand All @@ -21,13 +22,55 @@ export function useDescription(description: string): void {
if (STATE.recordingOptions) STATE.recordingDescription = description;
}

export function useLocalizations({
name,
description,
}: {
name?: LocalizationMap;
description?: LocalizationMap;
}): void {
if (!STATE.commandId) {
throw new Error(`Hooks must be called inside a command`);
}
if (STATE.recordingOptions) {
if (name) STATE.recordingNameLocalizations = name;
if (description) STATE.recordingDescriptionLocalizations = description;
}
}

export function useNameLocalizations(localizations: LocalizationMap): void {
if (!STATE.commandId) {
throw new Error(`Hooks must be called inside a command`);
}
if (STATE.recordingOptions) {
STATE.recordingNameLocalizations = localizations;
}
}

export function useDescriptionLocalizations(
localizations: LocalizationMap
): void {
if (!STATE.commandId) {
throw new Error(`Hooks must be called inside a command`);
}
if (STATE.recordingOptions)
STATE.recordingDescriptionLocalizations = localizations;
}

export function useDefaultPermission(permission: boolean): void {
if (!STATE.commandId) {
throw new Error(`Hooks must be called inside a command`);
}
if (STATE.recordingOptions) STATE.recordingDefaultPermission = permission;
}

export function useDMPermission(permission: boolean): void {
if (!STATE.commandId) {
throw new Error(`Hooks must be called inside a command`);
}
if (STATE.recordingOptions) STATE.recordingDMPermission = permission;
}

// ========================================================================================================
// | Message Component & Modal Hooks: |
// | https://discord.com/developers/docs/interactions/message-components#component-object-component-types |
Expand Down Expand Up @@ -100,24 +143,30 @@ export type AutocompleteHandler<T, Env> = (
ctx: ExecutionContext
) => Awaitable<Choice<T>[]>;

export interface OptionalOption<T, Env> {
export interface LocalizationOption {
localizations?: {
name?: LocalizationMap;
description?: LocalizationMap;
};
}
export interface OptionalOption<T, Env> extends LocalizationOption {
required?: false;
autocomplete?: AutocompleteHandler<T, Env>;
}
export interface RequiredOption<T, Env> {
export interface RequiredOption<T, Env> extends LocalizationOption {
required: true;
autocomplete?: AutocompleteHandler<T, Env>;
}

export interface OptionalChoicesOption<
Choices extends ReadonlyArray<Choice<string | number>>
> {
> extends LocalizationOption {
required?: false;
choices: Choices;
}
export interface RequiredChoicesOption<
Choices extends ReadonlyArray<Choice<string | number>>
> {
> extends LocalizationOption {
required: true;
choices: Choices;
}
Expand All @@ -126,6 +175,10 @@ export interface NumericOption {
min?: number;
max?: number;
}
export interface StringOption {
minLength?: number;
maxLength?: number;
}
export interface ChannelOption {
types?: ChannelType[];
}
Expand All @@ -135,7 +188,9 @@ type CombinedOption<T, Env> = {
autocomplete?: AutocompleteHandler<T, Env>;
choices?: ReadonlyArray<Choice<T>>;
} & NumericOption &
ChannelOption;
ChannelOption &
StringOption &
LocalizationOption;

function useOption<T, Env>(
type: ValueOf<typeof ApplicationCommandOptionType>,
Expand Down Expand Up @@ -166,13 +221,17 @@ function useOption<T, Env>(
STATE.recordingOptions.push({
type: type as number,
name,
name_localizations: options?.localizations?.name,
description,
description_localizations: options?.localizations?.description,
required: options?.required,
autocomplete: options?.autocomplete && true,
choices: normaliseChoices(options?.choices as any) as any,
channel_types: options?.types as any,
min_value: options?.min,
max_value: options?.max,
min_length: options?.minLength,
max_length: options?.maxLength,
});
}
return def;
Expand All @@ -181,27 +240,27 @@ function useOption<T, Env>(
export function useString<Env>(
name: string,
description: string,
options?: OptionalOption<string, Env>
options?: OptionalOption<string, Env> & StringOption
): string | null;
export function useString<Env>(
name: string,
description: string,
options: RequiredOption<string, Env>
options: RequiredOption<string, Env> & StringOption
): string;
export function useString<Choices extends ReadonlyArray<Choice<string>>>(
name: string,
description: string,
options: OptionalChoicesOption<Choices>
options: OptionalChoicesOption<Choices> & StringOption
): ChoiceValue<Choices[number]> | null;
export function useString<Choices extends ReadonlyArray<Choice<string>>>(
name: string,
description: string,
options: RequiredChoicesOption<Choices>
options: RequiredChoicesOption<Choices> & StringOption
): ChoiceValue<Choices[number]>;
export function useString<Env>(
name: string,
description: string,
options?: CombinedOption<string, Env>
options?: CombinedOption<string, Env> & StringOption
): string | null {
return useOption(
ApplicationCommandOptionType.STRING,
Expand Down
14 changes: 13 additions & 1 deletion src/commands/recorders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,22 @@ function recordCommand<Env>(
requireDescription = true
): Pick<
APIApplicationCommand,
"name" | "description" | "options" | "default_permission"
| "name"
| "name_localizations"
| "description"
| "options"
| "default_permission"
| "description_localizations"
| "dm_permission"
> {
STATE.commandId = commandId;
STATE.recordingOptions = [];
STATE.recordingDescription = "";
STATE.recordingDescriptionLocalizations = undefined;
STATE.recordingNameLocalizations = undefined;
STATE.recordingDefaultPermission = undefined;
STATE.componentHandlerCount = 0;
STATE.recordingDMPermission = undefined;
try {
// Run hooks and record options
command();
Expand All @@ -53,11 +62,14 @@ function recordCommand<Env>(

return {
name,
name_localizations: STATE.recordingNameLocalizations,
description: STATE.recordingDescription,
options: STATE.recordingOptions.length
? STATE.recordingOptions
: undefined,
default_permission: STATE.recordingDefaultPermission,
description_localizations: STATE.recordingDescriptionLocalizations,
dm_permission: STATE.recordingDMPermission,
};
} finally {
STATE.commandId = undefined;
Expand Down
9 changes: 7 additions & 2 deletions src/commands/state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {
APIApplicationCommandInteractionDataBasicOption,
APIApplicationCommandOption,
APIChatInputApplicationCommandInteractionDataResolved,
APIInteractionDataResolved,
LocalizationMap,
} from "discord-api-types/v9";
import { AutocompleteHandler } from "./hooks";
import { ComponentHandler, ModalHandler } from "./types";
Expand All @@ -13,14 +14,18 @@ interface State {
// Recorded command for deployment
recordingOptions?: APIApplicationCommandOption[];
recordingDescription: string;
recordingDescriptionLocalizations?: LocalizationMap;
recordingNameLocalizations?: LocalizationMap;

recordingDefaultPermission?: boolean;
recordingDMPermission?: boolean;

// Incoming interaction data
interactionOptions?: Map<
string,
APIApplicationCommandInteractionDataBasicOption
>; // name -> value
interactionResolved?: APIChatInputApplicationCommandInteractionDataResolved;
interactionResolved?: APIInteractionDataResolved;
interactionComponentData?: Map<string, string>; // custom_id -> data

// Component interaction and modal submit handlers
Expand Down
8 changes: 5 additions & 3 deletions src/jsx/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
APIButtonComponent,
APIEmbed,
APIInteractionResponseCallbackData,
APIMessageComponent,
APIMessageActionRowComponent,
APISelectMenuComponent,
Snowflake,
} from "discord-api-types/v9";
Expand All @@ -30,7 +30,9 @@ export interface MessageProps {
children?: (
| Child
| (APIEmbed & { [$embed]: true })
| (APIActionRowComponent<APIMessageComponent> & { [$actionRow]: true })
| (APIActionRowComponent<APIMessageActionRowComponent> & {
[$actionRow]: true;
})
| (APIButtonComponent & { [$actionRowChild]: true })
| (APISelectMenuComponent & { [$actionRowChild]: true })
)[];
Expand All @@ -44,7 +46,7 @@ export function Message(
// Sort children into correct slots
let content = undefined;
const embeds: APIEmbed[] = [];
const components: APIActionRowComponent<APIMessageComponent>[] = [];
const components: APIActionRowComponent<APIMessageActionRowComponent>[] = [];
for (const child of props.children?.flat(Infinity) ?? []) {
if (isEmptyChild(child)) continue;
if ((child as any)[$embed]) {
Expand Down
8 changes: 5 additions & 3 deletions src/jsx/Modal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {
APIActionRowComponent,
APIModalComponent,
APIModalActionRowComponent,
APITextInputComponent,
} from "discord-api-types/v9";
import { ComponentType } from "../api";
Expand All @@ -12,13 +12,15 @@ export interface ModalProps {
id: string;
title: string;
children?: (
| (APIActionRowComponent<APIModalComponent> & { [$actionRow]: true })
| (APIActionRowComponent<APIModalActionRowComponent> & {
[$actionRow]: true;
})
| (APITextInputComponent & { [$actionRowChild]: true })
)[];
}

export function Modal(props: ModalProps): ModalResponse {
const components: APIActionRowComponent<APIModalComponent>[] = [];
const components: APIActionRowComponent<APIModalActionRowComponent>[] = [];
for (const child of props.children?.flat(Infinity) ?? []) {
if (isEmptyChild(child)) continue;
if ((child as any)[$actionRow]) {
Expand Down
Loading