{"id":13998525,"url":"https://github.com/mrbbot/slshx","last_synced_at":"2025-04-10T00:17:46.891Z","repository":{"id":43411955,"uuid":"442464627","full_name":"mrbbot/slshx","owner":"mrbbot","description":"⚔️ Strongly-typed Discord commands on Cloudflare Workers","archived":false,"fork":false,"pushed_at":"2023-01-18T17:49:50.000Z","size":833,"stargazers_count":211,"open_issues_count":11,"forks_count":9,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-10T00:17:39.382Z","etag":null,"topics":["cloudflare","cloudflare-workers","discord","discord-api","discord-bot","discord-interactions","discord-js","discord-message-components","discord-slash-commands","discordjs","slash-commands"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrbbot.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-12-28T13:00:11.000Z","updated_at":"2025-03-09T20:17:26.000Z","dependencies_parsed_at":"2023-02-10T16:31:01.701Z","dependency_job_id":null,"html_url":"https://github.com/mrbbot/slshx","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbbot%2Fslshx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbbot%2Fslshx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbbot%2Fslshx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrbbot%2Fslshx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrbbot","download_url":"https://codeload.github.com/mrbbot/slshx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248131318,"owners_count":21052820,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","cloudflare-workers","discord","discord-api","discord-bot","discord-interactions","discord-js","discord-message-components","discord-slash-commands","discordjs","slash-commands"],"created_at":"2024-08-09T19:01:45.005Z","updated_at":"2025-04-10T00:17:46.866Z","avatar_url":"https://github.com/mrbbot.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# ⚔️ Slshx\n\n**Slshx** is a slightly wacky, experimental, library for building strongly-typed\n[Discord commands](https://discord.com/developers/docs/interactions/application-commands)\nthat run on [Cloudflare Workers](https://workers.cloudflare.com/), using a\nReact-inspired syntax. It focuses on providing a great local development\nexperience, powered by [🔥 Miniflare](https://miniflare.dev/).\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { CommandHandler, Message, createElement, createHandler, useDescription, useNumber } from \"slshx\";\n\nfunction add(): CommandHandler {\n  useDescription(\"Adds two numbers together\");\n  const a = useNumber(\"a\", \"1st number\", { required: true });\n  const b = useNumber(\"b\", \"2nd number\", { required: true });\n  return (interaction, env, ctx) =\u003e (\n    \u003cMessage ephemeral\u003e{a} + {b} = {a + b}\u003c/Message\u003e\n  );\n};\n\nconst handler = createHandler({\n  applicationId: \"...\",\n  applicationPublicKey: \"...\",\n  commands: { add },\n});\n\nexport default { fetch: handler };\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n## Features\n\n- ⚔️ Chat Input (Slash) Commands\n- 🙂 User Commands\n- ✉️ Message Commands\n- 💪 Strongly-Typed Command Options and API Bindings\n- 🔥 Auto-Deploy Commands on Change (think live reload, but for commands)\n- 👇 Interactive Message Components (Buttons, Select Menus)\n- ⚛️ React-Inspired Syntax\n- 😇 Autocomplete for Command Options\n- 🌲 Highly Tree-Shakeable\n- ✨ No Runtime Dependencies\n\n## Quick*ish* Start\n\n1. Clone the [`slshx-starter`](https://github.com/mrbbot/slshx-starter)\n   repository. This includes a [Miniflare](https://miniflare.dev/) and\n   [`esbuild`](https://esbuild.github.io/) setup that removes unneeded local\n   development code when deploying to Workers.\n\n   \u003e ⚠️ To enable auto-deployments on reload, Slshx requires the\n   \u003e [`--global-async-io` Miniflare flag](https://miniflare.dev/core/standards#global-functionality-limits)\n   \u003e to be set. `slshx-starter` automatically enables this for you.\n\n2. Copy the `env.example.jsonc` file to `env.jsonc`. ⚠️ Do not commit this file.\n3. Create a new application in the\n   [Discord Developer Portal](https://discord.com/developers/applications). Copy\n   the **Application ID** and **Public Key** into the `development` section of\n   `env.jsonc`.\u003cbr\u003e\n   ![Application ID and Public Key](./.github/screenshots/app-id-public-key.png)\n   \u003e You will probably want to create 2 applications: one for development, and\n   \u003e one for production, using your deployed worker's URL.\n4. Click on **OAuth2** in the sidebar, and copy your application's **Client\n   Secret** into the `development` section of `env.jsonc`. ⚠️ Do not share this\n   secret with anyone! Keep this tab open, we'll need to add some more stuff\n   later.\u003cbr\u003e ![Client Secret](./.github/screenshots/client-secret.png)\n5. Run `npm install` in your cloned repository, then `npm run dev` to start the\n   local development server.\n6. Setup\n   [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/tutorials/single-command)\n   so Discord can reach your local development server when invoking your\n   commands. Follow\n   [these instructions](https://developers.cloudflare.com/cloudflare-one/tutorials/single-command),\n   then when you're ready to start the tunnel, run:\u003cbr\u003e\n   `cloudflared tunnel --hostname \u003chostname\u003e --url localhost:8787 --name slshx`\n7. Make sure your tunnel is working by visiting `https://\u003chostname\u003e/` in your\n   browser. This should show the ⚔️ **Slshx** landing page. Click the **Add to\n   Server** button to connect your application with one of your servers. You may\n   want to create a new one just for testing your commands.\u003cbr\u003e\n   ![Slshx Landing Page](./.github/screenshots/landing.png)\n8. Copy the **ID** of the server you just added your application to into\n   `testServerId` in `env.jsonc`. You can find this by enabling **Developer\n   Mode** in **Discord's Advanced App Settings**, then right-clicking on the\n   server in the sidebar, and clicking **Copy ID** at the bottom of the menu.\n   Changes made to commands will appear instantly in this server during\n   development. 🔥\u003cbr\u003e\n   ![Enabling Developer Mode](./.github/screenshots/developer-mode.png)\u003cbr\u003e\n   ![Copy ID](./.github/screenshots/copy-id.png)\n9. Copy your Cloudflare Tunnel URL into the **Interactions Endpoint URL** field\n   under your application's **General Information** in the Discord Developer\n   Portal, and then click **Save Changes**. You should see some requests from\n   Discord validating your endpoint in the local server logs.\u003cbr\u003e\n   ![Interactions Endpoint URL](./.github/screenshots/interactions-endpoint-url.png)\n10. That's it! 🎉 You should now be able to try out the default `add` command in\n    your server. Try changing the message in `src/add.tsx`. Miniflare will\n    automatically reload your worker, and future command invocations will show\n    the new message.\u003cbr\u003e\n    ![Invoking the Command](./.github/screenshots/command-invoke.png)\u003cbr\u003e\n    ![Command Result](./.github/screenshots/command-result.png)\n\n## Using in Existing Workers\n\nThe core of Slshx is the `createHandler` function. It takes an options object\nand returns a function matching the signature of Cloudflare Workers\n[module `fetch` handlers](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#syntax-module-worker).\nIf the worker is running in Miniflare, and `applicationId`, `applicationSecret`\nand `testServerId` are specified, your commands are automatically deployed to\nthe test server. The returned `handler` will treat every incoming `POST`\n`request` as a\n[Discord interaction](https://discord.com/developers/docs/interactions/receiving-and-responding),\nunless you're running in Miniflare and send a `GET` request, in which case the\nlanding page will be returned instead.\n\n```tsx\nimport { authorizeResponse, createHandler } from \"slshx\";\n\nconst applicationId = \"...\";\nconst handler = createHandler({\n  applicationId,\n  applicationPublicKey: \"...\",\n  applicationSecret: \"...\", // optional\n  testServerId: \"...\", // optional\n  commands: {},\n});\n\nexport default {\n  async fetch(request, env, ctx) {\n    const { pathname } = new URL(request.url);\n    // Update your Interactions Endpoint URL to \"https://\u003chostname\u003e/interaction\".\n    if (pathname === \"/interaction\") {\n      return handler(request, env, ctx);\n    } else if (pathname === \"/authorize\") {\n      return authorizeResponse(applicationId);\n    } else {\n      // ...other handlers\n    }\n  },\n};\n```\n\n## Defining Slash Commands\n\nAll commands in Slshx are defined as synchronous functions that take no\nparameters and return another handler function that might be called once. They\nmust always call `useDescription`, and any other `use*` functions (referred to\nas hooks) before returning the handler. You **must not** use the return values\nof hooks outside a returned handler. Hooks must always be called in the same\norder, and must not be called conditionally.\n\nOnce you've defined your command, include it in your application by adding it to\nthe `commands` option passed to `createHandler`. The key used in this object\nwill be the name of the command (what the user types), and must not contain `:`,\n`/`, `$`, or `#` characters.\n\nWhen deploying commands, Slshx will run your command function _up to_ the first\n`return`, recording which hooks are called.\n\nWhen handling interactions, Slshx will run your command function with the\noptions provided by the user, returning these from hooks. The returned function\nwill then be called with `interaction, env, ctx`, at which point you are free to\nuse hook returns and should respond to the interaction.\n[`interaction` is the full incoming interaction](https://discord.com/developers/docs/interactions/receiving-and-responding),\nincluding the invoking `user`, server (`guild_id`) and continuation `token`.\n`env` and `ctx` are the same\n[parameters passed to the worker `handler`](https://developers.cloudflare.com/workers/runtime-apis/fetch-event#syntax-module-worker).\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { CommandHandler, createElement, createHandler, useDescription, useNumber } from \"slshx\";\nimport type { APIChatInputApplicationCommandInteraction } from \"discord-api-types/v9\";\n\ntype Env = { KV_NAMESPACE: KVNamespace; SECRET: string };\n\nfunction add(): CommandHandler {\n  // ✅: must call `useDescription`\n  // ✅: must call hooks before returning handler\n  // ✅: must call hooks in the same order each time\n  useDescription(\"Adds two numbers together\");\n  const a = useNumber(\"a\", \"1st number\", { required: true });\n  const b = useNumber(\"b\", \"2nd number\", { required: true });\n\n  // ❌: must not use hook return values outside handler\n  if (a \u003e 5) {\n    // ❌: must not call hooks conditionally\n    const c = useNumber(\"c\", \"3rd number\", { required: true });\n  }\n\n  // Return a handler function, this will get called at most once\n  return (interaction, env, ctx) =\u003e {\n    //    │            │    └ ExecutionContext\n    //    │            └ Env\n    //    └ APIChatInputApplicationCommandInteraction\n    //\n    // ✅: safe to use hook return values inside handler\n    return \u003cMessage\u003e{a} + {b} = {a + b}\u003c/Message\u003e;\n  };\n}\n\nconst handler = createHandler({\n  // ...\n  commands: { add },\n});\n\nexport default { fetch: handler };\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n### Options\n\nSlshx includes hooks for\n[all available option types](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type).\nIf a user doesn't provide a value for an option, the hook will return `null`.\n\nAll options take a `name` and `description`. They can be marked as `required`,\nin which case Discord will enforce that a value is provided before submitting\nthe interaction. The return type of the hook excludes `null` in this case.\n\nSome types have additional optional fields that control acceptable values.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { ChannelType } from \"slshx\";\nimport type { APIUser, APIInteractionDataResolvedChannel, APIRole, APIAttachment } from \"discord-api-types/v9\";\n\nfunction cmd(): CommandHandler {\n  useDescription(\"Command demonstrating option types\");\n  \n  const s1 = useString(\"name\", \"Description\");\n  //    └ string | null\n  const s2 = useString(\"name\", \"Description\", { required: true });\n  //    └ string\n\n  const i1 = useInteger(\"name\", \"Description\");\n  //    └ number | null\n  const i2 = useInteger(\"name\", \"Description\", { min: 5, max: 100 });\n\n  const b = useBoolean(\"name\", \"Description\");\n  //    └ boolean | null\n\n  const u = useUser(\"name\", \"Description\");\n  //    └ APIUser | null\n\n  const c1 = useChannel(\"name\", \"Description\");\n  //    └ APIInteractionDataResolvedChannel | null\n  const c2 = useChannel(\"name\", \"Description\", {\n    // https://discord.com/developers/docs/resources/channel#channel-object-channel-types\n    types: [ChannelType.GUILD_TEXT, ChannelType.DM],\n  });\n\n  const r = useRole(\"name\", \"Description\");\n  //    └ APIRole | null\n\n  const m = useMentionable(\"name\", \"Description\");\n  //    └ APIUser | APIRole | null\n\n  const n1 = useNumber(\"name\", \"Description\");\n  //    └ number | null\n  const n2 = useNumber(\"name\", \"Description\", { min: 5, max: 100 });\n  \n  const a = useAttachment(\"name\", \"Description\");\n  //    └ APIAttachment | null\n\n  return () =\u003e {}; // ...\n};\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n### Choices\n\nString, integer, and number options can be configured with up to 25 choices to\npick from. Like `required`, Discord will enforce that the value provided is one\nof these before submitting the interaction. You can optionally provide a `name`\nfor the choice. This will be displayed instead, but the `value` will still be\nreturned to you.\n\n```tsx\nfunction cmd(): CommandHandler {\n  useDescription(\"Command demonstrating choices\");\n\n  //    ┌ \"foo\" | \"bar\" | \"baz\"\n  const s = useString(\"str\", \"Description\", {\n    choices: [\"foo\", { value: \"bar\" }, { name: \"Baz\", value: \"baz\" }] as const,\n    // The `as const` is important here. Without it, the inferred type of `s`\n    // would just be `string`, instead of `\"foo\" | \"bar\" | \"baz\"`.\n    required: true, // Mark as `required` to exclude `null` from inferred type\n  });\n\n  //    ┌ 1 | 2 | 3 | null\n  const n = useNumber(\"num\", \"Description\", {\n    choices: [1, { value: 2 }, { name: \"Three\", value: 3 }] as const,\n  });\n\n  return () =\u003e {}; // ...\n}\n```\n\n![Choices](./.github/screenshots/choices.png)\n\n### Autocomplete\n\nIf you've got more than 25 choices, or don't know them ahead of time, you can\nuse\n[Discord's autocomplete feature](https://discord.com/developers/docs/interactions/application-commands#autocomplete).\nThis is a separate interaction Discord will submit to your application whenever\nthe user starts typing something for an option. Hooks return the correct values\non autocomplete interactions, so you're free to use those results when building\nsuggestions. Similarly to choices, you can optionally provide a `name` for the\nsuggestion, which will be displayed instead.\n\n```tsx\nimport type { APIApplicationCommandAutocompleteInteraction } from \"discord-api-types/payloads/v9/_interactions/autocomplete\"; // 🙁\n\ntype Env = { SONG_NAMESPACE: KVNamespace };\n\nfunction cover(): CommandHandler\u003cEnv\u003e {\n  useDescription(\"Get cover art for a song\");\n\n  const artist = useString(\"artist\", \"Artist of song\", { required: true });\n\n  //    ┌ string\n  const name = useString\u003cEnv\u003e(\"name\", \"Name of song\", {\n    required: true,\n    async autocomplete(interaction, env, ctx) {\n      //               │            │    └ ExecutionContext\n      //               │            └ Env\n      //               └ APIApplicationCommandAutocompleteInteraction\n      //\n      // `artist`, `name`, and `year` will have their current values set.\n      // We can use them when building suggestions for the song name.\n\n      // Can use the current option...\n      const songs = await env.SONG_NAMESPACE.list({ prefix: name });\n\n      const matching = songs.keys.filter((song) =\u003e {\n        // ...options defined before\n        if (artist \u0026\u0026 song.metadata.artist !== artist) return false;\n        // ...or after\n        if (year \u0026\u0026 song.metadata.year !== year) return false;\n        return true;\n      });\n\n      return matching.map((song) =\u003e song.name);\n      // Could also return an array of { name: \"...\", value: \"...\" } objects\n    },\n  });\n\n  const year = useInteger(\"year\", \"Year song was released\");\n\n  return () =\u003e {}; // ...\n}\n```\n\n\u003e ⚠️ Discord does not include full user, channel, role, mentionable or\n\u003e attachment objects in autocomplete interactions. If a user specifies a value\n\u003e for one of these options, the hook will return a partial object of the form\n\u003e `{ id: \"...\" }` instead.\n\n### Subcommands\n\nDiscord supports grouping chat commands into\n[subcommands and subcommand-groups](https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups).\nCommands can only be nested 2 levels deep. Note that using subcommands makes\nyour base command unusable, so you can't define a handler for `/a` if `/a e` is\na subcommand.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nconst handler = createHandler({\n  // ...\n  commands: {\n    a: {          // `a` is a command\n      b: {        // `b` is a subcommand-group\n        c: cmd1,  // `c` is a subcommand\n        d: cmd2,  // `d` is a subcommand\n      },\n      e: cmd3,    // `e` is a subcommand\n    },\n  },\n});\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n![Subcommands](./.github/screenshots/subcommands.png)\n\n### Default Permission\n\nBy default,\n[any server member can use your commands](https://discord.com/developers/docs/interactions/application-commands#permissions).\nIf you'd like them to be disabled by default, you can call\n`useDefaultPermission(false)`. The same rules for calling hooks apply. Note that\ncalling `useDefaultPermission(false)` in a subcommand will make the top-level\nbase command disabled by default, as permissions can only be applied at the\ncommand level. See the later section on\n[**Calling Discord APIs**](#calling-discord-apis) for instructions on granting\npermissions to some users/roles.\n\n```tsx\nfunction cmd(): CommandHandler {\n  useDefaultPermission(false);\n  return () =\u003e {}; // ...\n}\n```\n\n## Defining User Commands\n\nInstead of invoking commands via chat, Discord also supports invoking them\n[via a context menu on users](https://discord.com/developers/docs/interactions/application-commands#user-commands).\nUser commands are defined like regular slash commands and respond in exactly the\nsame way. The difference is that you can't call `useDescription` or any option\nhooks, and there's an extra\n[`user`](https://discord.com/developers/docs/resources/user#user-object)\nparameter passed to the handler function, containing the user the command was\ninvoked on. They're still defined as functions returning functions though, so\nyou can use [**Message Components**](#using-message-components) in your\nresponses.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { Message, UserCommandHandler, createElement, createHandler } from \"slshx\";\nimport type { APIUser, APIUserApplicationCommandInteraction } from \"discord-api-types/v9\";\n\nfunction greet(): UserCommandHandler {\n  return (interaction, env, ctx, user) =\u003e {\n    //    │                      └ APIUser\n    //    └ APIUserApplicationCommandInteraction\n    //\n    // interaction.data.target_id === user.id\n    return \u003cMessage\u003eHello {user.name}!\u003c/Message\u003e;\n  };\n};\n\nconst handler = createHandler({\n   // ...\n   userCommands: { \"Greet User\": greet }\n});\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n![Invoking the User Command](./.github/screenshots/user-invoke.png)\u003cbr\u003e\n![User Command Result](./.github/screenshots/user-result.png)\n\n## Defining Message Commands\n\nSimilarly, commands can also be invoked\n[via a context menu on messages](https://discord.com/developers/docs/interactions/application-commands#message-commands).\nInstead of a `user`, the extra handler parameter will contain the\n[`message`](https://discord.com/developers/docs/resources/channel#message-object)\nthe command was invoked on.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { Message, MessageCommand, createElement, createHandler } from \"slshx\";\nimport type { APIMessage, APIMessageApplicationCommandInteraction } from \"discord-api-types/v9\";\n\ntype Env = { BOOKMARKS_NAMESPACE: KVNamespace };\n\nfunction bookmark(): MessageCommandHandler\u003cEnv\u003e {\n  return async (interaction, env, ctx, message) =\u003e {\n    //          │                      └ APIMessage\n    //          └ APIMessageApplicationCommandInteraction\n    //\n    // interaction.data.target_id === message.id\n    await env.BOOKMARKS_NAMESPACE.put(message.id, message.content);\n    return \u003cMessage ephemeral\u003eBookmarked!\u003c/Message\u003e;\n  }\n};\n\nconst handler = createHandler({\n  // ...\n  messageCommands: { \"Bookmark Message\": bookmark },\n});\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n![Invoking the Message Command](./.github/screenshots/message-invoke.png)\u003cbr\u003e\n![Message Command Result](./.github/screenshots/message-result.png)\n\n## Responding to Commands\n\n### JSX\n\nSlshx allows you to respond using either JSX syntax (e.g. `\u003cMessage\u003e`) or\n[plain message objects](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-messages)\n(e.g. `{ content: \"...\" }`). These are functionally identical, JSX is just\nsyntactic sugar that improves code readability. From now on, we'll show both the\nJSX and plain object responses.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { CommandHandler, Message, createElement } from \"slshx\";\n\nfunction add(): CommandHandler {\n  // ...\n  return (interaction, env, ctx) =\u003e {\n    return \u003cMessage\u003e{a} + {b} = {a + b}\u003c/Message\u003e;\n    // ...is exactly the same as...\n    return { content: `${a} + ${b} = ${a + b}` };\n  };\n}\n```\n\u003c!-- prettier-ignore-end --\u003e\n\nIf you're using JSX, you **must import `createElement` and `Fragment` from\n`slshx`**. You'll also need a build tool configured to process JSX into regular\nJavaScript using Slshx's `createElement` and `Fragment` factories. The\n[`slshx-starter`](https://github.com/mrbbot/slshx-starter) repository includes\nall this configuration for you.\n\n```sh\n$ esbuild --jsx-factory=createElement --jsx-fragment=Fragment\n```\n\n```jsonc\n// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"jsxFactory\": \"createElement\",\n    \"jsxFragmentFactory\": \"Fragment\"\n  }\n}\n```\n\nLike React, you can define your own reusable components to use in responses.\nThese may contain fragments. In this example, we're using\n[**Message Components**](#using-message-components) to add interactivity.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { Button, Message, Row, createElement, Fragment, CommandHandler, useNumber } from \"slshx\";\n\ntype ConfirmButtonsProps = { yesId: string; noId: string };\nfunction ConfirmButtons({ yesId, noId }: ConfirmButtonsProps) {\n  return (\n    \u003c\u003e {/* \u003c- Fragment */}\n      \u003cButton id={yesId} success\u003eYes\u003c/Button\u003e\n      \u003cButton id={noId} danger\u003eNo\u003c/Button\u003e\n    \u003c/\u003e\n  );\n}\n\ntype AddMessageProps = { a: number; b: number; yesId: string; noId: string };\nfunction AddMessage({ a, b, yesId, noId }: AddMessageProps) {\n  return (\n    \u003cMessage\u003e\n      {a} + {b} = {a + b}?\n      \u003cRow\u003e\u003cConfirmButtons yesId={yesId} noId={noId} /\u003e\u003c/Row\u003e\n    \u003c/Message\u003e\n  );\n}\n\nfunction add(): CommandHandler {\n  useDescription(\"...\");\n  const a = useNumber(/* ... */);\n  const b = useNumber(/* ... */);\n  return () =\u003e \u003cAddMessage a={a} b={b} yesId={\"...\"} noId={\"...\"} /\u003e;\n}\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n### Deferring\n\nDiscord requires you to respond to interactions within 3 seconds. If you need\nlonger than this, you can defer your response for up to 15 minutes. To defer a\nresponse, return a generator function as your handler instead of a regular\nfunction, then `yield`.\n\n```tsx\nfunction deferred(): CommandHandler {\n  // ...\n  // The `*` marks this as a generator function\n  return async function* (interaction, env, ctx) {\n    // yield within 3 seconds to defer the response...\n    yield; // Discord will show \"\u003capp\u003e is thinking...\"\n    // ...then return within 15 minutes\n    return \u003cMessage\u003e...\u003c/Message\u003e; // Return the response when you're ready\n  };\n}\n```\n\n![Deferred Response](./.github/screenshots/deferred.png)\n\n### Content\n\nMessages can contain the same\n[Markdown syntax](https://support.discord.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-)\nyou'd normally use in Discord. If you're using JSX and would like to use\nnewlines or other trailing whitespace, you'll need to escape them with `{\" \"}`\n(e.g. `{\"\\n\"}`).\n\n````tsx\nfunction code(): CommandHandler {\n  // ...\n  return () =\u003e {\n    // With JSX\n    return (\n      \u003cMessage\u003e\n        This is how you log to the console in **JavaScript**:{\"\\n\"}\n        ```javascript{\"\\n\"}\n        console.log(\"Hello!\"){\"\\n\"}\n        ```\n      \u003c/Message\u003e\n    );\n\n    // Without JSX\n    return {\n      content: `This is how you log to the console in **JavaScript**:\n\\`\\`\\`javascript\nconsole.log(\"Hello!\")\n\\`\\`\\``,\n    };\n  };\n}\n````\n\n![Formatted Message](./.github/screenshots/markdown.png)\n\n### Mentions\n\nTo mention...\n\n- a **User:** include `\u003c@userId\u003e`\n- a **Role:** include `\u003c@\u0026roleId\u003e`\n- a **Channel:** include `\u003c#channelId\u003e`\n- **Everyone:** include `@everyone`\n- **Here:** include `@here`\n\n...in the message. You can control which mentions are allowed using the\n[`allowedMentions`/`allowed_mentions` property](https://discord.com/developers/docs/resources/channel#allowed-mentions-object).\n\n```tsx\nfunction hello(): CommandHandler {\n  // ...\n  return (interaction) =\u003e {\n    // Get the ID of the user who invoked the command\n    const userId = interaction.member?.user.id ?? \"\";\n\n    // With JSX\n    return (\n      \u003cMessage allowedMentions={{ users: [userId] }}\u003e\n        Hello {`\u003c@${userId}\u003e`}!\n      \u003c/Message\u003e\n    );\n\n    // Without JSX\n    return {\n      allowed_mentions: { users: [userId] },\n      content: `Hello \u003c@${userId}\u003e!`,\n    };\n  };\n}\n```\n\n![Mention](./.github/screenshots/mention.png)\n\n### Flags\n\nMessages can be marked as\n[`ephemeral`](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-data-flags),\nin which case they'll only be visible to the user who invoked the command. You\ncan also mark them as `tts`, which will cause Discord to speak the content using\ntext-to-speech.\n\n```tsx\nfunction add(): CommandHandler {\n  // ...\n  return () =\u003e {\n    // With JSX\n    return (\n      \u003cMessage ephemeral tts\u003e\n        {a} + {b} = {a + b}\n      \u003c/Message\u003e\n    );\n\n    // Without JSX\n    return {\n      flags: 64, // ephemeral\n      tts: true,\n      content: `${a} + ${b} = ${a + b}`,\n    };\n  };\n}\n```\n\n![Ephemeral Message](./.github/screenshots/command-result.png)\n\n### Attachments\n\nMessages can include file and image attachments. Slshx expects these as an array\nof [`File` objects](https://developer.mozilla.org/en-US/docs/Web/API/File).\n\n```tsx\nfunction file(): CommandHandler {\n  // ...\n  return () =\u003e {\n    const file = new File([\"Hello!\"], \"hello.txt\", { type: \"text/plain\" });\n\n    // With JSX\n    return \u003cMessage attachments={[file]}\u003eHere's a file:\u003c/Message\u003e;\n\n    // Without JSX\n    return { attachments: [file], content: \"Here's a file:\" };\n  };\n}\n```\n\n![Files](./.github/screenshots/file.png)\n\n### Embeds\n\nMessages can include up to 10\n[rich-embeds](https://discord.com/developers/docs/resources/channel#embed-object).\nEmbeds can include all sorts of things, including a title, description, URL,\nmedia or fields. Media can reference attachments using the `attachment://`\nscheme. If you're using JSX, `image`, `thumbnail`, `video`, `footer`,\n`provider`, and `author` properties can be defined as either `string`s or\n[full-objects](./src/jsx/embeds/Embed.ts).\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { Message, Embed, Field, createElement } from \"slshx\";\n\nfunction embed(): CommandHandler {\n  // ...\n  return async () =\u003e {\n    const image = await fetch(\"https://via.placeholder.com/300\");\n    const buffer = await image.arrayBuffer();\n    const file = new File([buffer], \"image.png\", { type: \"image/png\" });\n\n    // With JSX\n    return (\n      \u003cMessage attachments={[file]}\u003e\n        Message Content\n        \u003cEmbed\n          // All these properties are optional\n          title=\"Embed Title\"\n          url=\"https://miniflare.dev\"\n          timestamp={new Date()}\n          color={0x0094ff}\n          image=\"attachment://image.png\" // or image={{ url: \"...\", width: ..., height: ... }}\n          thumbnail=\"https://via.placeholder.com/100\"\n          footer=\"Footer\" // or footer={{ text: \"Footer\", iconUrl: \"...\" }}\n          author=\"Slshx\" // or author={{ name: \"Slshx\", url: \"...\", iconUrl: \"...\" }}\n        \u003e\n          Embed Description\n          \u003cField name=\"Field 1\"\u003eValue 1\u003c/Field\u003e\n          \u003cField name=\"Inline Field 2\" inline\u003eValue 2\u003c/Field\u003e\n          \u003cField name=\"Inline Field 3\" inline\u003eValue 3\u003c/Field\u003e\n        \u003c/Embed\u003e\n        {/* Can include up to 10 embeds here */}\n      \u003c/Message\u003e\n    );\n\n    // Without JSX\n    return {\n      attachments: [file],\n      content: \"Message Content\",\n      embeds: [\n        {\n          // All these properties are optional\n          title: \"Embed Title\",\n          description: \"Embed Description\",\n          url: \"https://miniflare.dev\",\n          timestamp: new Date().toISOString(),\n          color: 0x0094ff,\n          image: { url: \"attachment://image.png\" },\n          thumbnail: { url: \"https://via.placeholder.com/100\" },\n          footer: { text: \"Footer\" },\n          author: { name: \"Slshx\" },\n          fields: [\n            { name: \"Field 1\", value: \"Value 1\" },\n            { name: \"Inline Field 2\", value: \"Value 2\", inline: true },\n            { name: \"Inline Field 3\", value: \"Value 3\", inline: true },\n          ],\n        },\n      ],\n    };\n  };\n}\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n![Embed](./.github/screenshots/embed.png)\n\n## Using Message Components\n\n[Message Components](https://discord.com/developers/docs/interactions/message-components)\nallow you to add interactive elements such as buttons and select menus to your\nmessages. When a user interacts with a component, Discord submits an interaction\nto your application including a custom ID. Slshx uses this ID to route the\ninteraction to the correct handler.\n\n### Buttons\n\nTo generate a custom ID that includes the required Slshx routing information,\ncall the `useButton` hook. This takes a callback function taking an\n`interaction`, `env`, and `ctx` that will be called when the button is clicked.\n\nUnlike regular command invocations, component interactions like button clicks\ncan update the message that triggered them. There are\n[4 possible responses](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type)\nto a component interaction:\n\n\u003c!-- prettier-ignore-start --\u003e\n\n1. **Create a new message:** return a message exactly as we've been doing so far\n\n   ```tsx\n   import { CommandHandler, useButton, APIMessageComponentInteraction } from \"slshx\";\n   import type { APIMessageButtonInteractionData } from \"discord-api-types/v9\";\n\n   function cmd(): CommandHandler {\n     const buttonId = useButton((interaction, env, ctx) =\u003e {\n       //                        └ APIMessageComponentInteraction\u003cAPIMessageButtonInteractionData\u003e\n       // With JSX\n       return \u003cMessage\u003eButton clicked, and new message created!\u003c/Message\u003e;\n       // Without JSX\n       return { content: \"Button clicked, and new message created!!\" };\n     });\n     return () =\u003e {}; // ...\n   }\n   ```\n\n2. **Update the original message:** return a partial `\u003cMessage\u003e` with the\n   `update` property set, or a plain message object with the `[$update]`\n   property set to `true`\n\n   ```tsx\n   import { $update } from \"slshx\";\n\n   function cmd(): CommandHandler {\n     const buttonId = useButton((interaction, env, ctx) =\u003e {\n       // With JSX\n       return (\n         \u003cMessage update\u003eButton clicked, and original message updated!\u003c/Message\u003e\n       );\n       // Without JSX\n       return {\n         [$update]: true,\n         content: \"Button clicked, and original message updated!\",\n       };\n     });\n     return () =\u003e {}; // ...\n   }\n   ```\n\n3. **Defer the response, and then create a new message:** use a generator for\n   the callback function, `yield`, then return a message exactly as we've been\n   doing so far\n\n   ```tsx\n   function cmd(): CommandHandler {\n     const buttonId = useButton(async function* (interaction, env, ctx) {\n       // yield within 3 seconds to defer the response...\n       yield; // Discord will show \"\u003capp\u003e is thinking...\"\n       // ...then return within 15 minutes\n\n       // With JSX\n       return \u003cMessage\u003eButton clicked, and new message created!\u003c/Message\u003e;\n       // Without JSX\n       return { content: \"Button clicked, and new message created!\" };\n     });\n     return () =\u003e {}; // ...\n   }\n   ```\n\n4. **Defer the response, and then update the original message:** use a generator\n   for the callback function, `yield $update`, then return a partial `\u003cMessage\u003e`\n   or plain message object\n\n   ```tsx\n   import { $update } from \"slshx\";\n\n   function cmd(): CommandHandler {\n     const buttonId = useButton(async function* (interaction, env, ctx) {\n       // yield within 3 seconds to defer the response...\n       yield $update; // Discord WON'T show \"\u003capp\u003e is thinking...\"\n       // ...then return within 15 minutes\n\n       // With JSX\n       return \u003cMessage\u003eButton clicked, and original message updated!\u003c/Message\u003e;\n       // Without JSX\n       return { content: \"Button clicked, and original message updated!\" };\n     });\n     return () =\u003e {}; // ...\n   }\n   ```\n\n\u003c!-- prettier-ignore-end --\u003e\n\nOnce you have a routable custom ID, wire it up to a button in your original\ncommand response. You can add additional data to the **end** of this ID and\nSlshx will include it in the interaction when calling your handler. Use this to\nstore state you need to persist between interactions. IDs (including routing\ninformation) must be at most 100 characters long.\n\nButtons must be contained within action rows. An action row can contain up to 5\nbuttons. If you're using JSX and don't wrap your button in an action row, Slshx\nwill implicitly create one just for that button.\n\nButtons have\n[5 styles](https://discord.com/developers/docs/interactions/message-components#button-object-button-styles):\n\u003cspan style=\"color:#4a68ec\"\u003eprimary\u003c/span\u003e, secondary,\n\u003cspan style=\"color:#1db484\"\u003esuccess\u003c/span\u003e,\n\u003cspan style=\"color:#fc484a\"\u003edanger\u003c/span\u003e, and link. By default, the secondary\nstyle is used. Link buttons accept a URL instead of a custom ID, and do not\ntrigger an interaction with your application.\n\n\u003c!-- prettier-ignore-start --\u003e\n\n```tsx\nimport { ComponentType, ButtonStyle, createElement, Message, Row, Button, $update, useButton, CommandHandler } from \"slshx\";\n\nfunction buttons(): CommandHandler {\n  // ...\n  const buttonId1 = useButton((interaction, env, ctx) =\u003e {\n    const extraData = interaction.data.custom_id.substring(buttonId1.length);\n    // `extraData` will be \"extra\" when the \"Primary\" button is clicked\n\n    // With JSX\n    return \u003cMessage update\u003eButton clicked: {extraData}\u003c/Message\u003e;\n\n    // Without JSX\n    return {\n      [$update]: true,\n      content: `Button clicked: ${extraData}`,\n      // Using JSX will implicitly remove all buttons from the message unless\n      // they're redefined as children. If you'd like to keep them, you can\n      // remove this next line.\n      components: [],\n    };\n  });\n  const buttonId2 = useButton(/* ... */);\n  // ...\n  return () =\u003e {\n    // With JSX\n    return (\n      \u003cMessage\u003e\n        Press some buttons!\n        \u003cRow\u003e\n          \u003cButton id={buttonId1 + \"extra\"} primary\u003ePrimary\u003c/Button\u003e\n          \u003cButton id={buttonId2}\u003eSecondary\u003c/Button\u003e\n          \u003cButton id={buttonId3} success\u003eSuccess\u003c/Button\u003e\n          \u003cButton id={buttonId4} danger\u003eDanger\u003c/Button\u003e\n          \u003cButton url=\"https://miniflare.dev\"\u003eLink\u003c/Button\u003e\n        \u003c/Row\u003e\n        \u003cButton id={buttonId5}\u003eImplicit Row\u003c/Button\u003e\n      \u003c/Message\u003e\n    );\n\n    // Without JSX (this is where it starts to be really useful 😅)\n    return {\n      content: \"Press some buttons!\",\n      components: [\n        {\n          type: ComponentType.ACTION_ROW,\n          components: [\n            {\n              type: ComponentType.BUTTON,\n              custom_id: buttonId1 + \"extra\",\n              style: ButtonStyle.PRIMARY,\n              label: \"Primary\",\n            },\n            {\n              type: ComponentType.BUTTON,\n              custom_id: buttonId2,\n              style: ButtonStyle.SECONDARY,\n              label: \"Secondary\",\n            },\n            {\n              type: ComponentType.BUTTON,\n              custom_id: buttonId3,\n              style: ButtonStyle.SUCCESS,\n              label: \"Success\",\n            },\n            {\n              type: ComponentType.BUTTON,\n              custom_id: buttonId4,\n              style: ButtonStyle.DANGER,\n              label: \"Danger\",\n            },\n            {\n              type: ComponentType.BUTTON,\n              url: \"https://miniflare.dev\",\n              style: ButtonStyle.LINK,\n              label: \"Link\",\n            },\n          ],\n        },\n        {\n          type: ComponentType.ACTION_ROW,\n          components: [\n            {\n              type: ComponentType.BUTTON,\n              custom_id: buttonId5,\n              style: ButtonStyle.SECONDARY,\n              label: \"Implicit Row\",\n            },\n          ],\n        },\n      ],\n    };\n  };\n}\n```\n\n\u003c!-- prettier-ignore-end --\u003e\n\n![Buttons](./.github/screenshots/buttons.png)\n\nButtons can also be disabled or include emojis in their labels. Disabled buttons\naren't clickable, and cannot submit interactions. Emojis can either be strings\nor [objects](https://discord.com/developers/docs/resources/emoji#emoji-object)\ncontaining the emoji's `id`, `name`, and whether it's `animated`.\n\n```tsx\nfunction buttons(): CommandHandler {\n  // ...\n  return () =\u003e {\n    // With JSX\n    return (\n      \u003cMessage\u003e\n        Try to press this button!\n        \u003cButton\n          id={buttonId}\n          danger\n          disabled\n          emoji=\"☹️\" // or emoji={{ id: \"...\", name: \"...\", animated: false }}\n        \u003e\n          Disabled Button\n        \u003c/Button\u003e\n      \u003c/Message\u003e\n    );\n\n    // Without JSX\n    return {\n      content: \"Try press this button!\",\n      components: [\n        {\n          type: ComponentType.ACTION_ROW,\n          components: [\n            {\n              type: ComponentType.BUTTON,\n              custom_id: buttonId,\n              style: ButtonStyle.DANGER,\n              label: \"Disabled Button\",\n              disabled: true,\n              emoji: { name: \"☹️\" },\n            },\n          ],\n        },\n      ],\n    };\n  };\n}\n```\n\n![Disabled Emoji Button](./.github/screenshots/buttons-disabled-emoji.png)\n\n### Select Menus\n\nSelect menus let users select one or multiple options from a dropdown. You'll\nreceive an interaction whenever the user clicks outside the menu, after\nselecting some options. They can have up to 25 options, optional `placeholder`\ntext (for when nothing is selected), and can have their minimum/maximum\nselectable items configured.\n\nLike buttons, you first need to get a routable custom ID using the\n`useSelectMenu` hook, and then wire this up to a select menu in your original\ncommand response. They can also be disabled.\n\nMenus need to be included in an action row. However, they take up the full row,\nso this can't be shared with other components. If you're using JSX and don't\nwrap your menu in an action row, Slshx will implicitly create one for that menu.\n\nOptions must include a `value` and `label`. The selected `value`s are submitted\nwith the interaction. They may include a `description`, `emoji` or be marked as\nthe `default` option.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { ComponentType, createElement, Message, Select, Option, $update, useSelectMenu, CommandHandler, APIMessageComponentInteraction } from \"slshx\";\nimport type { APIMessageSelectMenuInteractionData } from \"discord-api-types/v9\";\n\nfunction selects(): CommandHandler {\n  // ...\n  const selectId = useSelectMenu((interaction, env, ctx) =\u003e {\n    //                            └ APIMessageComponentInteraction\u003cAPIMessageSelectMenuInteractionData\u003e\n     \n    // Array of selected values, e.g. [\"1\", \"3\"]\n    const selected = interaction.data.values;\n\n    // With JSX\n    return \u003cMessage update\u003eSelected: {selected.join(\",\")}\u003c/Message\u003e;\n\n    // Without JSX\n    return {\n      [$update]: true,\n      content: `Selected: ${selected.join(\", \")}`,\n      // Using JSX will implicitly remove all components from the message unless\n      // they're redefined as children. If you'd like to keep them, you can\n      // remove this next line.\n      components: [],\n    };\n  });\n\n  return (interaction, env, ctx) =\u003e {\n    // With JSX\n    return (\n      \u003cMessage\u003e\n        Select some options!\n        \u003cSelect\n          id={selectId}\n          placeholder=\"Select something...\"\n          min={1} // Minimum number of items to select, defaults to 1\n          max={2} // Maximum number of items to select, defaults to 1\n        \u003e\n          \u003cOption value=\"1\" default\u003eOne\u003c/Option\u003e\n          \u003cOption value=\"2\" description=\"1st prime number\"\u003eTwo\u003c/Option\u003e\n          \u003cOption value=\"3\" emoji=\"📐\"\u003eThree\u003c/Option\u003e\n        \u003c/Select\u003e\n      \u003c/Message\u003e\n    );\n\n    // Without JSX\n    return {\n      content: \"Select some options!\",\n      components: [\n        {\n          type: ComponentType.ACTION_ROW,\n          components: [\n            {\n              type: ComponentType.SELECT_MENU,\n              custom_id: selectId,\n              placeholder: \"Select something...\",\n              min_values: 1,\n              max_values: 2,\n              options: [\n                { value: \"1\", label: \"One\", default: true },\n                { value: \"2\", label: \"Two\", description: \"1st prime number\" },\n                { value: \"3\", label: \"Three\", emoji: { name: \"📐\" } },\n              ],\n            },\n          ],\n        },\n      ],\n    };\n  };\n}\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n![Select Menu](./.github/screenshots/select.png)\n\n## Using Modals\n\n[Modals](https://discord.com/developers/docs/interactions/receiving-and-responding#responding-to-an-interaction)\nallow you to respond to commands or message component interactions with dialog\nboxes containing text inputs. Instead of returning a `\u003cMessage\u003e`, return a\n`\u003cModal\u003e` or a plain message object with the `[$modal]` property set to `true`.\n\nTo generate a custom modal ID including the required Slshx routing information,\ncall the `useModal` hook. This takes a callback function taking an\n`interaction`, `env`, and `ctx` that will be called when the modal is submitted.\n\nTo add text inputs, call the `useInput` hook. This returns an `[id, value]`\ntuple containing a custom ID to identify the input and the submitted value. Note\nthat the `value` should only be used inside `useModal` callback functions.\n\n\u003c!-- prettier-ignore-start --\u003e\n```tsx\nimport { CommandHandler, useInput, useModal, createElement, Message, Modal, Input, $modal, ComponentType, TextInputStyle } from \"slshx\";\n\nexport function modals(): CommandHandler\u003cEnv\u003e {\n  // ...\n  const [nameId, nameValue] = useInput();\n  const [messageId, messageValue] = useInput();\n  const modalId = useModal\u003cEnv\u003e((interaction, env, ctx) =\u003e {\n    //                           └ APIModalSubmitInteraction\n\n    // With JSX\n    return \u003cMessage\u003eHello {nameValue}! {messageValue}\u003c/Message\u003e;\n\n    // Without JSX\n    return { content: `Hello ${nameValue}! ${messageValue}` };\n  });\n\n  return () =\u003e {\n    // With JSX\n    return (\n      \u003cModal id={modalId} title=\"Send Message\"\u003e\n        \u003cInput\n          id={nameId} // Only `id` and `label` are required\n          label=\"Name\"\n          required\n          value=\"Initial value\"\n          minLength={1}\n        /\u003e\n        \u003cInput\n          id={messageId}\n          label=\"Message\"\n          placeholder=\"Something to send\"\n          maxLength={1000}\n          paragraph // Multiline input\n        /\u003e\n      \u003c/Modal\u003e\n    );\n\n    // Without JSX\n    return {\n      [$modal]: true,\n      custom_id: modalId,\n      title: \"Send Message\",\n      components: [\n        {\n          type: ComponentType.ACTION_ROW,\n          components: [\n            {\n              type: ComponentType.TEXT_INPUT,\n              style: TextInputStyle.SHORT,\n              custom_id: nameId,\n              label: \"Name\",\n              required: true,\n              value: \"Initial value\",\n              min_length: 1,\n            },\n          ],\n        },\n        {\n          type: ComponentType.ACTION_ROW,\n          components: [\n            {\n              type: ComponentType.TEXT_INPUT,\n              style: TextInputStyle.PARAGRAPH,\n              custom_id: messageId,\n              label: \"Message\",\n              placeholder: \"Something to send\",\n              required: false, // Inputs are required by default\n              max_length: 1000,\n            },\n          ],\n        },\n      ],\n    };\n  };\n}\n```\n\u003c!-- prettier-ignore-end --\u003e\n\n![Modal](./.github/screenshots/modal.png)\n\n![Modal Submission](./.github/screenshots/modal-submit.png)\n\n## Errors\n\nDuring development, if a command, message component, or modal submission handler\nthrows an error, Slshx will respond with the message and stack trace. In\nproduction, the interaction will fail.\n\n![Development Error](./.github/screenshots/error-development.png)\n\n![Production Error](./.github/screenshots/error-production.png)\n\n## Deploying Commands Globally\n\nOnce you're happy with your commands, you can deploy them globally, making them\naccessible to all servers you've added your application to, not just\n`testServerId`. Changes may take up to an hour to propagate.\n\nYou'll need to deploy your Worker first with `wrangler publish`. Once you've\ndone this, you'll need to update the **Interactions Endpoint URL** in the\n[Discord Developer Portal](https://discord.com/developers/applications) to point\nto your deployed URL, instead of your Cloudflare Tunnel. You'll probably want to\ncreate 2 applications: one for development, using your tunnel URL, and one for\nproduction, using your deployed URL.\n\nIf you're using the [`slshx-starter`](https://github.com/mrbbot/slshx-starter)\ntemplate, run `npm run deploy:global` to deploy your commands globally.\n\nIf not, make sure your `applicationId` and `applicationSecret` are set to your\nproduction application's credentials, and visit your worker in the browser. You\nshould see the ⚔️ **Slshx** landing page. Click the **Deploy Commands Globally**\nbutton.\n\n![Slshx Landing Page](./.github/screenshots/landing.png)\n\n## Calling Discord APIs\n\n\u003e ⚠️ Slshx aims to abstract away most of the Discord API. You shouldn't use\n\u003e these functions unless you really need to.\n\nSometimes you might need to call Discord APIs to send additional messages, edit\nexisting ones, or update command permissions. Slshx exports typed functions for\ncalling APIs associated with interactions.\n\n### Authorisation\n\nSome of these APIs require an interaction token, which you can obtain from\n`interaction.token` in any command, component or autocomplete handler.\n\n```tsx\nimport { createFollowupMessage } from \"slshx\";\n\nconst applicationId = \"...\";\n\nfunction followup(): CommandHandler {\n  // ...\n  return (interaction, env, ctx) =\u003e {\n    async function sendFollowup() {\n      await scheduler.wait(1000);\n\n      // With JSX\n      let msg = \u003cMessage\u003eFollowup!\u003c/Message\u003e;\n      // Without JSX\n      msg = { content: \"Followup!\" };\n\n      await createFollowupMessage(applicationId, interaction.token, msg);\n    }\n\n    // Remember to `waitUntil` extra promises\n    ctx.waitUntil(sendFollowup());\n\n    return \u003cMessage\u003eI'll send something in a second!\u003c/Message\u003e;\n  };\n}\n```\n\nOthers require a bearer token which can be obtained using the `getBearerAuth`\nfunction, which takes your `applicationId` and `applicationSecret`.\n\n```ts\nimport { getBearerAuth, getGuildApplicationCommandPermissions } from \"slshx\";\n\nconst applicationId = \"...\";\nconst applicationSecret = \"...\";\nconst serverId = \"...\";\n\nconst auth = await getBearerAuth(applicationId, applicationSecret);\n\nawait getGuildApplicationCommandPermissions(applicationId, serverId, auth);\n```\n\n### List\n\n#### Interactions (Require Interaction Token)\n\n- [`getOriginalInteractionResponse(applicationId, interactionToken)`](https://discord.com/developers/docs/interactions/receiving-and-responding#get-original-interaction-response)\n- [`editOriginalInteractionResponse(applicationId, interactionToken, message*)`](https://discord.com/developers/docs/interactions/receiving-and-responding#edit-original-interaction-response)\n- [`deleteOriginalInteractionResponse(applicationId, interactionToken)`](https://discord.com/developers/docs/interactions/receiving-and-responding#delete-original-interaction-response)\n- [`createFollowupMessage(applicationId, interactionToken, message*)`](https://discord.com/developers/docs/interactions/receiving-and-responding#create-followup-message)\n- [`getFollowupMessage(applicationId, interactionToken, messageId)`](https://discord.com/developers/docs/interactions/receiving-and-responding#get-followup-message)\n- [`editFollowupMessage(applicationId, interactionToken, messageId, message*)`](https://discord.com/developers/docs/interactions/receiving-and-responding#edit-followup-message)\n- [`deleteFollowupMessage(applicationId, interactionToken, messageId)`](https://discord.com/developers/docs/interactions/receiving-and-responding#delete-followup-message)\n\n`message*` arguments accept the same message objects we've been returning from\ncommand handlers. This means you can use JSX, and `attachments` must be an array\nof [`File` objects](https://developer.mozilla.org/en-US/docs/Web/API/File).\n\n#### Commands (Require Bearer Token)\n\n- [`getGlobalApplicationCommands(applicationId, auth)`](https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands)\n- [`createGlobalApplicationCommand(applicationId, command, auth)`](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command)\n- [`getGlobalApplicationCommand(applicationId, commandId, auth)`](https://discord.com/developers/docs/interactions/application-commands#get-global-application-command)\n- [`editGlobalApplicationCommand(applicationId, commandId, command, auth)`](https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command)\n- [`deleteGlobalApplicationCommand(applicationId, commandId, auth)`](https://discord.com/developers/docs/interactions/application-commands#delete-global-application-command)\n- [`bulkOverwriteGlobalApplicationCommands(applicationId, commands, auth)`](https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands)\n- [`getGuildApplicationCommands(applicationId, guildId, auth)`](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands)\n- [`createGuildApplicationCommand(applicationId, guildId, command, auth)`](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command)\n- [`getGuildApplicationCommand(applicationId, guildId, commandId, auth)`](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command)\n- [`editGuildApplicationCommand(applicationId, guildId, commandId, command, auth)`](https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command)\n- [`deleteGuildApplicationCommand(applicationId, guildId, commandId, auth)`](https://discord.com/developers/docs/interactions/application-commands#delete-guild-application-command)\n- [`bulkOverwriteGuildApplicationCommands(applicationId, guildId, commands, auth)`](https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands)\n\n#### Permissions (Require Bearer Token)\n\n- [`getGuildApplicationCommandPermissions(applicationId, guildId, auth)`](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions)\n- [`getApplicationCommandPermissions(applicationId, guildId, commandId, auth)`](https://discord.com/developers/docs/interactions/application-commands#get-application-command-permissions)\n- [`editApplicationCommandPermissions(applicationId, guildId, commandId, permissions, auth)`](https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions)\n- [`bulkEditApplicationCommandPermissions(applicationId, guildId, permissions, auth)`](https://discord.com/developers/docs/interactions/application-commands#batch-edit-application-command-permissions)\n\n### Missing APIs\n\nIf an API does not have Slshx bindings, you can use the\n`call(method, path, body?, auth?)` function:\n\n- `method` must be a standard HTTP method\n- `path` will be appended to `https://discord.com/api/v9` to form the endpoint\n  URL\n- `body` can be an instance of `FormData` (sent as `multipart/form-data`),\n  `URLSearchParams` (sent as `application/x-www-form-urlencoded`), or an\n  arbitrary JSON-serializable object (sent as `application/json`). If `body` is\n  falsy, it's omitted.\n- `auth` can be an object of the form:\n  - `{ bearer: string }` (what `getBearerAuth` returns) to use `Bearer` token\n    authentication\n  - `{ username: string; password: string }` to use HTTP `Basic` authentication\n  - `{ bot: string }` to use `Bot` token authentication\n\nThis function is generic in `Body` and `Result`. You can find types for these in\nthe `discord-api-types` package. See [`src/api/`](./src/api) for examples of\nusing this function.\n\n## Notes\n\n### Rate Limits\n\nMost Discord APIs are rate limited to prevent abuse. Notably, the endpoint Slshx\nuses to update commands in your test server is limited to 2 requests per minute.\nSlshx will only call this API if commands have changed since the last code\nreload. If you hit the rate limit, wait the required time, then save your code\nagain.\n\n### Enums\n\nSlshx [redefines certain Discord enums](./src/api/enums.ts) instead of using the\ndefinitions in `discord-api-types`, which are declared as `ambient const enums`,\nand can't be accessed with the `--isolatedModules` flag. This flag is required\nwhen using TypeScript with `esbuild`.\n\nWe use `const`s as opposed to `enum`s as they still type-check with\n`discord-api-types`' `enum`s, and they're easier to tree shake.\n\n### History\n\nSlshx originally used generator functions to define commands. The syntax looked\nsomething like this:\n\n```tsx\nconst add: Command\u003cEnv\u003e = async function* () {\n  useDescription(\"Adds two numbers together\");\n  const a = useNumber(\"a\", \"1st number\", { required: true });\n  const b = useNumber(\"b\", \"2nd number\", { required: true });\n\n  // `yield` at least once, once all hooks were called\n  const [interaction, env, ctx] = yield;\n\n  // Optionally, `yield` again to defer the response\n  yield;\n\n  return \u003cMessage\u003e...\u003c/Message\u003e;\n};\n```\n\nThis has the advantage of using one fewer nesting levels when responding to\ninvocations. However, there were too many foot-guns and issues with this syntax:\n\n- You weren't meant to `await` before the first `yield`, despite it being an\n  `async`-generator\n- The type for `yield`-ed results had to be the same, even though the second\n  `yield` shouldn't return anything\n- You had to destructure the entire `yield`-ed tuple or index it manually,\n  otherwise TypeScript would complain\n- Code completion for plain object message fields in `return`s was broken\n\nI've kept it here though since I think it's still pretty neat, and I didn't know\nyou could resume generators with values (e.g. `[interaction, env, ctx]`) before\nthis project.\n\n## Acknowledgements\n\nThanks to the\n[`discord-api-types`](https://github.com/discordjs/discord-api-types)\nmaintainers for providing Discord API TypeScript definitions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrbbot%2Fslshx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrbbot%2Fslshx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrbbot%2Fslshx/lists"}