diff --git a/apps/rocko/src/index.ts b/apps/rocko/src/index.ts index 3bbc5d68..c5c67a00 100644 --- a/apps/rocko/src/index.ts +++ b/apps/rocko/src/index.ts @@ -1,5 +1,7 @@ import { serve } from "@carbonjs/nodejs" import { Client, Command, type CommandInteraction } from "carbon" +import { Subc } from "./subcommand.js" +import { inspect } from "node:util" class PingCommand extends Command { name = "ping" @@ -7,7 +9,7 @@ class PingCommand extends Command { defer = true async run(interaction: CommandInteraction) { - await sleep(7500) + await sleep(3000) interaction.reply({ content: "Pong" }) } } @@ -18,11 +20,13 @@ const client = new Client( publicKey: process.env.PUBLIC_KEY!, token: process.env.DISCORD_TOKEN! }, - [new PingCommand()] + [new PingCommand(), new Subc()] ) serve(client, { port: 3000 }) -const sleep = async (ms: number) => { +console.log(inspect(client.commands.map((x) => x.serialize()), false, null, true)) + +export const sleep = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/apps/rocko/src/subcommand.ts b/apps/rocko/src/subcommand.ts new file mode 100644 index 00000000..9bd8c949 --- /dev/null +++ b/apps/rocko/src/subcommand.ts @@ -0,0 +1,33 @@ +import { Command, Subcommand, type CommandInteraction } from "carbon"; +import { sleep } from "./index.js"; + +class Sub1 extends Command { + name = "sub1" + description = "Subcommand 1" + defer = true + + async run(interaction: CommandInteraction) { + await sleep(3000) + interaction.reply({ content: "Subcommand 1" }) + } +} + +class Sub2 extends Command { + name = "sub2" + description = "Subcommand 2" + defer = true + + async run(interaction: CommandInteraction) { + await sleep(3000) + interaction.reply({ content: "Subcommand 2" }) + } +} + + +export class Subc extends Subcommand { + name = "subc" + description = "Subcommands!" + defer = true + + subcommands = [new Sub1(), new Sub2()] +} \ No newline at end of file diff --git a/packages/carbon/package.json b/packages/carbon/package.json index 85e0371d..05b795d8 100644 --- a/packages/carbon/package.json +++ b/packages/carbon/package.json @@ -5,7 +5,7 @@ "main": "./dist/src/index.js", "scripts": { "build": "tsc", - "dev": "tsc -w", + "dev": "tsc -w -preserveWatchOutput", "typecheck": "tsc --noEmit" }, "license": "MIT", diff --git a/packages/carbon/src/classes/Client.ts b/packages/carbon/src/classes/Client.ts index cc7bbbc6..d871bb4c 100644 --- a/packages/carbon/src/classes/Client.ts +++ b/packages/carbon/src/classes/Client.ts @@ -4,14 +4,17 @@ import { InteractionType, MessageFlags, RouteBases, - Routes + Routes, + ApplicationCommandType } from "discord-api-types/v10" import { PlatformAlgorithm, isValidRequest } from "discord-verify" import { AutoRouter, type IRequestStrict, StatusError, json } from "itty-router" import { CommandInteraction } from "../structures/CommandInteraction.js" import { RestClient } from "../structures/RestClient.js" -import type { Command } from "./Command.js" +import { Command } from "./Command.js" import pkg from "../../package.json" assert { type: "json" } +import type { BaseCommand } from "../structures/_BaseCommand.js" +import { Subcommand } from "./Subcommand.js" /** * The options used for initializing the client @@ -34,7 +37,7 @@ export class Client { /** * The commands that the client has registered */ - commands: Command[] + commands: BaseCommand[] /** * The router used to handle requests */ @@ -49,7 +52,7 @@ export class Client { * @param options The options used to initialize the client * @param commands The commands that the client has registered */ - constructor(options: ClientOptions, commands: Command[]) { + constructor(options: ClientOptions, commands: BaseCommand[]) { this.options = options this.commands = commands // biome-ignore lint/suspicious/noExplicitAny: @@ -67,6 +70,7 @@ export class Client { const commands = this.commands.map((command) => { return command.serialize() }) + console.log(commands) await fetch( RouteBases.api + Routes.applicationCommands(this.options.clientId), { @@ -119,17 +123,46 @@ export class Client { const interaction = new CommandInteraction(this, rawInteraction) - if (command.defer) { - command.run(interaction) + if (command instanceof Command) { + if (command.defer) { + command.run(interaction) + return json({ + type: InteractionResponseType.DeferredChannelMessageWithSource, + flags: command.ephemeral ? MessageFlags.Ephemeral : 0 + }) + } return json({ - type: InteractionResponseType.DeferredChannelMessageWithSource, - flags: command.ephemeral ? MessageFlags.Ephemeral : 0 + type: InteractionResponseType.ChannelMessageWithSource, + content: "Man someone should really implement non-deferred replies huh" + }) + } + + if (command instanceof Subcommand) { + if (rawInteraction.data.type !== ApplicationCommandType.ChatInput) { + return json({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Subcommands must be used with ChatInput" + } + }) + } + const data = rawInteraction.data + const subcommand = command.subcommands.find( + (x) => x.name === data.options?.[0]?.name + ) + if (!subcommand) return new Response(null, { status: 400 }) + + if (subcommand.defer) { + subcommand.run(interaction) + return json({ + type: InteractionResponseType.DeferredChannelMessageWithSource, + flags: subcommand.ephemeral ? MessageFlags.Ephemeral : 0 + }) + } + return json({ + type: InteractionResponseType.ChannelMessageWithSource }) } - return json({ - type: InteractionResponseType.ChannelMessageWithSource, - content: "Man someone should really implement non-deferred replies huh" - }) }) } diff --git a/packages/carbon/src/classes/Command.ts b/packages/carbon/src/classes/Command.ts index 994c042d..17623063 100644 --- a/packages/carbon/src/classes/Command.ts +++ b/packages/carbon/src/classes/Command.ts @@ -1,40 +1,19 @@ -import type { RESTPostAPIChatInputApplicationCommandsJSONBody } from "discord-api-types/v10" +import { ApplicationCommandType } from "discord-api-types/v10" import type { CommandInteraction } from "../structures/CommandInteraction.js" +import { BaseCommand } from "../structures/_BaseCommand.js" /** - * Represents a command that the user creates + * Represents a standard command that the user creates */ -export abstract class Command { - /** - * The name of the command (e.g. "ping" for /ping) - */ - abstract name: string - /** - * A description of the command - */ - abstract description: string - /** - * Whether the command response should be automatically deferred - */ - defer = false - /** - * Whether the command response should be ephemeral - */ - ephemeral = false - +export abstract class Command extends BaseCommand { + type = ApplicationCommandType.ChatInput /** * The function that is called when the command is ran * @param interaction The interaction that triggered the command */ abstract run(interaction: CommandInteraction): Promise - /** - * Serializes the command into a JSON object that can be sent to Discord - */ - serialize() { - return { - name: this.name, - description: this.description - } satisfies RESTPostAPIChatInputApplicationCommandsJSONBody + serializeOptions() { + return [] } } diff --git a/packages/carbon/src/classes/Subcommand.ts b/packages/carbon/src/classes/Subcommand.ts new file mode 100644 index 00000000..0509ad08 --- /dev/null +++ b/packages/carbon/src/classes/Subcommand.ts @@ -0,0 +1,16 @@ +import { type APIApplicationCommandBasicOption, ApplicationCommandType } from "discord-api-types/v10"; +import { BaseCommand } from "../structures/_BaseCommand.js"; +import type { Command } from "./Command.js"; + +/** + * Represents a subcommand command that the user creates. + * You make this instead of a {@link Command} class when you want to have subcommands in your options. + */ +export abstract class Subcommand extends BaseCommand { + type = ApplicationCommandType.ChatInput + abstract subcommands: Command[] + + serializeOptions(): APIApplicationCommandBasicOption[] { + return this.subcommands.map((subcommand) => subcommand.serialize()) as unknown as APIApplicationCommandBasicOption[]; + } +} \ No newline at end of file diff --git a/packages/carbon/src/index.ts b/packages/carbon/src/index.ts index d5b2a2f0..5e29d52d 100644 --- a/packages/carbon/src/index.ts +++ b/packages/carbon/src/index.ts @@ -1,6 +1,7 @@ // ----- Classes ----- export * from "./classes/Client.js" export * from "./classes/Command.js" +export * from "./classes/Subcommand.js" // ----- Structures ----- export * from "./structures/Base.js" diff --git a/packages/carbon/src/structures/CommandInteraction.ts b/packages/carbon/src/structures/CommandInteraction.ts index 2108506f..3a0b61b1 100644 --- a/packages/carbon/src/structures/CommandInteraction.ts +++ b/packages/carbon/src/structures/CommandInteraction.ts @@ -15,13 +15,15 @@ export class CommandInteraction extends BaseInteraction { */ async reply(data: RESTPostAPIChannelMessageJSONBody) { // TODO: Handle non-deferred + + console.log(JSON.stringify(data, null, 2)) this.client.rest.patch( Routes.webhookMessage( this.rawData.application_id, this.rawData.token, "@original" ), - { body: JSON.stringify(data) } + { body: data } ) } } diff --git a/packages/carbon/src/structures/_BaseCommand.ts b/packages/carbon/src/structures/_BaseCommand.ts new file mode 100644 index 00000000..9013e8e3 --- /dev/null +++ b/packages/carbon/src/structures/_BaseCommand.ts @@ -0,0 +1,43 @@ +import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from "discord-api-types/v10" + +/** + * Represents the base data of a command that the user creates + */ +export abstract class BaseCommand { + /** + * The name of the command (e.g. "ping" for /ping) + */ + abstract name: string + /** + * A description of the command + */ + abstract description: string + /** + * Whether the command response should be automatically deferred + */ + defer = false + /** + * Whether the command response should be ephemeral + */ + ephemeral = false + /** + * The type of the command + */ + abstract type: ApplicationCommandType + + /** + * Serializes the command into a JSON object that can be sent to Discord + */ + serialize() { + const data: RESTPostAPIApplicationCommandsJSONBody = { + name: this.name, + description: this.description, + type: this.type, + options: this.serializeOptions() + } + + return data + } + + abstract serializeOptions(): RESTPostAPIApplicationCommandsJSONBody["options"] +} diff --git a/packages/carbon/src/utils.ts b/packages/carbon/src/utils.ts new file mode 100644 index 00000000..51f17ffa --- /dev/null +++ b/packages/carbon/src/utils.ts @@ -0,0 +1 @@ +export const Omit = (Class: new () => T, keys: K[]): new () => Omit => Class; diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json index 134f78ee..17aaae48 100644 --- a/packages/nodejs/package.json +++ b/packages/nodejs/package.json @@ -5,7 +5,7 @@ "main": "./dist/src/index.js", "scripts": { "build": "tsc", - "dev": "tsc -w", + "dev": "tsc -w -preserveWatchOutput", "typecheck": "tsc --noEmit" }, "license": "MIT", diff --git a/turbo/generators/templates/package.json.hbs b/turbo/generators/templates/package.json.hbs index 0b11bdb9..24b1ba32 100644 --- a/turbo/generators/templates/package.json.hbs +++ b/turbo/generators/templates/package.json.hbs @@ -5,7 +5,7 @@ "main": "./dist/src/index.js", "scripts": { "build": "tsc", - "dev": "tsc -w", + "dev": "tsc -w -preserveWatchOutput", "typecheck": "tsc --noEmit" }, "license": "MIT",