diff --git a/.changeset/wild-wombats-flow.md b/.changeset/wild-wombats-flow.md new file mode 100644 index 00000000..a56c27d1 --- /dev/null +++ b/.changeset/wild-wombats-flow.md @@ -0,0 +1,6 @@ +--- +"@buape/carbon": minor +"create-carbon": minor +--- + +refactor: replace creating handle with new adapters diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml deleted file mode 100644 index d7ad3ba3..00000000 --- a/.github/workflows/ci-main.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: CI - Main - -on: - push: - branches: - - main - -jobs: - biome: - name: Check Formatting - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Biome - uses: biomejs/setup-biome@v2 - - name: Run Biome - run: biome ci . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f042b36..d7fdbb1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,12 @@ name: CI on: + push: + branches: + - main merge_group: types: [checks_requested] - pull_request_target: + pull_request: types: - opened - synchronize @@ -12,10 +15,21 @@ on: - "!renovate/web" jobs: - build: - name: Check for Successful Build + validate: + name: ${{ matrix.task.name }} timeout-minutes: 15 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + task: + - name: Check Build + run: pnpm run ci + - name: Check Formatting + run: biome ci . + - name: Run Tests + run: pnpm run test + steps: - name: Check out code uses: actions/checkout@v4 @@ -23,31 +37,9 @@ jobs: - name: Setup uses: ./.github/actions/setup - - name: Build - run: pnpm run ci - - biome: - name: Check Formatting - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - name: Setup Biome + if: ${{ matrix.task.run == 'biome ci .' }} uses: biomejs/setup-biome@v2 - - name: Run Biome - run: biome ci . - - test: - name: Run Test Suites - timeout-minutes: 15 - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - - - name: Setup - uses: ./.github/actions/setup - - name: Run tests - run: pnpm run test + - name: ${{ matrix.task.name }} + run: ${{ matrix.task.run }} diff --git a/apps/cloudo/README.md b/apps/cloudo/README.md index cc5b2801..89f4fed9 100644 --- a/apps/cloudo/README.md +++ b/apps/cloudo/README.md @@ -2,6 +2,6 @@ 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. +To learn how to get started in development, deploy to production, or add commands, head over to the [documentation](https://carbon.buape.com/adapters/fetch/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/src/entry.ts b/apps/cloudo/src/entry.ts new file mode 100644 index 00000000..a7f19d5f --- /dev/null +++ b/apps/cloudo/src/entry.ts @@ -0,0 +1,19 @@ +import type { ExecutionContext, Request } from "@cloudflare/workers-types" + +declare global { + namespace NodeJS { + interface ProcessEnv extends Record {} + interface Process { + env: ProcessEnv + } + } + var process: NodeJS.Process +} + +export default { + async fetch(req: Request, env: NodeJS.ProcessEnv, ctx: ExecutionContext) { + Reflect.set(globalThis, "process", { env }) + const mod = await import("./index.js") + return mod.default.fetch(req, ctx) + } +} diff --git a/apps/cloudo/src/index.ts b/apps/cloudo/src/index.ts index e1cadcac..d02b0378 100644 --- a/apps/cloudo/src/index.ts +++ b/apps/cloudo/src/index.ts @@ -1,5 +1,5 @@ -import { Client, createHandle } from "@buape/carbon" -import { createHandler } from "@buape/carbon/adapters/cloudflare" +import { Client } from "@buape/carbon" +import { createHandler } from "@buape/carbon/adapters/fetch" import { ApplicationRoleConnectionMetadataType, LinkedRoles @@ -15,50 +15,62 @@ import SubcommandsCommand from "./commands/testing/subcommand.js" import SubcommandGroupsCommand from "./commands/testing/subcommandgroup.js" import UserCommand from "./commands/testing/user_command.js" -const handle = createHandle((env) => { - const client = new Client( +const linkedRoles = new LinkedRoles({ + metadata: [ { - 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: [ - { - 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 - } + key: "is_staff", + name: "Verified Staff", + description: "Whether the user is a verified staff member", + type: ApplicationRoleConnectionMetadataType.BooleanEqual } - }) - return [client, linkedRoles] + ], + metadataCheckers: { + is_staff: async (userId) => { + const isAllowed = ["439223656200273932"] + if (isAllowed.includes(userId)) return true + return false + } + } }) -const handler = createHandler(handle) +const client = new Client( + { + baseUrl: process.env.BASE_URL, + deploySecret: process.env.DEPLOY_SECRET, + clientId: process.env.DISCORD_CLIENT_ID, + clientSecret: process.env.DISCORD_CLIENT_SECRET, + publicKey: process.env.DISCORD_PUBLIC_KEY, + token: process.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() + ], + [linkedRoles] +) + +const handler = createHandler(client) export default { fetch: handler } + +declare global { + namespace NodeJS { + interface ProcessEnv { + BASE_URL: string + DEPLOY_SECRET: string + DISCORD_CLIENT_ID: string + DISCORD_CLIENT_SECRET: string + DISCORD_PUBLIC_KEY: string + DISCORD_BOT_TOKEN: string + } + } +} diff --git a/apps/cloudo/wrangler.toml b/apps/cloudo/wrangler.toml index 35b2458c..c92209bb 100644 --- a/apps/cloudo/wrangler.toml +++ b/apps/cloudo/wrangler.toml @@ -1,3 +1,3 @@ name = "cloudo" -main = "src/index.ts" +main = "src/entry.ts" compatibility_date = "2024-10-18" \ No newline at end of file diff --git a/apps/rocko/src/commands/testing/allow_mentions.ts b/apps/rocko/src/commands/testing/allow_mentions.ts index 3eeb84eb..5c699b0c 100644 --- a/apps/rocko/src/commands/testing/allow_mentions.ts +++ b/apps/rocko/src/commands/testing/allow_mentions.ts @@ -1,11 +1,14 @@ -import { Command, CommandInteraction } from "@buape/carbon"; +import { Command, type CommandInteraction } from "@buape/carbon" export default class MentionsCommand extends Command { - name = "mention" - description = "Allowed Mentions Test" - defer = true + name = "mention" + description = "Allowed Mentions Test" + defer = true - async run(interaction: CommandInteraction) { - await interaction.reply({content: `<@${interaction.userId}>`, allowedMentions: {parse: []}}) - } -} \ No newline at end of file + async run(interaction: CommandInteraction) { + await interaction.reply({ + content: `<@${interaction.userId}>`, + allowedMentions: { parse: [] } + }) + } +} diff --git a/apps/rocko/src/index.ts b/apps/rocko/src/index.ts index 8853518f..33204b2a 100644 --- a/apps/rocko/src/index.ts +++ b/apps/rocko/src/index.ts @@ -1,11 +1,12 @@ import "dotenv/config" -import { Client, createHandle } from "@buape/carbon" +import { Client } from "@buape/carbon" import { createServer } from "@buape/carbon/adapters/node" import { ApplicationRoleConnectionMetadataType, LinkedRoles } from "@buape/carbon/linked-roles" import PingCommand from "./commands/ping.js" +import MentionsCommand from "./commands/testing/allow_mentions.js" import AttachmentCommand from "./commands/testing/attachment.js" import ButtonCommand from "./commands/testing/button.js" import EphemeralCommand from "./commands/testing/ephemeral.js" @@ -16,53 +17,64 @@ 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" -import MentionsCommand from "./commands/testing/allow_mentions.js" -const handle = createHandle((env) => { - const client = new Client( +const linkedRoles = new LinkedRoles({ + metadata: [ { - 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(), - new MentionsCommand() - ] - ) - 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 - } + key: "random", + name: "Verified Staff", + description: "Whether the user is a verified staff member", + type: ApplicationRoleConnectionMetadataType.BooleanEqual + } + ], + metadataCheckers: { + random: async (userId) => { + const isAllowed = ["548150274414608399"] + if (isAllowed.includes(userId)) return true + return false } - }) - return [client, linkedRoles] + } }) -createServer(handle, { port: 3000 }) +const client = new Client( + { + baseUrl: process.env.BASE_URL, + deploySecret: process.env.DEPLOY_SECRET, + clientId: process.env.DISCORD_CLIENT_ID, + clientSecret: process.env.DISCORD_CLIENT_SECRET, + publicKey: process.env.DISCORD_PUBLIC_KEY, + token: process.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(), + new MentionsCommand() + ], + [linkedRoles] +) + +createServer(client, { port: 3000 }) + +declare global { + namespace NodeJS { + interface ProcessEnv { + BASE_URL: string + DEPLOY_SECRET: string + DISCORD_CLIENT_ID: string + DISCORD_CLIENT_SECRET: string + DISCORD_PUBLIC_KEY: string + DISCORD_BOT_TOKEN: string + } + } +} diff --git a/packages/carbon/src/abstracts/Plugin.ts b/packages/carbon/src/abstracts/Plugin.ts index 3a262f48..ce5c5ce9 100644 --- a/packages/carbon/src/abstracts/Plugin.ts +++ b/packages/carbon/src/abstracts/Plugin.ts @@ -1,12 +1,49 @@ +import type { Client } from "../classes/Client.js" + +/** + * The base class for all plugins + */ export abstract class Plugin { - routes: Route[] = [] + /** + * Registers the client with this plugin + * @param client The client to register + */ + registerClient?(client: Client): void + + /** + * Registers the routes of this plugin with the client + * @param client The client to register the routes with + */ + registerRoutes?(client: Client): void } export interface Route { + /** + * The HTTP method of the route + */ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" + + /** + * The relative path of the route + */ path: `/${string}` + + /** + * The handler function for the route + * @param req The request object + * @param ctx The context object + * @returns The response object or a promise that resolves to a response object + */ handler(req: Request, ctx?: Context): Response | Promise + + /** + * Whether this route requires authentication + */ protected?: boolean + + /** + * Whether this route is disabled + */ disabled?: boolean } diff --git a/packages/carbon/src/adapters/bun/index.ts b/packages/carbon/src/adapters/bun/index.ts index 340178ae..a4a17fd1 100644 --- a/packages/carbon/src/adapters/bun/index.ts +++ b/packages/carbon/src/adapters/bun/index.ts @@ -1,24 +1,20 @@ import Bun from "bun" -import type { Handle } from "../../createHandle.js" -import type { ServerOptions } from "../shared.js" +import type { Client } from "../../index.js" +import { createHandler } from "../fetch/index.js" -export type Server = ReturnType +export type Server = Bun.Server +export type ServerOptions = Omit /** - * 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, { ... }) - * ``` + * Creates a server for the client using Bun.serve + * @param client The Carbon client to create the server for + * @param options Additional options for the server + * @returns The Bun.Server instance */ -export function createServer(handle: Handle, options: ServerOptions): Server { - const fetch = handle(process.env) +export function createServer(client: Client, options: ServerOptions): Server { + const fetch = createHandler(client) return Bun.serve({ - fetch: (req) => fetch(req, {}), - port: options.port, - hostname: options.hostname + ...options, + fetch: (r) => fetch(r, {}) }) } diff --git a/packages/carbon/src/adapters/cloudflare/index.ts b/packages/carbon/src/adapters/cloudflare/index.ts deleted file mode 100644 index 05fc9104..00000000 --- a/packages/carbon/src/adapters/cloudflare/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/fetch/index.ts b/packages/carbon/src/adapters/fetch/index.ts new file mode 100644 index 00000000..dc2a4d42 --- /dev/null +++ b/packages/carbon/src/adapters/fetch/index.ts @@ -0,0 +1,52 @@ +import type { Context, Route } from "../../abstracts/Plugin.js" +import type { Client } from "../../classes/Client.js" + +export type Handler = (req: Request, ctx: Context) => Promise + +/** + * Creates a fetch handler function for the clients routes + * @param client The client to create the handler for + * @returns The handler function + */ +export function createHandler(client: Client): Handler { + return async (req: Request, ctx: Context) => { + const method = req.method + const url = new URL(req.url, "http://localhost") + const pathname = // + resolveRequestPathname(new URL(client.options.baseUrl), url) + if (!pathname) return new Response("Not Found", { status: 404 }) + + const matchedRoutesByPath = // + client.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 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) +} diff --git a/packages/carbon/src/adapters/next/index.ts b/packages/carbon/src/adapters/next/index.ts deleted file mode 100644 index 80a57116..00000000 --- a/packages/carbon/src/adapters/next/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 index b8d145c5..9b329af3 100644 --- a/packages/carbon/src/adapters/node/index.ts +++ b/packages/carbon/src/adapters/node/index.ts @@ -1,24 +1,21 @@ import * as Hono from "@hono/node-server" -import type { Handle } from "../../createHandle.js" -import type { ServerOptions } from "../shared.js" +import type { Client } from "../../index.js" +import { createHandler } from "../fetch/index.js" -export type Server = ReturnType +export type Server = Hono.ServerType +export type ServerOptions = Omit[0], "fetch"> /** - * 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, { ... }) - * ``` + * Creates a server for the client using Hono.serve under the hood + * @param client The Carbon client to create the server for + * @param options Additional options for the server + * @returns The server instance */ -export function createServer(handle: Handle, options: ServerOptions): Server { - const fetch = handle(process.env) +export function createServer(client: Client, options: ServerOptions): Server { + const fetch = createHandler(client) return Hono.serve({ - fetch: (req) => fetch(req, {}), - port: options.port, - hostname: options.hostname + // Weird type issue with options.createServer ?? + ...(options as object), + fetch: (r) => fetch(r, {}) }) } diff --git a/packages/carbon/src/adapters/shared.ts b/packages/carbon/src/adapters/shared.ts deleted file mode 100644 index 68736777..00000000 --- a/packages/carbon/src/adapters/shared.ts +++ /dev/null @@ -1,11 +0,0 @@ -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 3fd67f14..e127cd2d 100644 --- a/packages/carbon/src/classes/Client.ts +++ b/packages/carbon/src/classes/Client.ts @@ -11,7 +11,7 @@ import { Routes } from "discord-api-types/v10" import type { BaseCommand } from "../abstracts/BaseCommand.js" -import { type Context, Plugin } from "../abstracts/Plugin.js" +import type { Context, Plugin, Route } from "../abstracts/Plugin.js" import { channelFactory } from "../factories/channelFactory.js" import { CommandHandler } from "../internals/CommandHandler.js" import { ComponentHandler } from "../internals/ComponentHandler.js" @@ -76,7 +76,15 @@ export interface ClientOptions { /** * The main client used to interact with Discord */ -export class Client extends Plugin { +export class Client { + /** + * The routes that the client will handle + */ + routes: Route[] = [] + /** + * The plugins that the client has registered + */ + plugins: Plugin[] = [] /** * The options used to initialize the client */ @@ -109,10 +117,13 @@ export class Client extends Plugin { * Creates a new client * @param options The options used to initialize the client * @param commands The commands that the client has registered + * @param plugins The plugins that the client should use */ - constructor(options: ClientOptions, commands: BaseCommand[]) { - super() - + constructor( + options: ClientOptions, + commands: BaseCommand[], + plugins: Plugin[] = [] + ) { 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") @@ -121,7 +132,9 @@ export class Client extends Plugin { this.options = options this.commands = commands - this.appendRoutes() + + // Remove trailing slashes from the base URL + options.baseUrl = options.baseUrl.replace(/\/+$/, "") this.commandHandler = new CommandHandler(this) this.componentHandler = new ComponentHandler(this) @@ -129,6 +142,13 @@ export class Client extends Plugin { this.rest = new RequestClient(options.token, options.requestOptions) + this.appendRoutes() + for (const plugin of plugins) { + plugin.registerClient?.(this) + plugin.registerRoutes?.(this) + this.plugins.push(plugin) + } + if (!options.disableAutoRegister) { for (const command of commands) { for (const component of command.components) diff --git a/packages/carbon/src/createHandle.ts b/packages/carbon/src/createHandle.ts deleted file mode 100644 index d78a7a78..00000000 --- a/packages/carbon/src/createHandle.ts +++ /dev/null @@ -1,69 +0,0 @@ -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, "http://localhost") - 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 ef6f88f5..b8799512 100644 --- a/packages/carbon/src/index.ts +++ b/packages/carbon/src/index.ts @@ -1,5 +1,3 @@ -export * from "./createHandle.js" - // ----- Abstracts ----- export * from "./abstracts/AnySelectMenu.js" export * from "./abstracts/AnySelectMenuInteraction.js" @@ -67,7 +65,5 @@ export * from "./structures/User.js" // ----- Misc ----- export * from "discord-api-types/v10" -export * from "./adapters/shared.js" -export * from "./createHandle.js" export * from "./types.js" export * from "./utils.js" diff --git a/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts b/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts index 32d14aac..92129f34 100644 --- a/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts +++ b/packages/carbon/src/plugins/linked-roles/LinkedRoles.ts @@ -33,63 +33,61 @@ declare module "../../classes/Client.d.ts" { * * @example * ```ts - * import { createHandle, Client, ApplicationRoleConnectionMetadataType } from "@buape/carbon" + * import { 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] + * const linkedRoles = new LinkedRoles({ + * 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) + * } + * } * }) + * + * const client = new Client({ ... }, [ ... ], [linkedRoles]) * ``` */ export class LinkedRoles extends Plugin { - client: Client + client?: Client options: LinkedRolesOptions - constructor(client: Client, options: LinkedRolesOptions) { + constructor(options: LinkedRolesOptions) { super() + this.options = { ...options } + } + public registerClient(client: Client): void { if (!client.options.baseUrl) throw new Error("Missing base URL") if (!client.options.clientSecret) throw new Error("Missing client secret") - if (!client.options.deploySecret && !options.disableDeployRoute) + if (!client.options.deploySecret && !this.options.disableDeployRoute) throw new Error("Missing deploy secret") - this.client = client - this.options = { ...options } - this.appendRoutes() } - private appendRoutes() { - this.routes.push({ + public registerRoutes(client: Client) { + client.routes.push({ method: "GET", path: "/linked-roles/deploy", handler: this.handleDeployRequest.bind(this), protected: true, disabled: this.options.disableDeployRoute }) - this.routes.push({ + client.routes.push({ method: "GET", path: "/linked-roles/verify-user", handler: this.handleUserVerificationRequest.bind(this), disabled: this.options.disableVerifyUserRoute }) - this.routes.push({ + client.routes.push({ method: "GET", path: "/linked-roles/verify-user/callback", handler: this.handleUserVerificationCallbackRequest.bind(this), @@ -97,6 +95,10 @@ export class LinkedRoles extends Plugin { }) } + private assertRegistered(): asserts this is { client: Client } { + if (!this.client) throw new Error("Client not registered") + } + /** * Handle a request to deploy the linked roles to Discord * @returns A response @@ -111,6 +113,7 @@ export class LinkedRoles extends Plugin { * @returns A response */ public async handleUserVerificationRequest() { + this.assertRegistered() return new Response("Found", { status: 302, headers: { @@ -125,6 +128,8 @@ export class LinkedRoles extends Plugin { * @returns A response */ public async handleUserVerificationCallbackRequest(req: Request) { + this.assertRegistered() + const url = new URL(req.url) const code = String(url.searchParams.get("code")) @@ -186,6 +191,8 @@ export class LinkedRoles extends Plugin { } private async getOAuthTokens(code: string) { + this.assertRegistered() + const url = "https://discord.com/api/v10/oauth2/token" const body = new URLSearchParams({ client_id: this.client.options.clientId, @@ -218,6 +225,8 @@ export class LinkedRoles extends Plugin { metadata: Record, tokens: Tokens ) { + this.assertRegistered() + const url = `https://discord.com/api/v10/users/@me/applications/${this.client.options.clientId}/role-connection` const response = await fetch(url, { method: "PUT", @@ -237,6 +246,8 @@ export class LinkedRoles extends Plugin { } private async setMetadata(data: typeof this.options.metadata) { + this.assertRegistered() + const response = await fetch( `https://discord.com/api/v10/applications/${this.client.options.clientId}/role-connections/metadata`, { diff --git a/packages/create-carbon/src/tools/npmHelpers.ts b/packages/create-carbon/src/tools/npmHelpers.ts index a9bdc47f..996d098c 100644 --- a/packages/create-carbon/src/tools/npmHelpers.ts +++ b/packages/create-carbon/src/tools/npmHelpers.ts @@ -46,10 +46,10 @@ const dependencies = { typescript: undefined, // Node "@types/node": undefined, + dotenv: undefined, "tsc-watch": undefined, // Bun "@types/bun": undefined, - dotenv: undefined, // Cloudflare wrangler: undefined, "@cloudflare/workers-types": undefined, diff --git a/packages/create-carbon/template/_cloudflare/entry.ts.hbs b/packages/create-carbon/template/_cloudflare/entry.ts.hbs new file mode 100644 index 00000000..b1eb0bfe --- /dev/null +++ b/packages/create-carbon/template/_cloudflare/entry.ts.hbs @@ -0,0 +1,25 @@ +--- +{{#if (eq runtime "cloudflare")}} +path: src/entry.ts +{{/if}} +--- + +import type { Request, ExecutionContext } from '@cloudflare/workers-types' + +declare global { + namespace NodeJS { + interface ProcessEnv extends Record {} + interface Process { + env: ProcessEnv + } + } + var process: NodeJS.Process +} + +export default { + async fetch(req: Request, env: NodeJS.ProcessEnv, ctx: ExecutionContext) { + Reflect.set(globalThis, "process", { env }) + const mod = await import("./index.js") + return mod.default.fetch(req, ctx) + } +} \ No newline at end of file diff --git a/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs b/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs index 59331a7e..5114bb23 100644 --- a/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs +++ b/packages/create-carbon/template/_cloudflare/wrangler.toml.hbs @@ -5,5 +5,5 @@ path: wrangler.toml --- name = "{{name}}" -main = "src/index.ts" +main = "src/entry.ts" compatibility_date = "{{todaysDate}}" \ No newline at end of file diff --git a/packages/create-carbon/template/index.ts.hbs b/packages/create-carbon/template/index.ts.hbs index 2190a4c5..536f9eef 100644 --- a/packages/create-carbon/template/index.ts.hbs +++ b/packages/create-carbon/template/index.ts.hbs @@ -9,18 +9,16 @@ path: src/index.ts {{#if (eq runtime "node")}} import 'dotenv/config'; {{/if}} -import { Client, createHandle } from "@buape/carbon" +import { Client } 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") }} +{{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" +{{else if (or (eq runtime "cloudflare") (eq runtime "next"))}} +import { createHandler } from "@buape/carbon/adapters/fetch" {{/if}} {{#if (eq runtime "next")}} import PingCommand from "~/commands/ping" @@ -30,53 +28,65 @@ import PingCommand from "./commands/ping.js" import ButtonCommand from "./commands/button.js" {{/if}} -const handle = createHandle((env) => { - const client = new Client( +{{#if plugins.linkedRoles}} +const linkedRoles = new LinkedRoles({ + metadata: [ { - {{#if plugins.linkedRoles}} - baseUrl: String(env.BASE_URL), - {{/if}} - deploySecret: String(env.DEPLOY_SECRET), - clientId: String(env.DISCORD_CLIENT_ID), - {{#if plugins.linkedRoles}} - clientSecret: String(env.DISCORD_CLIENT_SECRET), - {{/if}} - publicKey: String(env.DISCORD_PUBLIC_KEY), - token: String(env.DISCORD_BOT_TOKEN) + key: "is_staff", + name: "Verified Staff", + description: "Whether the user is a verified staff member", + type: ApplicationRoleConnectionMetadataType.BooleanEqual }, - [ - 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 - } + ], + 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}} + +const client = new Client( + { + baseUrl: process.env.BASE_URL, + deploySecret: process.env.DEPLOY_SECRET, + clientId: process.env.DISCORD_CLIENT_ID, + {{#if plugins.linkedRoles}} + clientSecret: process.env.DISCORD_CLIENT_SECRET, + {{/if}} + publicKey: process.env.DISCORD_PUBLIC_KEY, + token: process.env.DISCORD_BOT_TOKEN, + }, + [ + new PingCommand(), + new ButtonCommand() + ]{{#if plugins.linkedRoles}}, + [linkedRoles]{{/if}} +) {{#if (or (eq runtime "node") (eq runtime "bun"))}} -createServer(handle, { port: 3000 }) +createServer(client, { port: 3000 }) {{else if (eq runtime "cloudflare")}} -const handler = createHandler(handle) +const handler = createHandler(client) export default { fetch: handler } {{else if (eq runtime "next")}} -const handler = createHandler(handle) +const handler = createHandler(client) export { handler as GET, handler as POST } -{{/if}} \ No newline at end of file +{{/if}} + +declare global { + namespace NodeJS { + interface ProcessEnv { + BASE_URL: string; + DEPLOY_SECRET: string; + DISCORD_CLIENT_ID: string; + {{#if plugins.linkedRoles}} + DISCORD_CLIENT_SECRET: string; + {{/if}} + DISCORD_PUBLIC_KEY: string; + DISCORD_BOT_TOKEN: string; + } + } +} diff --git a/packages/create-carbon/template/tsconfig.json.hbs b/packages/create-carbon/template/tsconfig.json.hbs index fc8b4d67..a537bf48 100644 --- a/packages/create-carbon/template/tsconfig.json.hbs +++ b/packages/create-carbon/template/tsconfig.json.hbs @@ -11,7 +11,7 @@ path: tsconfig.json {{#if (eq runtime "next")}} "lib": ["DOM", "DOM.Iterable", "ESNext"], {{else}} - "lib": ["es2022"], + "lib": ["dom", "es2022"], {{/if}} {{#if (eq runtime "next")}} "noEmit": true, diff --git a/website/app/layout.tsx b/website/app/layout.tsx index 4e3f0998..7d637acf 100644 --- a/website/app/layout.tsx +++ b/website/app/layout.tsx @@ -1,10 +1,10 @@ import "./global.css" import { RootProvider } from "fumadocs-ui/provider" import { Rubik } from "next/font/google" -import type { ReactNode } from "react" -import { baseUrl, createMetadata } from "./og/[...slug]/metadata" import Script from "next/script" +import type { ReactNode } from "react" import { env } from "~/env.mjs" +import { baseUrl, createMetadata } from "./og/[...slug]/metadata" const rubik = Rubik({ subsets: ["latin"], diff --git a/website/content/adapters/bun.mdx b/website/content/adapters/bun.mdx index f5fa2f3f..775706e1 100644 --- a/website/content/adapters/bun.mdx +++ b/website/content/adapters/bun.mdx @@ -7,8 +7,6 @@ icon: Dessert import { Step, Steps } from "fumadocs-ui/components/steps"; import { Workflow, Server } from "lucide-react"; -## Setup - } @@ -24,7 +22,7 @@ import { Workflow, Server } from "lucide-react"; /> -### Manual Setup +## 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. @@ -44,9 +42,9 @@ Using the `@buape/carbon/adapters/bun` package, you can create a server to host ```ts import { createServer } from '@buape/carbon/adapters/bun' -const handle = createHandle( ... ) +const client = new Client( ... ) -createServer(handle, { port: 3000 }) +createServer(client, { port: 3000 }) ``` @@ -143,7 +141,7 @@ You may also want to set up a process manager like [PM2](https://npmjs.com/packa -Reminder to deploy your commands to Discord using `/deploy?secret=`. +Remember to deploy your commands to Discord using `/deploy?secret=`. diff --git a/website/content/adapters/cloudflare.mdx b/website/content/adapters/fetch/cloudflare.mdx similarity index 69% rename from website/content/adapters/cloudflare.mdx rename to website/content/adapters/fetch/cloudflare.mdx index f125a61c..f91ebb93 100644 --- a/website/content/adapters/cloudflare.mdx +++ b/website/content/adapters/fetch/cloudflare.mdx @@ -7,8 +7,6 @@ icon: Cloud import { Step, Steps } from "fumadocs-ui/components/steps"; import { Workflow, Server } from "lucide-react"; -## Setup - } @@ -24,7 +22,7 @@ import { Workflow, Server } from "lucide-react"; /> -### Manual Setup +## 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. @@ -37,36 +35,59 @@ This is a continuation of the [Basic Setup](/getting-started/basic-setup) guide.
-### Create a Handler +### Create a Fetch 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. +Using the `@buape/carbon/adapters/fetch` 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' +```ts title="src/index.ts" +import { createHandler } from '@buape/carbon/adapters/fetch' -const handle = createHandle( ... ) +const client = new Client( ... ) -const handler = createHandler(handle) +const handler = createHandler(client) export default { fetch: handler } ``` - -## Running in Development + +### Create a Entry Point File + +To access environment variables globally in a Cloudflare Worker, Carbon uses a workaround by assigning the `process.env` object to the `globalThis` object before importing the main handler file. This approach allows you to access environment variables at the top level of your handler file, something that is not normally possible in Cloudflare Workers. + +```ts title="src/entry.ts" +import type { ExecutionContext } from '@cloudflare/workers-types' + +export default { + fetch(req: Request, env: Record, ctx: ExecutionContext) { + Reflect.set(globalThis, 'process', { env }) + const handle = await import('./index.js') + return handle.default.fetch(req, ctx) + } +} +``` + - -### Set Environment Variables +### Add a Wrangler Configuration -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. +You'll need to create a `wrangler.toml` file in the root of your project to configure your Cloudflare Worker to use the entry point file you created. This file should look something like this: +```toml title="wrangler.toml" +name = " ... " +main = "src/entry.ts" +compatibility_date = " ... " +``` + + +## Running in Development + ### 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, you may want to set it as `BASE_URL=""` in your `.dev.vars` file. +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). This created public URL will be referred to as `PUBLIC_URL` in the following steps. @@ -75,6 +96,13 @@ You can use the `--subdomain` flag to specify a custom subdomain for your proxy. + +### 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. + + + ### Configure Portal URLs @@ -116,6 +144,7 @@ Before deploying your bot, you'll need to set your environment variables. This c Promise) +// This new handler can be used with any fetch-compatible environment or framework +``` + +### Customizing Request Handling + +You can also extend this approach to customize routing and request handling for different paths or use cases. + +```ts +const handler = createHandler(client) + +// Bun or other frameworks +Bun.serve({ + fetch(req: Request) { + const url = new URL(req.url) + if (url.startsWith('/api/discord')) { + return handler(req) + } else { + // Handle other requests + } + } +}) +``` + +## Running in Development + +How you run your Carbon bot in development depends on the environment or framework you're using. Here are some common setups: + + + +### 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). This created public URL will be referred to as `PUBLIC_URL` in the following steps. + + + + +You can use the `--subdomain` flag to specify a custom subdomain for your proxy. + + + + +### Set Environment Variables + +First things first, you'll need to grab your public URL and your Discord application's secrets from the [Developer Portal](https://discord.com/developers/applications) and paste them into whatever environment variables file your setup uses. + + +`BASE_URL` should be your public URL plus the relative path - if any - to your bot's handler. +You can rename this variable if it conflicts with your existing environment variables. + + + + + +### 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 + +How you run your bot in development depends on the environment or framework you're 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 + +Deploying your Carbon bot to production also depends on the environment or framework you're using. You'll need to refer to the specific documentation for your setup. + + +Remember, +- you'll need a public URL for Discord to access your bot +- to deploy your commands to Discord using `/deploy?secret=`. + diff --git a/website/content/adapters/fetch/meta.json b/website/content/adapters/fetch/meta.json new file mode 100644 index 00000000..6e7688cf --- /dev/null +++ b/website/content/adapters/fetch/meta.json @@ -0,0 +1,3 @@ +{ + "defaultOpen": true +} diff --git a/website/content/adapters/next.mdx b/website/content/adapters/fetch/next.mdx similarity index 85% rename from website/content/adapters/next.mdx rename to website/content/adapters/fetch/next.mdx index 2a396db4..1c87eb7c 100644 --- a/website/content/adapters/next.mdx +++ b/website/content/adapters/fetch/next.mdx @@ -7,8 +7,6 @@ icon: PanelsTopLeft import { Step, Steps } from "fumadocs-ui/components/steps"; import { Workflow, Server } from "lucide-react"; -## Setup - } @@ -45,14 +43,14 @@ Ensure the file where you export your handler is placed at `src/app/api/discord/ ### 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. +Using the `@buape/carbon/adapters/fetch` package, you can create a handler that you can then export for Next.js API routes. This will handle incoming interactions and route them to your bot. ```ts -import { createHandler } from '@buape/carbon/adapters/next' +import { createHandler } from '@buape/carbon/adapters/fetch' -const handle = createHandle( ... ) +const client = new Client( ... ) -const handler = createHandler(handle) +const handler = createHandler(client) export { handler as GET, handler as POST } ``` @@ -62,17 +60,10 @@ 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, you may want to set it as `BASE_URL="/api/discord"` in your `.env.local` file. +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). This created public URL will be referred to as `PUBLIC_URL` in the following steps. @@ -82,16 +73,24 @@ You can use the `--subdomain` flag to specify a custom subdomain for your proxy. -### Configure Portal URLs +### Set Environment Variables -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`. +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. -`` refers to the public URL plus the relative path to your Next.js API routes, likely `/api/discord`. +`BASE_URL` should be your public URL **plus the relative path** to your Next.js API routes, likely `/api/discord`. +You can rename this variable if it conflicts with your existing environment variables. + +### 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 diff --git a/website/content/adapters/index.mdx b/website/content/adapters/index.mdx index 25d642a2..eb902573 100644 --- a/website/content/adapters/index.mdx +++ b/website/content/adapters/index.mdx @@ -3,64 +3,37 @@ 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. +import { Bun, Cloud, Dessert, Hexagon, PanelsTopLeft, Menu } from "lucide-react"; - } - 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." - /> + } + title="Cloudflare Workers" + href="/adapters/fetch/cloudflare" + description="Cloudflare Workers are a free and lightweight option for running Carbon bots with minimal setup. They have strict limits on memory, execution time, and Node.js library compatibility, which may restrict complex applications." + /> + } + title="Next.js" + href="/adapters/fetch/next" + description="Next.js can be integrated seamlessly with web apps, allowing unified projects. They can be hosted for free on Vercel, with usage limits, or deployed on a server for greater flexibility in handling heavier workloads." + /> + } + title="Node.js" + href="/adapters/node" + description="Node.js on a dedicated server offers full control for resource-heavy or custom API needs. Developers can use the entire Node.js library ecosystem but must handle infrastructure setup and maintenance." + /> + } + title="Bun" + href="/adapters/bun" + description="Bun is a fast and efficient runtime that can be run on dedicated servers. It is almost fully compatible with Node.js modules and delivers high performance, making it a solid alternative to Node.js for modern applications." + /> + } + title="More" + href="/adapters/fetch" + description="Discover additional ways to deploy Carbon bots with any environment or framework that supports the fetch request handler API. This flexibility allows you to integrate seamlessly across different setups without needing custom adapters." + /> - -## 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 index 63d5cbfe..fa79e3c5 100644 --- a/website/content/adapters/meta.json +++ b/website/content/adapters/meta.json @@ -1,12 +1,5 @@ { "title": "Adapters", "icon": "Cable", - "pages": [ - "--- Serverless ---", - "cloudflare", - "next", - "--- Server ---", - "node", - "bun" - ] + "pages": ["fetch", "node", "bun"] } diff --git a/website/content/adapters/node.mdx b/website/content/adapters/node.mdx index 99af1495..18f94c9c 100644 --- a/website/content/adapters/node.mdx +++ b/website/content/adapters/node.mdx @@ -1,14 +1,12 @@ --- title: Node.js -description: Learn how to set up and deploy your Carbon bot using Node.js, including development and production environments. +description: Learn how to set up and deploy your Carbon bot using the Node.js runtime, including development and production environments. icon: Hexagon --- import { Step, Steps } from "fumadocs-ui/components/steps"; import { Workflow, Server } from "lucide-react"; -## Setup - } @@ -24,7 +22,7 @@ import { Workflow, Server } from "lucide-react"; /> -### Manual Setup +## 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. @@ -44,9 +42,9 @@ Using the `@buape/carbon/adapters/node` package, you can create a server to host ```ts import { createServer } from '@buape/carbon/adapters/node' -const handle = createHandle( ... ) +const client = new Client( ... ) -createServer(handle, { port: 3000 }) +createServer(client, { port: 3000 }) ``` @@ -145,7 +143,7 @@ You may also want to set up a process manager like [PM2](https://npmjs.com/packa -Reminder to deploy your commands to Discord using `/deploy?secret=`. +Remember to deploy your commands to Discord using `/deploy?secret=`. diff --git a/website/content/classes/client.mdx b/website/content/classes/client.mdx index c38b30ad..aedda86a 100644 --- a/website/content/classes/client.mdx +++ b/website/content/classes/client.mdx @@ -8,19 +8,14 @@ The main class that is used to use Carbon is the [`Client`](/api/index/classes/C ## 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), - publicKey: String(env.PUBLIC_KEY), - token: String(env.TOKEN), - }, [new PingCommand()]) - return [client] -}) +const client = new Client({ + baseUrl: " ... ", + deploySecret: " ... ", + clientId: " ... ", + publicKey: " ... ", + token: " ... ", +}, [new PingCommand()]) ``` Here we have created a client with the following options: diff --git a/website/content/getting-started/basic-setup.mdx b/website/content/getting-started/basic-setup.mdx index 606ed709..9eb2fedc 100644 --- a/website/content/getting-started/basic-setup.mdx +++ b/website/content/getting-started/basic-setup.mdx @@ -36,29 +36,24 @@ Let's start by adding Carbon to your project: -### Create a Client and Handle Function +### Create a Client -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. +Next, create a new client instance by importing the `Client` class from the `@buape/carbon` package. The client requires a configuration object with your bot's credentials and an array of commands to register. ```ts title="src/index.ts" -import { createHandle, Client } from "@buape/carbon"; - -const handle = createHandle((env) => { - const client = new Client( - { - baseUrl: String(env.BASE_URL), - deploySecret: String(env.DEPLOY_SECRET), - clientId: String(env.DISCORD_CLIENT_ID), - publicKey: String(env.DISCORD_PUBLIC_KEY), - token: String(env.DISCORD_TOKEN), - }, - [] - ) - return [client]; -}); +import { Client } from "@buape/carbon"; + +const client = new Client({ + baseUrl: process.env.BASE_URL, + deploySecret: process.env.DEPLOY_SECRET, + clientId: process.env.DISCORD_CLIENT_ID, + publicKey: process.env.DISCORD_PUBLIC_KEY, + token: process.env.DISCORD_TOKEN, +}, []) ``` +The `deploySecret` is your own secret key used to verify requests to protected endpoints, such as the deploy commands endpoint, an endpoint that you do not want to be spammed. Setting environment variables will be covered in a later step. @@ -73,12 +68,12 @@ Now we'll create a simple command that responds with "Hello!" when invoked. This import { Command, type CommandInteraction } from "@buape/carbon"; export default class HelloCommand extends Command { - name = "hello"; - description = "Say hello to the bot"; + name = "hello"; + description = "Say hello to the bot"; - async run(interaction: CommandInteraction) { - await interaction.reply("Hello!"); - } + async run(interaction: CommandInteraction) { + await interaction.reply("Hello!"); + } } ``` @@ -87,13 +82,10 @@ Then, mount the command to your client to make it available for use. This step i ```ts title="src/index.ts" import HelloCommand from './commands/hello' -const handle = createHandle((env) => { - const client = new Client( - { ... }, - [new HelloCommand()] - ) - return [client] -}) +const client = new Client( + { ... }, + [new HelloCommand()] +) ``` @@ -104,26 +96,31 @@ const handle = createHandle((env) => { 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/plugins/index.mdx b/website/content/plugins/index.mdx index 59c7ffa1..2d38cd92 100644 --- a/website/content/plugins/index.mdx +++ b/website/content/plugins/index.mdx @@ -6,4 +6,4 @@ 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. +In Carbon, plugins are implemented as classes that extend the base [`Plugin`](/api/index/classes/Plugin) class. 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 index 0d96f37c..daa56258 100644 --- a/website/content/plugins/linked-roles.mdx +++ b/website/content/plugins/linked-roles.mdx @@ -9,21 +9,21 @@ 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. +Linked Roles are straightforward to use in Carbon, simplify create an instance and pass it to the plugins array in your `Client` constructor. You can only have five metadata per application, and they apply across all guilds your app is in. @@ -33,36 +33,30 @@ You can only have five metadata per application, and they apply across all guild ### 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`. +To add Linked Roles to your bot, you'll need to create a new instance of the `LinkedRoles` class and pass it 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 { Client, ApplicationRoleConnectionMetadataType } from "@buape/carbon" import { LinkedRoles } from "@buape/carbon/linked-roles" -const handle = createHandle((env) => { - const client = new Client({ - // Add these options and environment variables - baseUrl: String(env.BASE_URL), - clientSecret: String(env.CLIENT_SECRET), - }, [ ... ]) - 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] +const linkedRoles = new LinkedRoles({ + metadata: [ + { + key: 'is_staff', + name: 'Verified Staff', + description: 'Whether the user is a verified staff member', + type: ApplicationRoleConnectionMetadataType.BooleanEqual + } + ], + metadataCheckers: { + is_staff: async (userId) => { + // Check if the user is a staff member + return true + } + } }) + +const client = new Client({ ... }, [ ... ], [linkedRoles]) ``` diff --git a/website/env.mjs b/website/env.mjs index 6e447449..a083925c 100644 --- a/website/env.mjs +++ b/website/env.mjs @@ -5,14 +5,16 @@ export const env = createEnv({ extends: [], shared: {}, server: { - NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + NODE_ENV: z + .enum(["development", "production", "test"]) + .default("development") }, client: {}, runtimeEnv: { - NODE_ENV: process.env.NODE_ENV, + NODE_ENV: process.env.NODE_ENV }, skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION || process.env.npm_lifecycle_event === "lint" -}) \ No newline at end of file +}) diff --git a/website/next.config.mjs b/website/next.config.mjs index f01d38fa..0e622a8c 100644 --- a/website/next.config.mjs +++ b/website/next.config.mjs @@ -14,6 +14,16 @@ const config = { destination: "/getting-started/introduction", permanent: false }, + { + source: "/adapters/cloudflare", + destination: "/adapters/fetch/cloudflare", + permanent: false + }, + { + source: "/adapters/next", + destination: "/adapters/fetch/next", + permanent: false + }, { // Redirect old Carbon URLs to introduction page source: "/carbon/:path*",