diff --git a/packages/interaction-kit/src/application.ts b/packages/interaction-kit/src/application.ts index b5cf7467..c43a8343 100644 --- a/packages/interaction-kit/src/application.ts +++ b/packages/interaction-kit/src/application.ts @@ -1,20 +1,19 @@ -import type { FastifyRequest, FastifyReply } from "fastify"; - +import type { FastifyReply, FastifyRequest } from "fastify"; import fs from "node:fs"; import path from "node:path"; -import SlashCommand from "./commands/slash-command"; -import ContextMenu from "./commands/context-menu"; import Config from "./api/config"; +import ContextMenu from "./commands/context-menu"; +import SlashCommand from "./commands/slash-command"; +import { ExecutableComponent, isExecutableComponent } from "./components"; import { + ApplicationCommandType, Interaction as InteractionDefinition, Snowflake, - ApplicationCommandType, } from "./definitions"; import * as Interaction from "./interactions"; +import ApplicationCommandInteraction from "./interactions/application-commands/application-command-interaction"; import { InteractionKitCommand, SerializableComponent } from "./interfaces"; import startInteractionKitServer from "./server"; -import ApplicationCommandInteraction from "./interactions/application-commands/application-command-interaction"; -import { ExecutableComponent, isExecutableComponent } from "./components"; type ApplicationArgs = { applicationID: string; diff --git a/packages/interaction-kit/src/commands/slash-command.ts b/packages/interaction-kit/src/commands/slash-command.ts index b7d4f9d9..5c9b9b2b 100644 --- a/packages/interaction-kit/src/commands/slash-command.ts +++ b/packages/interaction-kit/src/commands/slash-command.ts @@ -1,43 +1,45 @@ -import { ApplicationCommand, ApplicationCommandType } from "../definitions"; import Application from "../application"; -import { Input } from "../components/inputs"; -import { Optional, InteractionKitCommand } from "../interfaces"; +import type { InputKey } from "../components/inputs"; +import { ApplicationCommand, ApplicationCommandType } from "../definitions"; import SlashCommandInteraction from "../interactions/application-commands/slash-command-interaction"; -import SlashCommandAutocompleteInteraction from "../interactions/autcomplete/application-command-autocomplete"; +import SlashCommandAutocompleteInteraction from "../interactions/autocomplete/application-command-autocomplete"; +import { InteractionKitCommand, Optional } from "../interfaces"; // TODO: options OR autocomplete -type CommandArgs = { +type CommandArgs = { name: string; description: string; defaultPermission?: boolean; - options?: Input[]; + options?: T; onAutocomplete?: ( interaction: SlashCommandAutocompleteInteraction, application: Application ) => void; - handler: (interaction: SlashCommandInteraction) => void; + handler: (interaction: SlashCommandInteraction) => void; }; -export default class SlashCommand - implements InteractionKitCommand +export default class SlashCommand< + V extends InputKey, + T extends readonly [V, ...V[]] | [] +> implements InteractionKitCommand> { public readonly type = ApplicationCommandType.CHAT_INPUT; name: string; #description: string; #defaultPermission: boolean; - #options: Map; - onAutocomplete?: ( interaction: SlashCommandAutocompleteInteraction, application: Application ) => void; handler: ( - interaction: SlashCommandInteraction, + interaction: SlashCommandInteraction, application: Application ) => void; + private readonly options: T; + constructor({ name, description, @@ -45,25 +47,15 @@ export default class SlashCommand onAutocomplete, handler, defaultPermission = true, - }: CommandArgs) { + }: CommandArgs) { // TODO: Validate: 1-32 lowercase character name matching ^[\w-]{1,32}$ this.name = name; this.#description = description; this.#defaultPermission = defaultPermission; this.handler = handler; this.onAutocomplete = onAutocomplete; - this.#options = new Map(); - - options?.forEach((option) => { - const key = option.name.toLowerCase(); - if (this.#options.has(key)) { - throw new Error( - `Option names must be unique (case insensitive). Duplicate name detected: ${key}` - ); - } - - this.#options.set(key, option); - }); + + this.options = options ?? ([] as T); } group() { @@ -83,13 +75,16 @@ export default class SlashCommand return false; } - if (this.#options.size !== (schema.options?.length ?? 0)) { + if (this.options.length !== (schema.options?.length ?? 0)) { return false; } return ( schema.options?.every( - (option) => this.#options.get(option.name)?.equals(option) ?? false + (option) => + this.options + .find((opt) => opt.name === option.name) + ?.equals(option) ?? false ) ?? true ); } @@ -105,10 +100,10 @@ export default class SlashCommand } // TODO: Sort these so that required options come first - if (this.#options.size > 0) { + if (this.options.length > 0) { payload.options = []; - Array.from(this.#options.entries()).forEach(([_, value]) => { + this.options.forEach((value) => { payload.options?.push(value.serialize()); }); } diff --git a/packages/interaction-kit/src/components/choices.ts b/packages/interaction-kit/src/components/choices.ts index 6b328595..8015cb54 100644 --- a/packages/interaction-kit/src/components/choices.ts +++ b/packages/interaction-kit/src/components/choices.ts @@ -84,3 +84,8 @@ export class SlashChoiceList< }); } } + +/** + * Choices.createSelectOptionList() + * Choices.createSlashChoiceList() + */ diff --git a/packages/interaction-kit/src/components/inputs.ts b/packages/interaction-kit/src/components/inputs.ts index ffa001d9..86ca7521 100644 --- a/packages/interaction-kit/src/components/inputs.ts +++ b/packages/interaction-kit/src/components/inputs.ts @@ -9,15 +9,6 @@ import { SlashChoiceList } from "./choices"; type InputChoiceValue = ApplicationCommandOptionChoice["value"]; -type InputArgs = { - type: ApplicationCommandOptionType; - name: string; - description: string; - required?: boolean; - choices?: SlashChoiceList; - options?: ApplicationCommandOption[]; -}; - export function isChoiceType( input: ApplicationCommandOption ): input is ApplicationCommandOptionWithChoice { @@ -31,11 +22,31 @@ export function isChoiceType( } } -export class Input - implements Serializable, Comparable +export interface InputKey + extends Serializable, + Comparable { + readonly name: string; + readonly type: ApplicationCommandOptionType; +} + +type InputArgs = { + type: U; + name: T; + description: string; + required?: boolean; + choices?: SlashChoiceList; + options?: ApplicationCommandOption[]; +}; + +export class Input< + Name extends string, + OptionType extends ApplicationCommandOptionType +> implements + Serializable, + Comparable { - public readonly type; - public readonly name; + public readonly name: Name; + public readonly type: OptionType; public readonly description; public readonly required; public readonly options; @@ -48,7 +59,7 @@ export class Input choices, options, required = false, - }: InputArgs) { + }: InputArgs) { this.type = type; this.name = name; this.description = description; @@ -121,62 +132,126 @@ export class Input } } -interface StringInputArgs extends Omit { +interface StringInputArgs< + Name extends string, + OptionType extends ApplicationCommandOptionType +> extends Omit, "type" | "options"> { choices?: SlashChoiceList; } -export class StringInput extends Input { - constructor(args: StringInputArgs) { +export class StringInput extends Input< + Name, + ApplicationCommandOptionType.STRING +> { + constructor( + args: StringInputArgs + ) { super({ type: ApplicationCommandOptionType.STRING, ...args }); } } -interface IntegerInputArgs extends Omit { +interface IntegerInputArgs< + Name extends string, + OptionType extends ApplicationCommandOptionType +> extends Omit, "type" | "options"> { choices?: SlashChoiceList; } -export class IntegerInput extends Input { - constructor(args: IntegerInputArgs) { +export class IntegerInput extends Input< + Name, + ApplicationCommandOptionType.INTEGER +> { + constructor( + args: IntegerInputArgs + ) { super({ type: ApplicationCommandOptionType.INTEGER, ...args }); } } -interface NumberInputArgs extends Omit { +interface NumberInputArgs< + Name extends string, + OptionType extends ApplicationCommandOptionType +> extends Omit, "type" | "options"> { choices?: SlashChoiceList; } -export class NumberInput extends Input { - constructor(args: NumberInputArgs) { +export class NumberInput extends Input< + Name, + ApplicationCommandOptionType.NUMBER +> { + constructor( + args: NumberInputArgs + ) { super({ type: ApplicationCommandOptionType.NUMBER, ...args }); } } -export class BooleanInput extends Input { - constructor(args: Omit) { +export class BooleanInput extends Input< + Name, + ApplicationCommandOptionType.BOOLEAN +> { + constructor( + args: Omit< + InputArgs, + "type" | "choices" | "options" + > + ) { super({ type: ApplicationCommandOptionType.BOOLEAN, ...args }); } } -export class UserInput extends Input { - constructor(args: Omit) { +export class UserInput extends Input< + Name, + ApplicationCommandOptionType.USER +> { + constructor( + args: Omit< + InputArgs, + "type" | "choices" | "options" + > + ) { super({ type: ApplicationCommandOptionType.USER, ...args }); } } -export class ChannelInput extends Input { - constructor(args: Omit) { +export class ChannelInput extends Input< + Name, + ApplicationCommandOptionType.CHANNEL +> { + constructor( + args: Omit< + InputArgs, + "type" | "choices" | "options" + > + ) { super({ type: ApplicationCommandOptionType.CHANNEL, ...args }); } } -export class RoleInput extends Input { - constructor(args: Omit) { +export class RoleInput extends Input< + Name, + ApplicationCommandOptionType.ROLE +> { + constructor( + args: Omit< + InputArgs, + "type" | "choices" | "options" + > + ) { super({ type: ApplicationCommandOptionType.ROLE, ...args }); } } -export class MentionableInput extends Input { - constructor(args: Omit) { +export class MentionableInput extends Input< + Name, + ApplicationCommandOptionType.MENTIONABLE +> { + constructor( + args: Omit< + InputArgs, + "type" | "choices" | "options" + > + ) { super({ type: ApplicationCommandOptionType.MENTIONABLE, ...args }); } } diff --git a/packages/interaction-kit/src/definitions/application-commands.ts b/packages/interaction-kit/src/definitions/application-commands.ts index 466a13eb..7e139014 100644 --- a/packages/interaction-kit/src/definitions/application-commands.ts +++ b/packages/interaction-kit/src/definitions/application-commands.ts @@ -165,7 +165,7 @@ export type ApplicationCommandInteractionData = { custom_id?: string; component_type?: ComponentType; target_id?: Snowflake; - values?: Array; + value?: Array; }; /** @link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure */ diff --git a/packages/interaction-kit/src/interactions/application-commands/slash-command-interaction.ts b/packages/interaction-kit/src/interactions/application-commands/slash-command-interaction.ts index e701f8d9..8da5f9e0 100644 --- a/packages/interaction-kit/src/interactions/application-commands/slash-command-interaction.ts +++ b/packages/interaction-kit/src/interactions/application-commands/slash-command-interaction.ts @@ -1,43 +1,53 @@ import type { FastifyReply, FastifyRequest } from "fastify"; +import { InputKey } from "../.."; import Application from "../../application"; import { - ApplicationCommandInteractionDataOption, ApplicationCommandType, Interaction as InteractionDefinition, - OptionType, } from "../../definitions"; import { InteractionKitCommand } from "../../interfaces"; import ApplicationCommandInteraction from "./application-command-interaction"; -export default class SlashCommandInteraction extends ApplicationCommandInteraction { - readonly #options: Map; +type InteractionOptions = { + [Key in T[number]["name"]]: Extract extends { + required: true; + } + ? Extract["type"] + : Extract["type"] | undefined; +}; + +// type SlashCommandInteractionBody< +// T extends readonly [InputKey, ...InputKey[]] | [] +// > = Omit & { +// data: Omit & { +// options: InteractionOptions; +// }; +// }; + +interface SlashCommandInteractionBody< + T extends readonly [InputKey, ...InputKey[]] | [] +> extends InteractionDefinition { + data: Omit & { + options: InteractionOptions; + }; +} + +export default class SlashCommandInteraction< + T extends readonly [InputKey, ...InputKey[]] | [] +> extends ApplicationCommandInteraction { + public readonly options: InteractionOptions; constructor( application: Application, - command: InteractionKitCommand, - request: FastifyRequest<{ Body: InteractionDefinition }>, + command: InteractionKitCommand>, + request: FastifyRequest<{ Body: SlashCommandInteractionBody }>, response: FastifyReply ) { super(application, request, response); - this.#options = new Map(); - - request.body?.data?.options?.forEach((option) => { - this.#options.set(option.name.toLowerCase(), option); - }); + this.options = request.body?.data?.options; } get commandType() { return ApplicationCommandType.CHAT_INPUT; } - - // TODO: Type? Should return an object where keys = #options keys, and value = ApplicationCommandInteractionDataOption - get options() { - return new Proxy( - {}, - { - get: (target, property): OptionType | null => - this.#options.get(property.toString())?.value ?? null, - } - ); - } } diff --git a/packages/interaction-kit/src/interactions/autcomplete/application-command-autocomplete.ts b/packages/interaction-kit/src/interactions/autocomplete/application-command-autocomplete.ts similarity index 68% rename from packages/interaction-kit/src/interactions/autcomplete/application-command-autocomplete.ts rename to packages/interaction-kit/src/interactions/autocomplete/application-command-autocomplete.ts index 1eb60b0a..32f0f73d 100644 --- a/packages/interaction-kit/src/interactions/autcomplete/application-command-autocomplete.ts +++ b/packages/interaction-kit/src/interactions/autocomplete/application-command-autocomplete.ts @@ -1,6 +1,7 @@ import type { FastifyReply, FastifyRequest } from "fastify"; -import SlashCommand from "../../commands/slash-command"; import Application from "../../application"; +import SlashCommand from "../../commands/slash-command"; +import { InputKey } from "../../components/inputs"; import { Interaction as InteractionDefinition, InteractionCallbackType, @@ -8,11 +9,15 @@ import { import AutocompleteInteraction from "./autocomplete-interaction"; import { SlashCommandAutocompleteType } from "./types"; -export default class SlashCommandAutocompleteInteraction extends AutocompleteInteraction { - public readonly command: SlashCommand; +export default class SlashCommandAutocompleteInteraction< + V extends InputKey, + T extends readonly [V, ...V[]] | [] +> extends AutocompleteInteraction { + public readonly command: SlashCommand; + constructor( application: Application, - command: SlashCommand, + command: SlashCommand, request: FastifyRequest<{ Body: InteractionDefinition }>, response: FastifyReply ) { diff --git a/packages/interaction-kit/src/interactions/autcomplete/autocomplete-interaction.ts b/packages/interaction-kit/src/interactions/autocomplete/autocomplete-interaction.ts similarity index 70% rename from packages/interaction-kit/src/interactions/autcomplete/autocomplete-interaction.ts rename to packages/interaction-kit/src/interactions/autocomplete/autocomplete-interaction.ts index d5621ab9..876d8538 100644 --- a/packages/interaction-kit/src/interactions/autcomplete/autocomplete-interaction.ts +++ b/packages/interaction-kit/src/interactions/autocomplete/autocomplete-interaction.ts @@ -1,29 +1,17 @@ import type { FastifyReply, FastifyRequest } from "fastify"; +import Application from "../../application"; +import { InputKey } from "../../components/inputs"; import { - ApplicationCommandInteractionDataOption, Interaction as InteractionDefinition, - InteractionApplicationCommandCallbackData, - InteractionCallbackType, - InteractionResponse, - InteractionRequestType, Snowflake, } from "../../definitions"; -import { PermissionFlags } from "../../definitions/messages"; -import Embed from "../../components/embed"; -import * as API from "../../api"; -import Application from "../../application"; -import { - Autocomplete, - Interaction, - InteractionMessageModifiers, - InteractionReply, - SerializableComponent, -} from "../../interfaces"; -import { isActionRow } from "../../components/action-row"; +import { Autocomplete } from "../../interfaces"; import { AutocompleteInteractionTypes, AutocompleteTypes } from "./types"; -export default class AutocompleteInteraction - implements Autocomplete +export default class AutocompleteInteraction< + U extends readonly [InputKey, ...InputKey[]] | [], + T extends AutocompleteTypes +> implements Autocomplete { public readonly name: string; public readonly token: string; diff --git a/packages/interaction-kit/src/interactions/autcomplete/types.ts b/packages/interaction-kit/src/interactions/autocomplete/types.ts similarity index 58% rename from packages/interaction-kit/src/interactions/autcomplete/types.ts rename to packages/interaction-kit/src/interactions/autocomplete/types.ts index 3ff6aaf9..24300bde 100644 --- a/packages/interaction-kit/src/interactions/autcomplete/types.ts +++ b/packages/interaction-kit/src/interactions/autocomplete/types.ts @@ -8,9 +8,10 @@ import { InteractionCallbackType } from "../../definitions/application-commands" export type AutocompleteInteractionTypes = InteractionCallbackType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT; -export type AutocompleteTypes = SlashCommandAutocompleteType; +export type AutocompleteTypes = + SlashCommandAutocompleteType; -export type SlashCommandAutocompleteType = - | StringInput - | IntegerInput - | NumberInput; +export type SlashCommandAutocompleteType = + | StringInput + | IntegerInput + | NumberInput; diff --git a/packages/interaction-kit/src/interactions/index.ts b/packages/interaction-kit/src/interactions/index.ts index afcd5aa4..e4bb191e 100644 --- a/packages/interaction-kit/src/interactions/index.ts +++ b/packages/interaction-kit/src/interactions/index.ts @@ -1,20 +1,21 @@ +import type { FastifyReply, FastifyRequest } from "fastify"; +import { ApplicationCommandInteraction, PingInteraction } from ".."; import Application from "../application"; -import type { FastifyRequest, FastifyReply } from "fastify"; +import { ExecutableComponent } from "../components"; +import { Button } from "../components/button"; +import Select from "../components/select"; import { ApplicationCommandType, ComponentType, Interaction as InteractionDefinition, InteractionRequestType, } from "../definitions"; +import { InteractionKitCommand } from "../interfaces"; +import ContextMenuInteraction from "./application-commands/context-menu-interaction"; +import SlashCommandInteraction from "./application-commands/slash-command-interaction"; +import SlashCommandAutocompleteInteraction from "./autocomplete/application-command-autocomplete"; import ButtonInteraction from "./message-components/button-interaction"; import SelectInteraction from "./message-components/select-interaction"; -import SlashCommandInteraction from "./application-commands/slash-command-interaction"; -import ContextMenuInteraction from "./application-commands/context-menu-interaction"; -import { ExecutableComponent } from "../components"; -import { Button } from "../components/button"; -import Select from "../components/select"; -import { InteractionKitCommand } from "../interfaces"; -import { ApplicationCommandInteraction, PingInteraction } from ".."; const autocompleteTypes = new Set([ InteractionRequestType.APPLICATION_COMMAND_AUTOCOMPLETE, diff --git a/packages/interaction-kit/src/interfaces.ts b/packages/interaction-kit/src/interfaces.ts index 13ca9b45..f738cf7f 100644 --- a/packages/interaction-kit/src/interfaces.ts +++ b/packages/interaction-kit/src/interfaces.ts @@ -8,11 +8,9 @@ import { Component, ComponentType, InteractionApplicationCommandCallbackData, - InteractionCallbackType, InteractionRequestType, Snowflake, } from "./definitions"; -import AutocompleteInteraction from "./interactions/autcomplete/autocomplete-interaction"; export type Optional = Omit & Partial>; diff --git a/packages/interaction-kit/src/scripts.ts b/packages/interaction-kit/src/scripts.ts index 57961c6e..2abaed98 100644 --- a/packages/interaction-kit/src/scripts.ts +++ b/packages/interaction-kit/src/scripts.ts @@ -42,10 +42,8 @@ function getChangeSet( } if (command.equals(signature)) { - // @ts-expect-error ???? changeSet.unchangedCommands.add(command.serialize()); } else { - // @ts-expect-error ???? changeSet.updatedCommands.add(command.serialize()); changeSet.hasChanges = true; } @@ -54,7 +52,6 @@ function getChangeSet( } // If the command does not exist, we add it else { - // @ts-expect-error ???? changeSet.newCommands.add(command.serialize()); changeSet.hasChanges = true; }