diff --git a/.changeset/slow-lies-tease.md b/.changeset/slow-lies-tease.md new file mode 100644 index 00000000..f8a9be42 --- /dev/null +++ b/.changeset/slow-lies-tease.md @@ -0,0 +1,6 @@ +--- +"create-carbon": minor +"@buape/carbon": minor +--- + +New Architecture diff --git a/.gitignore b/.gitignore index 4400893d..d33c0aec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,5 @@ build/ .next/ .DS_Store packages/*/docs/ -website/content/*/api -website/.source -!packages/create-carbon/templates/**/.env* \ No newline at end of file +website/content/api +website/.source \ No newline at end of file diff --git a/apps/cloudo/.dev.vars.example b/apps/cloudo/.dev.vars.example new file mode 100644 index 00000000..e7d23ae4 --- /dev/null +++ b/apps/cloudo/.dev.vars.example @@ -0,0 +1,6 @@ +BASE_URL= +DEPLOY_SECRET= +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_PUBLIC_KEY= +DISCORD_BOT_TOKEN= \ No newline at end of file diff --git a/apps/cloudo/.gitignore b/apps/cloudo/.gitignore new file mode 100644 index 00000000..f616187a --- /dev/null +++ b/apps/cloudo/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.wrangler/ +.dev.vars \ No newline at end of file diff --git a/apps/cloudo/README.md b/apps/cloudo/README.md index 1b51fe4c..cc5b2801 100644 --- a/apps/cloudo/README.md +++ b/apps/cloudo/README.md @@ -1,3 +1,7 @@ -# Cloudo +## cloudo -Cloudo is our test bot that runs on CF workers, to test that Carbon functions correctly. \ No newline at end of file +This is a [Discord](https://discord.dev) app made with [Carbon](https://carbon.buape.com) and generated with the [`create-carbon`](https://npmjs.com/create-carbon) tool. + +To learn how to get started in development, deploy to production, or add commands, head over to the [documentation](https://carbon.buape.com/adapters/cloudflare) for your runtime. + +If you need any assistance, you can join our [Discord](https://go.buape.com/carbon) and ask in the [`#support`](https://discord.com/channels/1280628625904894072/1280630704308486174) channel. \ No newline at end of file diff --git a/apps/cloudo/package.json b/apps/cloudo/package.json index ea44a4a7..e56e9b98 100644 --- a/apps/cloudo/package.json +++ b/apps/cloudo/package.json @@ -1,19 +1,19 @@ { "name": "demo-cloudo", - "type": "module", - "main": "./dist/src/index.js", "private": true, + "type": "module", + "main": "./src/index.ts", "scripts": { - "build": "wrangler deploy --dry-run", + "dev": "wrangler dev --port 3000", "deploy": "wrangler deploy", - "dev": "wrangler deploy && wrangler tail" + "wrangler": "wrangler" }, "dependencies": { "@buape/carbon": "workspace:*" }, - "license": "MIT", "devDependencies": { - "@cloudflare/workers-types": "4.20241004.0", - "wrangler": "3.80.1" + "@cloudflare/workers-types": "4.20241011.0", + "typescript": "5.6.3", + "wrangler": "3.81.0" } } diff --git a/apps/cloudo/src/commands/ping.ts b/apps/cloudo/src/commands/ping.ts index bb8b9880..c2624b40 100644 --- a/apps/cloudo/src/commands/ping.ts +++ b/apps/cloudo/src/commands/ping.ts @@ -2,12 +2,9 @@ import { Command, type CommandInteraction } from "@buape/carbon" export default class PingCommand extends Command { name = "ping" - description = "A simple ping command" - defer = false + description = "Replies with Pong!" async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) + await interaction.reply("Pong! Hello from cloudo!") } } diff --git a/apps/cloudo/src/commands/testing/button.ts b/apps/cloudo/src/commands/testing/button.ts index a93f76d0..a995e55a 100644 --- a/apps/cloudo/src/commands/testing/button.ts +++ b/apps/cloudo/src/commands/testing/button.ts @@ -13,27 +13,27 @@ export default class ButtonCommand extends Command { description = "A simple command with a button!" defer = true - components = [PingButton] + components = [ClickMeButton] async run(interaction: CommandInteraction) { await interaction.reply({ - content: "Pong!", - components: [new Row([new PingButton(), new Link()])] + content: "Look at this button!", + components: [new Row([new ClickMeButton(), new DocsButton()])] }) } } -class PingButton extends Button { - customId = "ping" - label = "Ping" - style = ButtonStyle.Primary as typeof Button.prototype.style +class ClickMeButton extends Button { + customId = "click-me" + label = "Click me!" + style = ButtonStyle.Primary async run(interaction: ButtonInteraction) { - await interaction.reply({ content: "OMG YOU CLICKED THE BUTTON" }) + await interaction.reply("You clicked the button!") } } -class Link extends LinkButton { - label = "Link" - url = "https://buape.com" +class DocsButton extends LinkButton { + label = "Carbon Documentation" + url = "https://carbon.buape.com" } diff --git a/apps/cloudo/src/commands/testing/ephemeral.ts b/apps/cloudo/src/commands/testing/ephemeral.ts new file mode 100644 index 00000000..ba000c65 --- /dev/null +++ b/apps/cloudo/src/commands/testing/ephemeral.ts @@ -0,0 +1,35 @@ +import { + Command, + type CommandInteraction, + CommandWithSubcommands +} from "@buape/carbon" + +class EphemeralNoDefer extends Command { + name = "no-defer" + description = "Ephemeral test" + ephemeral = true + defer = false + + async run(interaction: CommandInteraction): Promise { + await interaction.reply({ content: "Ephemeral no defer" }) + } +} + +class EphemeralDefer extends Command { + name = "defer" + description = "Ephemeral test" + ephemeral = true + defer = true + + async run(interaction: CommandInteraction): Promise { + await interaction.reply({ content: "Ephemeral defer" }) + } +} + +export default class EphemeralCommand extends CommandWithSubcommands { + name = "ephemeral" + description = "Ephemeral test" + ephemeral = true + + subcommands = [new EphemeralNoDefer(), new EphemeralDefer()] +} diff --git a/apps/cloudo/src/commands/testing/every_select.ts b/apps/cloudo/src/commands/testing/every_select.ts index 8b21223a..e7ba0744 100644 --- a/apps/cloudo/src/commands/testing/every_select.ts +++ b/apps/cloudo/src/commands/testing/every_select.ts @@ -14,8 +14,8 @@ import { type UserSelectMenuInteraction } from "@buape/carbon" -export default class SelectCommand extends Command { - name = "every_select" +export default class EverySelectCommand extends Command { + name = "every-select" description = "Send every select menu" defer = true @@ -28,16 +28,15 @@ export default class SelectCommand extends Command { ] async run(interaction: CommandInteraction) { - const row = new Row() - row.addComponent(new StringSelect()) - row.addComponent(new RoleSelect()) - row.addComponent(new MentionableSelect()) - row.addComponent(new ChannelSelect()) - row.addComponent(new UserSelect()) + const stringRow = new Row([new StringSelect()]) + const roleRow = new Row([new RoleSelect()]) + const mentionableRow = new Row([new MentionableSelect()]) + const channelRow = new Row([new ChannelSelect()]) + const userRow = new Row([new UserSelect()]) - interaction.reply({ + await interaction.reply({ content: "Select menus!!", - components: [row] + components: [stringRow, roleRow, mentionableRow, channelRow, userRow] }) } } @@ -47,7 +46,7 @@ class StringSelect extends StringSelectMenu { placeholder = "String select menu" options = [{ label: "Option 1", value: "option1" }] async run(interaction: StringSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } @@ -55,27 +54,27 @@ class RoleSelect extends RoleSelectMenu { customId = "role-select" placeholder = "Role select menu" async run(interaction: RoleSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } class MentionableSelect extends MentionableSelectMenu { customId = "mentionable-select" placeholder = "Mentionable select menu" async run(interaction: MentionableSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } class ChannelSelect extends ChannelSelectMenu { customId = "channel-select" placeholder = "Channel select menu" async run(interaction: ChannelSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } class UserSelect extends UserSelectMenu { customId = "user-select" placeholder = "User select menu" async run(interaction: UserSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } diff --git a/apps/cloudo/src/commands/testing/message_command.ts b/apps/cloudo/src/commands/testing/message_command.ts index f658576d..7d5513dd 100644 --- a/apps/cloudo/src/commands/testing/message_command.ts +++ b/apps/cloudo/src/commands/testing/message_command.ts @@ -11,6 +11,6 @@ export default class MessageCommand extends Command { type = ApplicationCommandType.Message async run(interaction: CommandInteraction) { - interaction.reply({ content: "Message command" }) + await interaction.reply({ content: "Message command" }) } } diff --git a/apps/cloudo/src/commands/testing/modal.ts b/apps/cloudo/src/commands/testing/modal.ts index ca4ab013..4d720c82 100644 --- a/apps/cloudo/src/commands/testing/modal.ts +++ b/apps/cloudo/src/commands/testing/modal.ts @@ -31,10 +31,14 @@ class TestModal extends Modal { new Row([new TextInputHeight()]) ] - run(interaction: ModalInteraction) { - return interaction.reply({ - content: `Hi ${interaction.fields.getText("name")}, you are ${interaction.fields.getText("age")} years old, and your favorite color is ${interaction.fields.getText("color")}. You are ${interaction.fields.getText("height") || "not"} tall.` - }) + async run(interaction: ModalInteraction) { + const name = interaction.fields.getText("name") + const age = interaction.fields.getText("age") + const color = interaction.fields.getText("color") + const height = interaction.fields.getText("height") || "not" + await interaction.reply( + `Hi ${name}, you are ${age} years old, and your favorite color is ${color}. You are ${height} tall.` + ) } } diff --git a/apps/cloudo/src/commands/testing/options.ts b/apps/cloudo/src/commands/testing/options.ts index 9a1cb742..6eed101b 100644 --- a/apps/cloudo/src/commands/testing/options.ts +++ b/apps/cloudo/src/commands/testing/options.ts @@ -1,27 +1,99 @@ import { - type APIApplicationCommandBasicOption, ApplicationCommandOptionType, + type AutocompleteInteraction, Command, - type CommandInteraction + type CommandInteraction, + type CommandOptions } from "@buape/carbon" -export default class Options extends Command { +export default class OptionsCommand extends Command { name = "options" description = "Options test" defer = true - options: APIApplicationCommandBasicOption[] = [ + options: CommandOptions = [ { name: "str", type: ApplicationCommandOptionType.String, description: "DESCRIPTION", - required: true + required: false + }, + { + name: "int", + type: ApplicationCommandOptionType.Integer, + description: "DESCRIPTION", + required: false + }, + { + name: "num", + type: ApplicationCommandOptionType.Number, + description: "DESCRIPTION", + required: false + }, + { + name: "bool", + type: ApplicationCommandOptionType.Boolean, + description: "DESCRIPTION", + required: false + }, + { + name: "user", + type: ApplicationCommandOptionType.User, + description: "DESCRIPTION", + required: false + }, + { + name: "channel", + type: ApplicationCommandOptionType.Channel, + description: "DESCRIPTION", + required: false + }, + { + name: "role", + type: ApplicationCommandOptionType.Role, + description: "DESCRIPTION", + required: false + }, + { + name: "mentionable", + type: ApplicationCommandOptionType.Mentionable, + description: "DESCRIPTION", + required: false + }, + { + name: "autocomplete", + type: ApplicationCommandOptionType.String, + description: "DESCRIPTION", + required: false, + autocomplete: true } ] + async autocomplete(interaction: AutocompleteInteraction) { + await interaction.respond([ + { + name: "That thing you said", + value: String(interaction.options.getFocused()) || "No focused option" + }, + { + name: "That thing you said but with a prefix", + value: `Prefix: ${String(interaction.options.getFocused())}` + } + ]) + } + async run(interaction: CommandInteraction) { - interaction.reply({ - content: `${interaction.options.getString("str")}` - }) + const str = interaction.options.getString("str") + const int = interaction.options.getInteger("int") + const num = interaction.options.getNumber("num") + const bool = interaction.options.getBoolean("bool") + const user = interaction.options.getUser("user") + const channel = await interaction.options.getChannel("channel") + const role = interaction.options.getRole("role") + const mentionable = await interaction.options.getMentionable("mentionable") + + await interaction.reply( + `You provided the following options:\n str: ${str}\n int: ${int}\n num: ${num}\n bool: ${bool}\n user: ${user?.id}\n channel: ${channel?.id}\n role: ${role?.id}\n mentionable: ${mentionable?.id}` + ) } } diff --git a/apps/cloudo/src/commands/testing/subcommand.ts b/apps/cloudo/src/commands/testing/subcommand.ts index 16c58ade..538bc251 100644 --- a/apps/cloudo/src/commands/testing/subcommand.ts +++ b/apps/cloudo/src/commands/testing/subcommand.ts @@ -4,30 +4,30 @@ import { CommandWithSubcommands } from "@buape/carbon" -class Sub1 extends Command { - name = "sub1" +class Command1 extends Command { + name = "command1" description = "Subcommand 1" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 1" }) + await interaction.reply({ content: "Subcommand 1" }) } } -class Sub2 extends Command { - name = "sub2" +class Command2 extends Command { + name = "command2" description = "Subcommand 2" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 2" }) + await interaction.reply({ content: "Subcommand 2" }) } } -export default class Subc extends CommandWithSubcommands { - name = "subc" +export default class SubcommandsCommand extends CommandWithSubcommands { + name = "subcommands" description = "Subcommands!" defer = true - subcommands = [new Sub1(), new Sub2()] + subcommands = [new Command1(), new Command2()] } diff --git a/apps/cloudo/src/commands/testing/subcommandgroup.ts b/apps/cloudo/src/commands/testing/subcommandgroup.ts index 85651bb3..799f267e 100644 --- a/apps/cloudo/src/commands/testing/subcommandgroup.ts +++ b/apps/cloudo/src/commands/testing/subcommandgroup.ts @@ -5,45 +5,45 @@ import { CommandWithSubcommands } from "@buape/carbon" -class Sub1 extends Command { - name = "sub1" +class Command1 extends Command { + name = "command1" description = "Subcommand 1" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 1" }) + await interaction.reply({ content: "Subcommand 1" }) } } -class Sub2 extends Command { - name = "sub2" +class Command2 extends Command { + name = "command2" description = "Subcommand 2" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 2" }) + await interaction.reply({ content: "Subcommand 2" }) } } -class Subc extends CommandWithSubcommands { - name = "subc" +class SubcommandsCommand1 extends CommandWithSubcommands { + name = "subcommand1" description = "Subcommands!" defer = true - subcommands = [new Sub1(), new Sub2()] + subcommands = [new Command1(), new Command2()] } -class Subc2 extends CommandWithSubcommands { - name = "subc2" +class SubcommandsCommand2 extends CommandWithSubcommands { + name = "subcommand2" description = "Subcommands!" defer = true - subcommands = [new Sub1(), new Sub2()] + subcommands = [new Command1(), new Command2()] } -export default class SubcG extends CommandWithSubcommandGroups { - name = "subg" +export default class SubcommandGroupsCommand extends CommandWithSubcommandGroups { + name = "subcommandgroups" description = "Subcommand group!" - subcommandGroups = [new Subc(), new Subc2()] + subcommandGroups = [new SubcommandsCommand1(), new SubcommandsCommand2()] } diff --git a/apps/cloudo/src/commands/testing/user_command.ts b/apps/cloudo/src/commands/testing/user_command.ts new file mode 100644 index 00000000..1e829a09 --- /dev/null +++ b/apps/cloudo/src/commands/testing/user_command.ts @@ -0,0 +1,16 @@ +import { + ApplicationCommandType, + Command, + type CommandInteraction +} from "@buape/carbon" + +export default class UserCommand extends Command { + name = "User Command" + description = "User command test" + defer = true + type = ApplicationCommandType.User + + async run(interaction: CommandInteraction) { + await interaction.reply({ content: "User command" }) + } +} diff --git a/apps/cloudo/src/index.ts b/apps/cloudo/src/index.ts index 209c2298..e1cadcac 100644 --- a/apps/cloudo/src/index.ts +++ b/apps/cloudo/src/index.ts @@ -1,44 +1,64 @@ -import { Client, ClientMode } from "@buape/carbon" -import type { ExecutionContext } from "@cloudflare/workers-types" - +import { Client, createHandle } from "@buape/carbon" +import { createHandler } from "@buape/carbon/adapters/cloudflare" +import { + ApplicationRoleConnectionMetadataType, + LinkedRoles +} from "@buape/carbon/linked-roles" import PingCommand from "./commands/ping.js" import ButtonCommand from "./commands/testing/button.js" -import SelectCommand from "./commands/testing/every_select.js" +import EphemeralCommand from "./commands/testing/ephemeral.js" +import EverySelectCommand from "./commands/testing/every_select.js" +import MessageCommand from "./commands/testing/message_command.js" import ModalCommand from "./commands/testing/modal.js" -import Options from "./commands/testing/options.js" -import Subc from "./commands/testing/subcommand.js" -import SubcG from "./commands/testing/subcommandgroup.js" - -type Env = { - CLIENT_ID: string - PUBLIC_KEY: string - DISCORD_TOKEN: string -} +import OptionsCommand from "./commands/testing/options.js" +import SubcommandsCommand from "./commands/testing/subcommand.js" +import SubcommandGroupsCommand from "./commands/testing/subcommandgroup.js" +import UserCommand from "./commands/testing/user_command.js" -export default { - async fetch(request: Request, _env: Env, ctx: ExecutionContext) { - const client = new Client( +const handle = createHandle((env) => { + const client = new Client( + { + baseUrl: String(env.BASE_URL), + deploySecret: String(env.DEPLOY_SECRET), + clientId: String(env.DISCORD_CLIENT_ID), + clientSecret: String(env.DISCORD_CLIENT_SECRET), + publicKey: String(env.DISCORD_PUBLIC_KEY), + token: String(env.DISCORD_BOT_TOKEN) + }, + [ + // commands/* + new PingCommand(), + // commands/testing/* + new ButtonCommand(), + new EphemeralCommand(), + new EverySelectCommand(), + new MessageCommand(), + new ModalCommand(), + new OptionsCommand(), + new SubcommandsCommand(), + new SubcommandGroupsCommand(), + new UserCommand() + ] + ) + const linkedRoles = new LinkedRoles(client, { + metadata: [ { - clientId: _env.CLIENT_ID, - publicKey: _env.PUBLIC_KEY, - token: _env.DISCORD_TOKEN, - mode: ClientMode.CloudflareWorkers - }, - [ - new ButtonCommand(), - new Options(), - new PingCommand(), - new SelectCommand(), - new Subc(), - new SubcG(), - new ModalCommand() - ] - ) - if (request.url.endsWith("/deploy")) { - await client.deployCommands() - return new Response("Deployed commands") + key: "is_staff", + name: "Verified Staff", + description: "Whether the user is a verified staff member", + type: ApplicationRoleConnectionMetadataType.BooleanEqual + } + ], + metadataCheckers: { + is_staff: async (userId) => { + const isAllowed = ["439223656200273932"] + if (isAllowed.includes(userId)) return true + return false + } } - const response = await client.router.fetch(request, ctx) - return response - } -} + }) + return [client, linkedRoles] +}) + +const handler = createHandler(handle) +export default { fetch: handler } diff --git a/apps/cloudo/tsconfig.json b/apps/cloudo/tsconfig.json index 9107d046..7801d8f3 100644 --- a/apps/cloudo/tsconfig.json +++ b/apps/cloudo/tsconfig.json @@ -1,9 +1,13 @@ { - "extends": "../../tsconfig.base.json", - "include": ["src/**/*.ts", "src/*.ts"], + "include": ["src"], + "exclude": ["node_modules", "dist"], "compilerOptions": { - "outDir": "./dist", - "rootDir": ".", - "types": ["@cloudflare/workers-types"] + "strict": true, + "skipLibCheck": true, + "lib": ["es2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": ".", + "outDir": "dist" } } diff --git a/apps/cloudo/wrangler.toml b/apps/cloudo/wrangler.toml index 887e73cd..35b2458c 100644 --- a/apps/cloudo/wrangler.toml +++ b/apps/cloudo/wrangler.toml @@ -1,3 +1,3 @@ -name = "carbon-test" +name = "cloudo" main = "src/index.ts" -compatibility_date = "2023-03-20" +compatibility_date = "2024-10-18" \ No newline at end of file diff --git a/apps/rocko/.env.example b/apps/rocko/.env.example index 60469898..e7d23ae4 100644 --- a/apps/rocko/.env.example +++ b/apps/rocko/.env.example @@ -1,4 +1,6 @@ -# This is a sample .env file for use with Rocko -CLIENT_ID= -PUBLIC_KEY= -DISCORD_TOKEN= +BASE_URL= +DEPLOY_SECRET= +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_PUBLIC_KEY= +DISCORD_BOT_TOKEN= \ No newline at end of file diff --git a/apps/rocko/.gitignore b/apps/rocko/.gitignore new file mode 100644 index 00000000..f94630c5 --- /dev/null +++ b/apps/rocko/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env \ No newline at end of file diff --git a/apps/rocko/README.md b/apps/rocko/README.md index c2041a87..e7626b2e 100644 --- a/apps/rocko/README.md +++ b/apps/rocko/README.md @@ -1,5 +1,7 @@ -# Rocko +## rocko -Rocko is our test bot that runs for us to test our changes to the framework. It has implementations of every single feature of the framework somewhere in its code. +This is a [Discord](https://discord.dev) app made with [Carbon](https://carbon.buape.com) and generated with the [`create-carbon`](https://npmjs.com/create-carbon) tool. -To run it, copy the `.env.example` file to `.env` and fill in the values. Then run `pnpm dev`. \ No newline at end of file +To learn how to get started in development, deploy to production, or add commands, head over to the [documentation](https://carbon.buape.com/adapters/node) for your runtime. + +If you need any assistance, you can join our [Discord](https://go.buape.com/carbon) and ask in the [`#support`](https://discord.com/channels/1280628625904894072/1280630704308486174) channel. \ No newline at end of file diff --git a/apps/rocko/package.json b/apps/rocko/package.json index 1bb83887..4a7b05ef 100644 --- a/apps/rocko/package.json +++ b/apps/rocko/package.json @@ -1,8 +1,8 @@ { "name": "demo-rocko", + "private": true, "type": "module", - "main": "./dist/src/index.js", - "private": "true", + "main": "./dist/index.js", "scripts": { "build": "tsc", "dev": "tsc-watch --onSuccess \"node .\"", @@ -10,8 +10,11 @@ }, "dependencies": { "@buape/carbon": "workspace:*", - "@buape/carbon-nodejs": "workspace:*", - "@types/node": "^20" + "dotenv": "16.4.5" }, - "license": "MIT" + "devDependencies": { + "@types/node": "22.7.6", + "tsc-watch": "6.2.0", + "typescript": "5.6.3" + } } diff --git a/apps/rocko/src/commands/ping.ts b/apps/rocko/src/commands/ping.ts index bb8b9880..dc20b15b 100644 --- a/apps/rocko/src/commands/ping.ts +++ b/apps/rocko/src/commands/ping.ts @@ -2,12 +2,9 @@ import { Command, type CommandInteraction } from "@buape/carbon" export default class PingCommand extends Command { name = "ping" - description = "A simple ping command" - defer = false + description = "Replies with Pong!" async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) + await interaction.reply("Pong! Hello from rocko!") } } diff --git a/apps/rocko/src/commands/testing/attachment.ts b/apps/rocko/src/commands/testing/attachment.ts index 79891fe6..28d755c7 100644 --- a/apps/rocko/src/commands/testing/attachment.ts +++ b/apps/rocko/src/commands/testing/attachment.ts @@ -1,14 +1,14 @@ import { readFileSync } from "node:fs" import { Command, type CommandInteraction } from "@buape/carbon" -export default class Attachment extends Command { +export default class AttachmentCommand extends Command { name = "attachment" description = "Attachment test" defer = true async run(interaction: CommandInteraction) { const file = new Blob([readFileSync("./kiai.png")]) - interaction.reply( + await interaction.reply( { content: "Testing" }, diff --git a/apps/rocko/src/commands/testing/button.ts b/apps/rocko/src/commands/testing/button.ts index 3c2a7e61..a995e55a 100644 --- a/apps/rocko/src/commands/testing/button.ts +++ b/apps/rocko/src/commands/testing/button.ts @@ -13,27 +13,27 @@ export default class ButtonCommand extends Command { description = "A simple command with a button!" defer = true - components = [PingButton] + components = [ClickMeButton] async run(interaction: CommandInteraction) { await interaction.reply({ - content: "Pong!", - components: [new Row([new PingButton(), new Link()])] + content: "Look at this button!", + components: [new Row([new ClickMeButton(), new DocsButton()])] }) } } -class PingButton extends Button { - customId = "ping" - label = "Ping" +class ClickMeButton extends Button { + customId = "click-me" + label = "Click me!" style = ButtonStyle.Primary async run(interaction: ButtonInteraction) { - await interaction.reply("OMG YOU CLICKED THE BUTTON") + await interaction.reply("You clicked the button!") } } -class Link extends LinkButton { - label = "Link" - url = "https://buape.com" +class DocsButton extends LinkButton { + label = "Carbon Documentation" + url = "https://carbon.buape.com" } diff --git a/apps/rocko/src/commands/testing/ephemeral.ts b/apps/rocko/src/commands/testing/ephemeral.ts index c7a589b0..ba000c65 100644 --- a/apps/rocko/src/commands/testing/ephemeral.ts +++ b/apps/rocko/src/commands/testing/ephemeral.ts @@ -4,14 +4,6 @@ import { CommandWithSubcommands } from "@buape/carbon" -export default class EphemeralCommand extends CommandWithSubcommands { - name = "ephemeral" - description = "Ephemeral test" - ephemeral = true - - subcommands = [new EphemeralNoDefer(), new EphemeralDefer()] -} - class EphemeralNoDefer extends Command { name = "no-defer" description = "Ephemeral test" @@ -19,17 +11,25 @@ class EphemeralNoDefer extends Command { defer = false async run(interaction: CommandInteraction): Promise { - return interaction.reply({ content: "Ephemeral no defer" }) + await interaction.reply({ content: "Ephemeral no defer" }) } } -export class EphemeralDefer extends Command { +class EphemeralDefer extends Command { name = "defer" description = "Ephemeral test" ephemeral = true defer = true async run(interaction: CommandInteraction): Promise { - return interaction.reply({ content: "Ephemeral defer" }) + await interaction.reply({ content: "Ephemeral defer" }) } } + +export default class EphemeralCommand extends CommandWithSubcommands { + name = "ephemeral" + description = "Ephemeral test" + ephemeral = true + + subcommands = [new EphemeralNoDefer(), new EphemeralDefer()] +} diff --git a/apps/rocko/src/commands/testing/every_select.ts b/apps/rocko/src/commands/testing/every_select.ts index 8b21223a..e7ba0744 100644 --- a/apps/rocko/src/commands/testing/every_select.ts +++ b/apps/rocko/src/commands/testing/every_select.ts @@ -14,8 +14,8 @@ import { type UserSelectMenuInteraction } from "@buape/carbon" -export default class SelectCommand extends Command { - name = "every_select" +export default class EverySelectCommand extends Command { + name = "every-select" description = "Send every select menu" defer = true @@ -28,16 +28,15 @@ export default class SelectCommand extends Command { ] async run(interaction: CommandInteraction) { - const row = new Row() - row.addComponent(new StringSelect()) - row.addComponent(new RoleSelect()) - row.addComponent(new MentionableSelect()) - row.addComponent(new ChannelSelect()) - row.addComponent(new UserSelect()) + const stringRow = new Row([new StringSelect()]) + const roleRow = new Row([new RoleSelect()]) + const mentionableRow = new Row([new MentionableSelect()]) + const channelRow = new Row([new ChannelSelect()]) + const userRow = new Row([new UserSelect()]) - interaction.reply({ + await interaction.reply({ content: "Select menus!!", - components: [row] + components: [stringRow, roleRow, mentionableRow, channelRow, userRow] }) } } @@ -47,7 +46,7 @@ class StringSelect extends StringSelectMenu { placeholder = "String select menu" options = [{ label: "Option 1", value: "option1" }] async run(interaction: StringSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } @@ -55,27 +54,27 @@ class RoleSelect extends RoleSelectMenu { customId = "role-select" placeholder = "Role select menu" async run(interaction: RoleSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } class MentionableSelect extends MentionableSelectMenu { customId = "mentionable-select" placeholder = "Mentionable select menu" async run(interaction: MentionableSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } class ChannelSelect extends ChannelSelectMenu { customId = "channel-select" placeholder = "Channel select menu" async run(interaction: ChannelSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } class UserSelect extends UserSelectMenu { customId = "user-select" placeholder = "User select menu" async run(interaction: UserSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply({ content: interaction.values.join(", ") }) } } diff --git a/apps/rocko/src/commands/testing/message_command.ts b/apps/rocko/src/commands/testing/message_command.ts index f658576d..7d5513dd 100644 --- a/apps/rocko/src/commands/testing/message_command.ts +++ b/apps/rocko/src/commands/testing/message_command.ts @@ -11,6 +11,6 @@ export default class MessageCommand extends Command { type = ApplicationCommandType.Message async run(interaction: CommandInteraction) { - interaction.reply({ content: "Message command" }) + await interaction.reply({ content: "Message command" }) } } diff --git a/apps/rocko/src/commands/testing/modal.ts b/apps/rocko/src/commands/testing/modal.ts index ca4ab013..4d720c82 100644 --- a/apps/rocko/src/commands/testing/modal.ts +++ b/apps/rocko/src/commands/testing/modal.ts @@ -31,10 +31,14 @@ class TestModal extends Modal { new Row([new TextInputHeight()]) ] - run(interaction: ModalInteraction) { - return interaction.reply({ - content: `Hi ${interaction.fields.getText("name")}, you are ${interaction.fields.getText("age")} years old, and your favorite color is ${interaction.fields.getText("color")}. You are ${interaction.fields.getText("height") || "not"} tall.` - }) + async run(interaction: ModalInteraction) { + const name = interaction.fields.getText("name") + const age = interaction.fields.getText("age") + const color = interaction.fields.getText("color") + const height = interaction.fields.getText("height") || "not" + await interaction.reply( + `Hi ${name}, you are ${age} years old, and your favorite color is ${color}. You are ${height} tall.` + ) } } diff --git a/apps/rocko/src/commands/testing/options.ts b/apps/rocko/src/commands/testing/options.ts index 99f2ea0e..6eed101b 100644 --- a/apps/rocko/src/commands/testing/options.ts +++ b/apps/rocko/src/commands/testing/options.ts @@ -6,7 +6,7 @@ import { type CommandOptions } from "@buape/carbon" -export default class Options extends Command { +export default class OptionsCommand extends Command { name = "options" description = "Options test" defer = true @@ -54,12 +54,6 @@ export default class Options extends Command { description: "DESCRIPTION", required: false }, - { - name: "attachment", - type: ApplicationCommandOptionType.Attachment, - description: "DESCRIPTION", - required: false - }, { name: "mentionable", type: ApplicationCommandOptionType.Mentionable, @@ -75,22 +69,31 @@ export default class Options extends Command { } ] - async run(interaction: CommandInteraction) { - await interaction.reply({ - content: `Str: ${interaction.options.getString("str")}\nInt: ${interaction.options.getInteger("int")}\nNum: ${interaction.options.getNumber("num")}\nBool: ${interaction.options.getBoolean("bool")}\nUser: ${interaction.options.getUser("user")}\nChannel: ${interaction.options.getChannel("channel")}\nRole: ${interaction.options.getRole("role")}\nMentionable: ${interaction.options.getMentionable("mentionable")}\nAutocomplete: ${interaction.options.getString("autocomplete")}` - }) - } - async autocomplete(interaction: AutocompleteInteraction) { await interaction.respond([ { name: "That thing you said", - value: `${interaction.options.getFocused() || "NONE"}` + value: String(interaction.options.getFocused()) || "No focused option" }, { - name: "That thing you said but with a 4", - value: `4: ${interaction.options.getFocused() || "NONE"}` + name: "That thing you said but with a prefix", + value: `Prefix: ${String(interaction.options.getFocused())}` } ]) } + + async run(interaction: CommandInteraction) { + const str = interaction.options.getString("str") + const int = interaction.options.getInteger("int") + const num = interaction.options.getNumber("num") + const bool = interaction.options.getBoolean("bool") + const user = interaction.options.getUser("user") + const channel = await interaction.options.getChannel("channel") + const role = interaction.options.getRole("role") + const mentionable = await interaction.options.getMentionable("mentionable") + + await interaction.reply( + `You provided the following options:\n str: ${str}\n int: ${int}\n num: ${num}\n bool: ${bool}\n user: ${user?.id}\n channel: ${channel?.id}\n role: ${role?.id}\n mentionable: ${mentionable?.id}` + ) + } } diff --git a/apps/rocko/src/commands/testing/subcommand.ts b/apps/rocko/src/commands/testing/subcommand.ts index 16c58ade..538bc251 100644 --- a/apps/rocko/src/commands/testing/subcommand.ts +++ b/apps/rocko/src/commands/testing/subcommand.ts @@ -4,30 +4,30 @@ import { CommandWithSubcommands } from "@buape/carbon" -class Sub1 extends Command { - name = "sub1" +class Command1 extends Command { + name = "command1" description = "Subcommand 1" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 1" }) + await interaction.reply({ content: "Subcommand 1" }) } } -class Sub2 extends Command { - name = "sub2" +class Command2 extends Command { + name = "command2" description = "Subcommand 2" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 2" }) + await interaction.reply({ content: "Subcommand 2" }) } } -export default class Subc extends CommandWithSubcommands { - name = "subc" +export default class SubcommandsCommand extends CommandWithSubcommands { + name = "subcommands" description = "Subcommands!" defer = true - subcommands = [new Sub1(), new Sub2()] + subcommands = [new Command1(), new Command2()] } diff --git a/apps/rocko/src/commands/testing/subcommandgroup.ts b/apps/rocko/src/commands/testing/subcommandgroup.ts index 85651bb3..799f267e 100644 --- a/apps/rocko/src/commands/testing/subcommandgroup.ts +++ b/apps/rocko/src/commands/testing/subcommandgroup.ts @@ -5,45 +5,45 @@ import { CommandWithSubcommands } from "@buape/carbon" -class Sub1 extends Command { - name = "sub1" +class Command1 extends Command { + name = "command1" description = "Subcommand 1" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 1" }) + await interaction.reply({ content: "Subcommand 1" }) } } -class Sub2 extends Command { - name = "sub2" +class Command2 extends Command { + name = "command2" description = "Subcommand 2" defer = true async run(interaction: CommandInteraction) { - interaction.reply({ content: "Subcommand 2" }) + await interaction.reply({ content: "Subcommand 2" }) } } -class Subc extends CommandWithSubcommands { - name = "subc" +class SubcommandsCommand1 extends CommandWithSubcommands { + name = "subcommand1" description = "Subcommands!" defer = true - subcommands = [new Sub1(), new Sub2()] + subcommands = [new Command1(), new Command2()] } -class Subc2 extends CommandWithSubcommands { - name = "subc2" +class SubcommandsCommand2 extends CommandWithSubcommands { + name = "subcommand2" description = "Subcommands!" defer = true - subcommands = [new Sub1(), new Sub2()] + subcommands = [new Command1(), new Command2()] } -export default class SubcG extends CommandWithSubcommandGroups { - name = "subg" +export default class SubcommandGroupsCommand extends CommandWithSubcommandGroups { + name = "subcommandgroups" description = "Subcommand group!" - subcommandGroups = [new Subc(), new Subc2()] + subcommandGroups = [new SubcommandsCommand1(), new SubcommandsCommand2()] } diff --git a/apps/rocko/src/commands/testing/user_command.ts b/apps/rocko/src/commands/testing/user_command.ts index 6fcc4eb4..1e829a09 100644 --- a/apps/rocko/src/commands/testing/user_command.ts +++ b/apps/rocko/src/commands/testing/user_command.ts @@ -11,6 +11,6 @@ export default class UserCommand extends Command { type = ApplicationCommandType.User async run(interaction: CommandInteraction) { - interaction.reply({ content: "User command" }) + await interaction.reply({ content: "User command" }) } } diff --git a/apps/rocko/src/index.ts b/apps/rocko/src/index.ts index eaae1bfe..3d88f5af 100644 --- a/apps/rocko/src/index.ts +++ b/apps/rocko/src/index.ts @@ -1,56 +1,66 @@ -import { dirname } from "node:path" -import { fileURLToPath } from "node:url" -import { Client, ClientMode } from "@buape/carbon" -import { loadCommands, serve } from "@buape/carbon-nodejs" +import "dotenv/config" +import { Client, createHandle } from "@buape/carbon" +import { createServer } from "@buape/carbon/adapters/node" import { ApplicationRoleConnectionMetadataType, LinkedRoles } from "@buape/carbon/linked-roles" -const __dirname = dirname(fileURLToPath(import.meta.url)) +import PingCommand from "./commands/ping.js" +import AttachmentCommand from "./commands/testing/attachment.js" +import ButtonCommand from "./commands/testing/button.js" +import EphemeralCommand from "./commands/testing/ephemeral.js" +import EverySelectCommand from "./commands/testing/every_select.js" +import MessageCommand from "./commands/testing/message_command.js" +import ModalCommand from "./commands/testing/modal.js" +import OptionsCommand from "./commands/testing/options.js" +import SubcommandsCommand from "./commands/testing/subcommand.js" +import SubcommandGroupsCommand from "./commands/testing/subcommandgroup.js" +import UserCommand from "./commands/testing/user_command.js" -if ( - !process.env.CLIENT_ID || - !process.env.PUBLIC_KEY || - !process.env.DISCORD_TOKEN || - !process.env.CLIENT_SECRET -) { - throw new Error("Missing environment variables") -} - -const client = new Client( - { - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - mode: ClientMode.NodeJS, - requestOptions: { - queueRequests: false - }, - autoDeploy: true - }, - await loadCommands("commands", __dirname) -) - -serve(client, { port: 3000 }) - -export const sleep = async (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -new LinkedRoles(client, { - clientSecret: process.env.CLIENT_SECRET, - baseUrl: "http://localhost:3000", - metadata: [ +const handle = createHandle((env) => { + const client = new Client( { - key: "is_shadow", - name: "Whether you are Shadow", - description: "You gotta be Shadow to get this one!", - type: ApplicationRoleConnectionMetadataType.BooleanEqual - } - ], - metadataCheckers: { - is_shadow: async (userId) => { - return userId === "439223656200273932" + baseUrl: String(env.BASE_URL), + deploySecret: String(env.DEPLOY_SECRET), + clientId: String(env.DISCORD_CLIENT_ID), + clientSecret: String(env.DISCORD_CLIENT_SECRET), + publicKey: String(env.DISCORD_PUBLIC_KEY), + token: String(env.DISCORD_BOT_TOKEN) + }, + [ + // commands/* + new PingCommand(), + // commands/testing/* + new AttachmentCommand(), + new ButtonCommand(), + new EphemeralCommand(), + new EverySelectCommand(), + new MessageCommand(), + new ModalCommand(), + new OptionsCommand(), + new SubcommandsCommand(), + new SubcommandGroupsCommand(), + new UserCommand() + ] + ) + const linkedRoles = new LinkedRoles(client, { + metadata: [ + { + key: "is_staff", + name: "Verified Staff", + description: "Whether the user is a verified staff member", + type: ApplicationRoleConnectionMetadataType.BooleanEqual + } + ], + metadataCheckers: { + is_staff: async (userId) => { + const isAllowed = ["439223656200273932"] + if (isAllowed.includes(userId)) return true + return false + } } - } + }) + return [client, linkedRoles] }) + +createServer(handle, { port: 3000 }) diff --git a/apps/rocko/tsconfig.json b/apps/rocko/tsconfig.json index 08a81c58..7801d8f3 100644 --- a/apps/rocko/tsconfig.json +++ b/apps/rocko/tsconfig.json @@ -1,8 +1,13 @@ { - "extends": "../../tsconfig.base.json", - "include": ["src/**/*.ts", "src/*.ts"], + "include": ["src"], + "exclude": ["node_modules", "dist"], "compilerOptions": { - "outDir": "./dist", - "rootDir": "." + "strict": true, + "skipLibCheck": true, + "lib": ["es2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": ".", + "outDir": "dist" } } diff --git a/formatter.mjs b/formatter.mjs index 7fd600d5..5ca42039 100644 --- a/formatter.mjs +++ b/formatter.mjs @@ -28,7 +28,6 @@ export function load(app) { if (!page.contents) return const rel = relative(root, dirname(page.filename)) const parts = rel.split(sep) - const pkg = parts[1] const dirParts = parts.slice(3) const dir = dirParts.length ? `${dirParts.join("/")}/` : "" page.contents = page.contents.replace( @@ -36,7 +35,7 @@ export function load(app) { (_, text, link) => { let newLink = link if (!link.includes("://")) { - const url = new URL(link, `http://e.com/${pkg}/api/${dir}`) + const url = new URL(link, `http://e.com/api/${dir}`) newLink = `${url.pathname}${url.search}${url.hash}` if (link.endsWith("/index")) { newLink = link.slice(0, -6) diff --git a/packages/carbon/package.json b/packages/carbon/package.json index 55c5801a..aa8ecaae 100644 --- a/packages/carbon/package.json +++ b/packages/carbon/package.json @@ -12,6 +12,10 @@ "./*": { "types": "./dist/src/plugins/*/index.d.ts", "import": "./dist/src/plugins/*/index.js" + }, + "./adapters/*": { + "types": "./dist/src/adapters/*/index.d.ts", + "import": "./dist/src/adapters/*/index.js" } }, "scripts": { @@ -23,9 +27,12 @@ "dependencies": { "@buape/carbon-request": "workspace:*", "@types/node": "^20", - "discord-api-types": "0.37.101", - "discord-verify": "1.2.0", - "itty-router": "5.0.18" + "discord-api-types": "0.37.101" + }, + "optionalDependencies": { + "@cloudflare/workers-types": "4.20240919.0", + "@hono/node-server": "1.13.1", + "@types/bun": "^1.1.10" }, "files": [ "dist", diff --git a/packages/carbon/src/abstracts/Plugin.ts b/packages/carbon/src/abstracts/Plugin.ts new file mode 100644 index 00000000..3a262f48 --- /dev/null +++ b/packages/carbon/src/abstracts/Plugin.ts @@ -0,0 +1,16 @@ +export abstract class Plugin { + routes: Route[] = [] +} + +export interface Route { + method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" + path: `/${string}` + handler(req: Request, ctx?: Context): Response | Promise + protected?: boolean + disabled?: boolean +} + +export interface Context { + // biome-ignore lint/suspicious/noExplicitAny: true any + waitUntil?(promise: Promise): void +} diff --git a/packages/carbon/src/adapters/bun/index.ts b/packages/carbon/src/adapters/bun/index.ts new file mode 100644 index 00000000..340178ae --- /dev/null +++ b/packages/carbon/src/adapters/bun/index.ts @@ -0,0 +1,24 @@ +import Bun from "bun" +import type { Handle } from "../../createHandle.js" +import type { ServerOptions } from "../shared.js" + +export type Server = ReturnType + +/** + * Creates a Bun server using the provided handle function and options + * @param handle The handle function created by {@link createHandle} + * @param options The server options including the port and hostname + * @returns The created server instance + * @example + * ```ts + * const server = createServer(handle, { ... }) + * ``` + */ +export function createServer(handle: Handle, options: ServerOptions): Server { + const fetch = handle(process.env) + return Bun.serve({ + fetch: (req) => fetch(req, {}), + port: options.port, + hostname: options.hostname + }) +} diff --git a/packages/carbon/src/adapters/cloudflare/index.ts b/packages/carbon/src/adapters/cloudflare/index.ts new file mode 100644 index 00000000..05fc9104 --- /dev/null +++ b/packages/carbon/src/adapters/cloudflare/index.ts @@ -0,0 +1,26 @@ +import type { ExecutionContext } from "@cloudflare/workers-types" +import type { Handle } from "../../createHandle.js" +import type { PartialEnv } from "../shared.js" + +export type Handler = ( + req: Request, + env: PartialEnv, + ctx: ExecutionContext +) => Promise + +/** + * Creates a Cloudflare handler function using the provided handle and handler options + * @param handle - The handle function to process requests + * @returns The created handler function + * @example + * ```ts + * const handler = createHandler(handle, { ... }) + * export default { fetch: handler } + * ``` + */ +export function createHandler(handle: Handle): Handler { + return (req: Request, env: PartialEnv, ctx: ExecutionContext) => { + const fetch = handle(env) + return fetch(req, ctx) + } +} diff --git a/packages/carbon/src/adapters/next/index.ts b/packages/carbon/src/adapters/next/index.ts new file mode 100644 index 00000000..80a57116 --- /dev/null +++ b/packages/carbon/src/adapters/next/index.ts @@ -0,0 +1,20 @@ +import type { Handle } from "../../createHandle.js" + +export type Handler = (req: Request) => Promise + +/** + * Creates a Next.js handler function using the provided handle and handler options + * @param handle - The handle function to process requests + * @returns The created handler function + * @example + * ```ts + * const handler = createHandler(handle, { ... }) + * export { handler as GET, handler as POST } + * ``` + */ +export function createHandler(handle: Handle): Handler { + return (req: Request) => { + const fetch = handle(process.env) + return fetch(req) + } +} diff --git a/packages/carbon/src/adapters/node/index.ts b/packages/carbon/src/adapters/node/index.ts new file mode 100644 index 00000000..b8d145c5 --- /dev/null +++ b/packages/carbon/src/adapters/node/index.ts @@ -0,0 +1,24 @@ +import * as Hono from "@hono/node-server" +import type { Handle } from "../../createHandle.js" +import type { ServerOptions } from "../shared.js" + +export type Server = ReturnType + +/** + * Creates a Node.js server using the provided handle function and options + * @param handle The handle function created by {@link createHandle} + * @param options The server options including the port and hostname + * @returns The created server instance + * @example + * ```ts + * const server = createServer(handle, { ... }) + * ``` + */ +export function createServer(handle: Handle, options: ServerOptions): Server { + const fetch = handle(process.env) + return Hono.serve({ + fetch: (req) => fetch(req, {}), + port: options.port, + hostname: options.hostname + }) +} diff --git a/packages/carbon/src/adapters/shared.ts b/packages/carbon/src/adapters/shared.ts new file mode 100644 index 00000000..68736777 --- /dev/null +++ b/packages/carbon/src/adapters/shared.ts @@ -0,0 +1,11 @@ +export type PartialEnv = Record + +// biome-ignore lint/suspicious/noEmptyInterface: future-proofing +export interface SharedOptions {} + +export interface ServerOptions extends SharedOptions { + port: number + hostname?: string +} + +export interface HandlerOptions extends SharedOptions {} diff --git a/packages/carbon/src/classes/Client.ts b/packages/carbon/src/classes/Client.ts index 2e4fb95f..6e5cf02a 100644 --- a/packages/carbon/src/classes/Client.ts +++ b/packages/carbon/src/classes/Client.ts @@ -10,8 +10,8 @@ import { InteractionType, Routes } from "discord-api-types/v10" -import { AutoRouter, type IRequestStrict, StatusError, json } from "itty-router" import type { BaseCommand } from "../abstracts/BaseCommand.js" +import { type Context, Plugin } from "../abstracts/Plugin.js" import { channelFactory } from "../factories/channelFactory.js" import { CommandHandler } from "../internals/CommandHandler.js" import { ComponentHandler } from "../internals/ComponentHandler.js" @@ -22,81 +22,65 @@ import { Role } from "../structures/Role.js" import { User } from "../structures/User.js" import { concatUint8Arrays, subtleCrypto, valueToUint8Array } from "../utils.js" -/** - * The mode that the client is running in. - * Different platforms have different requirements for how processes are handled. - */ -export enum ClientMode { - NodeJS = "node", - CloudflareWorkers = "cloudflare", - Bun = "bun", - Vercel = "vercel", - Web = "web" -} - /** * The options used for initializing the client */ export type ClientOptions = { /** - * If you want to have the root route for the interaction handler redirect to a different URL, you can set this. + * The base URL of the app */ - redirectUrl?: string + baseUrl: string /** - * The client ID of the bot + * The client ID of the app */ clientId: string /** - * The public key of the bot, used for interaction verification + * The deploy secret of the app, used for protecting the deploy route */ - publicKey: string + deploySecret: string /** - * The token of the bot + * The client secret of the app */ - token: string + clientSecret: string /** - * The mode of the client, generally where you are hosting the bot. If you have a different mode for your local development, make sure to set it to the local one. - * @example - * ```ts - * import { Client, ClientMode } from "@buape/carbon" - * - * const client = new Client({ - * clientId: "12345678901234567890", - * publicKey: "c1a2f941ae8ce6d776f7704d0bb3d46b863e21fda491cdb2bdba6b8bc5fe7269", - * token: "MTA4NjEwNTYxMDUxMDE1NTg1Nw.GNt-U8.OSHy-g-5FlfESnu3Z9MEEMJLHiRthXajiXNwiE", - * mode: process.env.NODE_ENV === "development" ? ClientMode.NodeJS : ClientMode.CloudflareWorkers - * }) - * ``` + * The public key of the app, used for interaction verification */ - mode: ClientMode + publicKey: string /** - * The route to use for interactions on your server. - * @default "/interaction" + * The token of the bot */ - interactionRoute?: string + token: string /** * The options used to initialize the request client, if you want to customize it. */ requestOptions?: RequestClientOptions - /** - * The port to run the server on, if you are using {@link ClientMode.Bun} mode. - */ - port?: number /** * Whether the commands should be deployed to Discord automatically. + * @default false */ autoDeploy?: boolean /** - * Whether components and modals should be registered automatically. - * If you don't want to do this (e.g. you are changing them at runtime), you can manually call {@link ComponentHandler#registerComponent} and {@link ModalHandler#registerModal} on the client. + * Whether components and modals should not be registered automatically. + * If you want you register components yourself (e.g. you are changing them at runtime), you can manually call {@link ComponentHandler#registerComponent} and {@link ModalHandler#registerModal} on the client. + * @default false + */ + disableAutoRegister?: boolean + /** + * Whether the deploy route should be disabled. + * @default false + */ + disableDeployRoute?: boolean + /** + * Whether the interactions route should + * @default false */ - autoRegister?: boolean + disableInteractionsRoute?: boolean } /** * The main client used to interact with Discord */ -export class Client { +export class Client extends Plugin { /** * The options used to initialize the client */ @@ -105,10 +89,6 @@ export class Client { * The commands that the client has registered */ commands: BaseCommand[] - /** - * The router used to handle requests - */ - router: ReturnType> /** * The rest client used to interact with the Discord API */ @@ -135,26 +115,25 @@ export class Client { * @param commands The commands that the client has registered */ constructor(options: ClientOptions, commands: BaseCommand[]) { + super() + + if (!options.baseUrl) throw new Error("Missing base URL") + if (!options.clientSecret) throw new Error("Missing client secret") if (!options.clientId) throw new Error("Missing client ID") if (!options.publicKey) throw new Error("Missing public key") if (!options.token) throw new Error("Missing token") this.options = options this.commands = commands + this.appendRoutes() this.commandHandler = new CommandHandler(this) this.componentHandler = new ComponentHandler(this) this.modalHandler = new ModalHandler(this) - const routerData = - this.options.mode === ClientMode.Bun && this.options.port - ? { port: this.options.port } - : {} - // biome-ignore lint/suspicious/noExplicitAny: - this.router = AutoRouter(routerData) this.rest = new RequestClient(options.token, options.requestOptions) - if (this.options.autoRegister) { + if (!this.options.disableAutoRegister) { for (const command of commands) { for (const component of command.components) this.componentHandler.registerComponent(new component()) @@ -162,129 +141,96 @@ export class Client { this.modalHandler.registerModal(new modal()) } } - if (this.options.autoDeploy) this.deployCommands() - this.setupRoutes() + if (this.options.autoDeploy) { + this.handleDeployRequest() + } } - /** - * Deploy the commands registered to Discord. - * This is automatically called when running in NodeJS mode. - */ - async deployCommands() { - try { - const commands = this.commands - .filter((x) => x.name !== "*") - .map((command) => { - return command.serialize() - }) - await this.rest.put(Routes.applicationCommands(this.options.clientId), { - body: commands - }) - console.log(`Deployed ${commands.length} commands to Discord`) - } catch (err) { - console.error("Failed to deploy commands") - console.error(err) - } + private appendRoutes() { + this.routes.push({ + method: "GET", + path: "/deploy", + handler: this.handleDeployRequest.bind(this), + protected: true, + disabled: this.options.disableDeployRoute + }) + this.routes.push({ + method: "POST", + path: "/interactions", + handler: this.handleInteractionsRequest.bind(this), + disabled: this.options.disableInteractionsRoute + }) } /** - * Setup the routes for the client + * Handle a request to deploy the commands to Discord + * @returns A response */ - private setupRoutes() { - this.router.get("/", () => { - if (this.options.redirectUrl) - return Response.redirect(this.options.redirectUrl, 302) - throw new StatusError(404) - }) - this.router.post( - this.options.interactionRoute || "/interaction", - async (req, ctx?: ExecutionContext) => { - return await this.handle(req, ctx) - } + public async handleDeployRequest() { + const commands = this.commands + .filter((c) => c.name !== "*") + .map((c) => c.serialize()) + await this.rest.put( + Routes.applicationCommands(this.options.clientId), // + { body: commands } ) + return new Response("OK", { status: 202 }) } /** - * If you want use a custom handler for HTTP requests instead of Carbon's router, you can use this method. + * Handle an interaction request from Discord * @param req The request to handle - * @param ctx Cloudflare Workers only. The execution context of the request, provided in the fetch handler from CF. - * @returns A response to send back to the client. + * @param ctx The context for the request + * @returns A response */ - public async handle(req: Request, ctx?: ExecutionContext) { - const isValid = await this.validateInteraction(req) - if (!isValid) { - return new Response("Invalid request signature", { status: 401 }) - } + public async handleInteractionsRequest(req: Request, ctx: Context) { + const isValid = await this.validateInteractionRequest(req) + if (!isValid) return new Response("Unauthorized", { status: 401 }) + + const interaction = (await req.json()) as APIInteraction - const rawInteraction = (await req.json()) as unknown as APIInteraction - if (rawInteraction.type === InteractionType.Ping) { - return json({ - type: InteractionResponseType.Pong - }) + if (interaction.type === InteractionType.Ping) { + return Response.json({ type: InteractionResponseType.Pong }) } - if (rawInteraction.type === InteractionType.ApplicationCommand) { - if (ctx?.waitUntil) { - ctx.waitUntil( - (async () => { - await this.commandHandler.handleCommandInteraction(rawInteraction) - })() - ) - } else { - await this.commandHandler.handleCommandInteraction(rawInteraction) - } + if (interaction.type === InteractionType.ApplicationCommand) { + const promise = this.commandHandler.handleCommandInteraction(interaction) + if (ctx?.waitUntil) ctx.waitUntil(promise) + else await promise } - if ( - rawInteraction.type === InteractionType.ApplicationCommandAutocomplete - ) { - if (ctx?.waitUntil) { - ctx.waitUntil( - (async () => { - await this.commandHandler.handleAutocompleteInteraction( - rawInteraction - ) - })() - ) - } else { - await this.commandHandler.handleAutocompleteInteraction(rawInteraction) - } + + if (interaction.type === InteractionType.ApplicationCommandAutocomplete) { + const promise = + this.commandHandler.handleAutocompleteInteraction(interaction) + if (ctx?.waitUntil) ctx.waitUntil(promise) + else await promise } - if (rawInteraction.type === InteractionType.MessageComponent) { - if (ctx?.waitUntil) { - ctx.waitUntil( - (async () => { - await this.componentHandler.handleInteraction(rawInteraction) - })() - ) - } else { - await this.componentHandler.handleInteraction(rawInteraction) - } + + if (interaction.type === InteractionType.MessageComponent) { + const promise = this.componentHandler.handleInteraction(interaction) + if (ctx?.waitUntil) ctx.waitUntil(promise) + else await promise } - if (rawInteraction.type === InteractionType.ModalSubmit) { - if (ctx?.waitUntil) { - ctx.waitUntil( - (async () => { - await this.modalHandler.handleInteraction(rawInteraction) - })() - ) - } else { - await this.modalHandler.handleInteraction(rawInteraction) - } + + if (interaction.type === InteractionType.ModalSubmit) { + const promise = this.modalHandler.handleInteraction(interaction) + if (ctx?.waitUntil) ctx.waitUntil(promise) + else await promise } - return new Response(null, { status: 202 }) + + return new Response("OK", { status: 202 }) } /** * Validate the interaction request * @param req The request to validate */ - private async validateInteraction(req: Request) { + private async validateInteractionRequest(req: Request) { const body = await req.clone().text() const signature = req.headers.get("X-Signature-Ed25519") const timestamp = req.headers.get("X-Signature-Timestamp") - if (!timestamp || !signature || req.method !== "POST" || !body) { - throw new StatusError(401) - } + if (!timestamp || !signature || req.method !== "POST" || !body) return false + try { const timestampData = valueToUint8Array(timestamp) const bodyData = valueToUint8Array(body) diff --git a/packages/carbon/src/createHandle.ts b/packages/carbon/src/createHandle.ts new file mode 100644 index 00000000..9d03527e --- /dev/null +++ b/packages/carbon/src/createHandle.ts @@ -0,0 +1,69 @@ +import type { Context, Plugin, Route } from "./abstracts/Plugin.js" +import type { PartialEnv } from "./adapters/shared.js" +import type { Client } from "./classes/Client.js" + +/** + * Creates a handle function that can be used to handle requests + * @param factory The factory function that creates the plugins + * @returns The handle function + * @example + * ```ts + * const handle = createHandle((env) => { + * const client = new Client({ ... }, [ ... ]) + * const linkedRoles = new LinkedRoles(client, { ... }) + * return [client, linkedRoles] + * }) + * ``` + */ +export function createHandle( + factory: (env: Env) => [Client, ...Plugin[]] +): Handle { + return (env: Env) => { + const [client, ...plugins] = factory(env) + const routes = [client, ...plugins].flatMap((plugin) => plugin.routes) + + return async (req: Request, ctx?: Context) => { + const method = req.method + const url = new URL(req.url) + const pathname = // + resolveRequestPathname(new URL(client.options.baseUrl), url) + if (!pathname) return new Response("Not Found", { status: 404 }) + + const matchedRoutesByPath = // + routes.filter((r) => r.path === pathname && !r.disabled) + const matchedRoutesByMethod = // + matchedRoutesByPath.filter((r) => r.method === method) + + if (matchedRoutesByMethod.length === 0) { + if (matchedRoutesByPath.length > 0) + return new Response("Method Not Allowed", { status: 405 }) + return new Response("Not Found", { status: 404 }) + } + + // Use the last matched route by method to allow for overriding + const route = matchedRoutesByMethod.at(-1) as Route + + const passedSecret = url.searchParams.get("secret") + if (route.protected && client.options.deploySecret !== passedSecret) + return new Response("Unauthorized", { status: 401 }) + + try { + return await route.handler(req, ctx) + } catch (error) { + console.error(error) + return new Response("Internal Server Error", { status: 500 }) + } + } + } +} + +function resolveRequestPathname(baseUrl: URL, reqUrl: URL) { + // Need to use pathname only due to host name being different in Cloudflare Tunnel + const basePathname = baseUrl.pathname.replace(/\/$/, "") + const reqPathname = reqUrl.pathname.replace(/\/$/, "") + if (!reqPathname.startsWith(basePathname)) return null + return reqPathname.slice(basePathname.length) +} + +export type Fetch = (req: Request, ctx?: Context) => Promise +export type Handle = (env: Env) => Fetch diff --git a/packages/carbon/src/index.ts b/packages/carbon/src/index.ts index 5205f36a..b63f56b7 100644 --- a/packages/carbon/src/index.ts +++ b/packages/carbon/src/index.ts @@ -1,3 +1,5 @@ +export * from "./createHandle.js" + // ----- Abstracts ----- export * from "./abstracts/AnySelectMenu.js" export * from "./abstracts/AnySelectMenuInteraction.js" diff --git a/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts b/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts index 9df60ede..501d89c8 100644 --- a/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts +++ b/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts @@ -1,3 +1,4 @@ +import { Plugin } from "../../abstracts/Plugin.js" import type { Client } from "../../classes/Client.js" import { ApplicationRoleConnectionMetadataType, @@ -12,100 +13,131 @@ type Tokens = { scope: string } +// TODO: IMO, the metadata for this should be handled similarly to the client and its commands +// That is passing an array of connection instances as the second argument to the constructor +// That is, maybe, for another pr though +// TODO: Improve the response messages + /** * This class is the main class that is used for the linked roles feature of Carbon. * It handles all the additional routes and oauth. * * @example * ```ts - * import { Client, LinkedRoles } from "@buape/carbon" - * - * const client = new Client({ - * clientId: "12345678901234567890", - * publicKey: "c1a2f941ae8ce6d776f7704d0bb3d46b863e21fda491cdb2bdba6b8bc5fe7269", - * token: "MTA4NjEwNTYxMDUxMDE1NTg1Nw.GNt-U8.OSHy-g-5FlfESnu3Z9MEEMJLHiRthXajiXNwiE" - * }) - * - * const allStaff = ["439223656200273932"] - * - * const linkedRoles = new LinkedRoles(client, { - * clientSecret: "Bb7aZcvRN-BhrhY2qrUO6QzOK4SeqonG", - * baseUrl: "https://example.com", - * metadata: [ - * { - * key: "is_staff", - * name: "Verified Staff", - * description: "Whether the user is a verified staff member", - * type: ApplicationRoleConnectionMetadataType.BooleanEqual - * }, - * ], - * metadataCheckers: { - * is_staff: async (userId) => { - * if (allStaff.includes(userId)) return true - * return false - * } - * } - * }) +* import { createHandle, Client, ApplicationRoleConnectionMetadataType } from "@buape/carbon" +* import { LinkedRoles } from "@buape/carbon/linked-roles" +* +* const handle = createHandle((env) => { +* const client = new Client({ ... }, [ ... ]) +* const linkedRoles = new LinkedRoles(client, { +* metadata: [ +* { +* key: 'is_staff', +* name: 'Verified Staff', +* description: 'Whether the user is a verified staff member', +* type: ApplicationRoleConnectionMetadataType.BooleanEqual +* } +* ], +* metadataCheckers: { +* is_staff: async (userId) => { +* const allStaff = ["439223656200273932"] +* return allStaff.includes(userId) +* } +* } +* }) +* return [client, linkedRoles] +* }) * ``` */ -export class LinkedRoles { +export class LinkedRoles extends Plugin { client: Client - options: Required + options: LinkedRolesOptions constructor(client: Client, options: LinkedRolesOptions) { + super() + this.client = client this.options = { ...options } - this.setupRoutes() - this.setMetadata(this.options.metadata) - console.log( - `Linked roles initialized\nRedirect URL: ${this.options.baseUrl}/connect/callback\nVerification URL: ${this.options.baseUrl}/connect` - ) + this.appendRoutes() } - private setupRoutes() { - this.client.router.get("/connect", () => { - const response = new Response(null, { - status: 302 - }) - response.headers.set( - "Location", - `https://discord.com/oauth2/authorize?client_id=${this.client.options.clientId}&redirect_uri=${encodeURIComponent(`${this.options.baseUrl}/connect/callback`)}&response_type=code&scope=identify+role_connections.write&prompt=none` - ) - return response + private appendRoutes() { + this.routes.push({ + method: "GET", + path: "/linked-roles/deploy", + handler: this.handleDeployRequest.bind(this), + protected: true, + disabled: this.options.disableDeployRoute + }) + this.routes.push({ + method: "GET", + path: "/linked-roles/verify-user", + handler: this.handleUserVerificationRequest.bind(this), + disabled: this.options.disableVerifyUserRoute + }) + this.routes.push({ + method: "GET", + path: "/linked-roles/verify-user/callback", + handler: this.handleUserVerificationCallbackRequest.bind(this), + disabled: this.options.disableVerifyUserCallbackRoute }) + } + + /** + * Handle a request to deploy the linked roles to Discord + * @returns A response + */ + public async handleDeployRequest() { + await this.setMetadata(this.options.metadata) + return new Response("OK", { status: 202 }) + } - this.client.router.get("/connect/callback", async (req) => { - try { - const code = req.query.code - - const tokens = await this.getOAuthTokens(code as string) - const authData = await ( - await fetch("https://discord.com/api/v10/oauth2/@me", { - headers: { - Authorization: `Bearer ${tokens.access_token}` - } - }) - ).json() - if (!authData.user) - return new Response("", { - status: 307, - headers: { - Location: `${this.options.baseUrl}/connect` - } - }) - - const newMetadata = await this.getMetadataFromCheckers(authData.user.id) - - await this.updateMetadata(authData.user?.id, newMetadata, tokens) - - return new Response("You can now close this tab.") - } catch (e) { - console.error(e) - return new Response("Error", { status: 500 }) + /** + * Handle the verify user request + * @returns A response + */ + public async handleUserVerificationRequest() { + return new Response("Found", { + status: 302, + headers: { + Location: `https://discord.com/oauth2/authorize?client_id=${this.client.options.clientId}&redirect_uri=${encodeURIComponent(`${this.client.options.baseUrl}/linked-roles/verify-user/callback`)}&response_type=code&scope=identify+role_connections.write&prompt=none` } }) } + /** + * Handle the verify user callback request + * @param req The request + * @returns A response + */ + public async handleUserVerificationCallbackRequest(req: Request) { + const url = new URL(req.url) + const code = String(url.searchParams.get("code")) + + const tokens = await this.getOAuthTokens(code as string) + const authData = await ( + await fetch("https://discord.com/api/v10/oauth2/@me", { + headers: { + Authorization: `Bearer ${tokens.access_token}` + } + }) + ).json() + if (!authData.user) + return new Response("", { + status: 307, + headers: { + Location: `${this.client.options.baseUrl}/connect` + } + }) + + const newMetadata = await this.getMetadataFromCheckers(authData.user.id) + + await this.updateMetadata(authData.user?.id, newMetadata, tokens) + + // IDEA: Maybe we can redirect to a success page instead of just a message + return new Response("You can now close this tab.") + } + private async getMetadataFromCheckers(userId: string) { const result: Record = {} for (const metadata of this.options.metadata) { @@ -143,10 +175,10 @@ export class LinkedRoles { const url = "https://discord.com/api/v10/oauth2/token" const body = new URLSearchParams({ client_id: this.client.options.clientId, - client_secret: this.options.clientSecret, + client_secret: this.client.options.clientSecret, grant_type: "authorization_code", code, - redirect_uri: `${this.options.baseUrl}/connect/callback` + redirect_uri: `${this.client.options.baseUrl}/linked-roles/verify-user/callback` }) const response = await fetch(url, { diff --git a/packages/carbon/src/plugins/linked-roles/types.ts b/packages/carbon/src/plugins/linked-roles/types.ts index 9de4f7e5..d3883ece 100644 --- a/packages/carbon/src/plugins/linked-roles/types.ts +++ b/packages/carbon/src/plugins/linked-roles/types.ts @@ -40,15 +40,6 @@ export enum ApplicationRoleConnectionMetadataType { * The options for the linked roles package */ export type LinkedRolesOptions = { - /** - * The client secret for the bot - */ - clientSecret: string - /** - * The base URL of where you are hosting your bot. - * This is used for redirect URLs to and from Discord's OAuth2 flow. - */ - baseUrl: string /** * The metadata that you want to check for, and that should show to the end-user on Discord. */ @@ -63,6 +54,21 @@ export type LinkedRolesOptions = { metadataCheckers: { [name: string]: (userId: string) => Promise } + /** + * Whether the deploy route should be disabled. + * @default false + */ + disableDeployRoute?: boolean + /** + * Whether the connect route should be disabled. + * @default false + */ + disableVerifyUserRoute?: boolean + /** + * Whether the connect callback route should be disabled. + * @default false + */ + disableVerifyUserCallbackRoute?: boolean } /** diff --git a/packages/carbon/typedoc.json b/packages/carbon/typedoc.json index 0c8909be..a8db9b9f 100644 --- a/packages/carbon/typedoc.json +++ b/packages/carbon/typedoc.json @@ -1,7 +1,11 @@ { "$schema": "https://typedoc.org/schema.json", "extends": ["../../typedoc.json"], - "entryPoints": ["src/index.ts", "src/plugins/*/index.ts"], + "entryPoints": [ + "src/index.ts", + "src/plugins/*/index.ts", + "src/adapters/*/index.ts" + ], "name": "@buape/carbon", "out": "docs", "excludeExternals": true, diff --git a/packages/create-carbon/package.json b/packages/create-carbon/package.json index adbcce21..a8bff502 100644 --- a/packages/create-carbon/package.json +++ b/packages/create-carbon/package.json @@ -15,10 +15,10 @@ "LICENSE" ], "dependencies": { - "@buape/carbon": "workspace:^0.5.0", - "@buape/carbon-nodejs": "workspace:^0.2.2", "@clack/prompts": "0.7.0", - "yocto-spinner": "^0.1.0" + "gray-matter": "4.0.3", + "handlebars": "4.7.8", + "yocto-spinner": "0.1.0" }, "devDependencies": { "@types/node": "^20" diff --git a/packages/create-carbon/src/index.ts b/packages/create-carbon/src/index.ts index fdb21741..dc843285 100644 --- a/packages/create-carbon/src/index.ts +++ b/packages/create-carbon/src/index.ts @@ -1,18 +1,12 @@ -import { mkdirSync, writeFileSync } from "node:fs" -import process from "node:process" -import { ClientMode } from "@buape/carbon" import * as p from "@clack/prompts" import yoctoSpinner from "yocto-spinner" -import { type Mode, modes } from "./modes.js" -import { createPackageJson } from "./tools/createPackageJson.js" -import { doesDirectoryExist, processFolder } from "./tools/files.js" -import { runPackageManagerCommand } from "./tools/runManagerCommand.js" -import { getFiles, packageManager, sleep } from "./utils.js" - -import { dirname } from "node:path" -import { fileURLToPath } from "node:url" -import { debug } from "./tools/debug.js" -const __dirname = dirname(fileURLToPath(import.meta.url)) +import { type Runtime, runtimes } from "./runtimes.js" +import { doesDirectoryExist } from "./tools/fileSystem.js" +import { + getPackageManager, + runPackageManagerCommand +} from "./tools/packageManager.js" +import { processTemplate } from "./tools/templateProcessor.js" // ================================================ Intro ================================================ @@ -34,216 +28,55 @@ if (p.isCancel(name)) { process.exit(1) } -const mode = await p.select({ - message: "What mode do you want to use Carbon in?", - options: modes +const runtime = await p.select({ + message: "What runtime do you want to use?", + options: runtimes }) - -if (p.isCancel(mode)) { +if (p.isCancel(runtime)) { p.outro("Cancelled") process.exit(1) } -if (!doesDirectoryExist(`${__dirname}/../../templates/${mode}`)) { - p.outro( - `No template found for ${mode} - This is a bug! Please report it to https://github.com/buape/carbon/issues` - ) - process.exit(1) -} - -// ================================================ Per-Mode Options ================================================ - -const replacers: Record = { - name, - packageManager: packageManager(), - // biome-ignore lint/style/noNonNullAssertion: dates don't just not exist - todaysDate: new Date().toISOString().split("T")[0]! -} - -if (mode === ClientMode.Bun) { - const options = await p.group( - { - port: async () => - ( - await p.text({ - message: "What port do you want to run your bot on?", - placeholder: "3000", - validate: (value) => { - if (!Number.isSafeInteger(Number(value))) - return "Port must be a number!" - if (Number(value) < 1024) return "Port must be greater than 1024!" - if (Number(value) > 65535) return "Port must be less than 65535!" - } - }) - ).toString() - }, - { - onCancel: () => { - p.outro("Cancelled") - process.exit(1) - } - } - ) - replacers.port = options.port -} else if (mode === ClientMode.NodeJS) { - const options = await p.group( - { - port: async () => - ( - await p.text({ - message: "What port do you want to run your bot on?", - placeholder: "3000", - validate: (value) => { - if (!Number.isSafeInteger(Number(value))) - return "Port must be a number!" - if (Number(value) < 1024) return "Port must be greater than 1024!" - if (Number(value) > 65535) return "Port must be less than 65535!" - } - }) - ).toString() - }, - { - onCancel: () => { - p.outro("Cancelled") - process.exit(1) - } - } - ) - replacers.port = options.port -} - const linkedRoles = await p.confirm({ - message: "Would you like to add Linked Roles to your bot?" + message: "Would you like to add linked roles to your app?", + initialValue: false }) - if (p.isCancel(linkedRoles)) { p.outro("Cancelled") process.exit(1) } -if (linkedRoles) { - replacers.linkedRolesImport = `import { LinkedRoles } from "@buape/carbon-linked-roles"\n` - replacers.linkedRolesCfEnv = ` - CLIENT_SECRET: string - BASE_URL: string -` - replacers.linkedRoles = - mode === "cloudflare" - ? ` - const isAllowed = ["439223656200273932"] - new LinkedRoles(client, { - clientSecret: env.CLIENT_SECRET, - baseUrl: env.BASE_URL, - metadata: [ - { - key: "is_staff", - name: "Verified Staff", - description: "Whether the user is a verified staff member", - type: ApplicationRoleConnectionMetadataType.BooleanEqual - }, - ], - metadataCheckers: { - is_allowed: async (userId) => { - if (isAllowed.includes(userId)) return true - return false - } - } - })` - : `const isAllowed = ["439223656200273932"] - -const linkedRoles = new LinkedRoles(client, { - clientSecret: process.env.CLIENT_SECRET, - baseUrl: process.env.BASE_URL, - metadata: [ - { - key: "is_staff", - name: "Verified Staff", - description: "Whether the user is a verified staff member", - type: ApplicationRoleConnectionMetadataType.BooleanEqual - }, - ], - metadataCheckers: { - is_allowed: async (userId) => { - if (isAllowed.includes(userId)) return true - return false - } - } -})` - replacers.linkedRolesEnv = `CLIENT_SECRET=""\nBASE_URL=""` - replacers.linkedRolesReadme = `\n\n## Linked Roles - -Since you added Linked Roles to your bot, make sure that you add the \`CLIENT_SECRET\` and \`BASE_URL\` env variables as well! - -Once you have your LinkedRoles instance, you need to set it on Discord so that users will use it for linked roles. You can see where to add this by clicking here, and set the linked role to /connect, so for example, https://my-carbon-worker.YOURNAME.workers.dev/connect. You'll also need to add a redirect URL to your Discord application, so that users can be redirected to your website after they login. You can go to the OAuth tab on the dashboard and add a redirect URL there of /connect/callback, so for example, https://my-carbon-worker.YOURNAME.workers.dev/connect/callback.\n` -} else { - replacers.linkedRolesImport = "" - replacers.linkedRoles = "" - replacers.linkedRolesEnv = "" - replacers.linkedRolesReadme = "" -} - // ================================================ Create Project ================================================ const spinner = yoctoSpinner({ text: "Creating project..." }) spinner.start() -debug(`Replacers: ${JSON.stringify(replacers, null, 2)}`) - -await sleep(1000) // Adding delay makes the user feel like it's actively working if it runs too fast - -// ================================================ Create folder and package.json ================================================ - -const packageJson = createPackageJson({ replacers, mode }) -const directory = mkdirSync(`${process.cwd()}/${name}`, { recursive: true }) -if (!directory) { - p.outro("Failed to create project folder") - process.exit(1) -} -writeFileSync(`${directory}/package.json`, packageJson) - -// ================================================ Copy in base Template ================================================ - -processFolder("_base", `${__dirname}/../../templates`, directory, replacers) - -// ================================================ Copy in mode template - root files ================================================ - -const templateFolderPath = `${__dirname}/../../templates/${mode}` -const allInTemplateFolder = getFiles(templateFolderPath, "") -if (!allInTemplateFolder) { - p.outro(`Failed to find template folder for ${mode}`) - process.exit(1) -} - -processFolder(mode, `${__dirname}/../../templates`, directory, replacers) - -// ================================================ Copy in mode template - subfolders ================================================ - -const templateFolders = allInTemplateFolder.filter((x) => !x.includes(".")) -for (const templateFolder of templateFolders) { - processFolder( - templateFolder, - templateFolderPath, - `${directory}/${templateFolder}`, - replacers - ) -} +const packageManager = getPackageManager() +await processTemplate({ + name, + runtime, + packageManager, + todaysDate: new Date().toISOString().split("T")[0] ?? "", + plugins: { linkedRoles } +}) -// ================================================ End template copy ================================================ spinner.stop() // ================================================ Install Dependencies ================================================ + const doInstall = await p.confirm({ - message: "Would you like to automatically install dependencies?" + message: `Would you like to automatically install dependencies with ${packageManager}?`, + initialValue: true }) if (p.isCancel(doInstall)) { p.outro("Cancelled") process.exit(1) } + if (doInstall === true) { const depsSpinner = yoctoSpinner({ text: "Installing dependencies..." }) depsSpinner.start() - await runPackageManagerCommand("install", directory) - depsSpinner.stop() + await runPackageManagerCommand("install", name) } // ================================================ Done ================================================ diff --git a/packages/create-carbon/src/modes.ts b/packages/create-carbon/src/runtimes.ts similarity index 69% rename from packages/create-carbon/src/modes.ts rename to packages/create-carbon/src/runtimes.ts index d34600c2..3f82f87a 100644 --- a/packages/create-carbon/src/modes.ts +++ b/packages/create-carbon/src/runtimes.ts @@ -1,20 +1,20 @@ -export const modes = [ +export const runtimes = [ { label: "Cloudflare Workers", value: "cloudflare" }, { - label: "Bun", - value: "bun" + label: "Next.js", + value: "next" }, { label: "Node.js", value: "node" }, { - label: "Next.js", - value: "nextjs" + label: "Bun", + value: "bun" } ] as const satisfies { label: string; value: string }[] -export type Mode = (typeof modes)[number]["value"] +export type Runtime = (typeof runtimes)[number]["value"] diff --git a/packages/create-carbon/src/tools/consoleDebug.ts b/packages/create-carbon/src/tools/consoleDebug.ts new file mode 100644 index 00000000..ae34ccfc --- /dev/null +++ b/packages/create-carbon/src/tools/consoleDebug.ts @@ -0,0 +1,11 @@ +const isTruthy = (value: string | undefined) => + value === "true" || value === "1" || value === "yes" || value === "y" + +/** + * Logs the provided arguments to the console if the DEBUG environment variable is set. + * @param args The arguments to log to the console. + */ +export const debug = (...args: unknown[]) => { + if (isTruthy(process.env.DEBUG) || process.env.NODE_ENV === "development") + console.log(...args) +} diff --git a/packages/create-carbon/src/tools/createPackageJson.ts b/packages/create-carbon/src/tools/createPackageJson.ts deleted file mode 100644 index 3ea5859d..00000000 --- a/packages/create-carbon/src/tools/createPackageJson.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { readFileSync } from "node:fs" - -import { dirname } from "node:path" -import { fileURLToPath } from "node:url" -import type { PackageJson } from "type-fest" -import type { Mode } from "../modes.js" - -export const createPackageJson = ({ - mode, - replacers -}: { - mode: Mode - replacers: Record -}) => { - const packageJson: Record = {} - packageJson.name = replacers.name - packageJson.private = true - switch (mode) { - case "node": - packageJson.main = "./dist/src/index.js" - packageJson.scripts = { - build: "tsc", - dev: "tsc -w", - start: "node ." - } - packageJson.dependencies = { - "@buape/carbon": getVersion("@buape/carbon"), - "@buape/carbon-nodejs": getVersion("@buape/carbon-nodejs") - } - packageJson.devDependencies = { - "@types/node": "latest", - typescript: "^5" - } - break - case "bun": - packageJson.main = "src/index.ts" - packageJson.scripts = { - start: "bun run ." - } - packageJson.dependencies = { - "@buape/carbon": "^0.4.2" //getVersion("@buape/carbon"), - } - packageJson.devDependencies = { - "@types/bun": "latest" - } - break - case "cloudflare": - packageJson.main = "src/index.ts" - packageJson.scripts = { - build: "wrangler deploy --dry-run", - deploy: "wrangler deploy", - dev: "wrangler deploy && wrangler tail" - } - packageJson.dependencies = { - "@buape/carbon": getVersion("@buape/carbon") - } - packageJson.devDependencies = { - "@cloudflare/workers-types": "4.20240909.0", - wrangler: "3.78.4" - } - break - case "nextjs": - packageJson.scripts = { - dev: "next dev --turbo", - build: "next build", - start: "next start" - } - packageJson.dependencies = { - "@buape/carbon": "^0.4.2", - next: "14.2.12", - react: "^18", - "react-dom": "^18" - } - packageJson.devDependencies = { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - typescript: "^5" - } - break - } - - packageJson.carbonMetadata = { - initVersion: getSelfVersion() - } - - return JSON.stringify(packageJson, null, 4) -} - -export const getSelfPackageJson = () => { - const __dirname = dirname(fileURLToPath(import.meta.url)) - const data = JSON.parse( - readFileSync(`${__dirname}/../../../package.json`, "utf-8") - ) as PackageJson - return data -} - -const getVersion = (_pkgName = "@buape/carbon") => { - // short circuit for testing - return "latest" - - // const pkg = getSelfPackageJson() - // return pkg.peerDependencies ? pkg.peerDependencies[pkgName] : "latest" -} - -const getSelfVersion = () => { - const pkg = getSelfPackageJson() - return pkg.version -} diff --git a/packages/create-carbon/src/tools/debug.ts b/packages/create-carbon/src/tools/debug.ts deleted file mode 100644 index 56cb764e..00000000 --- a/packages/create-carbon/src/tools/debug.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const debug = (...args: unknown[]) => { - if (process.env.DEBUG) console.log(...args) -} diff --git a/packages/create-carbon/src/tools/fileSystem.ts b/packages/create-carbon/src/tools/fileSystem.ts new file mode 100644 index 00000000..cf528707 --- /dev/null +++ b/packages/create-carbon/src/tools/fileSystem.ts @@ -0,0 +1,14 @@ +import { statSync } from "node:fs" + +/** + * Check if a directory exists + * @param name The name of the directory to check + */ +export const doesDirectoryExist = (name: string) => { + try { + if (statSync(`${name}`).isDirectory()) return true + return false + } catch (_e) { + return false + } +} diff --git a/packages/create-carbon/src/tools/files.ts b/packages/create-carbon/src/tools/files.ts deleted file mode 100644 index 0eb07bd6..00000000 --- a/packages/create-carbon/src/tools/files.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - mkdirSync, - readFileSync, - rmdirSync, - statSync, - unlinkSync, - writeFileSync -} from "node:fs" -import { getFiles, replacePlaceholders } from "../utils.js" -import { debug } from "./debug.js" - -export const doesDirectoryExist = (name: string) => { - try { - if (statSync(`${name}`).isDirectory()) return true - return false - } catch (_e) { - return false - } -} - -export const writeFile = ( - file: string, - templateFolder: string, - outputDirectory: string, - replacers: Record -) => { - debug(`Copying ${file} to ${outputDirectory} from ${templateFolder}`) - const fileName = file.replace(".template", "") - const template = readFileSync(`${templateFolder}/${file}`, "utf-8") - const data = replacePlaceholders(template, replacers) - writeFileSync(`${outputDirectory}/${fileName}`, data) -} -export const processFolder = ( - folder: string, - root: string, - outputRoot: string, - replacers: Record -) => { - try { - mkdirSync(`${outputRoot}`) - } catch {} - const thisFolderPath = `${root}/${folder}` - const all = getFiles(thisFolderPath, "") - const folders = all.filter((x) => !x.includes(".")) - const templates = all.filter((x) => x.endsWith(".template")) - const appenders = all.filter((x) => x.endsWith(".appender")) - const excludes = all.filter((x) => x.endsWith(".exclude")) - for (const template of templates) { - writeFile(template, thisFolderPath, outputRoot, replacers) - } - for (const appender of appenders) { - appendFile(appender, thisFolderPath, outputRoot, replacers) - } - for (const exclude of excludes) { - excludeFile(exclude, thisFolderPath, outputRoot) - } - for (const folder of folders) { - processFolder(folder, thisFolderPath, `${outputRoot}/${folder}`, replacers) - } -} -export const appendFile = ( - file: string, - templateFolder: string, - outputDirectory: string, - replacers: Record -) => { - debug(`Copying ${file} to ${outputDirectory} from ${templateFolder}`) - const fileName = file.replace(".appender", "") - const original = readFileSync(`${outputDirectory}/${fileName}`, "utf-8") - const template = readFileSync(`${templateFolder}/${file}`, "utf-8") - const newTemplate = `${original}\n${template}` - const data = replacePlaceholders(newTemplate, replacers) - writeFileSync(`${outputDirectory}/${fileName}`, data) -} -export const excludeFile = ( - file: string, - templateFolder: string, - outputDirectory: string -) => { - debug( - `Deleting ${file} from ${outputDirectory} because of exclude in ${templateFolder}` - ) - const fileName = file.replace(".exclude", "") - try { - rmdirSync(`${outputDirectory}/${fileName}`, { recursive: true }) - unlinkSync(`${outputDirectory}/${fileName}`) - } catch { - debug(`Exclude ${outputDirectory}/${fileName} not found`) - } -} diff --git a/packages/create-carbon/src/tools/npmHelpers.ts b/packages/create-carbon/src/tools/npmHelpers.ts new file mode 100644 index 00000000..a9bdc47f --- /dev/null +++ b/packages/create-carbon/src/tools/npmHelpers.ts @@ -0,0 +1,82 @@ +import { exec as execAsync } from "node:child_process" +import { promisify } from "node:util" +const exec = promisify(execAsync) + +/** + * Get the latest version of an npm package + * @param name The name of the package to get the version of + */ +export const getLatestNpmPackageVersion = async (name: string) => { + const response = await exec(`npm show "${name}" dist-tags.latest`) + return response.stdout.trim() +} + +/** + * Get the pinned version of an npm package + * @param name The name of the package to get the version of + * @param dist The pinned version of the package + */ +export const getPinnedNpmPackageVersion = async ( + name: string, + dist: string | number +) => { + if (dist === "workspace") return "workspace:*" + if (typeof dist === "string") return dist + const response = await exec(`npm show "${name}" versions --json`) + const versions = JSON.parse(response.stdout).reverse() as string[] + return versions.find((v) => `${v}.`.startsWith(`${dist}.`)) +} + +/** + * Get the version of an npm package + * @param name The name of the package to get the version of + * @param dist The pinned version of the package (optional) + */ +export const getNpmPackageVersion = async ( + name: string, + dist?: string | number +) => { + if (dist) return getPinnedNpmPackageVersion(name, dist) + return getLatestNpmPackageVersion(name) +} + +const dependencies = { + "@buape/carbon": + process.env.NODE_ENV === "development" ? "workspace" : undefined, + typescript: undefined, + // Node + "@types/node": undefined, + "tsc-watch": undefined, + // Bun + "@types/bun": undefined, + dotenv: undefined, + // Cloudflare + wrangler: undefined, + "@cloudflare/workers-types": undefined, + // Next.js + next: undefined, + react: undefined, + "react-dom": undefined, + "@types/react": undefined, + "@types/react-dom": undefined +} as const satisfies Record +type Dependency = keyof typeof dependencies + +/** + * Get the versions of the dependencies used in the project + * @returns The versions of the dependencies + */ +export const getDependencyVersions = async () => { + const versionPromises = Object.entries(dependencies).map( + async ([pkg, pin]) => { + const version = await getNpmPackageVersion(pkg, pin) + return [pkg, version] as [string, string] + } + ) + + const versions = {} as Record + const resolvedVersions = await Promise.all(versionPromises) + for (const [pkg, version] of resolvedVersions) + versions[pkg as Dependency] = version + return versions +} diff --git a/packages/create-carbon/src/tools/packageManager.ts b/packages/create-carbon/src/tools/packageManager.ts new file mode 100644 index 00000000..5f17c7aa --- /dev/null +++ b/packages/create-carbon/src/tools/packageManager.ts @@ -0,0 +1,35 @@ +import { execSync } from "node:child_process" + +/** + * Get the package manager being used from the user agent or the versions object + * @returns + */ +export const getPackageManager = () => { + const versions = process.versions + if (versions.bun) return "bun" + + const userAgent = process.env.npm_config_user_agent + if (userAgent) { + if (userAgent.startsWith("yarn")) return "yarn" + if (userAgent.startsWith("pnpm")) return "pnpm" + if (userAgent.startsWith("bun")) return "bun" + } + + return "npm" +} + +/** + * Run a package manager command in a directory + * @param command The command to run + * @param directory The directory to run the command in + */ +export const runPackageManagerCommand = async ( + command: string, + directory: string +) => { + const manager = getPackageManager() + execSync( + `cd ${directory} && ${manager} ${command}`, // + { stdio: process.env.DEBUG ? "inherit" : "ignore" } + ) +} diff --git a/packages/create-carbon/src/tools/runManagerCommand.ts b/packages/create-carbon/src/tools/runManagerCommand.ts deleted file mode 100644 index b7e7b2b9..00000000 --- a/packages/create-carbon/src/tools/runManagerCommand.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { execSync } from "node:child_process" -import { packageManager } from "../utils.js" - -export const runPackageManagerCommand = async ( - command: string, - directory: string -) => { - const manager = packageManager() - execSync(`cd ${directory} && ${manager} ${command}`, { - stdio: process.env.DEBUG ? "inherit" : "ignore" - }) -} diff --git a/packages/create-carbon/src/tools/templateProcessor.ts b/packages/create-carbon/src/tools/templateProcessor.ts new file mode 100644 index 00000000..ebda5ba7 --- /dev/null +++ b/packages/create-carbon/src/tools/templateProcessor.ts @@ -0,0 +1,118 @@ +import { + mkdirSync, + readFileSync, + readdirSync, + statSync, + writeFileSync +} from "node:fs" +import { dirname, resolve } from "node:path" +import matter from "gray-matter" +import Handlebars from "handlebars" +import type { Runtime } from "../runtimes.js" +import { debug } from "./consoleDebug.js" +import { getDependencyVersions } from "./npmHelpers.js" + +// a == b +Handlebars.registerHelper( + "eq", // + (a, b) => a === b +) +// a || b || c || d || e || f +Handlebars.registerHelper( + "or", // + (...args) => args.slice(0, -1).some(Boolean) +) +// Needed for accessing object properties with invalid names (e.g. @ / -) +// k ? o[k] : undefined +Handlebars.registerHelper( + "get", // + (o, k) => (k ? o[k] : undefined) +) + +interface TemplateContext { + name: string + runtime: Runtime + packageManager: string + todaysDate: string + plugins: Record<"linkedRoles", boolean> + versions: Record +} + +interface FrontMatter extends Record { + path?: string +} + +/** + * Processes the template using the provided context + * @param context The context to use for the compilation + */ +export const processTemplate = async ( + context: Omit +) => { + const templatePath = resolve(__dirname, "../../template") + debug("Processing template") + debug("Getting dependency versions") + const packageVersions = await getDependencyVersions() + const jointContext = { ...context, versions: packageVersions } + debug("Using context", jointContext) + processFolder(templatePath, jointContext) + debug("Template processed") +} + +/** + * Processes a folder, recursively processing all files and folders within it + * @param path The path of the folder to process + * @param context The context to use for the compilation + */ +const processFolder = (path: string, context: TemplateContext) => { + for (const item of readdirSync(path)) { + const itemPath = resolve(path, item) + const stats = statSync(itemPath) + if (stats.isDirectory()) { + processFolder(itemPath, context) + } else if (stats.isFile()) { + processFile(itemPath, context) + } + } +} + +/** + * Processes a file using Handlebars and writes it to the output directory + * @param path The path of the file to process + * @param context The context to use for the compilation + */ +const processFile = (path: string, context: TemplateContext) => { + const ext = path.split(".").pop() + let result = undefined + if (ext === "hbs") result = compileHbsFile(path, context) + else if (ext === "json") result = compileJsonFile(path, context) + else return debug(`Ignoring file ${path}`) + if (!result.meta.path) return debug(`Skipping file ${path}`) + + const outPath = resolve(process.cwd(), context.name, result.meta.path) + mkdirSync(dirname(outPath), { recursive: true }) + debug(`Writing file to ${outPath}`) + writeFileSync(outPath, result.body) +} + +const compileHbsFile = (path: string, context: TemplateContext) => { + debug(`Compiling Handlebars file ${path}`) + const source = readFileSync(path, "utf-8") + const template = Handlebars.compile(source) + const body = template(context) + const { content, data } = matter(body) + return { + body: content.trim().replace(/\n{3,}/g, "\n\n"), + meta: data as FrontMatter + } +} + +const compileJsonFile = (path: string, context: TemplateContext) => { + debug(`Reading JSON file ${path}`) + const source = readFileSync(path, "utf-8") + const { runtimeEquals, templatePath, outputPath } = JSON.parse(source) + if (runtimeEquals && context.runtime !== runtimeEquals) + return { body: "", meta: {} } + const body = readFileSync(resolve(dirname(path), templatePath)) + return { body, meta: { path: String(outputPath) } } +} diff --git a/packages/create-carbon/src/utils.ts b/packages/create-carbon/src/utils.ts deleted file mode 100644 index 3e31adb5..00000000 --- a/packages/create-carbon/src/utils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { existsSync, mkdirSync, readdirSync } from "node:fs" - -export const packageManager = () => { - const versions = process.versions - if (versions.bun) { - return "bun" - } - const userAgent = process.env.npm_config_user_agent - if (userAgent) { - if (userAgent.startsWith("yarn")) return "yarn" - if (userAgent.startsWith("pnpm")) return "pnpm" - if (userAgent.startsWith("bun")) return "bun" - } - - return "npm" -} - -export const sleep = async (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -/** - * Get all the files in all the subdirectories of a directory. - * @param directory - The directory to get the files from. - * @param fileExtension - The extension to search for. - * @param createDirIfNotFound - Whether or not the parent directory should be created if it doesn't exist. - * @returns The files in the directory. - */ -export const getFiles = ( - directory: string, - fileExtension: string, - createDirIfNotFound = false -): string[] => { - if (createDirIfNotFound && !existsSync(directory)) mkdirSync(directory) - return readdirSync(directory).filter((file) => file.endsWith(fileExtension)) -} - -export const replacePlaceholders = ( - template: string, - data: Record -) => { - let result = template - for (const [key, value] of Object.entries(data)) { - result = result.replace(`{{${key}}}`, value.toString()) - } - return result -} - -export const titleCase = (str: string) => { - return str.replace(/\w\S*/g, (txt) => { - return txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase() - }) -} diff --git a/packages/create-carbon/template/.env.hbs b/packages/create-carbon/template/.env.hbs new file mode 100644 index 00000000..671207fa --- /dev/null +++ b/packages/create-carbon/template/.env.hbs @@ -0,0 +1,17 @@ +--- +{{#if (eq runtime "cloudflare")}} +path: .dev.vars +{{else if (eq runtime "next")}} +path: .env.local +{{else}} +path: .env +{{/if}} +--- + + +BASE_URL= +DEPLOY_SECRET= +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_PUBLIC_KEY= +DISCORD_BOT_TOKEN= \ No newline at end of file diff --git a/packages/create-carbon/template/.gitignore.hbs b/packages/create-carbon/template/.gitignore.hbs new file mode 100644 index 00000000..614978ef --- /dev/null +++ b/packages/create-carbon/template/.gitignore.hbs @@ -0,0 +1,15 @@ +--- +path: .gitignore +--- + +node_modules/ +{{#if (eq runtime "cloudflare")}} +.wrangler/ +.dev.vars +{{else if (eq runtime "next")}} +.next/ +.env.local +{{else}} +dist/ +.env +{{/if}} \ No newline at end of file diff --git a/packages/create-carbon/template/README.md.hbs b/packages/create-carbon/template/README.md.hbs new file mode 100644 index 00000000..ce6ab46e --- /dev/null +++ b/packages/create-carbon/template/README.md.hbs @@ -0,0 +1,11 @@ +--- +path: README.md +--- + +## {{name}} + +This is a [Discord](https://discord.dev) app made with [Carbon](https://carbon.buape.com) and generated with the [`create-carbon`](https://npmjs.com/create-carbon) tool. + +To learn how to get started in development, deploy to production, or add commands, head over to the [documentation](https://carbon.buape.com/adapters/{{runtime}}) for your runtime. + +If you need any assistance, you can join our [Discord](https://go.buape.com/carbon) and ask in the [`#support`](https://discord.com/channels/1280628625904894072/1280630704308486174) channel. diff --git a/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs b/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs new file mode 100644 index 00000000..59331a7e --- /dev/null +++ b/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs @@ -0,0 +1,9 @@ +--- +{{#if (eq runtime "cloudflare")}} +path: wrangler.toml +{{/if}} +--- + +name = "{{name}}" +main = "src/index.ts" +compatibility_date = "{{todaysDate}}" \ No newline at end of file diff --git a/packages/create-carbon/template/_next/carbon-wordmark.png b/packages/create-carbon/template/_next/carbon-wordmark.png new file mode 100644 index 00000000..431715e9 Binary files /dev/null and b/packages/create-carbon/template/_next/carbon-wordmark.png differ diff --git a/packages/create-carbon/template/_next/carbon-wordmark.png.json b/packages/create-carbon/template/_next/carbon-wordmark.png.json new file mode 100644 index 00000000..70727634 --- /dev/null +++ b/packages/create-carbon/template/_next/carbon-wordmark.png.json @@ -0,0 +1,5 @@ +{ + "runtimeEquals": "next", + "templatePath": "carbon-wordmark.png", + "outputPath": "src/app/carbon-wordmark.png" +} diff --git a/packages/create-carbon/template/_next/favicon.ico b/packages/create-carbon/template/_next/favicon.ico new file mode 100644 index 00000000..dd634b64 Binary files /dev/null and b/packages/create-carbon/template/_next/favicon.ico differ diff --git a/packages/create-carbon/template/_next/favicon.ico.json b/packages/create-carbon/template/_next/favicon.ico.json new file mode 100644 index 00000000..5564e399 --- /dev/null +++ b/packages/create-carbon/template/_next/favicon.ico.json @@ -0,0 +1,5 @@ +{ + "runtimeEquals": "next", + "templatePath": "favicon.ico", + "outputPath": "src/app/favicon.ico" +} diff --git a/packages/create-carbon/template/_next/globals.css.hbs b/packages/create-carbon/template/_next/globals.css.hbs new file mode 100644 index 00000000..519dc324 --- /dev/null +++ b/packages/create-carbon/template/_next/globals.css.hbs @@ -0,0 +1,48 @@ +--- +{{#if (eq runtime "next")}} +path: src/app/globals.css +{{/if}} +--- + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/packages/create-carbon/template/_next/layout.tsx.hbs b/packages/create-carbon/template/_next/layout.tsx.hbs new file mode 100644 index 00000000..cd98a304 --- /dev/null +++ b/packages/create-carbon/template/_next/layout.tsx.hbs @@ -0,0 +1,33 @@ +--- +{{#if (eq runtime "next")}} +path: src/app/layout.tsx +{{/if}} +--- + +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", +}); + +export const metadata: Metadata = { + title: "Create Carbon", + description: "Generated by create carbon", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/next-env.d.ts.template b/packages/create-carbon/template/_next/next-env.d.ts.hbs similarity index 77% rename from packages/create-carbon/templates/nextjs/next-env.d.ts.template rename to packages/create-carbon/template/_next/next-env.d.ts.hbs index 40c3d680..c8e39a45 100644 --- a/packages/create-carbon/templates/nextjs/next-env.d.ts.template +++ b/packages/create-carbon/template/_next/next-env.d.ts.hbs @@ -1,3 +1,9 @@ +--- +{{#if (eq runtime "next")}} +path: next-env.d.ts +{{/if}} +--- + /// /// diff --git a/packages/create-carbon/template/_next/next.config.mjs.hbs b/packages/create-carbon/template/_next/next.config.mjs.hbs new file mode 100644 index 00000000..19c7998c --- /dev/null +++ b/packages/create-carbon/template/_next/next.config.mjs.hbs @@ -0,0 +1,10 @@ +--- +{{#if (eq runtime "next")}} +path: next.config.mjs +{{/if}} +--- + +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +export default nextConfig \ No newline at end of file diff --git a/packages/create-carbon/template/_next/page.module.css.hbs b/packages/create-carbon/template/_next/page.module.css.hbs new file mode 100644 index 00000000..06eb83f9 --- /dev/null +++ b/packages/create-carbon/template/_next/page.module.css.hbs @@ -0,0 +1,168 @@ +--- +{{#if (eq runtime "next")}} +path: src/app/page.module.css +{{/if}} +--- + +.page { + --gray-rgb: 0, 0, 0; + --gray-alpha-200: rgba(var(--gray-rgb), 0.08); + --gray-alpha-100: rgba(var(--gray-rgb), 0.05); + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + + display: grid; + grid-template-rows: 20px 1fr 20px; + align-items: center; + justify-items: center; + min-height: 100svh; + padding: 80px; + gap: 64px; + font-family: var(--font-geist-sans); +} + +@media (prefers-color-scheme: dark) { + .page { + --gray-rgb: 255, 255, 255; + --gray-alpha-200: rgba(var(--gray-rgb), 0.145); + --gray-alpha-100: rgba(var(--gray-rgb), 0.06); + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + } +} + +.main { + display: flex; + flex-direction: column; + gap: 32px; + grid-row-start: 2; +} + +.main ol { + font-family: var(--font-geist-mono); + padding-left: 0; + margin: 0; + font-size: 14px; + line-height: 24px; + letter-spacing: -0.01em; + list-style-position: inside; +} + +.main li:not(:last-of-type) { + margin-bottom: 8px; +} + +.main code { + font-family: inherit; + background: var(--gray-alpha-100); + padding: 2px 4px; + border-radius: 4px; + font-weight: 600; +} + +.ctas { + display: flex; + gap: 16px; +} + +.ctas a { + appearance: none; + border-radius: 128px; + height: 48px; + padding: 0 20px; + border: none; + border: 1px solid transparent; + transition: background 0.2s, color 0.2s, border-color 0.2s; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + line-height: 20px; + font-weight: 500; +} + +a.primary { + --foreground: #8298e0; + --background: #ffffff; + background: var(--foreground); + color: var(--background); + gap: 8px; +} + +a.secondary { + border-color: var(--gray-alpha-200); + min-width: 180px; +} + +.footer { + grid-row-start: 3; + display: flex; + gap: 24px; +} + +.footer a { + display: flex; + align-items: center; + gap: 8px; +} + +.footer img { + flex-shrink: 0; +} + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + a.primary:hover { + --button-primary-hover: #8089d9; + background: var(--button-primary-hover); + border-color: transparent; + } + + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; + } + + .footer a:hover { + text-decoration: underline; + text-underline-offset: 4px; + } +} + +@media (max-width: 600px) { + .page { + padding: 32px; + padding-bottom: 80px; + } + + .main { + align-items: center; + } + + .main ol { + text-align: center; + } + + .ctas { + flex-direction: column; + } + + .ctas a { + font-size: 14px; + height: 40px; + padding: 0 16px; + } + + a.secondary { + min-width: auto; + } + + .footer { + flex-wrap: wrap; + align-items: center; + justify-content: center; + } +} diff --git a/packages/create-carbon/template/_next/page.tsx.hbs b/packages/create-carbon/template/_next/page.tsx.hbs new file mode 100644 index 00000000..d6c5518c --- /dev/null +++ b/packages/create-carbon/template/_next/page.tsx.hbs @@ -0,0 +1,95 @@ +--- +{{#if (eq runtime "next")}} +path: src/app/page.tsx +{{/if}} +--- + +import Image from "next/image" +import styles from "./page.module.css" +import CarbonWordmark from "./carbon-wordmark.png" + +export default function Home() { + return ( +
+
+ Carbon logo +
    +
  1. + Set your environment variables in the .env.local file. +
  2. +
  3. + Edit the src/app/api/discord/[...slug]/route.ts file to + add your commands. +
  4. +
  5. + Deploy your application to Vercel or any hosting provider of your + choice. +
  6. +
+ + +
+ + +
+ ) +} diff --git a/packages/create-carbon/templates/_base/src/commands/button.ts.template b/packages/create-carbon/template/button.ts.hbs similarity index 61% rename from packages/create-carbon/templates/_base/src/commands/button.ts.template rename to packages/create-carbon/template/button.ts.hbs index 1b63ed57..d8eee697 100644 --- a/packages/create-carbon/templates/_base/src/commands/button.ts.template +++ b/packages/create-carbon/template/button.ts.hbs @@ -1,10 +1,14 @@ +--- +path: src/commands/button.ts +--- + import { Button, type ButtonInteraction, ButtonStyle, + LinkButton, Command, type CommandInteraction, - LinkButton, Row } from "@buape/carbon" @@ -17,30 +21,30 @@ export default class ButtonCommand extends Command { defer = true // Mount the components that are runnable here - components = [PingButton] + components = [ClickMeButton] // Run the command async run(interaction: CommandInteraction) { await interaction.reply({ content: "Look at this button!", - components: [new Row([new PingButton(), new Link()])] + components: [new Row([new ClickMeButton(), new DocsButton()])] }) } } // Create a button that will respond when you click on it -class PingButton extends Button { - customId = "click-me" - label = "Click me!" - style = ButtonStyle.Primary +class ClickMeButton extends Button { + customId = "click-me" + label = "Click me!" + style = ButtonStyle.Primary - async run(interaction: ButtonInteraction) { - await interaction.reply({ content: "You clicked the button!" }) - } + async run(interaction: ButtonInteraction) { + await interaction.reply("You clicked the button!") + } } // Create a button that will show a link to the Carbon website -class Link extends LinkButton { - label = "Carbon Website" +class DocsButton extends LinkButton { + label = "Carbon Documentation" url = "https://carbon.buape.com" } \ No newline at end of file diff --git a/packages/create-carbon/template/index.ts.hbs b/packages/create-carbon/template/index.ts.hbs new file mode 100644 index 00000000..560a13bf --- /dev/null +++ b/packages/create-carbon/template/index.ts.hbs @@ -0,0 +1,78 @@ +--- +{{#if (eq runtime "next")}} +path: src/app/api/discord/[...slug]/route.ts +{{else}} +path: src/index.ts +{{/if}} +--- + +{{#if (eq runtime "node")}} +import 'dotenv/config'; +{{/if}} +import { Client, createHandle } from "@buape/carbon" +{{#if plugins.linkedRoles}} +import { LinkedRoles, ApplicationRoleConnectionMetadataType } from "@buape/carbon/linked-roles" +{{/if}} +{{#if (eq runtime "node")}} +import { createServer } from "@buape/carbon/adapters/node" +{{else if (eq runtime "bun") }} +import { createServer } from "@buape/carbon/adapters/bun" +{{else if (eq runtime "cloudflare") }} +import { createHandler } from "@buape/carbon/adapters/cloudflare" +{{else if (eq runtime "next") }} +import { createHandler } from "@buape/carbon/adapters/nextjs" +{{/if}} +{{#if (eq runtime "next")}} +import PingCommand from "~/commands/ping" +import ButtonCommand from "~/commands/button" +{{else}} +import PingCommand from "./commands/ping.js" +import ButtonCommand from "./commands/button.js" +{{/if}} + +const handle = createHandle((env) => { + const client = new Client( + { + baseUrl: String(env.BASE_URL), + deploySecret: String(env.DEPLOY_SECRET), + clientId: String(env.DISCORD_CLIENT_ID), + clientSecret: String(env.DISCORD_CLIENT_SECRET), + publicKey: String(env.DISCORD_PUBLIC_KEY), + token: String(env.DISCORD_BOT_TOKEN) + }, + [ + new PingCommand(), + new ButtonCommand() + ] + ) + {{#if plugins.linkedRoles}} + const linkedRoles = new LinkedRoles(client, { + metadata: [ + { + key: "is_staff", + name: "Verified Staff", + description: "Whether the user is a verified staff member", + type: ApplicationRoleConnectionMetadataType.BooleanEqual + }, + ], + metadataCheckers: { + is_staff: async (userId) => { + const isAllowed = ["439223656200273932"] + if (isAllowed.includes(userId)) return true + return false + } + } + }) + {{/if}} + return [client{{#if plugins.linkedRoles}}, linkedRoles{{/if}}] +}) + +{{#if (or (eq runtime "node") (eq runtime "bun"))}} +createServer(handle, { port: 3000 }) +{{else if (eq runtime "cloudflare")}} +const handler = createHandler(handle) +export default { fetch: handler } +{{else if (eq runtime "next")}} +const handler = createHandler(handle) +export { handler as GET, handler as POST } +{{/if}} \ No newline at end of file diff --git a/packages/create-carbon/template/package.json.hbs b/packages/create-carbon/template/package.json.hbs new file mode 100644 index 00000000..c2a406ac --- /dev/null +++ b/packages/create-carbon/template/package.json.hbs @@ -0,0 +1,72 @@ +--- +path: package.json +--- + +{ + "name": "{{name}}", + "private": true, + "type": "module", + {{#if (eq runtime "node")}} + "main": "./dist/index.js", + "scripts": { + "build": "tsc", + "dev": "tsc-watch --onSuccess \"node .\"", + "start": "node ." + }, + "dependencies": { + "@buape/carbon": "{{get versions "@buape/carbon"}}", + "dotenv": "{{get versions "dotenv"}}" + }, + "devDependencies": { + "@types/node": "{{get versions "@types/node"}}", + "tsc-watch": "{{get versions "tsc-watch"}}", + "typescript": "{{get versions "typescript"}}" + } + {{else if (eq runtime "bun")}} + "main": "./src/index.ts", + "scripts": { + "dev": "bun run . --watch", + "start": "bun run ." + }, + "dependencies": { + "@buape/carbon": "{{get versions "@buape/carbon"}}" + }, + "devDependencies": { + "@types/bun": "{{get versions "@types/bun"}}", + "typescript": "{{get versions "typescript"}}" + } + {{else if (eq runtime "cloudflare")}} + "main": "./src/index.ts", + "scripts": { + "dev": "wrangler dev --port 3000", + "deploy": "wrangler deploy", + "wrangler": "wrangler" + }, + "dependencies": { + "@buape/carbon": "{{get versions "@buape/carbon"}}" + }, + "devDependencies": { + "@cloudflare/workers-types": "{{get versions "@cloudflare/workers-types"}}", + "typescript": "{{get versions "typescript"}}", + "wrangler": "{{get versions "wrangler"}}" + } + {{else if (eq runtime "next")}} + "scripts": { + "build": "next build", + "dev": "next dev --turbo", + "start": "next start" + }, + "dependencies": { + "@buape/carbon": "{{get versions "@buape/carbon"}}", + "next": "{{get versions "next"}}", + "react": "{{get versions "react"}}", + "react-dom": "{{get versions "react-dom"}}" + }, + "devDependencies": { + "@types/node": "{{get versions "@types/node"}}", + "@types/react": "{{get versions "@types/react"}}", + "@types/react-dom": "{{get versions "@types/react-dom"}}", + "typescript": "{{get versions "typescript"}}" + } + {{/if}} +} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/src/app/interaction/commands/ping.ts.template b/packages/create-carbon/template/ping.ts.hbs similarity index 58% rename from packages/create-carbon/templates/nextjs/src/app/interaction/commands/ping.ts.template rename to packages/create-carbon/template/ping.ts.hbs index 9f2f8306..8bccd7de 100644 --- a/packages/create-carbon/templates/nextjs/src/app/interaction/commands/ping.ts.template +++ b/packages/create-carbon/template/ping.ts.hbs @@ -1,13 +1,14 @@ +--- +path: src/commands/ping.ts +--- + import { Command, type CommandInteraction } from "@buape/carbon" export default class PingCommand extends Command { name = "ping" - description = "A simple ping command" + description = "Replies with Pong!" async run(interaction: CommandInteraction) { - - return interaction.reply({ - content: "Pong! Hello from {{name}}!" - }) + await interaction.reply("Pong! Hello from {{name}}!") } } \ No newline at end of file diff --git a/packages/create-carbon/template/tsconfig.json.hbs b/packages/create-carbon/template/tsconfig.json.hbs new file mode 100644 index 00000000..fc8b4d67 --- /dev/null +++ b/packages/create-carbon/template/tsconfig.json.hbs @@ -0,0 +1,35 @@ +--- +path: tsconfig.json +--- + +{ + "include": ["src"{{#if (eq runtime "next")}}, "next-env.d.ts", ".next/types/**/*.ts"{{/if}}], + "exclude": ["node_modules"{{#if (eq runtime "next")}}, ".next"{{else}}, "dist"{{/if}}], + "compilerOptions": { + "strict": true, + "skipLibCheck": true, + {{#if (eq runtime "next")}} + "lib": ["DOM", "DOM.Iterable", "ESNext"], + {{else}} + "lib": ["es2022"], + {{/if}} + {{#if (eq runtime "next")}} + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "isolatedModules": true, + "plugins": [{ "name": "next" }], + "jsx": "preserve", + "incremental": true, + "paths": { "~/*": ["src/*"] }, + {{else}} + "module": "NodeNext", + "moduleResolution": "NodeNext", + {{/if}} + "baseUrl": ".", + "outDir": "dist" + } +} \ No newline at end of file diff --git a/packages/create-carbon/templates/_base/.env.template b/packages/create-carbon/templates/_base/.env.template deleted file mode 100644 index ea3bd91f..00000000 --- a/packages/create-carbon/templates/_base/.env.template +++ /dev/null @@ -1,4 +0,0 @@ -CLIENT_ID="" -PUBLIC_KEY="" -DISCORD_TOKEN="" -{{linkedRolesEnv}} \ No newline at end of file diff --git a/packages/create-carbon/templates/_base/README.md.template b/packages/create-carbon/templates/_base/README.md.template deleted file mode 100644 index e1b1a23c..00000000 --- a/packages/create-carbon/templates/_base/README.md.template +++ /dev/null @@ -1,5 +0,0 @@ -# {{name}} - -This is a bot made with [Carbon](https://carbon.buape.com) and generated with the [`create-carbon`](https://github.com/buape/carbon/tree/main/packages/create-carbon) tool. - -If you need any assistance, you can join our [Discord](https://go.buape.com/carbon) and ask in the `#support` channel.{{linkedRolesReadme}} \ No newline at end of file diff --git a/packages/create-carbon/templates/_base/src/commands/ping.ts.template b/packages/create-carbon/templates/_base/src/commands/ping.ts.template deleted file mode 100644 index c346d8c2..00000000 --- a/packages/create-carbon/templates/_base/src/commands/ping.ts.template +++ /dev/null @@ -1,12 +0,0 @@ -import { Command, type CommandInteraction } from "@buape/carbon" - -export default class PingCommand extends Command { - name = "ping" - description = "A simple ping command" - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong! Hello from {{name}}!" - }) - } -} \ No newline at end of file diff --git a/packages/create-carbon/templates/bun/README.md.appender b/packages/create-carbon/templates/bun/README.md.appender deleted file mode 100644 index 776fb8d9..00000000 --- a/packages/create-carbon/templates/bun/README.md.appender +++ /dev/null @@ -1,15 +0,0 @@ -## Running - -To run your bot, there is a `start` script in your `package.json` file. You can run it with the following command: - -```bash -bun run start -``` - -This will start your bot with the environment variables from your `.env` file. - -## What's Next? - -Now that you have your bot running, you can start adding commands to it. You can do this by creating a new file in the `src/commands` folder, and adding a new class that extends the `Command` class. - -For more information on how to create commands, check out the [Carbon documentation](https://carbon.buape.com/carbon/classes/commands). \ No newline at end of file diff --git a/packages/create-carbon/templates/bun/src/index.ts.template b/packages/create-carbon/templates/bun/src/index.ts.template deleted file mode 100644 index 15c62755..00000000 --- a/packages/create-carbon/templates/bun/src/index.ts.template +++ /dev/null @@ -1,25 +0,0 @@ -import { Client, ClientMode } from "@buape/carbon" -import PingCommand from "./commands/ping.js" -import ButtonCommand from "./commands/button.js" -import { serve } from "bun" -{{linkedRolesImport}} - -if (!process.env.CLIENT_ID || !process.env.PUBLIC_KEY || !process.env.DISCORD_TOKEN) { - throw new Error("Missing environment variables"); -} - -const client = new Client( - { - mode: ClientMode.Bun, - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - autoDeploy: true, - port: {{port}} - }, - [new PingCommand(), new ButtonCommand()] -) -{{linkedRoles}} - -const server = serve(client.router); -console.log(`Listening on port ${server.port}`); diff --git a/packages/create-carbon/templates/bun/tsconfig.json.template b/packages/create-carbon/templates/bun/tsconfig.json.template deleted file mode 100644 index 6effca8d..00000000 --- a/packages/create-carbon/templates/bun/tsconfig.json.template +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "outDir": "dist", - "rootDir": ".", - "lib": ["ES2022"] - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/packages/create-carbon/templates/cloudflare/README.md.appender b/packages/create-carbon/templates/cloudflare/README.md.appender deleted file mode 100644 index c5020435..00000000 --- a/packages/create-carbon/templates/cloudflare/README.md.appender +++ /dev/null @@ -1,38 +0,0 @@ -## Setting Environment Variables - -Before you can deploy your bot, you need to set your tokens in your `.env` file. -You can run each of these commands to set your tokens: -```bash -wrangler secret put CLIENT_ID -wrangler secret put PUBLIC_KEY -wrangler secret put DISCORD_TOKEN -``` - -## Running - -To deploy your bot, there are a few scripts in your `package.json` file. - -You can use the `build` script to build your bot for production, but don't actually deploy it, so you can test that everything works. - -```bash -{{packageManager}} run build -``` - -The `deploy` script is used to deploy your bot to Cloudflare. You can run it with the following command: - -```bash -{{packageManager}} run deploy -``` - -You can use the `dev` script to deploy your bot and then watch the logs in real time in your console. -```bash -{{packageManager}} run dev -``` - -On your worker, there is a `/deploy` route that the template set up, you can go to that route in your browser to deploy your commands. - -## What's Next? - -Now that you have your bot running, you can start adding commands to it. You can do this by creating a new file in the `src/commands` folder, and adding a new class that extends the `Command` class. - -For more information on how to create commands, check out the [Carbon documentation](https://carbon.buape.com/carbon/classes/commands). \ No newline at end of file diff --git a/packages/create-carbon/templates/cloudflare/src/index.ts.template b/packages/create-carbon/templates/cloudflare/src/index.ts.template deleted file mode 100644 index 7b8fbae1..00000000 --- a/packages/create-carbon/templates/cloudflare/src/index.ts.template +++ /dev/null @@ -1,33 +0,0 @@ -import { Client, ClientMode } from "@buape/carbon" -import type { ExecutionContext } from "@cloudflare/workers-types" -{{linkedRolesImport}} - -import PingCommand from "./commands/ping.js" -import ButtonCommand from "./commands/button.js" - -type Env = { - CLIENT_ID: string - PUBLIC_KEY: string - DISCORD_TOKEN: string{{linkedRolesCfEnv}} -} - -export default { - async fetch(request: Request, _env: Env, ctx: ExecutionContext) { - const client = new Client( - { - clientId: _env.CLIENT_ID, - publicKey: _env.PUBLIC_KEY, - token: _env.DISCORD_TOKEN, - mode: ClientMode.CloudflareWorkers - }, - [new ButtonCommand(), new PingCommand()] - ) - {{linkedRoles}} - if (request.url.endsWith("/deploy")) { - await client.deployCommands() - return new Response("Deployed commands") - } - const response = await client.router.fetch(request, ctx) - return response - } -} diff --git a/packages/create-carbon/templates/cloudflare/tsconfig.json.template b/packages/create-carbon/templates/cloudflare/tsconfig.json.template deleted file mode 100644 index 1339bfbf..00000000 --- a/packages/create-carbon/templates/cloudflare/tsconfig.json.template +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "outDir": "dist", - "rootDir": ".", - "lib": ["ES2022"], - "types": ["@cloudflare/workers-types"] - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/packages/create-carbon/templates/cloudflare/wrangler.toml.template b/packages/create-carbon/templates/cloudflare/wrangler.toml.template deleted file mode 100644 index 6e8258e3..00000000 --- a/packages/create-carbon/templates/cloudflare/wrangler.toml.template +++ /dev/null @@ -1,3 +0,0 @@ -name = "{{name}}" -main = "src/index.ts" -compatibility_date = "{{todaysDate}}" diff --git a/packages/create-carbon/templates/nextjs/.env.exclude b/packages/create-carbon/templates/nextjs/.env.exclude deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/create-carbon/templates/nextjs/.env.local.template b/packages/create-carbon/templates/nextjs/.env.local.template deleted file mode 100644 index ea3bd91f..00000000 --- a/packages/create-carbon/templates/nextjs/.env.local.template +++ /dev/null @@ -1,4 +0,0 @@ -CLIENT_ID="" -PUBLIC_KEY="" -DISCORD_TOKEN="" -{{linkedRolesEnv}} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/README.md.appender b/packages/create-carbon/templates/nextjs/README.md.appender deleted file mode 100644 index 4b0aa6c0..00000000 --- a/packages/create-carbon/templates/nextjs/README.md.appender +++ /dev/null @@ -1,37 +0,0 @@ -## Project Structure - -The entire bot from Carbon is located in the `src/app/interaction` folder, which means the main route is mounted at `/interaction`. - -## Running - -To run your bot, there are a few scripts in your `package.json` file. - -### Development - -The `dev` script is used to run your site in development mode. You can run it with the following command: - -```bash -{{packageManager}} run dev -``` - -### Production - -To build your bot for production, you can run the `build` script. This will compile your entire site with Next.js. - -```bash -{{packageManager}} run build -``` - -The `start` script is used to run your site in production mode. You can run it with the following command: - -```bash -{{packageManager}} run start -``` - -This will start your bot with the environment variables from your `.env` file. - -## What's Next? - -Now that you have your bot running, you can start adding commands to it. You can do this by creating a new file in the `src/app/interaction/commands` folder, and adding a new class that extends the `Command` class. - -For more information on how to create commands, check out the [Carbon documentation](https://carbon.buape.com/carbon/classes/commands). \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/next.config.mjs.template b/packages/create-carbon/templates/nextjs/next.config.mjs.template deleted file mode 100644 index 1d614782..00000000 --- a/packages/create-carbon/templates/nextjs/next.config.mjs.template +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {} - -export default nextConfig diff --git a/packages/create-carbon/templates/nextjs/src/app/interaction/client.ts.template b/packages/create-carbon/templates/nextjs/src/app/interaction/client.ts.template deleted file mode 100644 index 7ddc1362..00000000 --- a/packages/create-carbon/templates/nextjs/src/app/interaction/client.ts.template +++ /dev/null @@ -1,20 +0,0 @@ -import { Client, ClientMode } from "@buape/carbon" -import PingCommand from "./commands/ping" -import ButtonCommand from "./commands/button" -{{linkedRolesImport}} - -if (!process.env.CLIENT_ID || !process.env.PUBLIC_KEY || !process.env.DISCORD_TOKEN) { - throw new Error("Missing environment variables") -} - -export const client = new Client( - { - mode: ClientMode.Bun, - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - port: 3000 - }, - [new PingCommand(), new ButtonCommand()] -) -{{linkedRoles}} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/src/app/interaction/commands/button.ts.template b/packages/create-carbon/templates/nextjs/src/app/interaction/commands/button.ts.template deleted file mode 100644 index 39e3195c..00000000 --- a/packages/create-carbon/templates/nextjs/src/app/interaction/commands/button.ts.template +++ /dev/null @@ -1,47 +0,0 @@ -import { - Button, - type ButtonInteraction, - ButtonStyle, - Command, - type CommandInteraction, - LinkButton, - Row -} from "@buape/carbon" - -export default class ButtonCommand extends Command { - // Set the name and description of the command - name = "button" - description = "A simple command with a button!" - - // Make the command automatically defer - defer = true - - // Mount the components that are runnable here - components = [PingButton] - - // Run the command - async run(interaction: CommandInteraction) { - await interaction.reply({ - content: "Look at this button!", - components: [new Row([new PingButton(), new Link()])] - }) - } -} - -// Create a button that will respond when you click on it -class PingButton extends Button { - customId = "click-me" - label = "Click me!" - // @ts-expect-error 2416 - style = ButtonStyle.Primary - - async run(interaction: ButtonInteraction) { - await interaction.reply({ content: "You clicked the button!" }) - } -} - -// Create a button that will show a link to the Carbon website -class Link extends LinkButton { - label = "Carbon Website" - url = "https://carbon.buape.com" -} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/src/app/interaction/deploy/route.ts.template b/packages/create-carbon/templates/nextjs/src/app/interaction/deploy/route.ts.template deleted file mode 100644 index 22985ab7..00000000 --- a/packages/create-carbon/templates/nextjs/src/app/interaction/deploy/route.ts.template +++ /dev/null @@ -1,8 +0,0 @@ -import { client } from "../client.ts.template" - -export function GET() { - client.deployCommands() - return new Response(null, { - status: 200 - }) -} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/src/app/interaction/route.ts.template b/packages/create-carbon/templates/nextjs/src/app/interaction/route.ts.template deleted file mode 100644 index 76322fff..00000000 --- a/packages/create-carbon/templates/nextjs/src/app/interaction/route.ts.template +++ /dev/null @@ -1,4 +0,0 @@ -import { client } from "./client" -export async function POST(request: Request) { - return client.handle(request) -} diff --git a/packages/create-carbon/templates/nextjs/src/app/layout.tsx.template b/packages/create-carbon/templates/nextjs/src/app/layout.tsx.template deleted file mode 100644 index e0ed1614..00000000 --- a/packages/create-carbon/templates/nextjs/src/app/layout.tsx.template +++ /dev/null @@ -1,18 +0,0 @@ -import type { Metadata } from "next" - -export const metadata: Metadata = { - title: "Create Carbon NextJS", - description: "Generated by create carbon", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ) -} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/src/app/page.tsx.template b/packages/create-carbon/templates/nextjs/src/app/page.tsx.template deleted file mode 100644 index dd7cd915..00000000 --- a/packages/create-carbon/templates/nextjs/src/app/page.tsx.template +++ /dev/null @@ -1,7 +0,0 @@ -export default function Home() { - return ( -
-
Hello world!
-
- ) -} \ No newline at end of file diff --git a/packages/create-carbon/templates/nextjs/src/commands.exclude b/packages/create-carbon/templates/nextjs/src/commands.exclude deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/create-carbon/templates/nextjs/tsconfig.json.template b/packages/create-carbon/templates/nextjs/tsconfig.json.template deleted file mode 100644 index dcfa651d..00000000 --- a/packages/create-carbon/templates/nextjs/tsconfig.json.template +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/packages/create-carbon/templates/node/README.md.appender b/packages/create-carbon/templates/node/README.md.appender deleted file mode 100644 index 669c0629..00000000 --- a/packages/create-carbon/templates/node/README.md.appender +++ /dev/null @@ -1,33 +0,0 @@ -## Running - -To run your bot, there are a few scripts in your `package.json` file. - -### Development - -The `dev` script is used to run your bot in development mode. You can run it with the following command: - -```bash -{{packageManager}} run dev -``` - -### Production - -To build your bot for production, you can run the `build` script. This will create a `dist` folder with your compiled code. - -```bash -{{packageManager}} run build -``` - -The `start` script is used to run your bot in production mode. You can run it with the following command: - -```bash -{{packageManager}} run start -``` - -This will start your bot with the environment variables from your `.env` file. - -## What's Next? - -Now that you have your bot running, you can start adding commands to it. You can do this by creating a new file in the `src/commands` folder, and adding a new class that extends the `Command` class. - -For more information on how to create commands, check out the [Carbon documentation](https://carbon.buape.com/carbon/classes/commands). \ No newline at end of file diff --git a/packages/create-carbon/templates/node/src/index.ts.template b/packages/create-carbon/templates/node/src/index.ts.template deleted file mode 100644 index b3510c35..00000000 --- a/packages/create-carbon/templates/node/src/index.ts.template +++ /dev/null @@ -1,28 +0,0 @@ -import { dirname } from "node:path" -import { fileURLToPath } from "node:url" -import { Client, ClientMode } from "@buape/carbon" -import { loadCommands, serve } from "@buape/carbon-nodejs" -const __dirname = dirname(fileURLToPath(import.meta.url)) -{{linkedRolesImport}} - -if ( - !process.env.CLIENT_ID || - !process.env.PUBLIC_KEY || - !process.env.DISCORD_TOKEN -) { - throw new Error("Missing environment variables") -} - -const client = new Client( - { - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - mode: ClientMode.NodeJS, - autoDeploy: true - }, - await loadCommands("commands", __dirname) -) -{{linkedRoles}} - -serve(client, { port: {{port}} }) diff --git a/packages/create-carbon/templates/node/tsconfig.json.template b/packages/create-carbon/templates/node/tsconfig.json.template deleted file mode 100644 index 6effca8d..00000000 --- a/packages/create-carbon/templates/node/tsconfig.json.template +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "outDir": "dist", - "rootDir": ".", - "lib": ["ES2022"] - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/packages/nodejs/CHANGELOG.md b/packages/nodejs/CHANGELOG.md deleted file mode 100644 index e4a2a528..00000000 --- a/packages/nodejs/CHANGELOG.md +++ /dev/null @@ -1,106 +0,0 @@ -# @buape/carbon-nodejs - -## 0.2.2 - -### Patch Changes - -- 9543dda: chore(deps): update dependency @hono/node-server to v1.13.0 -- 6451788: chore(deps): update dependency @hono/node-server to v1.13.1 -- ed0501d: chore(deps): update dependency @types/node to v22.5.5 - -## 0.2.1 - -### Patch Changes - -- 6b73575: chore: readme cleanups -- Updated dependencies [6b73575] - - @buape/carbon@0.4.2 - -## 0.2.0 - -### Minor Changes - -- 07b0817: fix: make side packages use peer dependencies - -### Patch Changes - -- Updated dependencies [558b73c] -- Updated dependencies [28f252f] -- Updated dependencies [50e360e] -- Updated dependencies [4475a84] -- Updated dependencies [29f8493] -- Updated dependencies [5bc4b84] -- Updated dependencies [6239590] -- Updated dependencies [0a22fa8] -- Updated dependencies [9456bf0] - - @buape/carbon@0.4.0 - -## 0.1.7 - -### Patch Changes - -- Updated dependencies [dcf5b44] -- Updated dependencies [c8e7c1c] - - @buape/carbon@0.3.2 - -## 0.1.6 - -### Patch Changes - -- Updated dependencies [c80ce0f] - - @buape/carbon@0.3.1 - -## 0.1.5 - -### Patch Changes - -- Updated dependencies [f8b608f] -- Updated dependencies [50c8200] -- Updated dependencies [a15bf55] -- Updated dependencies [51d84c3] - - @buape/carbon@0.3.0 - -## 0.1.4 - -### Patch Changes - -- 0b69408: deps: update dependency @types/node to v22.5.2 -- Updated dependencies [9e93027] -- Updated dependencies [ad9666b] - - @buape/carbon@0.2.0 - -## 0.1.3 - -### Patch Changes - -- Updated dependencies - - @buape/carbon@0.1.3 - -## 0.1.2 - -### Patch Changes - -- Updated dependencies [2bdc7f3] -- Updated dependencies [e79c435] -- Updated dependencies [917416a] - - @buape/carbon@0.1.2 - -## 0.1.1 - -### Patch Changes - -- Updated dependencies [8438b8d] - - @buape/carbon@0.1.1 - -## 0.1.0 - -### Minor Changes - -- 1a00131: Initial beta! - - Featuring interactions, commands, messages, subcommands, options, linked roles, and more! - -### Patch Changes - -- Updated dependencies [1a00131] - - @buape/carbon@0.1.0 diff --git a/packages/nodejs/README.md b/packages/nodejs/README.md deleted file mode 100644 index 339f49c3..00000000 --- a/packages/nodejs/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# @buape/carbon-nodejs - -
-Carbon Wordmark - -Discord -NPM Version -NPM Downloads -
- -This package is part of [the Carbon framework](https://github.com/buape/carbon), check it out for more details. - -You can find the documentation for this package [here](https://carbon.buape.com/carbon/nodejs). \ No newline at end of file diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json deleted file mode 100644 index 51567c27..00000000 --- a/packages/nodejs/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@buape/carbon-nodejs", - "version": "0.2.2", - "type": "module", - "main": "./dist/src/index.js", - "repository": "github:buape/carbon", - "scripts": { - "build": "tsc", - "dev": "tsc -w", - "docs": "typedoc" - }, - "license": "MIT", - "dependencies": { - "@hono/node-server": "1.13.1", - "@types/node": "^20", - "path": "0.12.7" - }, - "peerDependencies": { - "@buape/carbon": "workspace:^0.5.0" - }, - "devDependencies": { - "@buape/carbon": "workspace:*" - }, - "files": [ - "dist", - "LICENSE" - ] -} diff --git a/packages/nodejs/src/CommandFetcher.ts b/packages/nodejs/src/CommandFetcher.ts deleted file mode 100644 index ab6aefcf..00000000 --- a/packages/nodejs/src/CommandFetcher.ts +++ /dev/null @@ -1,64 +0,0 @@ -import path from "node:path" -import type { BaseCommand } from "@buape/carbon" -import { getFiles } from "./utils.js" - -/** - * Load commands from a folder. - * - * This folder should be structured as follows: - * ``` - * commands - * ├── parentFolder - * │ ├── command1.js - * │ └── command2.js - * ├── command3.js - * └── command4.js - * ``` - * - * In this example, `command1.js` and `command2.js` are in a parent folder, while `command3.js` and `command4.js` are in the main folder. - * The parent folder is used to group commands together, but it is not required. If you do not want to group commands, you can put them in the main folder. - * - * To load commands with this example, you would use loadCommands("commands", __dirname). - * - * @param folderPath - The path to the main command folder - * @param dirname - The name of the main directory of your application. If you are using ES Modules, this can be found with the following code: - * ```js - * import { fileURLToPath } from "node:url" - * import { dirname } from "node:path" - * const __dirname = dirname(fileURLToPath(import.meta.url)) - * ``` - * @returns The loaded commands - */ -export const loadCommands = async (folderPath: string, dirname: string) => { - const commands: BaseCommand[] = [] - const mainFolderPath = path.join(dirname, folderPath) - - const parentFolderNames = getFiles(mainFolderPath, "", true) - - const parentFolders = parentFolderNames.filter( - (folder) => !folder.includes(".") - ) - const parentFolderFiles = parentFolderNames.filter((folder) => - folder.endsWith(".js") - ) - - for await (const fileName of parentFolderFiles) { - const filePath = path.join(mainFolderPath, fileName) - const fileUrl = `file://${filePath.replace(/\\/g, "/")}` - const file = await import(fileUrl) - const cmd = new file.default() - commands.push(cmd) - } - - for (const parentFolder of parentFolders) { - const files = getFiles(path.join(mainFolderPath, parentFolder), "js") - for await (const fileName of files) { - const filePath = path.join(mainFolderPath, parentFolder, fileName) - const fileUrl = `file://${filePath.replace(/\\/g, "/")}` - const file = await import(fileUrl) - const cmd = new file.default() - commands.push(cmd) - } - } - return commands -} diff --git a/packages/nodejs/src/index.ts b/packages/nodejs/src/index.ts deleted file mode 100644 index 2fbf4943..00000000 --- a/packages/nodejs/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Client } from "@buape/carbon" -import { serve as honoServe } from "@hono/node-server" - -export const serve = (client: Client, options: { port: number }) => { - console.log(`Serving client on port ${options.port}`) - honoServe({ fetch: client.router.fetch, port: options.port }) -} - -export * from "./CommandFetcher.js" diff --git a/packages/nodejs/src/utils.ts b/packages/nodejs/src/utils.ts deleted file mode 100644 index fc9c0a80..00000000 --- a/packages/nodejs/src/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { existsSync, mkdirSync, readdirSync } from "node:fs" - -/** - * Get all the files in all the subdirectories of a directory. - * @param directory - The directory to get the files from. - * @param fileExtension - The extension to search for. - * @param createDirIfNotFound - Whether or not the parent directory should be created if it doesn't exist. - * @returns The files in the directory. - */ -export const getFiles = ( - directory: string, - fileExtension: string, - createDirIfNotFound = false -): string[] => { - if (createDirIfNotFound && !existsSync(directory)) mkdirSync(directory) - return readdirSync(directory).filter((file) => file.endsWith(fileExtension)) -} diff --git a/packages/nodejs/tests/index.test.ts b/packages/nodejs/tests/index.test.ts deleted file mode 100644 index 5d34346d..00000000 --- a/packages/nodejs/tests/index.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { expect, test } from "vitest" - -test("Dummy test", () => expect(1).toBe(1)) diff --git a/packages/nodejs/tsconfig.json b/packages/nodejs/tsconfig.json deleted file mode 100644 index 458e52de..00000000 --- a/packages/nodejs/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": [ - "src/**/*.ts", - "src/*.ts" - ], - "compilerOptions": { - "outDir": "dist", - "rootDir": "." - } -} \ No newline at end of file diff --git a/packages/nodejs/typedoc.json b/packages/nodejs/typedoc.json deleted file mode 100644 index 03facf60..00000000 --- a/packages/nodejs/typedoc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://typedoc.org/schema.json", - "extends": ["../../typedoc.json"], - "entryPoints": ["src/index.ts"], - "name": "@buape/carbon-nodejs", - "out": "docs" -} diff --git a/packages/request/typedoc.json b/packages/request/typedoc.json deleted file mode 100644 index c0be60b5..00000000 --- a/packages/request/typedoc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://typedoc.org/schema.json", - "extends": ["../../typedoc.json"], - "entryPoints": ["src/index.ts"], - "name": "@buape/carbon-request", - "out": "docs" -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cbfb7be..80b7886d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,7 +23,7 @@ importers: version: 2.27.9 '@turbo/gen': specifier: 2.1.3 - version: 2.1.3(@types/node@22.7.5)(typescript@5.6.2) + version: 2.1.3(@types/node@22.7.6)(typescript@5.6.2) tsc-watch: specifier: 6.2.0 version: 6.2.0(typescript@5.6.2) @@ -50,7 +50,7 @@ importers: version: 5.6.2 vitest: specifier: 2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@22.7.6) apps/cloudo: dependencies: @@ -59,23 +59,33 @@ importers: version: link:../../packages/carbon devDependencies: '@cloudflare/workers-types': - specifier: 4.20241004.0 - version: 4.20241004.0 + specifier: 4.20241011.0 + version: 4.20241011.0 + typescript: + specifier: 5.6.3 + version: 5.6.3 wrangler: - specifier: 3.80.1 - version: 3.80.1(@cloudflare/workers-types@4.20241004.0) + specifier: 3.81.0 + version: 3.81.0(@cloudflare/workers-types@4.20241011.0) apps/rocko: dependencies: '@buape/carbon': specifier: workspace:* version: link:../../packages/carbon - '@buape/carbon-nodejs': - specifier: workspace:* - version: link:../../packages/nodejs + dotenv: + specifier: 16.4.5 + version: 16.4.5 + devDependencies: '@types/node': - specifier: ^20 - version: 20.16.11 + specifier: 22.7.6 + version: 22.7.6 + tsc-watch: + specifier: 6.2.0 + version: 6.2.0(typescript@5.6.3) + typescript: + specifier: 5.6.3 + version: 5.6.3 packages/carbon: dependencies: @@ -88,48 +98,36 @@ importers: discord-api-types: specifier: 0.37.101 version: 0.37.101 - discord-verify: - specifier: 1.2.0 - version: 1.2.0 - itty-router: - specifier: 5.0.18 - version: 5.0.18 + optionalDependencies: + '@cloudflare/workers-types': + specifier: 4.20240919.0 + version: 4.20240919.0 + '@hono/node-server': + specifier: 1.13.1 + version: 1.13.1(hono@4.6.3) + '@types/bun': + specifier: ^1.1.10 + version: 1.1.10 packages/create-carbon: dependencies: - '@buape/carbon': - specifier: workspace:^0.5.0 - version: link:../carbon - '@buape/carbon-nodejs': - specifier: workspace:^0.2.2 - version: link:../nodejs '@clack/prompts': specifier: 0.7.0 version: 0.7.0 + gray-matter: + specifier: 4.0.3 + version: 4.0.3 + handlebars: + specifier: 4.7.8 + version: 4.7.8 yocto-spinner: - specifier: ^0.1.0 + specifier: 0.1.0 version: 0.1.0 devDependencies: '@types/node': specifier: ^20 version: 20.16.11 - packages/nodejs: - dependencies: - '@hono/node-server': - specifier: 1.13.1 - version: 1.13.1(hono@4.6.3) - '@types/node': - specifier: ^20 - version: 20.16.11 - path: - specifier: 0.12.7 - version: 0.12.7 - devDependencies: - '@buape/carbon': - specifier: workspace:* - version: link:../carbon - packages/request: {} website: @@ -145,10 +143,10 @@ importers: version: 10.0.2(fumadocs-core@13.4.10(@types/react@18.3.11)(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) fumadocs-typescript: specifier: 2.1.0 - version: 2.1.0(typescript@5.6.2) + version: 2.1.0(typescript@5.6.3) fumadocs-ui: specifier: 13.4.10 - version: 13.4.10(@types/react-dom@18.3.0)(@types/react@18.3.11)(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2))) + version: 13.4.10(@types/react-dom@18.3.0)(@types/react@18.3.11)(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3))) lucide-react: specifier: 0.447.0 version: 0.447.0(react@18.3.1) @@ -173,7 +171,7 @@ importers: version: 2.0.13 '@types/node': specifier: 22.7.5 - version: 20.16.11 + version: 22.7.5 '@types/react': specifier: 18.3.11 version: 18.3.11 @@ -188,7 +186,7 @@ importers: version: 8.4.47 tailwindcss: specifier: 3.4.13 - version: 3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2)) + version: 3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3)) packages: @@ -324,42 +322,45 @@ packages: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} - '@cloudflare/workerd-darwin-64@1.20240925.0': - resolution: {integrity: sha512-KdLnSXuzB65CbqZPm+qYzk+zkQ1tUNPaaRGYVd/jPYAxwwtfTUQdQ+ahDPwVVs2tmQELKy7ZjQjf2apqSWUfjw==} + '@cloudflare/workerd-darwin-64@1.20241011.1': + resolution: {integrity: sha512-gZ2PrMCQ4WdDCB+V6vsB2U2SyYcmgaGMEa3GGjcUfC79L/8so3Vp/bO0eCoLmvttRs39wascZ+JiWL0HpcZUgA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20240925.0': - resolution: {integrity: sha512-MiQ6uUmCXjsXgWNV+Ock2tp2/tYqNJGzjuaH6jFioeRF+//mz7Tv7J7EczOL4zq+TH8QFOh0/PUsLyazIWVGng==} + '@cloudflare/workerd-darwin-arm64@1.20241011.1': + resolution: {integrity: sha512-c26TYtS0e3WZ09nL/a8YaEqveCsTlgDm12ehPMNua9u68sh1KzETMl2G45O934m8UrI3Rhpv2TTecO0S5b9exA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20240925.0': - resolution: {integrity: sha512-Rjix8jsJMfsInmq3Hm3fmiRQ+rwzuWRPV1pg/OWhMSfNP7Qp2RCU+RGkhgeR9Z5eNAje0Sn2BMrFq4RvF9/yRA==} + '@cloudflare/workerd-linux-64@1.20241011.1': + resolution: {integrity: sha512-pl4xvHNXnm3cYh5GwHadOTQRWt4Ih/gzCOb6RW4n78oNQQydFvpwqYAjbYk32y485feLhdTKXut/MgZAyWnKyQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20240925.0': - resolution: {integrity: sha512-VYIPeMHQRtbwQoIjUwS/zULlywPxyDvo46XkTpIW5MScEChfqHvAYviQ7TzYGx6Q+gmZmN+DUB2KOMx+MEpCxA==} + '@cloudflare/workerd-linux-arm64@1.20241011.1': + resolution: {integrity: sha512-I4HAF2Qe8xgIjAdE53viT2fDdHXkrb3Be0L3eWeeP5SEkOtQ4cHLqsOV7yhUWOJpHiI1XCDcf+wdfn0PB/EngQ==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20240925.0': - resolution: {integrity: sha512-C8peGvaU5R51bIySi1VbyfRgwNSSRknqoFSnSbSBI3uTN3THTB3UnmRKy7GXJDmyjgXuT9Pcs1IgaWNubLtNtw==} + '@cloudflare/workerd-windows-64@1.20241011.1': + resolution: {integrity: sha512-oVr1Cb7NkDpukd7v68FdxOH8vaHRSzHkX9uE/IttHd2yPK6mwOS220nIxK9UMcx5CwZmrgphRwtZwSYVk/lREQ==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-shared@0.5.4': - resolution: {integrity: sha512-PNL/0TjKRdUHa1kwgVdqUNJVZ9ez4kacsi8omz+gv859EvJmsVuGiMAClY2YfJnC9LVKhKCcjqmFgKNXG9/IXA==} + '@cloudflare/workers-shared@0.6.0': + resolution: {integrity: sha512-rfUCvb3hx4AsvdUZsxgk9lmgEnQehqV3jdtXLP/Xr0+P56n11T/0nXNMzmn7Nnv+IJFOV6X9NmFhuMz4sBPw7w==} engines: {node: '>=16.7.0'} - '@cloudflare/workers-types@4.20241004.0': - resolution: {integrity: sha512-3LrPvtecs4umknOF1bTPNLHUG/ZjeSE6PYBQ/tbO7lwaVhjZTaTugiaCny2byrZupBlVNuubQVktcAgMfw0C1A==} + '@cloudflare/workers-types@4.20240919.0': + resolution: {integrity: sha512-DZwTpZVAV+fKTLxo6ntC2zMNRL/UJwvtMKUt/U7ZyJdR+t0qcBUZGx8jLi9gOFWYxkzO3s7slajwkR2hQRPXYQ==} + + '@cloudflare/workers-types@4.20241011.0': + resolution: {integrity: sha512-emwBnuFB/2lS1z6NXAeBqrSL8Xwnr7YpgdLuchOmgu/igqBsLLNPBb4Qmgh3neFWUe9wbzQyx030836YF3c3Xw==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -1541,11 +1542,8 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - '@types/body-parser@1.19.5': - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/bun@1.1.10': + resolution: {integrity: sha512-76KYVSwrHwr9zsnk6oLXOGs9KvyBg3U066GLO4rk6JZk1ypEPGCUDZ5yOiESyIHWs9cx9iC8r01utYN32XdmgA==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1559,21 +1557,12 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/express-serve-static-core@4.19.5': - resolution: {integrity: sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==} - - '@types/express@4.17.21': - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - '@types/inquirer@6.5.0': resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} @@ -1583,9 +1572,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -1598,33 +1584,27 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@20.12.14': + resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} + '@types/node@20.16.11': resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@22.7.6': + resolution: {integrity: sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==} + '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} - '@types/qs@6.9.16': - resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} '@types/react@18.3.11': resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} - '@types/send@0.17.4': - resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} - - '@types/serve-static@1.15.7': - resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} @@ -1637,6 +1617,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -1814,6 +1797,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bun-types@1.1.29: + resolution: {integrity: sha512-En3/TzSPMPyl5UlUB1MHzHpcrZDakTm7mS203eLoX1fBoEa3PW+aSS8GAqVJ7Is/m34Z5ogL+ECniLY0uDaCPw==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1960,8 +1946,8 @@ packages: constant-case@2.0.0: resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} - cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} core-js-pure@3.38.1: @@ -2054,16 +2040,16 @@ packages: discord-api-types@0.37.101: resolution: {integrity: sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==} - discord-verify@1.2.0: - resolution: {integrity: sha512-8qlrMROW8DhpzWWzgNq9kpeLDxKanWa4EDVoj/ASVv2nr+dSr4JPmu2tFSydf3hAGI/OIJTnZyD0JulMYIxx4w==} - engines: {node: '>=16'} - dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dot-case@2.1.1: resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -2410,9 +2396,6 @@ packages: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2528,9 +2511,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - itty-router@5.0.18: - resolution: {integrity: sha512-mK3ReOt4ARAGy0V0J7uHmArG2USN2x0zprZ+u+YgmeRjXTDbaowDy3kPcsmQY6tH+uHhDgpWit9Vqmv/4rTXwA==} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -2844,8 +2824,8 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - miniflare@3.20240925.1: - resolution: {integrity: sha512-odavnAwWLevMWOi/efIdAI9AVlg8C8NfXe2YLoAeG+Fj5BDHPDxCoY7AjZvBj3CJ7bszkoYyhoPEH60X+Vk+7g==} + miniflare@3.20241011.0: + resolution: {integrity: sha512-Mb3U9+QvKgIUl9LgHwBxEz8WajMRYqO5mMHRtO8yHjNCLGh24I6Ts9z13zRAYGPDd1xBQ1o983fHT9S+tn6r+A==} engines: {node: '>=16.13'} hasBin: true @@ -3069,9 +3049,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path@0.12.7: - resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} - pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -3164,10 +3141,6 @@ packages: printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -3744,6 +3717,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -3755,6 +3733,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -3762,8 +3743,8 @@ packages: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} - unenv-nightly@2.0.0-20240919-125358-9a64854: - resolution: {integrity: sha512-XjsgUTrTHR7iw+k/SRTNjh6EQgwpC9voygnoCJo5kh4hKqsSDHUW84MhL9EsHTNfLctvVBHaSw8e2k3R2fKXsQ==} + unenv-nightly@2.0.0-20241009-125958-e8ea22f: + resolution: {integrity: sha512-hRxmKz1iSVRmuFx/vBdPsx7rX4o7Cas9vdjDNeUeWpQTK2LzU3Xy3Jz0zbo7MJX0bpqo/LEFCA+GPwsbl6zKEQ==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3837,9 +3818,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.10.4: - resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -3934,17 +3912,17 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - workerd@1.20240925.0: - resolution: {integrity: sha512-/Jj6+yLwfieZGEt3Kx4+5MoufuC3g/8iFaIh4MPBNGJOGYmdSKXvgCqz09m2+tVCYnysRfbq2zcbVxJRBfOCqQ==} + workerd@1.20241011.1: + resolution: {integrity: sha512-ORobT1XDkE+p+36yk6Szyw68bWuGSmuwIlDnAeUOfnYunb/Txt0jg7ydzfwr4UIsof7AH5F1nqZms5PWLu05yw==} engines: {node: '>=16'} hasBin: true - wrangler@3.80.1: - resolution: {integrity: sha512-5pBZpzrGpfAtIzF3iBXfZZ9DoP8OjwfIUYjYmddfkmoe6fSyY5r/+IFP56fb/biK5q6FV5HxUnonDiuNyJKnug==} + wrangler@3.81.0: + resolution: {integrity: sha512-sa5dhLJAMmYtl/dJWDJ92sdnKj0VUC0DYBfGqbhd5xn7CDdn1oGhICDXtx2E6BNhQ1L+4d9oAcP/oQvOs5gKLA==} engines: {node: '>=16.17.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20240925.0 + '@cloudflare/workers-types': ^4.20241011.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -3999,8 +3977,8 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} - youch@3.3.3: - resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} + youch@3.3.4: + resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} @@ -4213,27 +4191,30 @@ snapshots: dependencies: mime: 3.0.0 - '@cloudflare/workerd-darwin-64@1.20240925.0': + '@cloudflare/workerd-darwin-64@1.20241011.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20240925.0': + '@cloudflare/workerd-darwin-arm64@1.20241011.1': optional: true - '@cloudflare/workerd-linux-64@1.20240925.0': + '@cloudflare/workerd-linux-64@1.20241011.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20240925.0': + '@cloudflare/workerd-linux-arm64@1.20241011.1': optional: true - '@cloudflare/workerd-windows-64@1.20240925.0': + '@cloudflare/workerd-windows-64@1.20241011.1': optional: true - '@cloudflare/workers-shared@0.5.4': + '@cloudflare/workers-shared@0.6.0': dependencies: mime: 3.0.0 zod: 3.23.8 - '@cloudflare/workers-types@4.20241004.0': {} + '@cloudflare/workers-types@4.20240919.0': + optional: true + + '@cloudflare/workers-types@4.20241011.0': {} '@cspotcode/source-map-support@0.8.1': dependencies: @@ -4482,6 +4463,7 @@ snapshots: '@hono/node-server@1.13.1(hono@4.6.3)': dependencies: hono: 4.6.3 + optional: true '@isaacs/cliui@8.0.2': dependencies: @@ -5157,13 +5139,13 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.7.0 - '@tailwindcss/typography@0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2)))': + '@tailwindcss/typography@0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2)) + tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3)) '@tootallnate/quickjs-emscripten@0.23.0': {} @@ -5175,7 +5157,7 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@turbo/gen@2.1.3(@types/node@22.7.5)(typescript@5.6.2)': + '@turbo/gen@2.1.3(@types/node@22.7.6)(typescript@5.6.2)': dependencies: '@turbo/workspaces': 2.1.3 commander: 10.0.1 @@ -5185,7 +5167,7 @@ snapshots: node-plop: 0.26.3 picocolors: 1.0.1 proxy-agent: 6.4.0 - ts-node: 10.9.2(@types/node@22.7.5)(typescript@5.6.2) + ts-node: 10.9.2(@types/node@22.7.6)(typescript@5.6.2) update-check: 1.5.4 validate-npm-package-name: 5.0.1 transitivePeerDependencies: @@ -5214,14 +5196,10 @@ snapshots: dependencies: '@types/estree': 1.0.6 - '@types/body-parser@1.19.5': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.16.11 - - '@types/connect@3.4.38': + '@types/bun@1.1.10': dependencies: - '@types/node': 20.16.11 + bun-types: 1.1.29 + optional: true '@types/debug@4.1.12': dependencies: @@ -5235,31 +5213,15 @@ snapshots: '@types/estree@1.0.6': {} - '@types/express-serve-static-core@4.19.5': - dependencies: - '@types/node': 20.16.11 - '@types/qs': 6.9.16 - '@types/range-parser': 1.2.7 - '@types/send': 0.17.4 - - '@types/express@4.17.21': - dependencies: - '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.5 - '@types/qs': 6.9.16 - '@types/serve-static': 1.15.7 - '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.7.5 + '@types/node': 20.16.11 '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 - '@types/http-errors@2.0.4': {} - '@types/inquirer@6.5.0': dependencies: '@types/through': 0.0.33 @@ -5271,18 +5233,21 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/mime@1.3.5': {} - '@types/minimatch@5.1.2': {} '@types/ms@0.7.34': {} '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 '@types/node@12.20.55': {} + '@types/node@20.12.14': + dependencies: + undici-types: 5.26.5 + optional: true + '@types/node@20.16.11': dependencies: undici-types: 6.19.8 @@ -5291,11 +5256,11 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/prop-types@15.7.13': {} - - '@types/qs@6.9.16': {} + '@types/node@22.7.6': + dependencies: + undici-types: 6.19.8 - '@types/range-parser@1.2.7': {} + '@types/prop-types@15.7.13': {} '@types/react-dom@18.3.0': dependencies: @@ -5306,20 +5271,9 @@ snapshots: '@types/prop-types': 15.7.13 csstype: 3.1.3 - '@types/send@0.17.4': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.16.11 - - '@types/serve-static@1.15.7': - dependencies: - '@types/http-errors': 2.0.4 - '@types/node': 20.16.11 - '@types/send': 0.17.4 - '@types/through@0.0.33': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 '@types/tinycolor2@1.4.6': {} @@ -5327,6 +5281,11 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.5.12': + dependencies: + '@types/node': 20.16.11 + optional: true + '@ungap/structured-clone@1.2.0': {} '@vitest/expect@2.1.2': @@ -5336,13 +5295,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.7(@types/node@22.7.5))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.7(@types/node@22.7.6))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: - vite: 5.4.7(@types/node@22.7.5) + vite: 5.4.7(@types/node@22.7.6) '@vitest/pretty-format@2.1.2': dependencies: @@ -5502,6 +5461,12 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bun-types@1.1.29: + dependencies: + '@types/node': 20.12.14 + '@types/ws': 8.5.12 + optional: true + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -5658,7 +5623,7 @@ snapshots: snake-case: 2.1.0 upper-case: 1.1.3 - cookie@0.5.0: {} + cookie@0.7.2: {} core-js-pure@3.38.1: {} @@ -5739,16 +5704,14 @@ snapshots: discord-api-types@0.37.101: {} - discord-verify@1.2.0: - dependencies: - '@types/express': 4.17.21 - dlv@1.1.3: {} dot-case@2.1.1: dependencies: no-case: 2.3.2 + dotenv@16.4.5: {} + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -6044,18 +6007,18 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-typescript@2.1.0(typescript@5.6.2): + fumadocs-typescript@2.1.0(typescript@5.6.3): dependencies: fast-glob: 3.3.2 hast-util-to-jsx-runtime: 2.3.0 mdast-util-from-markdown: 2.0.1 mdast-util-gfm: 3.0.0 mdast-util-to-hast: 13.2.0 - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - fumadocs-ui@13.4.10(@types/react-dom@18.3.0)(@types/react@18.3.11)(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2))): + fumadocs-ui@13.4.10(@types/react-dom@18.3.0)(@types/react@18.3.11)(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3))): dependencies: '@radix-ui/react-accordion': 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-collapsible': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6064,7 +6027,7 @@ snapshots: '@radix-ui/react-popover': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tailwindcss/typography': 0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2))) + '@tailwindcss/typography': 0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3))) class-variance-authority: 0.7.0 cmdk: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fumadocs-core: 13.4.10(@types/react@18.3.11)(next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -6253,7 +6216,8 @@ snapshots: no-case: 2.3.2 upper-case: 1.1.3 - hono@4.6.3: {} + hono@4.6.3: + optional: true html-void-elements@3.0.0: {} @@ -6294,8 +6258,6 @@ snapshots: once: 1.4.0 wrappy: 1.0.2 - inherits@2.0.3: {} - inherits@2.0.4: {} ini@1.3.8: {} @@ -6412,8 +6374,6 @@ snapshots: isexe@2.0.0: {} - itty-router@5.0.18: {} - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -6985,7 +6945,7 @@ snapshots: mimic-fn@2.1.0: {} - miniflare@3.20240925.1: + miniflare@3.20241011.0: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.12.1 @@ -6995,9 +6955,9 @@ snapshots: glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.4 - workerd: 1.20240925.0 + workerd: 1.20241011.1 ws: 8.18.0 - youch: 3.3.3 + youch: 3.3.4 zod: 3.23.8 transitivePeerDependencies: - bufferutil @@ -7234,11 +7194,6 @@ snapshots: path-type@4.0.0: {} - path@0.12.7: - dependencies: - process: 0.11.10 - util: 0.10.4 - pathe@1.1.2: {} pathval@2.0.0: {} @@ -7277,13 +7232,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.47 - postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2)): + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3)): dependencies: lilconfig: 3.1.2 yaml: 2.5.1 optionalDependencies: postcss: 8.4.47 - ts-node: 10.9.2(@types/node@20.16.11)(typescript@5.6.2) + ts-node: 10.9.2(@types/node@22.7.5)(typescript@5.6.3) postcss-nested@6.2.0(postcss@8.4.47): dependencies: @@ -7318,8 +7273,6 @@ snapshots: printable-characters@1.0.42: {} - process@0.11.10: {} - property-information@6.5.0: {} proxy-agent@6.4.0: @@ -7782,7 +7735,7 @@ snapshots: tailwind-merge@2.5.3: {} - tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2)): + tailwindcss@3.4.13(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -7801,7 +7754,7 @@ snapshots: postcss: 8.4.47 postcss-import: 15.1.0(postcss@8.4.47) postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2)) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3)) postcss-nested: 6.2.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -7857,33 +7810,33 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.2): + ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.16.11 + '@types/node': 22.7.5 acorn: 8.12.1 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.6.2 + typescript: 5.6.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optional: true - ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.2): + ts-node@10.9.2(@types/node@22.7.6)(typescript@5.6.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.7.5 + '@types/node': 22.7.6 acorn: 8.12.1 acorn-walk: 8.3.4 arg: 4.1.3 @@ -7902,6 +7855,14 @@ snapshots: string-argv: 0.3.2 typescript: 5.6.2 + tsc-watch@6.2.0(typescript@5.6.3): + dependencies: + cross-spawn: 7.0.3 + node-cleanup: 2.1.2 + ps-tree: 1.2.0 + string-argv: 0.3.2 + typescript: 5.6.3 + tslib@1.14.1: {} tslib@2.7.0: {} @@ -7961,6 +7922,8 @@ snapshots: typescript@5.6.2: {} + typescript@5.6.3: {} + uc.micro@2.1.0: {} ufo@1.5.4: {} @@ -7968,13 +7931,16 @@ snapshots: uglify-js@3.19.3: optional: true + undici-types@5.26.5: + optional: true + undici-types@6.19.8: {} undici@5.28.4: dependencies: '@fastify/busboy': 2.1.1 - unenv-nightly@2.0.0-20240919-125358-9a64854: + unenv-nightly@2.0.0-20241009-125958-e8ea22f: dependencies: defu: 6.1.4 ohash: 1.1.4 @@ -8060,10 +8026,6 @@ snapshots: util-deprecate@1.0.2: {} - util@0.10.4: - dependencies: - inherits: 2.0.3 - v8-compile-cache-lib@3.0.1: {} validate-npm-package-name@5.0.1: {} @@ -8078,12 +8040,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.2(@types/node@22.7.5): + vite-node@2.1.2(@types/node@22.7.6): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.7(@types/node@22.7.5) + vite: 5.4.7(@types/node@22.7.6) transitivePeerDependencies: - '@types/node' - less @@ -8095,19 +8057,19 @@ snapshots: - supports-color - terser - vite@5.4.7(@types/node@22.7.5): + vite@5.4.7(@types/node@22.7.6): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.22.4 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.6 fsevents: 2.3.3 - vitest@2.1.2(@types/node@22.7.5): + vitest@2.1.2(@types/node@22.7.6): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.7(@types/node@22.7.5)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.7(@types/node@22.7.6)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 @@ -8122,11 +8084,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.7(@types/node@22.7.5) - vite-node: 2.1.2(@types/node@22.7.5) + vite: 5.4.7(@types/node@22.7.6) + vite-node: 2.1.2(@types/node@22.7.6) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.6 transitivePeerDependencies: - less - lightningcss @@ -8157,35 +8119,35 @@ snapshots: wordwrap@1.0.0: {} - workerd@1.20240925.0: + workerd@1.20241011.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240925.0 - '@cloudflare/workerd-darwin-arm64': 1.20240925.0 - '@cloudflare/workerd-linux-64': 1.20240925.0 - '@cloudflare/workerd-linux-arm64': 1.20240925.0 - '@cloudflare/workerd-windows-64': 1.20240925.0 + '@cloudflare/workerd-darwin-64': 1.20241011.1 + '@cloudflare/workerd-darwin-arm64': 1.20241011.1 + '@cloudflare/workerd-linux-64': 1.20241011.1 + '@cloudflare/workerd-linux-arm64': 1.20241011.1 + '@cloudflare/workerd-windows-64': 1.20241011.1 - wrangler@3.80.1(@cloudflare/workers-types@4.20241004.0): + wrangler@3.81.0(@cloudflare/workers-types@4.20241011.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 - '@cloudflare/workers-shared': 0.5.4 + '@cloudflare/workers-shared': 0.6.0 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 chokidar: 3.6.0 esbuild: 0.17.19 - miniflare: 3.20240925.1 + miniflare: 3.20241011.0 nanoid: 3.3.7 path-to-regexp: 6.3.0 resolve: 1.22.8 resolve.exports: 2.0.2 selfsigned: 2.4.1 source-map: 0.6.1 - unenv: unenv-nightly@2.0.0-20240919-125358-9a64854 - workerd: 1.20240925.0 + unenv: unenv-nightly@2.0.0-20241009-125958-e8ea22f + workerd: 1.20241011.1 xxhash-wasm: 1.0.2 optionalDependencies: - '@cloudflare/workers-types': 4.20241004.0 + '@cloudflare/workers-types': 4.20241011.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -8228,9 +8190,9 @@ snapshots: yoctocolors@2.1.1: {} - youch@3.3.3: + youch@3.3.4: dependencies: - cookie: 0.5.0 + cookie: 0.7.2 mustache: 4.2.0 stacktracey: 2.1.8 diff --git a/website/app/[...slug]/page.tsx b/website/app/[...slug]/page.tsx index 2b5aff27..aeafb3cc 100644 --- a/website/app/[...slug]/page.tsx +++ b/website/app/[...slug]/page.tsx @@ -11,7 +11,7 @@ import type { Metadata } from "next" import { notFound } from "next/navigation" import type { ReactElement } from "react" import { utils } from "~/app/source" -import { useMDXComponents } from "~/mdx-components" +import { useMDXComponents } from "~/components/mdx-components" import { docsOptions } from "../layout.config" import { createMetadata } from "../og/[...slug]/metadata" diff --git a/website/app/[...slug]/toggle.tsx b/website/app/[...slug]/toggle.tsx deleted file mode 100644 index 00128fdb..00000000 --- a/website/app/[...slug]/toggle.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { RootToggle } from "fumadocs-ui/components/layout/root-toggle" -import { modes } from "~/modes" - -export function Toggle() { - return ( - ({ - url: `/${mode.param}`, - icon: ( - - ), - title: mode.name, - description: mode.description - }))} - /> - ) -} diff --git a/website/app/layout.config.tsx b/website/app/layout.config.tsx index 327abc3b..f642c787 100644 --- a/website/app/layout.config.tsx +++ b/website/app/layout.config.tsx @@ -1,10 +1,8 @@ -import { RootToggle } from "fumadocs-ui/components/layout/root-toggle" import type { HomeLayoutProps } from "fumadocs-ui/home-layout" import type { DocsLayoutProps } from "fumadocs-ui/layout" -import { BookIcon, Heart, LayoutTemplateIcon } from "lucide-react" +import { Heart, LayoutTemplateIcon } from "lucide-react" import Image from "next/image" import { utils } from "~/app/source" -import { modes } from "~/modes" export const baseOptions: HomeLayoutProps = { githubUrl: "https://github.com/buape/carbon", @@ -30,7 +28,7 @@ export const baseOptions: HomeLayoutProps = { links: [ { text: "Showcase", - url: "/carbon/even-more/powered-by-carbon", + url: "/even-more/powered-by-carbon", icon: }, { @@ -48,25 +46,5 @@ export const docsOptions: DocsLayoutProps = { ...baseOptions.nav, transparentMode: "none", children: undefined - }, - sidebar: { - banner: ( - ({ - url: `/${mode.param}`, - icon: ( - - ), - title: mode.name, - description: mode.description - }))} - /> - ) } } diff --git a/website/app/page.tsx b/website/app/page.tsx deleted file mode 100644 index 11b63103..00000000 --- a/website/app/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation" - -export default function HomePage() { - redirect("/carbon") -} diff --git a/website/components/mdx-components.tsx b/website/components/mdx-components.tsx new file mode 100644 index 00000000..e6fb5992 --- /dev/null +++ b/website/components/mdx-components.tsx @@ -0,0 +1,24 @@ +import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock" +import { ImageZoom } from "fumadocs-ui/components/image-zoom" +import { Tab, Tabs } from "fumadocs-ui/components/tabs" +import defaultComponents from "fumadocs-ui/mdx" +import type { MDXComponents } from "mdx/types" +import { PackageManager } from "./package-manager" + +export function useMDXComponents(): MDXComponents { + return { + ...defaultComponents, + + pre: ({ ref: _ref, ...props }) => ( + +
{props.children}
+
+ ), + + Tabs, + Tab, + ImageZoom, + + PackageManager + } +} diff --git a/website/components/package-manager.tsx b/website/components/package-manager.tsx new file mode 100644 index 00000000..a8492b54 --- /dev/null +++ b/website/components/package-manager.tsx @@ -0,0 +1,72 @@ +import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock" +import { Tab, Tabs } from "fumadocs-ui/components/tabs" + +export namespace PackageManager { + const Managers = // + ["npm", "pnpm", "yarn", "bun"] as const satisfies [string, ...string[]] + const ManagerInstallCommand = { + npm: "npm install", + pnpm: "pnpm install", + yarn: "yarn install", + bun: "bun install" + } + const ManagerAddCommand = { + npm: "npm install", + pnpm: "pnpm add", + yarn: "yarn add", + bun: "bun add" + } + const ManagerRunCommand = { + npm: "npm run", + pnpm: "pnpm run", + yarn: "yarn run", + bun: "bun run" + } + const ManagerExecuteCommand = { + npm: "npx", + pnpm: "pnpm dlx", + yarn: "yarn dlx", + bun: "bunx" + } + + export function Install({ + managers = Managers, + packages = [] + }: { managers?: string[]; packages?: string[] }) { + const map = packages.length ? ManagerAddCommand : ManagerInstallCommand + + return ( + + {managers.map((m) => ( + + +
{[map[m as never], ...packages].join(" ")}
+
+
+ ))} +
+ ) + } + + export function Run({ + managers = Managers, + scripts, + executor = false + }: { managers?: string[]; scripts: string[]; executor?: boolean }) { + const map = executor ? ManagerExecuteCommand : ManagerRunCommand + + return ( + + {managers.map((m) => ( + + +
+								{scripts.map((s) => `${map[m as never]} ${s}`).join("\n")}
+							
+
+
+ ))} +
+ ) + } +} diff --git a/website/content/adapters/bun.mdx b/website/content/adapters/bun.mdx new file mode 100644 index 00000000..3aabc828 --- /dev/null +++ b/website/content/adapters/bun.mdx @@ -0,0 +1,149 @@ +--- +title: Bun +description: Learn how to set up and deploy your Carbon bot using the Bun runtime, including development and production environments. +icon: Dessert +--- + +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Workflow, Server } from "lucide-react"; + +## Setup + + + } + title="Automatic Setup" + href="/getting-started/basic-setup#automatic-setup" + description="Quickly set up your Carbon bot with minimal configuration using the automatic setup guide." + /> + } + title="Other Runtimes" + href="/adapters" + description="Explore how to set up and deploy your Carbon bot using different runtimes." + /> + + +### Manual Setup + +This is a continuation of the [Basic Setup](/getting-started/basic-setup) guide. If you haven't already, make sure to follow the steps in the guide before proceeding. + + +{/* Sync the step count with basic setup page */} +
+
+
+
+
+ + +### Create a Server + +Using the `@buape/carbon/adapters/bun` package, you can create a server to host your bot. This server will handle incoming interactions and route them to your bot. + +```ts +import { createServer } from '@buape/carbon/adapters/bun' + +const handle = createHandle( ... ) + +createServer(handle, { port: 3000 }) +``` + + + + +## Running in Development + + + +### Set Environment Variables + +First things first, you'll need to grab your Discord application's secrets from the [Developer Portal](https://discord.com/developers/applications) and paste them in your `.env` file. + + + + +### Set Up a Proxy + +Discord requires a public URL to route interactions to your project. To achieve this, you'll need to set up a proxy. The simplest way to do this is by using [`localtunnel`](https://www.npmjs.com/package/localtunnel). Once you have the public URL, set it as `BASE_URL=""` in your `.env` file. + + + + +You can use the `--subdomain` flag to specify a custom subdomain for your proxy. + + + + +### Configure Portal URLs + +Now that you have a public URL, navigate back to the [Discord Developer Portal](https://discord.com/developers/applications) and set the "Interactions Endpoint URL" to `/interactions`. + + + + +### Invite your App + +You'll need to invite your app to your server to interact with it. To do so, navigate to the Installation tab of your app in the [Discord Developer Portal](https://discord.com/developers/applications). + + + + +### Run the Bot + +You can now run your bot using: + + + + + +### Deploy Your Commands to Discord + +Finally, to deploy your commands to Discord, navigate to `/deploy?secret=` in your browser. This will send your command data to Discord to register them with your bot. + + + +## Deploying to Production + + + +### Prepare a Server + +Since you're deploying a Bun project, you'll need a server to host your bot. You'll also need to set up [Bun](https://bun.sh/) and [Git](https://git-scm.com/) on your server. + + + + +### Initialize a Git Repository + +You'll need to transfer your project files to your server, this can be done using [Git](https://git-scm.com/) and [GitHub](https://github.com/). To do this, initialize a new Git repository, commit your files and push them to GitHub. On your server you can then clone your repository. + + +Make sure not to commit your `.env` file or any secrets to your repository. + + + + +### Prepare Environment + +Almost there! Now just like in development, you'll need to set up a proxy, configure your environment variables and set up your Discord app's URLs. Additionally, you'll need to install your dependencies using: + + + + + +### Start the Bot + +Now that you've prepared your environment, you can finally build and start your bot using: + + + +You may also want to set up a process manager like [PM2](https://npmjs.com/package/pm2) to keep your bot running in the background. + + + + +Reminder to deploy your commands to Discord using `/deploy?secret=`. + + + diff --git a/website/content/adapters/cloudflare.mdx b/website/content/adapters/cloudflare.mdx new file mode 100644 index 00000000..20bb46f1 --- /dev/null +++ b/website/content/adapters/cloudflare.mdx @@ -0,0 +1,144 @@ +--- +title: Cloudflare Workers +description: Learn how to set up and deploy your Carbon bot using Cloudflare Workers, including development and production environments. +icon: Cloud +--- + +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Workflow, Server } from "lucide-react"; + +## Setup + + + } + title="Automatic Setup" + href="/getting-started/basic-setup#automatic-setup" + description="Quickly set up your Carbon bot with minimal configuration using the automatic setup guide." + /> + } + title="Other Runtimes" + href="/adapters" + description="Explore how to set up and deploy your Carbon bot using different runtimes." + /> + + +### Manual Setup + +This is a continuation of the [Basic Setup](/getting-started/basic-setup) guide. If you haven't already, make sure to follow the steps in the guide before proceeding. + + +{/* Sync the step count with basic setup page */} +
+
+
+
+
+ + +### Create a Handler + +Using the `@buape/carbon/adapters/cloudflare` package, you can create a handler that you can then export for Cloudflare Workers. This server will handle incoming interactions and route them to your bot. + +```ts +import { createHandler } from '@buape/carbon/adapters/cloudflare' + +const handle = createHandle( ... ) + +const handler = createHandler(handle) +export default { fetch: handler } +``` + + + + +## Running in Development + + + +### Set Environment Variables + +First things first, you'll need to grab your Discord application's secrets from the [Developer Portal](https://discord.com/developers/applications) and paste them in your `.dev.vars` file. + + + + +### Start a Proxy + +Discord requires a public URL to route interactions to your project. To achieve this, you'll need to set up a proxy. The simplest way to do this is by using [`localtunnel`](https://www.npmjs.com/package/localtunnel). Once you have the public URL, set it as `BASE_URL=""` in your `.dev.vars` file. + + + + +You can use the `--subdomain` flag to specify a custom subdomain for your proxy. + + + + +### Configure Portal URLs + +Now that you have a public URL, navigate back to the [Discord Developer Portal](https://discord.com/developers/applications) and set the "Interactions Endpoint URL" to `/interactions`. + + + + +### Invite your App + +You'll need to invite your app to your server to interact with it. To do so, navigate to the Installation tab of your app in the [Discord Developer Portal](https://discord.com/developers/applications). + + + + +### Run the Bot + +You can now run your bot using: + + + + + +### Deploy Your Commands to Discord + +Finally, to deploy your commands to Discord, navigate to `/deploy?secret=` in your browser. This will send your command data to Discord to register them with your bot. + + + + +## Deploying to Production + + + +### Prepare Environment + +Before deploying your bot, you'll need to set your environment variables. This can be done using the Wrangler CLI. + +..workers.dev", + "wrangler secret put DISCORD_PUBLIC_KEY", + "wrangler secret put DISCORD_CLIENT_ID", + "wrangler secret put DISCORD_CLIENT_SECRET", + "wrangler secret put DISCORD_BOT_TOKEN", + ]} +/> + + +Remember to [configure your portal URLs](#configuring-portal-urls) to the URL of your Cloudflare Worker. + + + + +### Deploy to Cloudflare + +Once you've set your environment variables, you can deploy your bot with the following command: + + + + + +Remember to deploy your commands to Discord using `/deploy?secret=`. + + + diff --git a/website/content/adapters/index.mdx b/website/content/adapters/index.mdx new file mode 100644 index 00000000..25d642a2 --- /dev/null +++ b/website/content/adapters/index.mdx @@ -0,0 +1,66 @@ +--- +title: Adapters +description: Explore the different runtimes supported by Carbon, including serverless options like Cloudflare Workers and traditional server environments like Node.js and Bun. +--- + +import { Cloud, PanelsTopLeft, Hexagon, Dessert } from "lucide-react"; + +Carbon is designed to be flexible and adaptable to different server environments. Whether you prefer a serverless runtime like Cloudflare Workers, a traditional server environment like Node.js, or a lightweight alternative like Bun, Carbon functions the same across all runtimes. This means you can choose the runtime that best fits your needs without worrying about compatibility. Below, we compare serverless and traditional server environments to help you make an informed decision: + +- **Serverless**: Serverless runtimes are a great choice if you want a bot that can scale automatically and handle a large number of interactions with low latency. Keep in mind that serverless runtimes may have limitations on execution time and memory usage, so make sure to choose a runtime that fits your bot's requirements. + +- **Server**: Traditional server environments are a great choice if you want full control over your server environment and the ability to deploy your bot on any server. Keep in mind that you will need to manage your server environment, including scaling, monitoring, and maintenance. + +## Serverless Runtimes + +### Cloudflare Workers + +Cloudflare Workers is a quick and free option for hosting your bot, and is the primary runtime that we at Buape use Carbon on. It provides a scalable and globally distributed platform, ideal for handling a large number of interactions with low latency. + +### Next.js + +Next.js allows you to integrate your bot with your Next.js app, running Carbon on an API route, and can be deployed on Vercel. This is perfect for managing both your bot and web app in a single project. + + + } + title="Cloudflare Workers" + href="/adapters/cloudflare" + description="Deploy your Carbon bot using Cloudflare Workers for a scalable, serverless environment." + /> + } + title="Next.js" + href="/adapters/next" + description="Integrate your Carbon bot with a Next.js application for seamless ingration with your website." + /> + + +## Servers + +### Node.js + +Node.js is a flexible choice for running your bot on any server. It supports various hosting providers and offers full control over your server environment. + +### Bun + +Bun is a fast and lightweight alternative to Node.js, designed for improved performance and lower resource usage. It is ideal for developers prioritizing performance. + + + } + title="Node.js" + href="/adapters/node" + description="Deploy your Carbon bot using Node.js for a flexible and robust server environment." + /> + } + title="Bun" + href="/adapters/bun" + description="Run your Carbon bot with Bun for a fast and lightweight alternative to Node.js." + /> + + +## Other Runtimes + +If your preferred runtime is not listed, you can use the `handle` method to integrate Carbon with any server environment. diff --git a/website/content/adapters/meta.json b/website/content/adapters/meta.json new file mode 100644 index 00000000..63d5cbfe --- /dev/null +++ b/website/content/adapters/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Adapters", + "icon": "Cable", + "pages": [ + "--- Serverless ---", + "cloudflare", + "next", + "--- Server ---", + "node", + "bun" + ] +} diff --git a/website/content/adapters/next.mdx b/website/content/adapters/next.mdx new file mode 100644 index 00000000..6d6ab425 --- /dev/null +++ b/website/content/adapters/next.mdx @@ -0,0 +1,117 @@ +--- +title: Next.js +description: Learn how to set up and deploy your Carbon bot using Next.js, including development and production environments. +icon: PanelsTopLeft +--- + +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Workflow, Server } from "lucide-react"; + +## Setup + + + } + title="Automatic Setup" + href="/getting-started/basic-setup#automatic-setup" + description="Quickly set up your Carbon bot with minimal configuration using the automatic setup guide." + /> + } + title="Other Runtimes" + href="/adapters" + description="Explore how to set up and deploy your Carbon bot using different runtimes." + /> + + +### Manual Setup + +This is a continuation of the [Basic Setup](/getting-started/basic-setup) guide. If you haven't already, make sure to follow the steps in the guide before proceeding. + + +{/* Sync the step count with basic setup page */} +
+
+
+
+
+ + +### Create a Handler + +Using the `@buape/carbon/adapters/next` package, you can create a handler that you can then export for Next.js API routes. This server will handle incoming interactions and route them to your bot. + +```ts +import { createHandler } from '@buape/carbon/adapters/next' + +const handle = createHandle( ... ) + +const handler = createHandler(handle) +export { handler as GET, handler as POST } +``` + + + + +## Running in Development + + + +### Set Environment Variables + +First things first, you'll need to grab your Discord application's secrets from the [Developer Portal](https://discord.com/developers/applications) and paste them in your `.env.local` file. + + + + +### Start a Proxy + +Discord requires a public URL to route interactions to your project. To achieve this, you'll need to set up a proxy. The simplest way to do this is by using [`localtunnel`](https://www.npmjs.com/package/localtunnel). Once you have the public URL, set it as `BASE_URL="/api/discord"` in your `.env.local` file. + + + + +You can use the `--subdomain` flag to specify a custom subdomain for your proxy. + + + + +### Configure Portal URLs + +Now that you have a public URL, navigate back to the [Discord Developer Portal](https://discord.com/developers/applications) and set the "Interactions Endpoint URL" to `/interactions`. + + +`` refers to the public URL plus the relative path to your Next.js API routes, likely `/api/discord`. + + + + + +### Invite your App + +You'll need to invite your app to your server to interact with it. To do so, navigate to the Installation tab of your app in the [Discord Developer Portal](https://discord.com/developers/applications). + + + + +### Run the Bot + +You can now run your bot using: + + + + + +### Deploy Your Commands to Discord + +Finally, to deploy your commands to Discord, navigate to `/deploy?secret=` in your browser. This will send your command data to Discord to register them with your bot. + + + +## Deploying to Production + +Your new app can be deployed anywhere you can run a Next.js app. For detailed instructions, refer to the [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying). Once you do have a deployment, remember to [configure your portal URLs](#configuring-portal-urls) to the URL of your running app. + + +Remember to deploy your commands to Discord using `/deploy?secret=`. + \ No newline at end of file diff --git a/website/content/adapters/node.mdx b/website/content/adapters/node.mdx new file mode 100644 index 00000000..f3f479cc --- /dev/null +++ b/website/content/adapters/node.mdx @@ -0,0 +1,151 @@ +--- +title: Node.js +description: Learn how to set up and deploy your Carbon bot using Node.js, including development and production environments. +icon: Hexagon +--- + +import { Step, Steps } from "fumadocs-ui/components/steps"; +import { Workflow, Server } from "lucide-react"; + +## Setup + + + } + title="Automatic Setup" + href="/getting-started/basic-setup#automatic-setup" + description="Quickly set up your Carbon bot with minimal configuration using the automatic setup guide." + /> + } + title="Other Runtimes" + href="/adapters" + description="Explore how to set up and deploy your Carbon bot using different runtimes." + /> + + +### Manual Setup + +This is a continuation of the [Basic Setup](/getting-started/basic-setup) guide. If you haven't already, make sure to follow the steps in the guide before proceeding. + + +{/* Sync the step count with basic setup page */} +
+
+
+
+
+ + +### Create a Server + +Using the `@buape/carbon/adapters/node` package, you can create a server to host your bot. This server will handle incoming interactions and route them to your bot. + +```ts +import { createServer } from '@buape/carbon/adapters/node' + +const handle = createHandle( ... ) + +createServer(handle, { port: 3000 }) +``` + + + + +## Running in Development + + + + +### Set Environment Variables + +First things first, you'll need to grab your Discord application's secrets from the [Developer Portal](https://discord.com/developers/applications) and paste them in your `.env` file. + + + + +### Start a Proxy + +Discord requires a public URL to route interactions to your project. To achieve this, you'll need to set up a proxy. The simplest way to do this is by using [`localtunnel`](https://www.npmjs.com/package/localtunnel). Once you have the public URL, set it as `BASE_URL=""` in your `.env` file. + + + + +You can use the `--subdomain` flag to specify a custom subdomain for your proxy. + + + + +### Configure Portal URLs + +Now that you have a public URL, navigate back to the [Discord Developer Portal](https://discord.com/developers/applications) and set the "Interactions Endpoint URL" to `/interactions`. + + + + +### Invite your App + +You'll need to invite your app to your server to interact with it. To do so, navigate to the Installation tab of your app in the [Discord Developer Portal](https://discord.com/developers/applications). + + + + +### Run the Bot + +You can now run your bot using: + + + + + +### Deploy Your Commands to Discord + +Finally, to deploy your commands to Discord, navigate to `/deploy?secret=` in your browser. This will send your command data to Discord to register them with your bot. + + + + +## Deploying to Production + + + +### Prepare a Server + +Since you're deploying a Node.js project, you'll need a server to host your bot. You'll also need to set up [Node.js](https://nodejs.org/) and [Git](https://git-scm.com/) on your server. + + + + +### Initialize a Git Repository + +You'll need to transfer your project files to your server, this can be done using [Git](https://git-scm.com/) and [GitHub](https://github.com/). To do this, initialize a new Git repository, commit your files and push them to GitHub. On your server you can then clone your repository. + + +Make sure not to commit your `.env` file or any secrets to your repository. + + + + +### Prepare Environment + +Almost there! Now just like in development, you'll need to set up a proxy, configure your environment variables and set up your Discord app's URLs. Additionally you'll need to install your dependencies using: + + + + + +### Build and Start the Bot + +Now that you've prepared your environment, you can finally build and start your bot using: + + + +You may also want to set up a process manager like [PM2](https://npmjs.com/package/pm2) to keep your bot running in the background. + + + + +Reminder to deploy your commands to Discord using `/deploy?secret=`. + + + diff --git a/website/content/carbon/classes/client.mdx b/website/content/carbon/classes/client.mdx deleted file mode 100644 index 42b47e04..00000000 --- a/website/content/carbon/classes/client.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Client -description: The main class that is used to use Carbon ---- - -The main class that is used to use Carbon is the [`Client`](/carbon/api/classes/Client) class. Everything all connects to this one class, and it is the main entry point for your bot. - -## Creating a Client - -To create a client, you need to provide it with your client's options, and a list of commands. - -```ts title="src/index.ts" -const client = new Client({ - clientId: "12345678901234567890", - publicKey: "c1a2f941ae8ce6d776f7704d0bb3d46b863e21fda491cdb2bdba6b8bc5fe7269", - token: "MTA4NjEwNTYxMDUxMDE1NTg1Nw.GNt-U8.OSHy-g-5FlfESnu3Z9MEEMJLHiRthXajiXNwiE", - mode: ClientMode.NodeJS -}, [new PingCommand()]) -``` - -Here we have created a client with the following options: - -- `clientId`: The client ID of your bot -- `publicKey`: The public key of your bot -- `token`: The token of your bot -- `mode`: The mode of your bot, which is `ClientMode.NodeJS` in this case - -And we have also provided it with a list of commands, which in this case is just the `PingCommand` we created earlier. - -See the [Getting Started](/carbon/getting-started) guide for more information on how to create a client specific to your environment. \ No newline at end of file diff --git a/website/content/carbon/classes/components/meta.json b/website/content/carbon/classes/components/meta.json deleted file mode 100644 index 754e04d2..00000000 --- a/website/content/carbon/classes/components/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Components", - "description": "Components are used to create interactive elements in your bot, such as buttons, select menus, and text inputs." -} diff --git a/website/content/carbon/classes/embeds.mdx b/website/content/carbon/classes/embeds.mdx deleted file mode 100644 index daf2a11d..00000000 --- a/website/content/carbon/classes/embeds.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Embeds -description: Embeds are used to display rich content in messages ---- - -Embeds are used to display rich content in messages. They are created by extending the [`Embed`](/carbon/api/classes/Embed) class, and adding a `title`, `description`, and `url` property. - -```ts title="src/components/embed.ts" -class Embed extends Embed { - title = "Embed Test" - description = "This is an embed test" - url = "https://buape.com" -} -``` - -Here we have created an embed with a title, description, and URL. You can use this embed in a message by using the [`Embed`](/carbon/api/classes/Embed) class. - -## Example Embed -Here is an example embed with all the properties you can use. -```ts -class E extends Embed { - title = "Embed Test" - description = "This is an embed test" - url = "https://buape.com" - timestamp = new Date().toString() - color = 16711680 - footer = { - text: "Footer Text", - icon: "https://cdn.buape.com/CarbonWordmark.png" - } - image = "https://cdn.buape.com/CarbonWordmark.png" - thumbnail = "https://cdn.buape.com/CarbonWordmark.png" - author = { - name: "Author Name", - icon: "https://buape.com" - } - fields = [ - { - name: "Field 1", - value: "Value 1", - inline: true - }, - { - name: "Field 2", - value: "Value 2", - inline: true - } - ] -} -``` - diff --git a/website/content/carbon/classes/modals.mdx b/website/content/carbon/classes/modals.mdx deleted file mode 100644 index 7ad6b0b9..00000000 --- a/website/content/carbon/classes/modals.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Modals -description: Modals are popup forms that can be used to collect user input ---- - -Modals are popup forms that can be used to collect user input. They are created by extending the [`Modal`](/carbon/api/classes/Modal) class, and adding a `title` and `components` property. All the components must be [`TextInput`](/carbon/classes/TextInput) classes. - -```ts title="src/commands/modal.ts" -class ModalCommand extends Modal { - title = "Test Modal" - customId = "test-modal" - - components = [ - new Row([new TextInputHi()]), - new Row([new TextInputAge()]) - ] - - run(interaction: ModalInteraction) { - return interaction.reply({ - content: `Hi ${interaction.fields.getText("name")}, you are ${interaction.fields.getText("age")} years old, and your favorite color is ${interaction.fields.getText("color")}. You are ${interaction.fields.getText("height") || "not"} tall.` - }) - } -} -class TextInputHi extends TextInput { - label = "Tell me about your life" - customId = "life" - style = TextInputStyle.Paragraph -} - -class TextInputAge extends TextInput { - label = "How old are you?" - customId = "age" - style = TextInputStyle.Short -} -``` - diff --git a/website/content/carbon/even-more/index.mdx b/website/content/carbon/even-more/index.mdx deleted file mode 100644 index 7eced4fb..00000000 --- a/website/content/carbon/even-more/index.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Even More -index: true -icon: Heart ---- \ No newline at end of file diff --git a/website/content/carbon/even-more/meta.json b/website/content/carbon/even-more/meta.json deleted file mode 100644 index 89a27337..00000000 --- a/website/content/carbon/even-more/meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Even More" -} diff --git a/website/content/carbon/even-more/powered-by-carbon.mdx b/website/content/carbon/even-more/powered-by-carbon.mdx deleted file mode 100644 index 78fb50ff..00000000 --- a/website/content/carbon/even-more/powered-by-carbon.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Powered by Carbon -description: See some cool apps powered by Carbon ---- - -Here are some cool projects that are powered by Carbon that you can check out! - -- [Evaluate](https://evaluate.run/products/discord-bot) - Run code snippets directly in your Discord server with the Evaluate bot! Evaluate supports over 70 languages and is perfect for quickly testing code and sharing results with your friends. - -- [DistractionAction](https://discord.com/oauth2/authorize?client_id=1213517500516667412) - Are you sick and tired of procrastinating on Discord? Use DistractionAction to take charge of your life! Mute yourself from any server you want, to stay on track and get your work done! - diff --git a/website/content/carbon/getting-started/bun.mdx b/website/content/carbon/getting-started/bun.mdx deleted file mode 100644 index 9110cbc6..00000000 --- a/website/content/carbon/getting-started/bun.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Bun -description: Getting started with Carbon on Bun ---- -import { File, Folder, Files } from 'fumadocs-ui/components/files'; -import { LayoutDashboard, FolderGit } from 'lucide-react'; - - -## File Structure - - - - - - - - - - - - -## Getting Started - -### Installation -First, we need to install Carbon. - - -### Setting up your .env - -Let's start by creating your `.env` file, which will contain the secrets and tokens for your app. - -```txt title=".env" -CLIENT_ID="" -PUBLIC_KEY="" -DISCORD_TOKEN="" -``` - -Now, you need to get your client ID, public key, and token, and fill those out there. [You can learn more about these here.](/carbon/helpful-guides/developer-portal/create-a-bot) - -### Creating a Client - -Now let's start your `index.ts` file. It will be the entry point for your worker, and the main file that will be executed when your worker is deployed. - -First, we need to import the Carbon client. -```ts -// Import the Carbon client -import { Client, ClientMode } from "@buape/carbon" -``` - -We also need to import the serve function from `bun`. -```ts -import { serve } from "bun" -``` - -Now, let's make our Client. -```ts -const client = new Client( - { - mode: ClientMode.Bun, - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - port: 3000 - }, - [] // We'll add commands here later -); -``` - -And finally, we need to host Carbon. -```ts -serve(client.router) -``` - -### Adding Commands - -Now that we have our client, we can start adding commands to it. Let's create a new file called `ping.ts` in the `src/commands` folder. -```ts title="src/commands/ping.ts" -import { Command, type CommandInteraction } from "@buape/carbon" - -export default class PingCommand extends Command { - name = "ping" - description = "A simple ping command" - defer = false - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) - } -} -``` - -This is a basic command, you set the name and description, and then add a run function that will be called when the command is used. - -Now, let's add this command to our client. -```ts title="src/index.ts" -import { PingCommand } from "./commands/ping.js" -const client = new Client( - { - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - mode: ClientMode.Bun, - port: 3000 - }, - [new PingCommand()] -) -``` - -And that's it! You now have a basic Carbon bot running on Bun. -If you want to add more commands, just create more classes in the `src/commands` folder, and add them to the client in the `src/index.ts` file. - -## Running - -To run your bot, let's add some scripts to your `package.json` file. - - -Let's also add some scripts to our `package.json` file. -```json title="package.json" -{ - // ... - - "scripts": { - "start": "bun run index.ts" - }, - - // ... -} -``` - -Then you can use this command: - - -This will run your bot with the environment variables from your `.env` file. - -Now you'll need to setup your bot on the [Discord Developer Portal](https://discord.com/developers/applications). Take a look at the following cards for more information. - - - } - href="/carbon/helpful-guides/developer-portal/urls" - title="Discord Developer Portal" - description="Learn how to setup your bot on the Discord Developer Portal." - /> - } - href="/" - title="Example Project" - description="See a full example project for Carbon on Bun." - /> - diff --git a/website/content/carbon/getting-started/cloudflare-workers.mdx b/website/content/carbon/getting-started/cloudflare-workers.mdx deleted file mode 100644 index 3cd84402..00000000 --- a/website/content/carbon/getting-started/cloudflare-workers.mdx +++ /dev/null @@ -1,193 +0,0 @@ ---- -title: Cloudflare Workers -description: Getting started with Carbon on Cloudflare Workers ---- -import { File, Folder, Files } from 'fumadocs-ui/components/files'; -import { LayoutDashboard, FolderGit } from 'lucide-react'; - - -Cloudflare Workers is a quick and free option for hosting your bot, and is the primary runtime that we at Buape Studios use Carbon on. - -## File Structure - - - - - - - - - - - - -## Getting Started - -### Installation -First, we need to install Carbon, as well as Wrangler to help us deploy our worker. - - -Let's also add `@cloudflare/workers-types`, so that we can use the `ExecutionContext` type. - - -### Setting up your Worker - -Let's start by creating your `wrangler.toml` file, which will contain the configuration for your worker. - -```toml title="wrangler.toml" -name = "my-carbon-worker" -main = "src/index.ts" -compatibility_date = "2024-09-03" -``` - -Now, you need to get your client ID, public key, and token. [You can learn more about these here.](/carbon/developer-portal/create-a-bot) - -Let's add these to your worker: - - - -After each of these commands, you'l be prompted to enter the value. - -### Creating a Client - -Now let's start your `index.ts` file. It will be the entry point for your worker, and the main file that will be executed when your worker is deployed. - -First, we need to import the Carbon client. -```ts -// Import the Carbon client -import { Client, ClientMode } from "@buape/carbon" -``` - -We also need to get two types: we need to create a type for our environment variables that we set in the wrangler.toml file, and we need to import the `ExecutionContext` type from `@cloudflare/workers-types`. -```ts -import type { ExecutionContext } from "@cloudflare/workers-types/2023-07-01" - -type Env = { - CLIENT_ID: string - PUBLIC_KEY: string - DISCORD_TOKEN: string -} -``` - -In Cloudflare Workers, everything is exported in one object. The `fetch` function is the entry point for your worker. -```ts -export default { - async fetch(request: Request, _env: Env, ctx: ExecutionContext) { - // Place your code here. - } -} -``` - -Now, let's add some code to actually use Carbon. First, we will create a new Carbon client. - -```ts -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext) { - // Create a new Carbon client. - const client = new Client( - { - // Put your client ID, public key, and token here, _env will have them from your `wrangler.toml` file. - clientId: env.CLIENT_ID, - publicKey: env.PUBLIC_KEY, - token: env.DISCORD_TOKEN, - mode: ClientMode.CloudflareWorkers - }, - [] - ) - // This is how you pass control from your cf worker to Carbon. Make sure you pass the ctx part as well, so that awaits work properly within Carbon. - const response = await client.router.fetch(request, ctx) - // Finally, return the response from Carbon. - return response - } -} -``` - -### Adding Commands - -Now that we have our client, we can start adding commands to it. Let's create a new file called `ping.ts` in the `src/commands` folder. -```ts title="src/commands/ping.ts" -import { Command, type CommandInteraction } from "@buape/carbon" - -export default class PingCommand extends Command { - name = "ping" - description = "A simple ping command" - defer = false - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) - } -} -``` - -This is a basic command, you set the name and description, and then add a run function that will be called when the command is used. - -Now, let's add this command to our client. -```ts title="src/index.ts" -const client = new Client( - { - clientId: env.CLIENT_ID, - publicKey: env.PUBLIC_KEY, - token: env.DISCORD_TOKEN, - mode: ClientMode.CloudflareWorkers - }, - [ - new PingCommand() - ] -) -``` - -Finally, we need to add a way to deploy your commands to Discord. We can do this by adding a deploy route to our client. -Add this right above the `client.router.fetch` line: -```ts title="src/index.ts" -client.router.get("/deploy", () => { - return client.deployCommands() -}) -``` - - -And that's it! You now have a basic Carbon bot running on Cloudflare Workers. -If you want to add more commands, just create more classes in the `src/commands` folder, and add them to the client in the `src/index.ts` file. - -## Deploying - -To deploy your bot, let's add some scripts to your `package.json` file. - - - -Let's also add some scripts to our `package.json` file. -```json title="package.json" -{ - // ... - - "scripts": { - "build": "wrangler deploy", - "tail": "wrangler tail" - } - - // ... -} -``` - -Then you can use these like this: - - -This will push your code to Cloudflare, and deploy your worker. You can also use the `tail` command to see the logs from your worker. - -Now you'll need to setup your bot on the [Discord Developer Portal](https://discord.com/developers/applications). Take a look at the following cards for more information. - - - } - href="/carbon/helpful-guides/developer-portal/urls" - title="Discord Developer Portal" - description="Learn how to setup your bot on the Discord Developer Portal." - /> - } - href="/" - title="Example Project" - description="See a full example project for Carbon on Cloudflare Workers." - /> - diff --git a/website/content/carbon/getting-started/index.mdx b/website/content/carbon/getting-started/index.mdx deleted file mode 100644 index 2ce5d686..00000000 --- a/website/content/carbon/getting-started/index.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Getting Started -description: Getting started with Carbon -index: true -icon: CirclePlay ---- - -## Which runtime should I use? - - -## Cloudflare Workers - -Cloudflare Workers is a quick and free option for hosting your bot, and is the primary runtime that we at Buape Studios use Carbon on. Just expose the `handle` method on the client, and you're good to go! - -## Node.js - -Node.js is a great choice if you want a bot that can run on any server. You simply install the `@buape/carbon-nodejs` package, and use the server provided there! - -## Next.js - -Next.js is a great choice if you want to make a bot that integrates with your Next.js app. Carbon will run on an API route, so you can have everything in one place. - -## Bun - -Bun is a fast and lightweight alternative to Node.js, and is a great option for running Carbon on a server. You just need to pass a port to Carbon, and it will use Bun's built-in HTTP server. - -## Other Runtimes - -If you want to use Carbon on a server that is not one of the above, you can use the `handle` method on the client. Check out the [Custom Router](/carbon/helpful-guides/custom-router) guide for more information on how to use it. - -## Ready to get started? - -Choose your runtime, and start building your bot! \ No newline at end of file diff --git a/website/content/carbon/getting-started/meta.json b/website/content/carbon/getting-started/meta.json deleted file mode 100644 index 32767cd4..00000000 --- a/website/content/carbon/getting-started/meta.json +++ /dev/null @@ -1 +0,0 @@ -{ "title": "Getting Started" } diff --git a/website/content/carbon/getting-started/nextjs.mdx b/website/content/carbon/getting-started/nextjs.mdx deleted file mode 100644 index 8e6f97b7..00000000 --- a/website/content/carbon/getting-started/nextjs.mdx +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Next.js -description: Getting started with Carbon on Next.js ---- -import { File, Folder, Files } from 'fumadocs-ui/components/files'; -import { LayoutDashboard, FolderGit } from 'lucide-react'; - - -Next.js is a great React framework for building web apps, and works great with Carbon. - -## File Structure - - - - - - - - - - - - - - - -## Getting Started - -### Installation -First, we need to install Carbon. - - -We're going to assume that you already have a working Next.js app and that it's hosted on Vercel, so we'll just add Carbon on top of it. - -### Creating a Client - -Now let's start your `route.ts` file. The route.ts file is where Next.js will look for your API routes, and Carbon will be running on one of those. - -First, we need to import the Carbon client. -```ts -// Import the Carbon client -import { Client, ClientMode } from "@buape/carbon" -``` - -Now, let's add some code to actually use Carbon. First, we will create a new Carbon client. - -```ts -const client = new Client({ - clientId: env.DISCORD_CLIENT_ID, - publicKey: env.DISCORD_PUBLIC_KEY, - token: env.DISCORD_TOKEN, - mode: ClientMode.Vercel, - redirectUrl: getBaseUrl() -}, []) -``` - -### Adding Commands - -Now that we have our client, we can start adding commands to it. Let's create a new file called `ping.ts` in the `commands` folder. -```ts title="app/discord/commands/ping.ts" -import { Command, type CommandInteraction } from "@buape/carbon" - -export class PingCommand extends Command { - name = "ping" - description = "A simple ping command" - defer = false - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) - } -} -``` - -This is a basic command, you set the name and description, and then add a run function that will be called when the command is used. - -Now, let's add this command to our client. -```ts -const client = new Client( - { - clientId: env.CLIENT_ID, - publicKey: env.PUBLIC_KEY, - token: env.DISCORD_TOKEN, - mode: ClientMode.Vercel - }, - [ - new PingCommand() - ] -) -``` - -Finally, we need to add a way to deploy your commands to Discord. We can do this by adding a deploy route to our client. -Add this right above the `client.router.fetch` line: -```ts title="app/discord/deploy/route.ts" -import { client } from "../route" - -export function GET() { - client.deployCommands() - return new Response(null, { - status: 200 - }) -} -``` - - -And that's it! You now have a basic Carbon bot running on Next.js. -If you want to add more commands, just create more classes in the `commands` folder, and add them to the client in the `route.ts` file. - - -Now you'll need to setup your bot on the [Discord Developer Portal](https://discord.com/developers/applications). Take a look at the following cards for more information. - - - } - href="/carbon/helpful-guides/developer-portal/urls" - title="Discord Developer Portal" - description="Learn how to setup your bot on the Discord Developer Portal." - /> - } - href="/" - title="Example Project" - description="See a full example project for Carbon on Next.js." - /> - diff --git a/website/content/carbon/getting-started/nodejs.mdx b/website/content/carbon/getting-started/nodejs.mdx deleted file mode 100644 index 594cd4de..00000000 --- a/website/content/carbon/getting-started/nodejs.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Node.js -description: Getting started with Carbon on Node.js ---- -import { File, Folder, Files } from 'fumadocs-ui/components/files'; -import { LayoutDashboard, FolderGit } from 'lucide-react'; - - -Node.js is a popular choice for running your bot, and has first-class support from Carbon. - -## File Structure - - - - - - - - - - - - -## Getting Started - -### Installation -First, we need to install Carbon and its NodeJS package, as well as env-cmd to help us set our environment variables, and the typings for node. - - -### Setting up your .env - -Let's start by creating your `.env` file, which will contain the secrets and tokens for your app. - -```txt title=".env" -CLIENT_ID="" -PUBLIC_KEY="" -DISCORD_TOKEN="" -``` - -Now, you need to get your client ID, public key, and token, and fill those out there. [You can learn more about these here.](/carbon/developer-portal/create-a-bot) - -### Creating a Client - -Now let's start your `index.ts` file. It will be the entry point for your worker, and the main file that will be executed when your worker is deployed. - -First, we need to import the Carbon client. -```ts -// Import the Carbon client -import { Client, ClientMode } from "@buape/carbon" -``` - -We also need to import the NodeJS helpers from `@buape/carbon-nodejs`. -```ts -import { dirname } from "node:path" -import { fileURLToPath } from "node:url" -import { loadCommands, serve } from "@buape/carbon-nodejs" -const __dirname = dirname(fileURLToPath(import.meta.url)) -``` - -Now, let's make our Client. -```ts -const client = new Client( - { - clientId: process.env.CLIENT_ID, - publicKey: process.env.PUBLIC_KEY, - token: process.env.DISCORD_TOKEN, - mode: ClientMode.NodeJS, - }, - await loadCommands("commands", __dirname) -) -``` - -And finally, we need to host Carbon on a port. -```ts -serve(client, { port: 3000 }) -``` - -### Adding Commands - -Now that we have our client, we can start adding commands to it. Let's create a new file called `ping.ts` in the `src/commands` folder. -```ts title="src/commands/ping.ts" -import { Command, type CommandInteraction } from "@buape/carbon" - -export default class PingCommand extends Command { - name = "ping" - description = "A simple ping command" - defer = false - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) - } -} -``` - -This is a basic command, you set the name and description, and then add a run function that will be called when the command is used. - -Now, let's add this command to our client. We already created the `__dirname` variable for where our code is running earlier, so we can use that and load all the commands in that folder. -```ts title="src/index.ts" -const client = new Client( - { - clientId: env.CLIENT_ID, - publicKey: env.PUBLIC_KEY, - token: env.DISCORD_TOKEN, - mode: ClientMode.NodeJS, - }, - await loadCommands("commands", __dirname) -) -``` - -And that's it! You now have a basic Carbon bot running on NodeJS. -If you want to add more commands, just create more classes in the `src/commands` folder, and add them to the client in the `src/index.ts` file. - -## Running - -To run your bot, let's add some scripts to your `package.json` file. - - -Let's also add some scripts to our `package.json` file. -```json title="package.json" -{ - // ... - - "scripts": { - "start": "tsc && env-cmd --node ." - }, - - // ... -} -``` - -Then you can use this command: - - -This will compile your code, and run your bot with the environment variables from your `.env` file. - -Now you'll need to setup your bot on the [Discord Developer Portal](https://discord.com/developers/applications). Take a look at the following cards for more information. - - - } - href="/carbon/helpful-guides/developer-portal/urls" - title="Discord Developer Portal" - description="Learn how to setup your bot on the Discord Developer Portal." - /> - } - href="/" - title="Example Project" - description="See a full example project for Carbon on NodeJS." - /> - diff --git a/website/content/carbon/helpful-guides/custom-router.mdx b/website/content/carbon/helpful-guides/custom-router.mdx deleted file mode 100644 index be63d127..00000000 --- a/website/content/carbon/helpful-guides/custom-router.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Custom Router ---- - -If you want to use a custom router for your bot, you can use the `handle` method on the client. - -```ts -import { Client, ClientMode } from "@buape/carbon" -import { AutoRouter } from 'itty-router' - -const client = new Client({ - clientId: "12345678901234567890", - publicKey: "c1a2f941ae8ce6d776f7704d0bb3d46b863e21fda491cdb2bdba6b8bc5fe7269", - token: "MTA4NjEwNTYxMDUxMDE1NTg1Nw.GNt-U8.OSHy-g-5FlfESnu3Z9MEEMJLHiRthXajiXNwiE", - mode: ClientMode.NodeJS -}) - -const router = AutoRouter() -router.get("/", () => { - return new Response("Hello world") -}) - -router.post("/interaction", async (req) => { - return await client.handle(req) -}) -``` - diff --git a/website/content/carbon/helpful-guides/developer-portal/create-a-bot.mdx b/website/content/carbon/helpful-guides/developer-portal/create-a-bot.mdx deleted file mode 100644 index 04434062..00000000 --- a/website/content/carbon/helpful-guides/developer-portal/create-a-bot.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Create a Bot ---- - -The [Discord Developer Portal](https://discord.com/developers/applications) is the main place that you can manage your bot on Discord. - -## Creating a Bot - -To create a Discord bot, you'll need to head over to the [Discord Developer Portal](https://discord.com/developers/applications) and create a new application by using the New Application button in the top left. Give your application a name and click Create. - -You should now see a page that shows information about your bot. You can add an avatar and bio for your bot here. - - - -You'll also want to copy the Client ID and the Public Key there, you'll need those for your application. - -Once you've saved your changes, click on the Bot tab on the left panel. - -After you open the bot tab, you should see a blue button that says Reset Token. Click on it and confirm that you want to reset your bot's token. You should now see a new section called Token with a button that says Copy. When we ask you to paste your bot's token somewhere, this is the value that you need to put in. - -Tokens are in the format similar to this one: -``` -MTA4NjEwNTYxMDUxMDE1NTg1Nw.GNt-U8.OSHy-g-5FlfESnu3Z9MEEMJLHiRthXajiXNwiE -``` - -If you accidentally leak your bot's token or lose it, you need to come back to this page and reset your bot's token again which will reveal the new token, invalidating all old ones. - - \ No newline at end of file diff --git a/website/content/carbon/helpful-guides/developer-portal/index.mdx b/website/content/carbon/helpful-guides/developer-portal/index.mdx deleted file mode 100644 index fda2e5f1..00000000 --- a/website/content/carbon/helpful-guides/developer-portal/index.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Developer Portal -index: true ---- \ No newline at end of file diff --git a/website/content/carbon/helpful-guides/developer-portal/meta.json b/website/content/carbon/helpful-guides/developer-portal/meta.json deleted file mode 100644 index 95196789..00000000 --- a/website/content/carbon/helpful-guides/developer-portal/meta.json +++ /dev/null @@ -1 +0,0 @@ -{ "title": "Developer Portal" } diff --git a/website/content/carbon/helpful-guides/developer-portal/urls.mdx b/website/content/carbon/helpful-guides/developer-portal/urls.mdx deleted file mode 100644 index c63b43b0..00000000 --- a/website/content/carbon/helpful-guides/developer-portal/urls.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Setting URLs ---- - -The [Discord Developer Portal](https://discord.com/developers/applications) is the main place that you can manage your bot on Discord. - -## Setting your interaction URL - -Once you have Carbon running, you'll want to set your interaction URL. This is the URL that Discord will send interactions to. -Unless you've modified it, this will be at `https://DOMAIN.COM/interaction`, replacing `DOMAIN.COM` with your domain. - -Go to the Information tab on the left panel for your bot, and look below the public key. - - - -Put your interaction URL in the Interaction URL field, and click Save Changes. -Discord will then attempt to send a test interaction to your URL, and if it succeeds, you're good to go! -If not, make sure you've set the URL correctly, and make sure that Carbon is actually running. \ No newline at end of file diff --git a/website/content/carbon/helpful-guides/index.mdx b/website/content/carbon/helpful-guides/index.mdx deleted file mode 100644 index 07959136..00000000 --- a/website/content/carbon/helpful-guides/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Helpful Guides -index: true -icon: Library ---- - diff --git a/website/content/carbon/helpful-guides/meta.json b/website/content/carbon/helpful-guides/meta.json deleted file mode 100644 index c4671cef..00000000 --- a/website/content/carbon/helpful-guides/meta.json +++ /dev/null @@ -1 +0,0 @@ -{ "title": "Helpful Guides" } diff --git a/website/content/carbon/helpful-guides/partials.mdx b/website/content/carbon/helpful-guides/partials.mdx deleted file mode 100644 index 281399bd..00000000 --- a/website/content/carbon/helpful-guides/partials.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Partials ---- - -Partials are a concept in the Discord API that allows the API to send only a subset of the data needed for a certain structure, such as only the ID of a user or the channel ID + message ID of a message. This is useful for reducing the amount of data that needs to be sent, and can improve performance. - -In Carbon, you may sometimes get partials on things such as `Interaction#message`. This is because the original data for that interaction only had a partial. Luckily, there is a `fetch()` method on all classes that may be partial (as well as a boolean `partial` property on all classes), which will fetch the full data of the class. - -Because of this, many properties on a structure, such as a `Message`, will be potentially undefined, and you should check for that before using them. \ No newline at end of file diff --git a/website/content/carbon/helpful-guides/responses.mdx b/website/content/carbon/helpful-guides/responses.mdx deleted file mode 100644 index a7f174bf..00000000 --- a/website/content/carbon/helpful-guides/responses.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Replying vs Deferring vs Acknowledging ---- - -In the Discord interaction system, there are 3 ways to respond to an interaction: - -## 1. Replying - -This is the simplest way to respond to an interaction, and is used for most commands. It is done by using the `reply` method on the `CommandInteraction` class. - -```ts -async run(interaction: CommandInteraction) { - await interaction.reply({ content: "Hello world" }) -} -``` - -## 2. Deferring - -Deferring is used when you need more time to process an interaction. When you defer an interaction, the bot will send a loading state to the user. You then have up to 15 minutes to reply to the interaction afterwards, using the normal [reply](#replying) method. - - - Deferring an interaction must be `await`ed! If you don't respond within 15 minutes, the interaction will show an error as if you never replied in the first place. - - -```ts -async run(interaction: CommandInteraction) { - await interaction.defer() - await interaction.reply({ content: "Hello world" }) -} -``` - -## 3. Acknowledging - -Acknowledging is used when you want to acknowledge an interaction, but you don't want to reply to it. You can only use this for component-based interactions. -This is useful for things such as a game bot where you want to trigger an action and then only edit the original message. - -```ts -async run(interaction: ButtonInteraction) { - await interaction.acknowledge() -} -``` diff --git a/website/content/carbon/http-bot.mdx b/website/content/carbon/http-bot.mdx deleted file mode 100644 index 6fb917be..00000000 --- a/website/content/carbon/http-bot.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "What are HTTP Bots?" -icon: CloudCog ---- - -If you've been around Discord for a while, you've probably seen bots in various servers. These bots have been around almost since the beginning of Discord, and for the most part, they all rely on Discord's API Gateway (which runs over a websocket) for incoming events and interactions with the bot. - -However, there are some downsides to this approach for some applications, mainly that the API Gateway is, by nature, not compatible with serverless applications, as it requires a websocket connection to be established. This means that you can't use it with a platform like Vercel, which is a serverless platform. - -That's where HTTP bots come in. By default, Discord will send all interactions through your Gateway connection, but you can opt-in to HTTP-based interactions instead by adding a Interactions Endpoint URL to your app's settings. - -## How do HTTP bots work? - -HTTP bots work by having Discord send a POST request to the Interactions Endpoint URL you set in your app's settings. This request will contain the data of the interaction, and the bot will respond with a JSON object containing the response data. - -This is a bit more complicated than a normal interaction, as it requires a separate request to be sent, but it also has some advantages. For example, you can use a platform like Vercel to host your bot, and you don't need to worry about websockets or any other complicated setup. - -## Advantages of HTTP - -- Compatible with serverless applications and runtimes -- No need to worry about websockets -- Can be infinitely scalable through standard HTTP load balancing - -## Downsides of HTTP - -HTTP bots have their own set of downsides, however, and the biggest one is that they will only work with interactions (slash commands, buttons, etc), and you don't get any of the other events such as GUILD_MEMBER_UPDATE or MESSAGE_CREATE. - -While this does mean you don't need to worry about intents or listening to events, it does mean that if you want to make a bot that interacts with other parts of Discord without users triggering your bot themselves, you'll need to use a Gateway-based bot. - -However, you can use a hybrid approach if you choose. You can have your bot use HTTP for all interactions, and use the Gateway for all other events. This way, you can still use HTTP for things like slash commands, but still have every other event that Discord gives you. - -## Learn more - -If you want to learn more about HTTP bots and how interactions work, you can check out the [Discord Developer Documentation](https://discord.com/developers/docs/interactions/overview) for more information. - - - - diff --git a/website/content/carbon/index.mdx b/website/content/carbon/index.mdx deleted file mode 100644 index 4b27cc08..00000000 --- a/website/content/carbon/index.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Introduction -index: true -icon: Album ---- -![Wordmark](https://cdn.buape.com/CarbonWordmark.png) - -Carbon is a powerful framework designed for building HTTP Discord bots. - - - } - href="/carbon/getting-started" - title="Getting Started" - description="Learn how to get started with Carbon." - /> - } - href="/carbon/classes" - title="Code" - description="Learn about the different classes that make up Carbon." - /> - } - href="/carbon/helpful-guides" - title="Helpful Guides" - description="Learn how to use Carbon effectively." - /> - } - href="/carbon/even-more" - title="Even More" - description="More from the Carbon team." - /> - - - -## Join the Community - -Join our community on [Discord](https://go.buape.com/Carbon) to connect with other developers, share your projects, and get support. -[![Discord Invite](http://invidget.switchblade.xyz/rT8vZAmVaQ)](https://go.buape.com/Carbon) - -We’re excited to see what you create with Carbon! \ No newline at end of file diff --git a/website/content/carbon/plugins/index.mdx b/website/content/carbon/plugins/index.mdx deleted file mode 100644 index d9751bb8..00000000 --- a/website/content/carbon/plugins/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Plugins -index: true -icon: Cable ---- - diff --git a/website/content/carbon/plugins/linked-roles.mdx b/website/content/carbon/plugins/linked-roles.mdx deleted file mode 100644 index 008a6b99..00000000 --- a/website/content/carbon/plugins/linked-roles.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Linked Roles -description: Linked Roles are a handy feature of Discord that allows you to create roles that users have to meet certain criteria in order to claim those roles. ---- - -Linked Roles are a handy feature of Discord that allows you to create roles that users have to meet certain criteria in order to claim those roles. - -[You can read more about the feature from an admin standpoint here](https://support.discord.com/hc/en-us/articles/10388356626711-Connections-Linked-Roles-Admins), or [from a user standpoint here.](https://support.discord.com/hc/en-us/articles/8063233404823-Connections-Linked-Roles-Community-Members). - -## Usage - -Linked Roles are straightforward to use in Carbon, all you need is to already have an instance of the [`Client`](/carbon/api/classes/Client) from Carbon. - -Then, create a new instance of the `LinkedRoles` class, and pass it your metadata and checkers, along with some options for the base URL and client secret and your client. - -> [!IMPORTANT] -> You can only have 5 metadata per application, and they apply across all guilds your app is in. - -### Installation - - - -### Example - -```ts title="src/index.ts" -import { Client } from "@buape/carbon" -import { LinkedRoles } from "@buape/carbon/linked-roles" - -const client = new Client({ - clientId: "12345678901234567890", - publicKey: "c1a2f941ae8ce6d776f7704d0bb3d46b863e21fda491cdb2bdba6b8bc5fe7269", - token: "MTA4NjEwNTYxMDUxMDE1NTg1Nw.GNt-U8.OSHy-g-5FlfESnu3Z9MEEMJLHiRthXajiXNwiE" -}) - -const allStaff = ["439223656200273932"] - -const linkedRoles = new LinkedRoles(client, { - clientSecret: "Bb7aZcvRN-BhrhY2qrUO6QzOK4SeqonG", - baseUrl: "https://example.com", - metadata: [ - { - key: "is_staff", - name: "Verified Staff", - description: "Whether the user is a verified staff member", - type: ApplicationRoleConnectionMetadataType.BooleanEqual - }, - ], - metadataCheckers: { - is_staff: async (userId) => { - if (allStaff.includes(userId)) return true - return false - } - } -}) -``` - -Once you have your `LinkedRoles` instance, you need to set it on Discord so that users will use it for linked roles. -[You can see where to add this by clicking here](/carbon/developer-portal/urls), and set the linked role to `/connect`, so for example, `https://my-carbon-worker.YOURNAME.workers.dev/connect`. -You'll also need to add a redirect URL to your Discord application, so that users can be redirected to your website after they login. -You can go to the OAuth tab on the dashboard and add a redirect URL there of `/connect/callback`, so for example, `https://my-carbon-worker.YOURNAME.workers.dev/connect/callback`. \ No newline at end of file diff --git a/website/content/carbon/plugins/meta.json b/website/content/carbon/plugins/meta.json deleted file mode 100644 index c79c9c0c..00000000 --- a/website/content/carbon/plugins/meta.json +++ /dev/null @@ -1 +0,0 @@ -{ "title": "Plugins" } diff --git a/website/content/classes/client.mdx b/website/content/classes/client.mdx new file mode 100644 index 00000000..65f1ef6e --- /dev/null +++ b/website/content/classes/client.mdx @@ -0,0 +1,36 @@ +--- +title: Client +description: The main class that is used to use Carbon +icon: Pyramid +--- + +The main class that is used to use Carbon is the [`Client`](/api/index/classes/Client) class. Everything all connects to this one class, and it is the main instance for your bot. + +## Creating a Client + +A client must be created within your [`createHandle`](/api/index/functions/createHandle) factory. + +```ts title="src/index.ts" +const handle = createHandle((env) => { + const client = new Client({ + baseUrl: String(env.BASE_URL), + deploySecret: String(env.DEPLOY_SECRET), + clientId: String(env.CLIENT_ID), + clientSecret: String(env.CLIENT_SECRET), + publicKey: String(env.PUBLIC_KEY), + token: String(env.TOKEN), + }, [new PingCommand()]) + return [client] +}) +``` + +Here we have created a client with the following options: + +- `baseUrl`: The base URL of your bot, relative to your public URL +- `deploySecret`: The deploy secret of your bot, used as a password for deploying commands and other sensitive matters +- `clientId`: The Discord client ID of your bot +- `clientSecret`: The Discord client secret of your bot +- `publicKey`: The Discord public key of your bot +- `token`: The Discord token of your bot + +And we have also provided it with a list of commands, which in this case is just the `PingCommand` we created earlier. diff --git a/website/content/carbon/classes/commands.mdx b/website/content/classes/commands.mdx similarity index 57% rename from website/content/carbon/classes/commands.mdx rename to website/content/classes/commands.mdx index 07ae40b5..3844c65a 100644 --- a/website/content/carbon/classes/commands.mdx +++ b/website/content/classes/commands.mdx @@ -1,27 +1,25 @@ --- title: Commands -description: Commands are the main way to interact with your bot, including Context Menu apps and slash commands +description: Commands are the main way to interact with your bot, including Context Menu apps and slash commands. +icon: SquareSlash --- Commands are the main way to interact with your bot. They are used to handle user input and respond to it. ## Creating a Command -To create a command, you need to create a class that extends the `Command` class. This class will handle the command and its functionality. - +To create a command, you need to create a class that extends the [`Command`](/api/index/classes/Command) class. This class will handle the command and its functionality. ```ts title="src/commands/ping.ts" -import { Command, type CommandInteraction } from "@buape/carbon" +import { Command, type CommandInteraction } from "@buape/carbon"; export default class PingCommand extends Command { - name = "ping" - description = "A simple ping command" - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "Pong!" - }) - } + name = "ping" + description = "A simple ping command" + + async run(interaction: CommandInteraction) { + await interaction.reply("Pong!") + } } ``` @@ -35,29 +33,29 @@ The `name` and `description` properties are used to set the name and description The `type` property is used to set the type of the command. This is used to determine which type of command it is. The available types are: -- `ChatInput`: This is a command that can be used in a chat input. -- `User`: This is a command that can be used in a user context. -- `Message`: This is a command that can be used in a message context. +- `ChatInput`: This is a command that can be used in a chat input. +- `User`: This is a command that can be used in a user context. +- `Message`: This is a command that can be used in a message context. ### Options The `options` property is used to set the options of the command. These are only used for chat input commands. -- `String`: Basic string input -- `Integer`: Integer input -- `Number`: Number input -- `Boolean`: Boolean input (with autocomplete to show True and False) -- `User`: User object (with autocomplete to show users in the server. Also accepts just an ID which will be resolved to a user object) -- `Channel`: Channel object (with autocomplete to show channels in the server. Also accepts just an ID which will be resolved to a channel object) -- `Role`: Role object (with autocomplete to show roles in the server. Also accepts just an ID which will be resolved to a role object) -- `Mentionable`: Mentionable object (with autocomplete to show both users and roles in the server) -- `Attachment`: File attachment +- `String`: Basic string input +- `Integer`: Integer input +- `Number`: Number input +- `Boolean`: Boolean input (with autocomplete to show True and False) +- `User`: User object (with autocomplete to show users in the server. Also accepts just an ID which will be resolved to a user object) +- `Channel`: Channel object (with autocomplete to show channels in the server. Also accepts just an ID which will be resolved to a channel object) +- `Role`: Role object (with autocomplete to show roles in the server. Also accepts just an ID which will be resolved to a role object) +- `Mentionable`: Mentionable object (with autocomplete to show both users and roles in the server) +- `Attachment`: File attachment ### Adding your Components The `components` property is used to set the components of the command. You need to pass every component that you might use in the command, and they will be automatically registered. -[See this page for more information about components.](/carbon/classes/components) +[See this page for more information about components.](/classes/components) ### Automatic Defer @@ -70,17 +68,17 @@ You can also create a wildcard command, which will be called when no command is To create a wildcard command, you just have to create a command like normal, but set the name to `"*"`. This will be called when no other command is found. ```ts title="src/commands/_maintenance.ts" -import { Command, type CommandInteraction } from "@buape/carbon" +import { Command, type CommandInteraction } from "@buape/carbon"; export default class MaintenanceCommand extends Command { - name = "*" - description = "Maintenance mode" - defer = false - - async run(interaction: CommandInteraction) { - return interaction.reply({ - content: "The bot is currently under maintenance. Please try again later." - }) - } + name = "*" + description = "Maintenance mode" + defer = false + + async run(interaction: CommandInteraction) { + await interaction.reply( + "The bot is currently under maintenance. Please try again later." + ) + } } -``` \ No newline at end of file +``` diff --git a/website/content/carbon/classes/components/buttons.mdx b/website/content/classes/components/buttons.mdx similarity index 63% rename from website/content/carbon/classes/components/buttons.mdx rename to website/content/classes/components/buttons.mdx index a35f6405..47267d7a 100644 --- a/website/content/carbon/classes/components/buttons.mdx +++ b/website/content/classes/components/buttons.mdx @@ -1,6 +1,7 @@ --- title: Buttons description: Buttons appear below messages and can be used to trigger actions or link to external sources. +icon: MousePointer --- Buttons appear below messages and can be used to trigger actions or link to external sources. @@ -10,17 +11,15 @@ Buttons appear below messages and can be used to trigger actions or link to exte To create a button, you need to create a class that extends the `Button` class. This class will handle the button and its functionality. ```ts title="src/buttons/ping.ts" -import { Button, type ButtonInteraction } from "@buape/carbon" +import { Button, type ButtonInteraction } from "@buape/carbon"; export default class PingButton extends Button { - customId = "ping" - label = "Ping" - - async run(interaction: ButtonInteraction) { - return interaction.reply({ - content: "Pong!" - }) - } + customId = "ping" + label = "Ping" + + async run(interaction: ButtonInteraction) { + await interaction.reply("Pong!") + } } ``` @@ -32,23 +31,23 @@ The custom ID is what is set on the button to differentiate it from other button The `style` property is used to set the style of the button. This is used to determine the style of the button. The available styles are: -- `Primary`: A blurple colored button -- `Secondary`: A gray colored button -- `Success`: A green colored button -- `Danger`: A red colored button -- `Link`: A gray button solely used for links +- `Primary`: A blurple colored button +- `Secondary`: A gray colored button +- `Success`: A green colored button +- `Danger`: A red colored button +- `Link`: A gray button solely used for links ## Link Buttons -Link buttons are used to link to external sources. They are created by extending the [`LinkButton`](/carbon/api/classes/LinkButton) class, and adding a `url` property. +Link buttons are used to link to external sources. They are created by extending the `LinkButton` class, and adding a `url` property. ```ts title="src/buttons/link.ts" -import { Button, type ButtonInteraction } from "@buape/carbon" +import { Button, type ButtonInteraction } from "@buape/carbon"; export default class LinkButton extends Button { - label = "Link" - url = "https://buape.com" + label = "Carbon Documentation" + url = "https://carbon.buape.com" } ``` -Note that these buttons do not trigger any actions on the bot side, and are only used to link to external sources. \ No newline at end of file +Note that these buttons do not trigger any actions on the bot side, and are only used to link to external sources. diff --git a/website/content/carbon/classes/components/index.mdx b/website/content/classes/components/index.mdx similarity index 86% rename from website/content/carbon/classes/components/index.mdx rename to website/content/classes/components/index.mdx index c693cfca..b88a406e 100644 --- a/website/content/carbon/classes/components/index.mdx +++ b/website/content/classes/components/index.mdx @@ -5,7 +5,7 @@ index: true Components are used to create interactive elements in your bot, such as buttons, select menus, and text inputs. -When you create a component, you will need to pass it to the [`components` property of the `Command` class](/carbon/api/classes/CommandInteraction#properties). This will allow it to be registered with the bot as a handler. +When you create a component, you will need to pass it to the [`components` property of the `Command` class](/api/classes/CommandInteraction#properties). This will allow it to be registered with the bot as a handler. When you pass it, make sure you aren't instantiating it, but rather just passing the class itself. ```ts title="src/commands/ping.ts" diff --git a/website/content/classes/components/meta.json b/website/content/classes/components/meta.json new file mode 100644 index 00000000..de1d93a7 --- /dev/null +++ b/website/content/classes/components/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Components", + "icon": "Component" +} diff --git a/website/content/carbon/classes/components/select-menus.mdx b/website/content/classes/components/select-menus.mdx similarity index 63% rename from website/content/carbon/classes/components/select-menus.mdx rename to website/content/classes/components/select-menus.mdx index 27e30791..863f7a53 100644 --- a/website/content/carbon/classes/components/select-menus.mdx +++ b/website/content/classes/components/select-menus.mdx @@ -1,9 +1,10 @@ --- title: Select Menus description: Select menus are used to select an option from a list of options in a dropdown +icon: SquareDashedMousePointer --- -Select menus are used to select an option from a list of options in a dropdown. They are created by extending the [`SelectMenu`](/carbon/api/classes/SelectMenu) class, and adding a `options` property. +Select menus are used to select an option from a list of options in a dropdown. They are created by extending the `SelectMenu` class, and adding a `options` property. ```ts title="src/components/select-menu.ts" class StringSelect extends StringSelectMenu { @@ -11,7 +12,7 @@ class StringSelect extends StringSelectMenu { placeholder = "String select menu" options = [{ label: "Option 1", value: "option1" }, { label: "Option 2", value: "option2" }] async run(interaction: StringSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply(interaction.values.join(", ")) } } @@ -19,30 +20,30 @@ class RoleSelect extends RoleSelectMenu { customId = "role-select" placeholder = "Role select menu" async run(interaction: RoleSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply(interaction.values.join(", ")) } } class MentionableSelect extends MentionableSelectMenu { customId = "mentionable-select" placeholder = "Mentionable select menu" async run(interaction: MentionableSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply(interaction.values.join(", ")) } } class ChannelSelect extends ChannelSelectMenu { customId = "channel-select" placeholder = "Channel select menu" async run(interaction: ChannelSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply(interaction.values.join(", ")) } } class UserSelect extends UserSelectMenu { customId = "user-select" placeholder = "User select menu" async run(interaction: UserSelectMenuInteraction) { - interaction.reply({ content: interaction.values.join(", ") }) + await interaction.reply(interaction.values.join(", ")) } } ``` -Here we have 5 examples of select menus, each with a different type of option. Only the StringSelectMenu class requires you to provide options, the other 4 will automatically be populated by Discord (and allow searching as well) +Here we have five examples of select menus, each with a different type of option. Only the StringSelectMenu class requires you to provide options, the other 4 will automatically be populated by Discord (and allow searching as well). diff --git a/website/content/carbon/classes/components/text-inputs.mdx b/website/content/classes/components/text-inputs.mdx similarity index 63% rename from website/content/carbon/classes/components/text-inputs.mdx rename to website/content/classes/components/text-inputs.mdx index 4b9f5782..bf129adf 100644 --- a/website/content/carbon/classes/components/text-inputs.mdx +++ b/website/content/classes/components/text-inputs.mdx @@ -1,9 +1,10 @@ --- title: Text Inputs -description: Text Inputs are blank text fields used solely within Modals to collect user input +description: Text Inputs are blank text fields used solely within Modals to collect user input. +icon: TextCursorInput --- -Text Inputs are blank text fields used solely within [Modals](/carbon/classes/modals) to collect user input. They are created by extending the [`TextInput`](/carbon/api/classes/TextInput) class, and adding a `label` and `placeholder` property. +Text Inputs are blank text fields used solely within [Modals](/api/classes/modals) to collect user input. They are created by extending the `TextInput` class, and adding a `label` and `placeholder` property. ```ts title="src/components/text-input.ts" class TextInputHi extends TextInput { diff --git a/website/content/classes/embeds.mdx b/website/content/classes/embeds.mdx new file mode 100644 index 00000000..791b711c --- /dev/null +++ b/website/content/classes/embeds.mdx @@ -0,0 +1,53 @@ +--- +title: Embeds +description: Embeds are used to display rich content in messages. +icon: SquareMenu +--- + +Embeds are used to display rich content in messages. They are created by extending the [`Embed`](/api/index/classes/Embed) class, and adding a `title`, `description`, and `url` property. + +```ts +class TestEmbed extends Embed { + title = "Embed Test"; + description = "This is an embed test"; + url = "https://buape.com"; +} +``` + +Here we have created an embed with a title, description, and URL. You can use this embed in a message by using the [`Embed`](/api/index/classes/Embed) class. + +## Example Embed + +Here is an example embed with all the properties you can use: + +```ts +class TestEmbed extends Embed { + title = "Embed Test" + description = "This is an embed test" + url = "https://carbon.buape.com" + timestamp = new Date().toString() + color = 16711680; + footer = { + text: "Footer Text", + icon: "https://cdn.buape.com/CarbonWordmark.png", + }; + image = "https://cdn.buape.com/CarbonWordmark.png"; + thumbnail = "https://cdn.buape.com/CarbonWordmark.png"; + author = { + name: "Author Name", + icon: "https://buape.com", + }; + fields = [ + { + name: "Field 1", + value: "Value 1", + inline: true, + }, + { + name: "Field 2", + value: "Value 2", + inline: true, + }, + ]; +} +``` diff --git a/website/content/carbon/classes/index.mdx b/website/content/classes/index.mdx similarity index 82% rename from website/content/carbon/classes/index.mdx rename to website/content/classes/index.mdx index 08fb91df..efb44bf7 100644 --- a/website/content/carbon/classes/index.mdx +++ b/website/content/classes/index.mdx @@ -1,9 +1,8 @@ --- title: Classes index: true -icon: Code --- Carbon is built upon many sets of classes, that you will extend in your own code, including commands, components, and the Carbon client itself. -Each of these classes has a set of properties on the class itself that you can set, as well as methods that Carbon will call, such as [Command#run](/carbon/api/classes/Command#run). \ No newline at end of file +Each of these classes has a set of properties on the class itself that you can set, as well as methods that Carbon will call, such as [Command#run](/api/index/classes/Command#run). \ No newline at end of file diff --git a/website/content/classes/meta.json b/website/content/classes/meta.json new file mode 100644 index 00000000..b9c3476e --- /dev/null +++ b/website/content/classes/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Classes", + "icon": "Shapes", + "pages": ["client", "..."] +} diff --git a/website/content/classes/modals.mdx b/website/content/classes/modals.mdx new file mode 100644 index 00000000..2c6e095e --- /dev/null +++ b/website/content/classes/modals.mdx @@ -0,0 +1,40 @@ +--- +title: Modals +description: Modals are popup forms that can be used to collect user input. +icon: Captions +--- + +Modals are popup forms that can be used to collect user input. They are created by extending the [`Modal`](/api/index/classes/Modal) class, and adding a `title` and `components` property. All the components must be [`TextInput`](/api/index/classes/TextInput) classes. + +```ts title="src/commands/modal.ts" +class ModalCommand extends Modal { + title = "Test Modal" + customId = "test-modal" + + components = [ + new Row([new TextInputHi()]), + new Row([new TextInputAge()]) + ] + + async run(interaction: ModalInteraction) { + const name = interaction.fields.getText("name") + const age = interaction.fields.getText("age") + const color = interaction.fields.getText("color") + const height = interaction.fields.getText("height") || "not"; + await interaction.reply( + `Hi ${name}, you are ${age} years old, and your favorite color is ${color}. You are ${height} tall.` + ) + } +} +class TextInputHi extends TextInput { + label = "Tell me about your life" + customId = "life" + style = TextInputStyle.Paragraph +} + +class TextInputAge extends TextInput { + label = "How old are you?" + customId = "age" + style = TextInputStyle.Short +} +``` diff --git a/website/content/concepts/http-bots.mdx b/website/content/concepts/http-bots.mdx new file mode 100644 index 00000000..5f6e064e --- /dev/null +++ b/website/content/concepts/http-bots.mdx @@ -0,0 +1,35 @@ +--- +title: HTTP Bots +description: Discover how HTTP bots work in Discord, their advantages and downsides, and how they enable compatibility with serverless environments. +icon: Waypoints +--- + +If you've been active on Discord, you've likely encountered bots in various servers. These bots have been around almost since the beginning of Discord, and for the most part, they all rely on Discord's API Gateway (which runs over a WebSocket) for receiving events and interactions with the bot. + +However, there are some downsides to this approach for some applications, mainly that the API Gateway is, by nature, not compatible with serverless applications, as it requires a constant connection to be established. This makes it incompatible with serverless platforms like Vercel or Cloudflare Workers. + +This is where HTTP bots come into play. Instead of relying on a WebSocket connection, you can configure Discord to send interactions via HTTP. By adding an 'Interactions Endpoint URL' to your app's settings, you can opt-in to HTTP-based interactions, enabling compatibility with serverless environments. + +## How HTTP Bots Work + +HTTP bots operate by having Discord send a POST request to your interactions endpoint specifided in your app's settings. This request contains the interaction data that might have instead been sent over a websocket connection. The bot can then respond with a JSON object containing the response data. + +Although this method is slightly more complex than handling interactions directly, as it necessitates an additional request, it offers several benefits. For example, you can use platforms like Vercel to host your bot, which removes the need to manage WebSockets or other intricate setups. + +## Advantages of HTTP Bots + +- Compatible with serverless applications and runtimes +- No need to worry about WebSockets +- Can be infinitely scalable through standard HTTP load balancing + +## Downsides of HTTP Bots + +HTTP bots have their own set of downsides, however, and the biggest one is that they will only work with interactions (slash commands, message components, modals, etc), and you don't get any of the other events such as GUILD_MEMBER_UPDATE or MESSAGE_CREATE. + +While this does mean you don't need to worry about intents or listening to events, it does mean that if you want to make a bot that interacts with other parts of Discord without users triggering your bot themselves, you'll need to use a Gateway-based bot. + +However, you can use a hybrid approach if you choose. You can have your bot use HTTP for all interactions, and use the Gateway for all other events. This way, you can still use HTTP for things like slash commands, but still have every other event that Discord gives you. + +## Learn More + +If you want to learn more about HTTP bots and how interactions work, you can check out the [Discord Developer Documentation](https://discord.com/developers/docs/interactions/overview) for more information. diff --git a/website/content/concepts/index.mdx b/website/content/concepts/index.mdx new file mode 100644 index 00000000..59b8d1c5 --- /dev/null +++ b/website/content/concepts/index.mdx @@ -0,0 +1,6 @@ +--- +title: Concepts & Guides +description: Learn about the core concepts of Carbon and Discord bots. +index: true +--- + diff --git a/website/content/concepts/interaction-responses.mdx b/website/content/concepts/interaction-responses.mdx new file mode 100644 index 00000000..f44e8aef --- /dev/null +++ b/website/content/concepts/interaction-responses.mdx @@ -0,0 +1,39 @@ +--- +title: Replying vs Defering vs Achnowledging +description: Understand the different ways to respond to Discord interactions, including replying, deferring, and acknowledging, and learn when to use each method. +icon: MessageCircle +--- + +In the Discord interaction system, there are three ways to respond to an interaction: + +## 1. Replying + +This is the simplest way to respond to an interaction and is used for most commands. It can be done using the `reply` method on any interaction class. Keep in mind, an initial response, whether a reply or a defer, must be made within three seconds of receiving the interaction. If you don't respond within this time frame, the interaction will show an error as if you never replied. + +```ts +async run(interaction: CommandInteraction) { + await interaction.reply({ content: "Hello, world!" }) +} +``` + +## 2. Deferring + +Deferring is used when you need more time to process an interaction. When you defer an interaction, the bot sends a loading state to the user. You then have up to 15 minutes to reply using the normal [reply](#1-replying) method. + +```ts +async run(interaction: CommandInteraction) { + await interaction.defer() + // perform a time-consuming task + await interaction.reply({ content: "Hello, world!" }) +} +``` + +## 3. Acknowledging + +Acknowledging is used when you want to acknowledge an interaction without replying to it. This can only be used for message component-based interactions. It is useful for scenarios like game bots where you want to trigger an action and then only edit the original message. + +```ts +async run(interaction: ButtonInteraction) { + await interaction.acknowledge() +} +``` diff --git a/website/content/concepts/meta.json b/website/content/concepts/meta.json new file mode 100644 index 00000000..8cfca921 --- /dev/null +++ b/website/content/concepts/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Concepts & Guides", + "icon": "BookOpen", + "pages": ["http-bots", "interaction-responses", "partial-structures"] +} diff --git a/website/content/concepts/partial-structures.mdx b/website/content/concepts/partial-structures.mdx new file mode 100644 index 00000000..9b302d28 --- /dev/null +++ b/website/content/concepts/partial-structures.mdx @@ -0,0 +1,11 @@ +--- +title: Partial Structures +description: Learn about partial structures in the Discord API and how to handle them in Carbon, including retrieving full data and checking for undefined properties. +icon: SquareDashed +--- + +Partials are a concept in the Discord API that allows the API to send only a subset of the data needed for a certain structure, such as just the ID of a user or the channel ID and message ID of a message. This helps reduce the amount of data transmitted, improving performance. + +In Carbon, you may sometimes encounter partials in elements like `Interaction#message`. This occurs because the original data for that interaction only included a partial. Fortunately, all classes that may be partial are correctly typed, and a boolean `partial` property, have a `fetch()` method that allows you to retrieve the full data for the class. + +Due to this, many properties on a structure, such as a `Message`, might be undefined. Therefore, you should always check for the presence of these properties before using them. diff --git a/website/content/create-carbon/index.mdx b/website/content/create-carbon/index.mdx deleted file mode 100644 index 82ce2447..00000000 --- a/website/content/create-carbon/index.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: create-carbon ---- - -(COMING SOON!) - -`create-carbon` is a CLI tool that helps you create a new Carbon project. - -## Installation - -To install `create-carbon`, you can use the following command: - -```bash -npm install -g create-carbon -``` - -## Usage - -Once you have `create-carbon` installed, you can use it to create a new Carbon project. To do this, you can run the following command: - -```bash -create-carbon my-project -``` - -This will create a new directory called `my-project` with a basic Carbon project inside it. You can then navigate to the directory and run `pnpm install` to install the dependencies. - -## Example - -Here is an example of how you can use `create-carbon` to create a new Carbon project: - -```bash -create-carbon my-project -cd my-project -pnpm install -``` - -This will create a new directory called `my-project` with a basic Carbon project inside it. You can then navigate to the directory and run `pnpm install` to install the dependencies. - -## Contributing - -We welcome contributions to `create-carbon`! If you're interested in contributing, please check out the [Contributing Guide](https://carbon.buape.com/docs/helpful-guides/contributing) for more information, and join our [Discord](https://go.buape.com/carbon) to get involved! \ No newline at end of file diff --git a/website/content/create-carbon/meta.json b/website/content/create-carbon/meta.json deleted file mode 100644 index 3b333fd1..00000000 --- a/website/content/create-carbon/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "root": true, - "pages": ["--- Usage ---", "index", "--- API Reference ---", "...api"] -} diff --git a/website/content/carbon/even-more/contributing.mdx b/website/content/even-more/contributing.mdx similarity index 59% rename from website/content/carbon/even-more/contributing.mdx rename to website/content/even-more/contributing.mdx index 592a4bc8..863985c2 100644 --- a/website/content/carbon/even-more/contributing.mdx +++ b/website/content/even-more/contributing.mdx @@ -1,6 +1,7 @@ --- title: Contributing description: Learn how to contribute to Carbon. +icon: GitFork --- First off, thanks for taking the time to contribute! ❤️ @@ -8,17 +9,18 @@ First off, thanks for taking the time to contribute! ❤️ All types of contributions are encouraged and valued. You can see below for different ways to help and details about how Carbon handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. And if you like Carbon, but just don't have time to contribute, that's fine. There are other easy ways to support Carbon and show your appreciation, which we would also be very happy about: -- Star us on GitHub -- [Sponsor us](https://github.com/sponsors/buape) -- Refer this project in your project's readme -- Tell others about us! + +- Star us on GitHub +- [Sponsor us](https://github.com/sponsors/buape) +- Refer this project in your project's readme +- Tell others about us! ## Contributing Guidelines -- All contributions must be made in the form of a pull request. -- All pull requests must be reviewed and approved by at least one maintainer before they can be merged. -- All pull requests must be made to the `main` branch. -- All pull requests must pass the CI checks. -- All pull requests must have a clear and concise description of the changes made, and how they have been tested. -We prefer that all PRs also have an associated issue, so that we can track the progress of the PR and keep the discussion focused on the issue at hand. +- All contributions must be made in the form of a pull request. +- All pull requests must be reviewed and approved by at least one maintainer before they can be merged. +- All pull requests must be made to the `main` branch. +- All pull requests must pass the CI checks. +- All pull requests must have a clear and concise description of the changes made, and how they have been tested. +We prefer that all PRs also have an associated issue, so that we can track the progress of the PR and keep the discussion focused on the issue at hand. diff --git a/website/content/even-more/meta.json b/website/content/even-more/meta.json new file mode 100644 index 00000000..b897c85b --- /dev/null +++ b/website/content/even-more/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Even More", + "icon": "Heart", + "pages": ["contributing", "powered-by-carbon", "why-classes"] +} diff --git a/website/content/even-more/powered-by-carbon.mdx b/website/content/even-more/powered-by-carbon.mdx new file mode 100644 index 00000000..0e710c03 --- /dev/null +++ b/website/content/even-more/powered-by-carbon.mdx @@ -0,0 +1,11 @@ +--- +title: Powered by Carbon +description: See some cool apps powered by Carbon. +icon: BatteryCharging +--- + +Here are some cool projects that are powered by Carbon that you can check out! + +- [Evaluate](https://evaluate.run/products/discord-bot) - Run code snippets directly in your Discord server with the Evaluate bot! Evaluate supports over 70 languages and is perfect for quickly testing code and sharing results with your friends. + +- [DistractionAction](https://discord.com/oauth2/authorize?client_id=1213517500516667412) - Are you sick and tired of procrastinating on Discord? Use DistractionAction to take charge of your life! Mute yourself from any server you want, to stay on track and get your work done! diff --git a/website/content/carbon/even-more/why-classes.mdx b/website/content/even-more/why-classes.mdx similarity index 75% rename from website/content/carbon/even-more/why-classes.mdx rename to website/content/even-more/why-classes.mdx index b04954eb..137567c2 100644 --- a/website/content/carbon/even-more/why-classes.mdx +++ b/website/content/even-more/why-classes.mdx @@ -1,5 +1,6 @@ --- title: Why Classes? +icon: Shapes --- Hi, I'm Shadow, and I'm the designer and original developer of Carbon. @@ -9,9 +10,11 @@ When you compare Carbon to other frameworks, you may notice that Carbon is a bit ## History of Carbon ### Beginnings + When I started making Discord bots for public use instead of for my personal servers, I created a library that I used based on other bots that I had worked on, as well as some of my own ideas (which can still be found at [`@buape/lib`](https://npm.im/@buape/lib). However, it felt very hard to use sometimes. If we wanted to add any features such as cooldowns or implementing permission checks at the command level, we had to modify the lib itself to support that, and it slowed down development time a lot. In fact, for [Kiai](https://kiai.app), we ended up forking the library into Kiai's repo itself so we could make those modifications, meaning any changes we made would have to be manually upstreamed to the library later on. As you can imagine, this development process often felt very slow and clunky. ### CrossBuild + After using that library for a while, I decided to make my own library and publish it, and I called it [CrossBuild](https://github.com/crossbuildjs/crossbuild). CrossBuild was "a library that empowers bot developers to build bots for Discord and Guilded at the same time." CrossBuild was designed around the concept of a base set of classes and then several extension plugins for each platform that CrossBuild supported, such as Discord Interactions and Guilded. @@ -21,67 +24,68 @@ So, CrossBuild was archived, and I went back to working on other projects. ### Buape Studio's Internal Bot -At Buape Studios, we have an internal bot that handles all our emails, staff management, internal API, and several other tasks. It runs fully on Cloudflare Workers, and was my first time building out a full HTTP-based Discord bot from scratch. I didn't use any frameworks to build it, only the raw Discord API and typings from [`discord-api-types`](https://npm.im/discord-api-types). It was a really good experience. The code may have been messy, but it worked well and had several QOL features for me to use. +At Buape Studios, we have an internal bot that handles all our emails, staff management, internal API, and several other tasks. It runs fully on Cloudflare Workers, and was my first time building out a full HTTP-based Discord bot from scratch. I didn't use any frameworks to build it, only the raw Discord API and typings from [`discord-api-types`](https://npm.im/discord-api-types). It was a really good experience. The code may have been messy, but it worked well and had several QOL features for me to use. Here are some snippets from the original version of the bot: ```ts abstract class BaseCommand { - abstract name: string - abstract description: string - abstract guildOnly?: boolean - abstract defer?: boolean + abstract name: string; + abstract description: string; + abstract guildOnly?: boolean; + abstract defer?: boolean; } export abstract class Command extends BaseCommand { - abstract run( - interaction: APIApplicationCommandInteraction, - env: Env - ): Promise + abstract run( + interaction: APIApplicationCommandInteraction, + env: Env + ): Promise; } export abstract class CommandWithOptions extends Command { - abstract options: APIApplicationCommandBasicOption[] + abstract options: APIApplicationCommandBasicOption[]; } export abstract class CommandWithAutocomplete extends CommandWithOptions { - abstract autocomplete( - interaction: APIApplicationCommandAutocompleteInteraction, - env: Env - ): Promise + abstract autocomplete( + interaction: APIApplicationCommandAutocompleteInteraction, + env: Env + ): Promise; } ``` + ```ts export default class Cmd implements CommandWithSubcommands { - name = "staff" - description = "Staff management commands" - guildOnly = true - - subcommands = [ - new AddStaff(), - new RemoveStaff(), - new SyncStaff(), - new SetTimeZone() - ] + name = "staff"; + description = "Staff management commands"; + guildOnly = true; + + subcommands = [ + new AddStaff(), + new RemoveStaff(), + new SyncStaff(), + new SetTimeZone(), + ]; } class SyncStaff implements Command { - name = "sync" - description = "Sync staff roles with the database" - defer = true - - async run( - _interaction: APIApplicationCommandInteraction, - env: Env - ): Promise { - await syncAllStaff(env) - return { - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: "Synced staff roles with the database" - } - } - } + name = "sync"; + description = "Sync staff roles with the database"; + defer = true; + + async run( + _interaction: APIApplicationCommandInteraction, + env: Env + ): Promise { + await syncAllStaff(env); + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Synced staff roles with the database", + }, + }; + } } ``` @@ -97,4 +101,4 @@ Every part of Carbon is built to be extendable. Want to add something to the cli I've been using Carbon for a while now, and I've noticed that it's not perfect. We're always open to feedback and suggestions, so if you have any, please let me know! I'd love to hear about any bugs you find, or any features you'd like to see added. -If you're interested in contributing to Carbon, you can check out the [Contributing Guide](https://carbon.buape.com/carbon/helpful-guides/contributing) for more information, and join our [Discord](https://go.buape.com/Carbon) to get involved! \ No newline at end of file +If you're interested in contributing to Carbon, you can check out the [Contributing Guide](/even-more/contributing) for more information, and join our [Discord](https://go.buape.com/Carbon) to get involved! diff --git a/website/content/getting-started/basic-setup.mdx b/website/content/getting-started/basic-setup.mdx new file mode 100644 index 00000000..05c47609 --- /dev/null +++ b/website/content/getting-started/basic-setup.mdx @@ -0,0 +1,129 @@ +--- +title: Basic Usage +description: How to set up and configure a Carbon project, including both automatic and manual setup methods. +icon: SwatchBook +--- + +## Automatic Setup + +We recommend starting a new Carbon project using [`create-carbon`](https://npmjs.com/package/create-carbon), which will set everything up automatically for you. To create a project, run: + + + +You'll be prompted to enter a project name, select a runtime, and configure some other options. Once you've answered all the questions, `create-carbon` will generate a new project for you. + +## Manual Setup + +If you prefer to set up a Carbon project manually, follow the steps below: + +import { Steps, Step } from "fumadocs-ui/components/steps"; + + + + +### Set Up a TypeScript Project + +First, set up a new TypeScript project. You can follow the official [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) for detailed instructions on how to get started. + + + + +### Install the Package + +Let's start by adding Carbon to your project: + + + + + +### Create a Client and Handle Function + +Next, create a handle function by passing a client factory to the [`createHandle`](/api/functions/createhandle) function. The factory should return an array of plugins, the first being the client, and the rest being any other optional plugins you may want to add. + +```ts title="src/index.ts" +import { createHandle, Client } from "@buape/carbon"; + +const handle = createHandle((env) => { + const client = new Client( + { + baseUrl: String(env.BASE_URL), + clientId: String(env.DISCORD_CLIENT_ID), + clientSecret: String(env.DISCORD_CLIENT_SECRET), + publicKey: String(env.DISCORD_PUBLIC_KEY), + token: String(env.DISCORD_TOKEN), + }, + [] + ) + return [client]; +}); +``` + + + Setting environment variables will be covered in a later step. + + + + + +### Create a Command + +Now we'll create a simple command that responds with "Hello!" when invoked. This command will serve as a basic example to demonstrate how to set up and handle interactions with your bot. + +```ts title="src/commands/hello.ts" +import { Command, type CommandInteraction } from "@buape/carbon"; + +export default class HelloCommand extends Command { + name = "hello"; + description = "Say hello to the bot"; + + async run(interaction: CommandInteraction) { + await interaction.reply("Hello!"); + } +} +``` + +Then, mount the command to your client to make it available for use. This step involves importing the command and adding it to the client's configuration. + +```ts title="src/index.ts" +import HelloCommand from './commands/hello' + +const handle = createHandle((env) => { + const client = new Client( + { ... }, + [HelloCommand] + ) + return [client] +}) +``` + + + + +### Use an Adapter + +You'll now need to set up an adapter to wrap your handle function to work with your runtime, pick an adapter from the list below to continue. + + + + + + + + + diff --git a/website/content/getting-started/introduction.mdx b/website/content/getting-started/introduction.mdx new file mode 100644 index 00000000..d83662a5 --- /dev/null +++ b/website/content/getting-started/introduction.mdx @@ -0,0 +1,15 @@ +--- +title: Introduction +description: Introduction to Carbon, a framework for building HTTP-based Discord bots, including community resources and support. +icon: BookMarked +--- + +![Carbon Wordmark](https://cdn.buape.com/CarbonWordmarkTransparent.png) + +Carbon is a powerful and versatile framework designed for building HTTP-based Discord bots. It provides a robust set of tools and features that enable developers to create efficient and scalable bots with ease. Whether you're deploying on Cloudflare Workers, Node.js, Next.js, or Bun, Carbon offers the flexibility to choose the runtime that best fits your needs. + +## Join the Community + +Join our community on [Discord](https://go.buape.com/Carbon) to connect with fellow developers, share your projects, and get support. We look forward to seeing the innovative bots you create with Carbon! + +[![Discord Invite](http://invidget.switchblade.xyz/rT8vZAmVaQ?theme=light)](https://go.buape.com/carbon) diff --git a/website/content/getting-started/meta.json b/website/content/getting-started/meta.json new file mode 100644 index 00000000..f954b477 --- /dev/null +++ b/website/content/getting-started/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Getting Started", + "icon": "CirclePlay", + "pages": ["introduction", "basic-setup"] +} diff --git a/website/content/carbon/meta.json b/website/content/meta.json similarity index 68% rename from website/content/carbon/meta.json rename to website/content/meta.json index af48b469..fe7c2811 100644 --- a/website/content/carbon/meta.json +++ b/website/content/meta.json @@ -2,14 +2,11 @@ "title": "Carbon Docs", "root": true, "pages": [ - "index", "getting-started", - "http-bot", - "...", - "--- Usage ---", + "concepts", "classes", + "adapters", "plugins", - "helpful-guides", "even-more", "--- API Reference ---", "...api" diff --git a/website/content/nodejs/index.mdx b/website/content/nodejs/index.mdx deleted file mode 100644 index ff282b18..00000000 --- a/website/content/nodejs/index.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Carbon's NodeJS Wrapper ---- - -This package is a helper package for Carbon, that provides a simple way to run Carbon on a port locally, as well as a function to automatically load all commands in a folder. \ No newline at end of file diff --git a/website/content/nodejs/meta.json b/website/content/nodejs/meta.json deleted file mode 100644 index 3b333fd1..00000000 --- a/website/content/nodejs/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "root": true, - "pages": ["--- Usage ---", "index", "--- API Reference ---", "...api"] -} diff --git a/website/content/plugins/index.mdx b/website/content/plugins/index.mdx new file mode 100644 index 00000000..59c7ffa1 --- /dev/null +++ b/website/content/plugins/index.mdx @@ -0,0 +1,9 @@ +--- +title: Plugins +index: true +--- + + +Plugins in Carbon are modular components that extend the functionality of the core system. They allow for the addition of specific features or integrations separate from the client itself. Each plugin encapsulates a distinct piece of functionality, such as handling specific types of requests, managing user roles, or integrating with external services. + +In Carbon, plugins are implemented as classes that extend the base [`Plugin`](/api/index/classes/Plugin) class, [`Client`](/api/index/classes/Client) itself is really a plugin. They can be instantiated and configured with various options to tailor their behavior to the needs of the application. For example, the [`LinkedRoles`](/api/index/plugins/linked-roles) plugin manages user roles and OAuth routes, providing a way to verify and assign roles based on specific criteria. diff --git a/website/content/plugins/linked-roles.mdx b/website/content/plugins/linked-roles.mdx new file mode 100644 index 00000000..70f85961 --- /dev/null +++ b/website/content/plugins/linked-roles.mdx @@ -0,0 +1,76 @@ +--- +title: Linked Roles +description: How to use Linked Roles in Carbon to create roles that users can claim based on specific criteria, including setup and deployment instructions. +icon: Link +--- + +import { Steps, Step } from "fumadocs-ui/components/steps"; + +Linked Roles are a handy feature of Discord that allows you to create roles that users have to meet certain criteria in order to claim those roles. + + + + + + +## Usage + +Linked Roles are straightforward to use in Carbon, simplify create an instance and pass it to the plugins array in your `createHandle` function factory. + + +You can only have five metadata per application, and they apply across all guilds your app is in. + + + + +### Add a Linked Roles Instance + +To add Linked Roles to your bot, you'll need to create a new instance of the `LinkedRoles` class and pass it your client and some metadata. The metadata is an array of objects that define the criteria for each role. In this example, we're creating a role that can only be claimed by users who have the `is_staff` metadata set to `true`. + +```ts title="src/index.ts" +import { createHandle, Client, ApplicationRoleConnectionMetadataType } from "@buape/carbon" +import { LinkedRoles } from "@buape/carbon/linked-roles" + +const handle = createHandle((env) => { + const client = new Client({ ... }, [ ... ]) + const linkedRoles = new LinkedRoles(client, { + metadata: [ + { + key: 'is_staff', + name: 'Verified Staff', + description: 'Whether the user is a verified staff member', + type: ApplicationRoleConnectionMetadataType.BooleanEqual + } + ], + metadataCheckers: { + is_staff: async (userId) => { + const allStaff = ["439223656200273932"] + return allStaff.includes(userId) + } + } + }) + return [client, linkedRoles] +}) +``` + + + +### Configure Portal URLs + +Just like with interactions, you'll need to configure some URLs in your [Discord Developer Portal](https://discord.com/developers/applications) to handle Linked Roles. Firstly, set "Linked Roles Verification URL" to `/linked-roles/verify-user` and add a OAuth2 redirect URL of `/linked-roles/verify-user/callback`. + + + +### Deploy Your Metadata to Discord + +Finally, to deploy your linked roles metadata to Discord, navigate to `/linked-roles/deploy?secret=` in your browser. This will send your metadata to Discord, where it will be used to create the roles. + + diff --git a/website/content/plugins/meta.json b/website/content/plugins/meta.json new file mode 100644 index 00000000..0b9b9f27 --- /dev/null +++ b/website/content/plugins/meta.json @@ -0,0 +1,5 @@ +{ + "title": "Plugins", + "icon": "Puzzle", + "pages": ["linked-roles"] +} diff --git a/website/content/request/index.mdx b/website/content/request/index.mdx deleted file mode 100644 index 87f0ff1e..00000000 --- a/website/content/request/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Carbon's RequestClient ---- - -This is the internal package that handles making requests to the Discord API. -It is used by Carbon, and you should not need to use it directly, but feel free to if you feel like living dangerously. diff --git a/website/content/request/meta.json b/website/content/request/meta.json deleted file mode 100644 index 3b333fd1..00000000 --- a/website/content/request/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "root": true, - "pages": ["--- Usage ---", "index", "--- API Reference ---", "...api"] -} diff --git a/website/copy-api.sh b/website/copy-api.sh index 8a9bbdf7..43452a79 100755 --- a/website/copy-api.sh +++ b/website/copy-api.sh @@ -1,8 +1,6 @@ #!/bin/bash -WEBSITE_DIR="$(dirname "$(realpath "$0")")" -CONTENT_DIR="$WEBSITE_DIR/content" -PACKAGES_DIR="$WEBSITE_DIR/../packages" +## FUNCTIONS ## # Convert a kebab-case string to Title Case # $1: The kebab-case string @@ -10,81 +8,59 @@ kebab_to_title_case() { echo "$1" | sed -e 's/-/ /g' -e 's/\b\(.\)/\u\1/g' } -# Copy API docs for each package -# $1: The package directory -copy_api_docs() { - for dir in $1; do - local pkg_name="$(basename "$dir")" - local from_dir="$PACKAGES_DIR/$pkg_name/docs" - local to_dir="$CONTENT_DIR/$pkg_name/api" - - rm -rf "$to_dir" - mkdir -p "$to_dir" - cp -r "$from_dir"/* "$to_dir" - done -} - # Add meta.json files to the API doc pages that need them # $1: The API directory add_meta_files() { - for dir in $1; do - if [ -d "$api_dir/type-aliases" ]; then - echo "{ \"title\": \"Type Aliases\" }" >"$api_dir/type-aliases/meta.json" - fi - done + if [ -d "$1/type-aliases" ]; then + echo "{ \"title\": \"Type Aliases\" }" >"$1/type-aliases/meta.json" + fi } # Process the subpackage (aka exports) of a package -# $1: The package name +# $1: The parent package name # $2: The glob of subpackages process_subpackage() { for dir in $2; do - local pkg_name="$(basename "$dir")" - local scope_name="$(basename "$(dirname "$dir")")" - if [ "$pkg_name" = "*" ]; then continue; fi - echo "Processing subpackage @buape/$1/$pkg_name" + local parent_name="$1" + local this_name="$(basename "$dir")" + local this_type="$(basename "$(dirname "$dir")")" + if [ "$this_name" = "*" ]; then continue; fi - if [ "$pkg_name" = "index" ]; then - local name="@buape/$1" + if [ "$this_name" = "index" ]; then + local name="@buape/$parent_name" local title="Core" rm -rf "$dir/../index.mdx" - elif [ "$scope_name" = "plugins" ]; then - local name="@buape/$1/$pkg_name" - local title="$(kebab_to_title_case "$pkg_name")" - elif [ "$scope_name" = "adapters" ]; then - local name="@buape/$1/adapters\/$pkg_name" - local title="$(kebab_to_title_case "$pkg_name")" + elif [ "$this_type" = "plugins" ]; then + local name="@buape/$parent_name/$this_name" + local title="$(kebab_to_title_case "$this_name")" + elif [ "$this_type" = "adapters" ]; then + local name="@buape/$parent_name/adapters/$this_name" + local title="$(kebab_to_title_case "$this_name")" fi + echo "Processing subpackage $name" escaped_name=$(echo "$name" | sed 's/@/\\@/g') escaped_name=$(echo "$escaped_name" | sed 's/\//\\/g') sed -i -e "s/title: \(.*\)/title: \"$escaped_name\"/" "$dir/index.mdx" sed -i -e "s/## Index/ /" "$dir/index.mdx" echo "{ \"title\": \"$title\" }" >"$dir/meta.json" - add_meta_files "$dir" done } -# Process a glob of packages -# $1: The glob of packages -process_package() { - for dir in $1; do - local pkg_name="$(basename "$dir")" +## MAIN ## - echo "Processing package @buape/$pkg_name" - copy_api_docs "$dir" +PKG_NAME="carbon" +THIS_DIR="$(dirname "$(realpath "$0")")" +INPUT_DIR="$THIS_DIR/../packages/$PKG_NAME/docs" +OUTPUT_DIR="$THIS_DIR/content/api" - local api_dir="$CONTENT_DIR/$pkg_name/api" - # Process each of the core subpackages separately - if [ "$pkg_name" = "carbon" ]; then - process_subpackage "carbon" "$api_dir/index" - process_subpackage "carbon" "$api_dir/plugins/*" - # process_subpackage "carbon" "$api_dir/adapters/*" - else - add_meta_files "$api_dir" - fi - done -} +echo "Processing package @buape/$PKG_NAME" +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" +cp -r "$INPUT_DIR"/* "$OUTPUT_DIR" -process_package "$PACKAGES_DIR/*" +process_subpackage "$PKG_NAME" "$OUTPUT_DIR/index" +process_subpackage "$PKG_NAME" "$OUTPUT_DIR/plugins/*" +process_subpackage "$PKG_NAME" "$OUTPUT_DIR/adapters/*" +add_meta_files "$OUTPUT_DIR" diff --git a/website/mdx-components.tsx b/website/mdx-components.tsx deleted file mode 100644 index 05cc24e3..00000000 --- a/website/mdx-components.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock" -import { ImageZoom } from "fumadocs-ui/components/image-zoom" -import { Tab, Tabs } from "fumadocs-ui/components/tabs" -import defaultComponents from "fumadocs-ui/mdx" -import { CirclePlay, Code, Heart, Library } from "lucide-react" - -import type { MDXComponents } from "mdx/types" - -export function useMDXComponents(): MDXComponents { - return { - ...defaultComponents, - CirclePlay, - Code, - Library, - Heart, - pre: ({ ref: _ref, ...props }) => ( - -
{props.children}
-
- ), - Tab, - ImageZoom, - CommandTabs: ({ - args, - command, - executer = false, - platforms = ["pnpm", "npm", "yarn"] - }: { - args: string[] - command: string - executer: boolean - platforms?: string[] - }) => ( - - {platforms.map((runner) => ( - - -
-								{args
-									.map(
-										(x) =>
-											`${
-												executer
-													? runner === "npm"
-														? "npx"
-														: runner === "bun"
-															? "bunx"
-															: runner === "pnpm"
-																? "pnpm dlx"
-																: runner === "yarn"
-																	? "yarn dlx"
-																	: runner
-													: runner
-											} ${command === "add" && runner === "npm" ? "install" : command} ${x}`
-									)
-									.join("\n")}
-							
-
-
- ))} -
- ) - } -} diff --git a/website/modes.ts b/website/modes.ts deleted file mode 100644 index b30c3eb4..00000000 --- a/website/modes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Computer, Link, type LucideIcon, Package } from "lucide-react" - -export interface Mode { - param: string - name: string - description: string - icon: LucideIcon -} - -export const modes: Mode[] = [ - { - param: "carbon", - name: "Carbon", - description: "@buape/carbon", - icon: Package - }, - { - param: "nodejs", - name: "NodeJS", - description: "@buape/carbon-nodejs", - icon: Computer - }, - { - param: "request", - name: "Request", - description: "@buape/carbon-request", - icon: Link - } -] diff --git a/website/next.config.mjs b/website/next.config.mjs index 4b9cfaaa..f01d38fa 100644 --- a/website/next.config.mjs +++ b/website/next.config.mjs @@ -7,7 +7,20 @@ const config = { reactStrictMode: true, images: { unoptimized: true - } + }, + redirects: async () => [ + { + source: "/", + destination: "/getting-started/introduction", + permanent: false + }, + { + // Redirect old Carbon URLs to introduction page + source: "/carbon/:path*", + destination: "/getting-started/introduction", + permanent: false + } + ] } export default withMDX(config) diff --git a/website/type-aliases.meta.json b/website/type-aliases.meta.json deleted file mode 100644 index bfcb1fb8..00000000 --- a/website/type-aliases.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Type Aliases" -}