botflare 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ type Upload = {
4
4
  data: Blob | ArrayBuffer | Uint8Array<ArrayBuffer> | string;
5
5
  description?: string;
6
6
  };
7
+ /** @internal */
7
8
  type S = {
8
9
  AccountResponse: {
9
10
  id: string;
@@ -4606,6 +4607,7 @@ type S = {
4606
4607
  execute_slack_compatible_webhook_body: S["SlackWebhook"];
4607
4608
  execute_slack_compatible_webhook_result: string | null;
4608
4609
  };
4610
+ /** @internal */
4609
4611
  type Routes = {
4610
4612
  "/applications/@me": {
4611
4613
  get: {
@@ -7264,21 +7266,33 @@ type Locales = Partial<Record<`${Locale}`, {
7264
7266
  name?: string;
7265
7267
  description?: string;
7266
7268
  }>>;
7269
+ /**
7270
+ * A button on a message. A button with an `id` runs the handler with that id in the `buttons` of
7271
+ * `bot()`, and takes a `style`: `primary` (default), `secondary`, `success` or `danger`. A button
7272
+ * with a `url` opens the link and needs no handler. A button needs a `label`, an `emoji` or both.
7273
+ * An `emoji` is a Unicode emoji, or a custom emoji such as `<:name:id>`.
7274
+ */
7267
7275
  type Button = ({
7276
+ /** The handler name, then an optional `:` and a value, such as `page:2`. */
7268
7277
  id: string;
7278
+ /** `primary` (default), `secondary`, `success` or `danger`. */
7269
7279
  style?: ButtonStyle;
7270
7280
  url?: never;
7271
7281
  } | {
7282
+ /** The link the button opens. */
7272
7283
  url: string;
7273
7284
  id?: never;
7274
7285
  style?: never;
7275
7286
  }) & ({
7287
+ /** The text on the button. */
7276
7288
  label: string;
7289
+ /** A Unicode emoji, or a custom emoji such as `<:name:id>`. */
7277
7290
  emoji?: string;
7278
7291
  } | {
7279
7292
  label?: string;
7280
7293
  emoji: string;
7281
7294
  }) & {
7295
+ /** `true` greys the button out. */
7282
7296
  disabled?: boolean;
7283
7297
  };
7284
7298
  type ButtonStyle = "primary" | "secondary" | "success" | "danger";
@@ -7330,20 +7344,39 @@ type SelectMenu = {
7330
7344
  options?: Record<string, string>;
7331
7345
  channelTypes?: ChannelType[];
7332
7346
  };
7347
+ /** What a message holds besides its text. */
7333
7348
  type MessageOptions = {
7349
+ /** Buttons, five to a row. Pass `[]` to remove the buttons of the message. */
7334
7350
  buttons?: Button[];
7351
+ /** Select menus made with `select()`, one to a row, above the buttons. */
7335
7352
  selects?: SelectMenu[];
7353
+ /** Embeds, as in the Discord API, such as `{ title, description }`. */
7336
7354
  embeds?: S["RichEmbed"][];
7355
+ /**
7356
+ * Who the message pings. By default it pings nobody, even for `@everyone` or `<@id>` in the
7357
+ * text. Pass `{ users: [id] }` or `{ parse: ['roles'] }` to ping.
7358
+ */
7337
7359
  allowedMentions?: S["MessageAllowedMentionsRequest"];
7360
+ /** Files to attach. */
7338
7361
  files?: Upload[];
7339
7362
  };
7363
+ /** {@link MessageOptions} for the first answer or a follow-up. */
7340
7364
  type ReplyOptions = MessageOptions & {
7365
+ /** `true` shows the message to the user who ran the interaction only. */
7341
7366
  ephemeral?: boolean;
7342
7367
  };
7368
+ /** Calls that send more after the first answer. Each returns an `Error` if Discord refuses. */
7343
7369
  type FollowUps = {
7370
+ /**
7371
+ * Runs `work` once Discord has the first answer. Call it before you return the answer. `work`
7372
+ * can call the other follow-up calls.
7373
+ */
7344
7374
  after(work: () => Promise<void>): void;
7375
+ /** Sends a new message. */
7345
7376
  followUp(content: string, options?: ReplyOptions): Promise<Error | null>;
7377
+ /** Edits the first answer. */
7346
7378
  editReply(content: string, options?: MessageOptions): Promise<Error | null>;
7379
+ /** Deletes the first answer. */
7347
7380
  deleteReply(): Promise<Error | null>;
7348
7381
  };
7349
7382
  type Fields = Record<string, {
@@ -7366,21 +7399,42 @@ type Modal<F extends Fields = Fields> = {
7366
7399
  } & FollowUps): Response | Promise<Response>;
7367
7400
  };
7368
7401
  type Permission = Exclude<keyof typeof PermissionFlagsBits, "ManageEmojisAndStickers">;
7402
+ /** What the handler of a slash command gets. */
7369
7403
  type Context<O extends Options> = {
7404
+ /** The interaction as Discord sent it. */
7370
7405
  interaction: APIChatInputApplicationCommandInteraction;
7406
+ /** The option values, keyed by name. An option that is not required can be `undefined`. */
7371
7407
  options: { [K in keyof O]: O[K] extends Option<keyof OptionValue, infer R, infer V> ? R extends true ? V : V | undefined : never; };
7408
+ /** Answers within 3 seconds. Return what it gives. */
7372
7409
  reply: (content: string, options?: ReplyOptions) => Response;
7410
+ /**
7411
+ * Shows "thinking…" now, then edits the answer to what `work` returns: the text, or
7412
+ * `{ content, buttons, embeds }`. Use it for work that takes longer than 3 seconds.
7413
+ */
7373
7414
  defer: (work: () => Promise<string | ({
7374
7415
  content: string;
7375
7416
  } & MessageOptions)>, options?: {
7376
7417
  ephemeral?: boolean;
7377
7418
  }) => Response;
7419
+ /** Opens a modal made with `modal()`. `value` comes back to its handler as `c.value`. */
7378
7420
  modal: (modal: Modal, value?: string) => Response;
7379
7421
  } & FollowUps;
7422
+ /** Who sees a command or a context menu item, and where. */
7380
7423
  type Access = {
7424
+ /**
7425
+ * The name and description in other languages, keyed by Discord locale, such as
7426
+ * `{ th: { name: 'ทอย', description: 'ทอยลูกเต๋า' } }`. The handler still gets the default name.
7427
+ */
7381
7428
  locales?: Locales;
7429
+ /**
7430
+ * Who sees it by default: members with all of these permissions, such as `['ManageGuild']`.
7431
+ * Pass `[]` to show it to admins only. Server admins can change this in Server Settings.
7432
+ * Applies to a top-level command only.
7433
+ */
7382
7434
  permissions?: Permission[];
7435
+ /** Where it shows: `guild`, `bot_dm` or `private_channel`. */
7383
7436
  contexts?: ("guild" | "bot_dm" | "private_channel")[];
7437
+ /** How the app must be installed for it: `guild` or `user`. */
7384
7438
  installs?: ("guild" | "user")[];
7385
7439
  };
7386
7440
  type Command<O extends Options = Options> = ({
@@ -7456,12 +7510,34 @@ type CronContext = {
7456
7510
  send: Send;
7457
7511
  };
7458
7512
  type BotConfig = {
7513
+ /** The commands, made with {@link command} or `contextMenu()`. */
7459
7514
  commands: (Command | ContextMenu)[];
7515
+ /**
7516
+ * Answers a click, keyed by the button id given to `reply`. An id such as `page:2` runs the
7517
+ * `page` handler with `c.value` set to `'2'`. `c.update` edits the message that holds the button.
7518
+ * Pass `{ buttons: [] }` to remove them. `c.deferUpdate(work)` keeps the message as it is and
7519
+ * edits it with what `work` returns, for a click that takes longer than 3 seconds.
7520
+ */
7460
7521
  buttons?: Record<string, (c: ButtonContext) => Response | Promise<Response>>;
7522
+ /** The select menus the bot answers, made with `select()`. */
7461
7523
  selects?: Select<SelectType, unknown>[];
7524
+ /** The modals the bot answers, made with {@link modal}. */
7462
7525
  modals?: Modal[];
7526
+ /**
7527
+ * The name of the Durable Object binding that holds the class from `gateway()`. Open the Worker
7528
+ * URL once after the first deploy to start the connection. A GET request or any Cron Trigger
7529
+ * starts it again if it is down.
7530
+ */
7463
7531
  gateway?: string;
7532
+ /**
7533
+ * Gets errors from deferred work. If that work fails, the bot also edits the answer to
7534
+ * "Something went wrong." so it does not stay on "thinking…".
7535
+ */
7464
7536
  onError?: ErrorHandler;
7537
+ /**
7538
+ * Runs on a Cron Trigger, keyed by the cron expression from your Wrangler config. `c.send` posts
7539
+ * a message to a channel with `DISCORD_TOKEN`, and returns an `Error` if Discord refuses.
7540
+ */
7465
7541
  cron?: Record<string, (c: CronContext) => void | Promise<void>>;
7466
7542
  };
7467
7543
  /** The Worker handler from {@link bot}. */
@@ -7482,24 +7558,6 @@ type Bot = {
7482
7558
  * the hex public key of your application in the Discord Developer Portal.
7483
7559
  *
7484
7560
  * @param config What the bot answers.
7485
- * @param config.commands The commands, made with {@link command} or `contextMenu()`.
7486
- * @param config.modals The modals the bot answers, made with {@link modal}.
7487
- * @param config.onError Gets errors from deferred work. If that work fails, the bot also edits the
7488
- * answer to "Something went wrong." so it does not stay on "thinking…".
7489
- * @param config.gateway The name of the Durable Object binding that holds the class from `gateway()`.
7490
- * Open the Worker URL once after the first deploy to start the connection. A GET request or any
7491
- * Cron Trigger starts it again if it is down.
7492
- * @param config.cron Runs on a Cron Trigger, keyed by the cron expression from your Wrangler config.
7493
- * `c.send` posts a message to a channel with `DISCORD_TOKEN`, with optional `buttons` and `embeds`,
7494
- * and returns an `Error` if Discord refuses.
7495
- * @param config.buttons Answers a click, keyed by the button id given to `reply`. An id such as
7496
- * `page:2` runs the `page` handler with `c.value` set to `'2'`. `update` edits the message that
7497
- * holds the button. Pass `{ buttons: [] }` to remove them.
7498
- * A button takes a `label`, an `emoji` or both, and a `style`: `primary` (default), `secondary`,
7499
- * `success` or `danger`. A button with a `url` instead of an `id` opens the link and needs no
7500
- * handler. `disabled: true` greys a button out. `c.deferUpdate(work)` keeps the message as it is
7501
- * and edits it with what `work` returns, for a click that takes longer than 3 seconds.
7502
- * @param config.selects The select menus the bot answers, made with `select()`.
7503
7561
  * @returns The Worker handler. Export it as the default export of your Worker. Pass it to
7504
7562
  * `gateway({ bot })` to answer interactions that arrive over the gateway too.
7505
7563
  * @example
@@ -7509,4 +7567,4 @@ type Bot = {
7509
7567
  */
7510
7568
  declare function bot(config: BotConfig): Bot;
7511
7569
  export { Locales as C, Suggestion as S, AutocompleteContext as _, Select as a, OptionValue as b, Access as c, ContextMenu as d, MenuContext as f, SelectType as g, Modal as h, ErrorHandler as i, Command as l, Fields as m, bot as n, SelectContext as o, MenuType as p, Send as r, SelectValue as s, Bot as t, Context as u, ChannelType as v, Routes as w, Options as x, Option as y };
7512
- //# sourceMappingURL=bot-icKel3r9.d.mts.map
7570
+ //# sourceMappingURL=bot-CW_jhC5U.d.mts.map
package/dist/cli.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  import { r as putCommands } from "./discord-DEZe1asd.mjs";
3
3
  import { n as payload } from "./command-CBeslrzu.mjs";
4
4
  import { registerHooks } from "node:module";
5
+ import { existsSync } from "node:fs";
5
6
  import { resolve } from "node:path";
6
7
  import { pathToFileURL } from "node:url";
7
8
  import { parseArgs } from "node:util";
@@ -22,6 +23,7 @@ function syncEnv(vars) {
22
23
  }];
23
24
  }
24
25
  // cli/index.ts
26
+ const ENV_FILES = [".dev.vars", ".env"];
25
27
  const plural = (n) => `${n} command${n === 1 ? "" : "s"}`;
26
28
  const WORKERS_STUB = `data:text/javascript,${encodeURIComponent([
27
29
  "export const env = {};",
@@ -44,6 +46,7 @@ async function main() {
44
46
  cancel("Usage: botflare sync <worker entry>");
45
47
  return 1;
46
48
  }
49
+ for (const file of ENV_FILES) if (existsSync(file)) process.loadEnvFile(file);
47
50
  const [envError, env] = syncEnv(process.env);
48
51
  if (envError) {
49
52
  cancel(envError.message);
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/utils/env.ts","../cli/index.ts"],"sourcesContent":["export type SyncEnv = { token: string; applicationId: string; guildId: string | undefined };\n\nconst SNOWFLAKE = /^\\d{17,20}$/;\n\nexport function syncEnv(vars: Record<string, string | undefined>): [Error, null] | [null, SyncEnv] {\n const token = vars.DISCORD_TOKEN;\n const applicationId = vars.DISCORD_APPLICATION_ID;\n const guildId = vars.DISCORD_GUILD_ID || undefined;\n\n if (!token) return [new Error('DISCORD_TOKEN is missing'), null];\n if (!applicationId || !SNOWFLAKE.test(applicationId)) {\n return [\n new Error('DISCORD_APPLICATION_ID must be the application id from the Developer Portal'),\n null,\n ];\n }\n if (guildId && !SNOWFLAKE.test(guildId)) {\n return [new Error('DISCORD_GUILD_ID must be a server id'), null];\n }\n\n return [null, { token, applicationId, guildId }];\n}\n","#!/usr/bin/env node\nimport { registerHooks } from 'node:module';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { parseArgs } from 'node:util';\n\nimport { cancel, intro, outro, spinner } from '@clack/prompts';\n\nimport { putCommands } from '#lib/discord';\nimport { type Command, type ContextMenu, payload } from '#utils/command';\nimport { syncEnv } from '#utils/env';\n\nconst plural = (n: number): string => `${n} command${n === 1 ? '' : 's'}`;\n\nconst WORKERS_STUB = `data:text/javascript,${encodeURIComponent(\n [\n 'export const env = {};',\n 'export const exports = {};',\n 'export function waitUntil() {}',\n 'export class DurableObject {}',\n 'export class WorkerEntrypoint {}',\n 'export class WorkflowEntrypoint {}',\n 'export class RpcTarget {}',\n ].join('\\n'),\n)}`;\n\nregisterHooks({\n resolve: (specifier, context, next) =>\n specifier.startsWith('cloudflare:')\n ? { url: WORKERS_STUB, shortCircuit: true }\n : next(specifier, context),\n});\n\nasync function main(): Promise<number> {\n intro('botflare sync');\n\n const { positionals } = parseArgs({ allowPositionals: true });\n const [task, entry] = positionals;\n if (task !== 'sync' || !entry) {\n cancel('Usage: botflare sync <worker entry>');\n return 1;\n }\n\n const [envError, env] = syncEnv(process.env);\n if (envError) {\n cancel(envError.message);\n return 1;\n }\n\n let mod: { default?: { commands?: (Command | ContextMenu)[] } };\n try {\n mod = await import(pathToFileURL(resolve(entry)).href);\n } catch (error) {\n cancel(`Cannot load ${entry}: ${error instanceof Error ? error.message : String(error)}`);\n return 1;\n }\n\n const commands = mod.default?.commands;\n if (!commands) {\n cancel(`${entry} must export default bot({ commands })`);\n return 1;\n }\n\n const target = env.guildId ? `server ${env.guildId}` : 'every server';\n const progress = spinner();\n progress.start(`Sending ${plural(commands.length)} to ${target}`);\n\n const [putError, count] = await putCommands(env, commands.map(payload));\n if (putError) {\n progress.error(putError.message);\n return 1;\n }\n\n progress.stop(`Discord saved ${plural(count)}`);\n outro(`Type / in ${env.guildId ? 'the server' : 'a server with the bot'} to try them.`);\n\n return 0;\n}\n\nprocess.exitCode = await main();\n"],"mappings":";;;;;;;;;AAEA,MAAM,YAAY;AAElB,SAAgB,QAAQ,MAA2E;CACjG,MAAM,QAAQ,KAAK;CACnB,MAAM,gBAAgB,KAAK;CAC3B,MAAM,UAAU,KAAK,oBAAoB,KAAA;CAEzC,IAAI,CAAC,OAAO,OAAO,iBAAC,IAAI,MAAM,0BAA0B,GAAG,IAAI;CAC/D,IAAI,CAAC,iBAAiB,CAAC,UAAU,KAAK,aAAa,GACjD,OAAO,iBACL,IAAI,MAAM,6EAA6E,GACvF,IACF;CAEF,IAAI,WAAW,CAAC,UAAU,KAAK,OAAO,GACpC,OAAO,iBAAC,IAAI,MAAM,sCAAsC,GAAG,IAAI;CAGjE,OAAO,CAAC,MAAM;EAAE;EAAO;EAAe;CAAQ,CAAC;AACjD;;ACTA,MAAM,UAAU,MAAsB,GAAG,EAAE,UAAU,MAAM,IAAI,KAAK;AAEpE,MAAM,eAAe,wBAAwB,mBAC3C;CACE;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI,CACb;AAEA,cAAc,EACZ,UAAU,WAAW,SAAS,SAC5B,UAAU,WAAW,aAAa,IAC9B;CAAE,KAAK;CAAc,cAAc;AAAK,IACxC,KAAK,WAAW,OAAO,EAC/B,CAAC;AAED,eAAe,OAAwB;CACrC,MAAM,eAAe;CAErB,MAAM,EAAE,gBAAgB,UAAU,EAAE,kBAAkB,KAAK,CAAC;CAC5D,MAAM,CAAC,MAAM,SAAS;CACtB,IAAI,SAAS,UAAU,CAAC,OAAO;EAC7B,OAAO,qCAAqC;EAC5C,OAAO;CACT;CAEA,MAAM,CAAC,UAAU,OAAO,QAAQ,QAAQ,GAAG;CAC3C,IAAI,UAAU;EACZ,OAAO,SAAS,OAAO;EACvB,OAAO;CACT;CAEA,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,OAAO,cAAc,QAAQ,KAAK,CAAC,CAAC,CAAC;CACnD,SAAS,OAAO;EACd,OAAO,eAAe,MAAM,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;EACxF,OAAO;CACT;CAEA,MAAM,WAAW,IAAI,SAAS;CAC9B,IAAI,CAAC,UAAU;EACb,OAAO,GAAG,MAAM,uCAAuC;EACvD,OAAO;CACT;CAEA,MAAM,SAAS,IAAI,UAAU,UAAU,IAAI,YAAY;CACvD,MAAM,WAAW,QAAQ;CACzB,SAAS,MAAM,WAAW,OAAO,SAAS,MAAM,EAAE,MAAM,QAAQ;CAEhE,MAAM,CAAC,UAAU,SAAS,MAAM,YAAY,KAAK,SAAS,IAAI,OAAO,CAAC;CACtE,IAAI,UAAU;EACZ,SAAS,MAAM,SAAS,OAAO;EAC/B,OAAO;CACT;CAEA,SAAS,KAAK,iBAAiB,OAAO,KAAK,GAAG;CAC9C,MAAM,aAAa,IAAI,UAAU,eAAe,wBAAwB,cAAc;CAEtF,OAAO;AACT;AAEA,QAAQ,WAAW,MAAM,KAAK"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/utils/env.ts","../cli/index.ts"],"sourcesContent":["export type SyncEnv = { token: string; applicationId: string; guildId: string | undefined };\n\nconst SNOWFLAKE = /^\\d{17,20}$/;\n\nexport function syncEnv(vars: Record<string, string | undefined>): [Error, null] | [null, SyncEnv] {\n const token = vars.DISCORD_TOKEN;\n const applicationId = vars.DISCORD_APPLICATION_ID;\n const guildId = vars.DISCORD_GUILD_ID || undefined;\n\n if (!token) return [new Error('DISCORD_TOKEN is missing'), null];\n if (!applicationId || !SNOWFLAKE.test(applicationId)) {\n return [\n new Error('DISCORD_APPLICATION_ID must be the application id from the Developer Portal'),\n null,\n ];\n }\n if (guildId && !SNOWFLAKE.test(guildId)) {\n return [new Error('DISCORD_GUILD_ID must be a server id'), null];\n }\n\n return [null, { token, applicationId, guildId }];\n}\n","#!/usr/bin/env node\nimport { existsSync } from 'node:fs';\nimport { registerHooks } from 'node:module';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { parseArgs } from 'node:util';\n\nimport { cancel, intro, outro, spinner } from '@clack/prompts';\n\nimport { putCommands } from '#lib/discord';\nimport { type Command, type ContextMenu, payload } from '#utils/command';\nimport { syncEnv } from '#utils/env';\n\nconst ENV_FILES = ['.dev.vars', '.env'];\n\nconst plural = (n: number): string => `${n} command${n === 1 ? '' : 's'}`;\n\nconst WORKERS_STUB = `data:text/javascript,${encodeURIComponent(\n [\n 'export const env = {};',\n 'export const exports = {};',\n 'export function waitUntil() {}',\n 'export class DurableObject {}',\n 'export class WorkerEntrypoint {}',\n 'export class WorkflowEntrypoint {}',\n 'export class RpcTarget {}',\n ].join('\\n'),\n)}`;\n\nregisterHooks({\n resolve: (specifier, context, next) =>\n specifier.startsWith('cloudflare:')\n ? { url: WORKERS_STUB, shortCircuit: true }\n : next(specifier, context),\n});\n\nasync function main(): Promise<number> {\n intro('botflare sync');\n\n const { positionals } = parseArgs({ allowPositionals: true });\n const [task, entry] = positionals;\n if (task !== 'sync' || !entry) {\n cancel('Usage: botflare sync <worker entry>');\n return 1;\n }\n\n for (const file of ENV_FILES) if (existsSync(file)) process.loadEnvFile(file);\n\n const [envError, env] = syncEnv(process.env);\n if (envError) {\n cancel(envError.message);\n return 1;\n }\n\n let mod: { default?: { commands?: (Command | ContextMenu)[] } };\n try {\n mod = await import(pathToFileURL(resolve(entry)).href);\n } catch (error) {\n cancel(`Cannot load ${entry}: ${error instanceof Error ? error.message : String(error)}`);\n return 1;\n }\n\n const commands = mod.default?.commands;\n if (!commands) {\n cancel(`${entry} must export default bot({ commands })`);\n return 1;\n }\n\n const target = env.guildId ? `server ${env.guildId}` : 'every server';\n const progress = spinner();\n progress.start(`Sending ${plural(commands.length)} to ${target}`);\n\n const [putError, count] = await putCommands(env, commands.map(payload));\n if (putError) {\n progress.error(putError.message);\n return 1;\n }\n\n progress.stop(`Discord saved ${plural(count)}`);\n outro(`Type / in ${env.guildId ? 'the server' : 'a server with the bot'} to try them.`);\n\n return 0;\n}\n\nprocess.exitCode = await main();\n"],"mappings":";;;;;;;;;;AAEA,MAAM,YAAY;AAElB,SAAgB,QAAQ,MAA2E;CACjG,MAAM,QAAQ,KAAK;CACnB,MAAM,gBAAgB,KAAK;CAC3B,MAAM,UAAU,KAAK,oBAAoB,KAAA;CAEzC,IAAI,CAAC,OAAO,OAAO,iBAAC,IAAI,MAAM,0BAA0B,GAAG,IAAI;CAC/D,IAAI,CAAC,iBAAiB,CAAC,UAAU,KAAK,aAAa,GACjD,OAAO,iBACL,IAAI,MAAM,6EAA6E,GACvF,IACF;CAEF,IAAI,WAAW,CAAC,UAAU,KAAK,OAAO,GACpC,OAAO,iBAAC,IAAI,MAAM,sCAAsC,GAAG,IAAI;CAGjE,OAAO,CAAC,MAAM;EAAE;EAAO;EAAe;CAAQ,CAAC;AACjD;;ACRA,MAAM,YAAY,CAAC,aAAa,MAAM;AAEtC,MAAM,UAAU,MAAsB,GAAG,EAAE,UAAU,MAAM,IAAI,KAAK;AAEpE,MAAM,eAAe,wBAAwB,mBAC3C;CACE;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI,CACb;AAEA,cAAc,EACZ,UAAU,WAAW,SAAS,SAC5B,UAAU,WAAW,aAAa,IAC9B;CAAE,KAAK;CAAc,cAAc;AAAK,IACxC,KAAK,WAAW,OAAO,EAC/B,CAAC;AAED,eAAe,OAAwB;CACrC,MAAM,eAAe;CAErB,MAAM,EAAE,gBAAgB,UAAU,EAAE,kBAAkB,KAAK,CAAC;CAC5D,MAAM,CAAC,MAAM,SAAS;CACtB,IAAI,SAAS,UAAU,CAAC,OAAO;EAC7B,OAAO,qCAAqC;EAC5C,OAAO;CACT;CAEA,KAAK,MAAM,QAAQ,WAAW,IAAI,WAAW,IAAI,GAAG,QAAQ,YAAY,IAAI;CAE5E,MAAM,CAAC,UAAU,OAAO,QAAQ,QAAQ,GAAG;CAC3C,IAAI,UAAU;EACZ,OAAO,SAAS,OAAO;EACvB,OAAO;CACT;CAEA,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,OAAO,cAAc,QAAQ,KAAK,CAAC,CAAC,CAAC;CACnD,SAAS,OAAO;EACd,OAAO,eAAe,MAAM,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAAG;EACxF,OAAO;CACT;CAEA,MAAM,WAAW,IAAI,SAAS;CAC9B,IAAI,CAAC,UAAU;EACb,OAAO,GAAG,MAAM,uCAAuC;EACvD,OAAO;CACT;CAEA,MAAM,SAAS,IAAI,UAAU,UAAU,IAAI,YAAY;CACvD,MAAM,WAAW,QAAQ;CACzB,SAAS,MAAM,WAAW,OAAO,SAAS,MAAM,EAAE,MAAM,QAAQ;CAEhE,MAAM,CAAC,UAAU,SAAS,MAAM,YAAY,KAAK,SAAS,IAAI,OAAO,CAAC;CACtE,IAAI,UAAU;EACZ,SAAS,MAAM,SAAS,OAAO;EAC/B,OAAO;CACT;CAEA,SAAS,KAAK,iBAAiB,OAAO,KAAK,GAAG;CAC9C,MAAM,aAAa,IAAI,UAAU,eAAe,wBAAwB,cAAc;CAEtF,OAAO;AACT;AAEA,QAAQ,WAAW,MAAM,KAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"command-CBeslrzu.mjs","names":[],"sources":["../src/utils/permissions.ts","../src/utils/command.ts"],"sourcesContent":["import type { PermissionFlagsBits } from 'discord-api-types/v10';\n\nexport type Permission = Exclude<keyof typeof PermissionFlagsBits, 'ManageEmojisAndStickers'>;\n\nconst BIT: Record<Permission, number> = {\n CreateInstantInvite: 0,\n KickMembers: 1,\n BanMembers: 2,\n Administrator: 3,\n ManageChannels: 4,\n ManageGuild: 5,\n AddReactions: 6,\n ViewAuditLog: 7,\n PrioritySpeaker: 8,\n Stream: 9,\n ViewChannel: 10,\n SendMessages: 11,\n SendTTSMessages: 12,\n ManageMessages: 13,\n EmbedLinks: 14,\n AttachFiles: 15,\n ReadMessageHistory: 16,\n MentionEveryone: 17,\n UseExternalEmojis: 18,\n ViewGuildInsights: 19,\n Connect: 20,\n Speak: 21,\n MuteMembers: 22,\n DeafenMembers: 23,\n MoveMembers: 24,\n UseVAD: 25,\n ChangeNickname: 26,\n ManageNicknames: 27,\n ManageRoles: 28,\n ManageWebhooks: 29,\n ManageGuildExpressions: 30,\n UseApplicationCommands: 31,\n RequestToSpeak: 32,\n ManageEvents: 33,\n ManageThreads: 34,\n CreatePublicThreads: 35,\n CreatePrivateThreads: 36,\n UseExternalStickers: 37,\n SendMessagesInThreads: 38,\n UseEmbeddedActivities: 39,\n ModerateMembers: 40,\n ViewCreatorMonetizationAnalytics: 41,\n UseSoundboard: 42,\n CreateGuildExpressions: 43,\n CreateEvents: 44,\n UseExternalSounds: 45,\n SendVoiceMessages: 46,\n SetVoiceChannelStatus: 48,\n SendPolls: 49,\n UseExternalApps: 50,\n PinMessages: 51,\n BypassSlowmode: 52,\n};\n\nexport function permissionBits(names: Permission[]): number {\n return [...new Set(names)].reduce((bits, name) => bits + 2 ** BIT[name], 0);\n}\n","import type {\n APIApplicationCommandInteractionDataOption,\n APIChatInputApplicationCommandInteraction,\n APIMessage,\n APIMessageApplicationCommandInteraction,\n APIUser,\n APIUserApplicationCommandInteraction,\n ApplicationCommandOptionType,\n} from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { type Locales, localePayload } from '#utils/locale';\nimport type { FollowUps, MessageOptions, ReplyOptions } from '#utils/message';\nimport type { Modal } from '#utils/modal';\nimport {\n type CommandOption,\n type Option,\n type Options,\n type OptionValue,\n optionsPayload,\n} from '#utils/option';\nimport { type Permission, permissionBits } from '#utils/permissions';\n\nexport type Context<O extends Options> = {\n interaction: APIChatInputApplicationCommandInteraction;\n options: {\n [K in keyof O]: O[K] extends Option<keyof OptionValue, infer R, infer V>\n ? R extends true\n ? V\n : V | undefined\n : never;\n };\n reply: (content: string, options?: ReplyOptions) => Response;\n defer: (\n work: () => Promise<string | ({ content: string } & MessageOptions)>,\n options?: { ephemeral?: boolean },\n ) => Response;\n modal: (modal: Modal, value?: string) => Response;\n} & FollowUps;\n\nexport type Access = {\n locales?: Locales;\n permissions?: Permission[];\n contexts?: ('guild' | 'bot_dm' | 'private_channel')[];\n installs?: ('guild' | 'user')[];\n};\n\nconst CONTEXT = { guild: 0, bot_dm: 1, private_channel: 2 } as const;\nconst INSTALL = { guild: 0, user: 1 } as const;\n\nfunction accessPayload(access: Access): Partial<Schemas['ApplicationCommandUpdateRequest']> {\n const fields: Partial<Schemas['ApplicationCommandUpdateRequest']> = localePayload(access.locales);\n if (access.permissions) fields.default_member_permissions = permissionBits(access.permissions);\n if (access.contexts) fields.contexts = access.contexts.map((c) => CONTEXT[c]);\n if (access.installs) fields.integration_types = access.installs.map((i) => INSTALL[i]);\n\n return fields;\n}\n\nexport type Command<O extends Options = Options> =\n | ({\n name: string;\n description: string;\n options: O;\n run(c: Context<O>): Response | Promise<Response>;\n } & Access)\n | ({ name: string; description: string; subcommands: Command[] } & Access);\n\nexport type MenuType = 'user' | 'message';\n\nexport type MenuContext<T extends MenuType> = {\n interaction: T extends 'user'\n ? APIUserApplicationCommandInteraction\n : APIMessageApplicationCommandInteraction;\n target: T extends 'user' ? APIUser : APIMessage;\n reply: Context<Options>['reply'];\n defer: Context<Options>['defer'];\n modal: Context<Options>['modal'];\n} & FollowUps;\n\nexport type ContextMenu<T extends MenuType = MenuType> = {\n menu: T;\n name: string;\n run(c: MenuContext<T>): Response | Promise<Response>;\n} & Access;\n\nconst SUBCOMMAND: ApplicationCommandOptionType.Subcommand = 1;\nconst GROUP: ApplicationCommandOptionType.SubcommandGroup = 2;\n\nfunction childPayload(child: Command): CommandOption {\n const { name, description } = child;\n if (!('subcommands' in child)) {\n return {\n type: SUBCOMMAND,\n name,\n description,\n ...localePayload(child.locales),\n options: optionsPayload(child.options),\n } as CommandOption;\n }\n\n return {\n type: GROUP,\n name,\n description,\n ...localePayload(child.locales),\n options: child.subcommands.map(childPayload),\n } as CommandOption;\n}\n\nexport function payload(\n command: Command | ContextMenu,\n): Schemas['ApplicationCommandUpdateRequest'] {\n const access = accessPayload(command);\n if ('menu' in command) {\n delete access.description_localizations;\n return { type: command.menu === 'user' ? 2 : 3, name: command.name, ...access };\n }\n\n const { name, description } = command;\n const options =\n 'subcommands' in command\n ? command.subcommands.map(childPayload)\n : optionsPayload(command.options);\n\n return { type: 1, name, description, options, ...access };\n}\n\nexport function findCommand(\n commands: Map<string, Command>,\n data: { name: string; options?: APIApplicationCommandInteractionDataOption[] },\n): {\n found: Command | undefined;\n given: APIApplicationCommandInteractionDataOption[];\n path: string;\n} {\n const path = [data.name];\n let found = commands.get(data.name);\n let given = data.options ?? [];\n while (found && 'subcommands' in found) {\n const picked = given[0];\n found = found.subcommands.find((child) => child.name === picked?.name);\n path.push(picked?.name ?? '');\n given = picked && 'options' in picked ? (picked.options ?? []) : [];\n }\n\n return { found, given, path: `/${path.join(' ')}` };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"command-CBeslrzu.mjs","names":[],"sources":["../src/utils/permissions.ts","../src/utils/command.ts"],"sourcesContent":["import type { PermissionFlagsBits } from 'discord-api-types/v10';\n\nexport type Permission = Exclude<keyof typeof PermissionFlagsBits, 'ManageEmojisAndStickers'>;\n\nconst BIT: Record<Permission, number> = {\n CreateInstantInvite: 0,\n KickMembers: 1,\n BanMembers: 2,\n Administrator: 3,\n ManageChannels: 4,\n ManageGuild: 5,\n AddReactions: 6,\n ViewAuditLog: 7,\n PrioritySpeaker: 8,\n Stream: 9,\n ViewChannel: 10,\n SendMessages: 11,\n SendTTSMessages: 12,\n ManageMessages: 13,\n EmbedLinks: 14,\n AttachFiles: 15,\n ReadMessageHistory: 16,\n MentionEveryone: 17,\n UseExternalEmojis: 18,\n ViewGuildInsights: 19,\n Connect: 20,\n Speak: 21,\n MuteMembers: 22,\n DeafenMembers: 23,\n MoveMembers: 24,\n UseVAD: 25,\n ChangeNickname: 26,\n ManageNicknames: 27,\n ManageRoles: 28,\n ManageWebhooks: 29,\n ManageGuildExpressions: 30,\n UseApplicationCommands: 31,\n RequestToSpeak: 32,\n ManageEvents: 33,\n ManageThreads: 34,\n CreatePublicThreads: 35,\n CreatePrivateThreads: 36,\n UseExternalStickers: 37,\n SendMessagesInThreads: 38,\n UseEmbeddedActivities: 39,\n ModerateMembers: 40,\n ViewCreatorMonetizationAnalytics: 41,\n UseSoundboard: 42,\n CreateGuildExpressions: 43,\n CreateEvents: 44,\n UseExternalSounds: 45,\n SendVoiceMessages: 46,\n SetVoiceChannelStatus: 48,\n SendPolls: 49,\n UseExternalApps: 50,\n PinMessages: 51,\n BypassSlowmode: 52,\n};\n\nexport function permissionBits(names: Permission[]): number {\n return [...new Set(names)].reduce((bits, name) => bits + 2 ** BIT[name], 0);\n}\n","import type {\n APIApplicationCommandInteractionDataOption,\n APIChatInputApplicationCommandInteraction,\n APIMessage,\n APIMessageApplicationCommandInteraction,\n APIUser,\n APIUserApplicationCommandInteraction,\n ApplicationCommandOptionType,\n} from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { type Locales, localePayload } from '#utils/locale';\nimport type { FollowUps, MessageOptions, ReplyOptions } from '#utils/message';\nimport type { Modal } from '#utils/modal';\nimport {\n type CommandOption,\n type Option,\n type Options,\n type OptionValue,\n optionsPayload,\n} from '#utils/option';\nimport { type Permission, permissionBits } from '#utils/permissions';\n\n/** What the handler of a slash command gets. */\nexport type Context<O extends Options> = {\n /** The interaction as Discord sent it. */\n interaction: APIChatInputApplicationCommandInteraction;\n /** The option values, keyed by name. An option that is not required can be `undefined`. */\n options: {\n [K in keyof O]: O[K] extends Option<keyof OptionValue, infer R, infer V>\n ? R extends true\n ? V\n : V | undefined\n : never;\n };\n /** Answers within 3 seconds. Return what it gives. */\n reply: (content: string, options?: ReplyOptions) => Response;\n /**\n * Shows \"thinking…\" now, then edits the answer to what `work` returns: the text, or\n * `{ content, buttons, embeds }`. Use it for work that takes longer than 3 seconds.\n */\n defer: (\n work: () => Promise<string | ({ content: string } & MessageOptions)>,\n options?: { ephemeral?: boolean },\n ) => Response;\n /** Opens a modal made with `modal()`. `value` comes back to its handler as `c.value`. */\n modal: (modal: Modal, value?: string) => Response;\n} & FollowUps;\n\n/** Who sees a command or a context menu item, and where. */\nexport type Access = {\n /**\n * The name and description in other languages, keyed by Discord locale, such as\n * `{ th: { name: 'ทอย', description: 'ทอยลูกเต๋า' } }`. The handler still gets the default name.\n */\n locales?: Locales;\n /**\n * Who sees it by default: members with all of these permissions, such as `['ManageGuild']`.\n * Pass `[]` to show it to admins only. Server admins can change this in Server Settings.\n * Applies to a top-level command only.\n */\n permissions?: Permission[];\n /** Where it shows: `guild`, `bot_dm` or `private_channel`. */\n contexts?: ('guild' | 'bot_dm' | 'private_channel')[];\n /** How the app must be installed for it: `guild` or `user`. */\n installs?: ('guild' | 'user')[];\n};\n\nconst CONTEXT = { guild: 0, bot_dm: 1, private_channel: 2 } as const;\nconst INSTALL = { guild: 0, user: 1 } as const;\n\nfunction accessPayload(access: Access): Partial<Schemas['ApplicationCommandUpdateRequest']> {\n const fields: Partial<Schemas['ApplicationCommandUpdateRequest']> = localePayload(access.locales);\n if (access.permissions) fields.default_member_permissions = permissionBits(access.permissions);\n if (access.contexts) fields.contexts = access.contexts.map((c) => CONTEXT[c]);\n if (access.installs) fields.integration_types = access.installs.map((i) => INSTALL[i]);\n\n return fields;\n}\n\nexport type Command<O extends Options = Options> =\n | ({\n name: string;\n description: string;\n options: O;\n run(c: Context<O>): Response | Promise<Response>;\n } & Access)\n | ({ name: string; description: string; subcommands: Command[] } & Access);\n\nexport type MenuType = 'user' | 'message';\n\nexport type MenuContext<T extends MenuType> = {\n interaction: T extends 'user'\n ? APIUserApplicationCommandInteraction\n : APIMessageApplicationCommandInteraction;\n target: T extends 'user' ? APIUser : APIMessage;\n reply: Context<Options>['reply'];\n defer: Context<Options>['defer'];\n modal: Context<Options>['modal'];\n} & FollowUps;\n\nexport type ContextMenu<T extends MenuType = MenuType> = {\n menu: T;\n name: string;\n run(c: MenuContext<T>): Response | Promise<Response>;\n} & Access;\n\nconst SUBCOMMAND: ApplicationCommandOptionType.Subcommand = 1;\nconst GROUP: ApplicationCommandOptionType.SubcommandGroup = 2;\n\nfunction childPayload(child: Command): CommandOption {\n const { name, description } = child;\n if (!('subcommands' in child)) {\n return {\n type: SUBCOMMAND,\n name,\n description,\n ...localePayload(child.locales),\n options: optionsPayload(child.options),\n } as CommandOption;\n }\n\n return {\n type: GROUP,\n name,\n description,\n ...localePayload(child.locales),\n options: child.subcommands.map(childPayload),\n } as CommandOption;\n}\n\nexport function payload(\n command: Command | ContextMenu,\n): Schemas['ApplicationCommandUpdateRequest'] {\n const access = accessPayload(command);\n if ('menu' in command) {\n delete access.description_localizations;\n return { type: command.menu === 'user' ? 2 : 3, name: command.name, ...access };\n }\n\n const { name, description } = command;\n const options =\n 'subcommands' in command\n ? command.subcommands.map(childPayload)\n : optionsPayload(command.options);\n\n return { type: 1, name, description, options, ...access };\n}\n\nexport function findCommand(\n commands: Map<string, Command>,\n data: { name: string; options?: APIApplicationCommandInteractionDataOption[] },\n): {\n found: Command | undefined;\n given: APIApplicationCommandInteractionDataOption[];\n path: string;\n} {\n const path = [data.name];\n let found = commands.get(data.name);\n let given = data.options ?? [];\n while (found && 'subcommands' in found) {\n const picked = given[0];\n found = found.subcommands.find((child) => child.name === picked?.name);\n path.push(picked?.name ?? '');\n given = picked && 'options' in picked ? (picked.options ?? []) : [];\n }\n\n return { found, given, path: `/${path.join(' ')}` };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"discord-DEZe1asd.mjs","names":[],"sources":["../src/utils/locale.ts","../src/utils/option.ts","../src/utils/upload.ts","../src/lib/rest.ts","../src/utils/button.ts","../src/utils/select.ts","../src/utils/message.ts","../src/lib/discord.ts"],"sourcesContent":["import type { Locale } from 'discord-api-types/v10';\n\nexport type Locales = Partial<Record<`${Locale}`, { name?: string; description?: string }>>;\n\nexport function localePayload(locales: Locales | undefined): {\n name_localizations?: Record<string, string>;\n description_localizations?: Record<string, string>;\n} {\n const names: Record<string, string> = {};\n const descriptions: Record<string, string> = {};\n for (const [locale, text] of Object.entries(locales ?? {})) {\n if (text.name !== undefined) names[locale] = text.name;\n if (text.description !== undefined) descriptions[locale] = text.description;\n }\n\n const fields: ReturnType<typeof localePayload> = {};\n if (Object.keys(names).length > 0) fields.name_localizations = names;\n if (Object.keys(descriptions).length > 0) fields.description_localizations = descriptions;\n\n return fields;\n}\n","import type {\n APIApplicationCommandAutocompleteInteraction,\n APIApplicationCommandInteractionDataOption,\n APIChatInputApplicationCommandInteractionData,\n APIAttachment,\n APIInteractionDataResolvedChannel,\n APIInteractionDataResolvedGuildMember,\n APIRole,\n APIUser,\n ApplicationCommandOptionType,\n} from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { type Locales, localePayload } from '#utils/locale';\n\nexport type OptionValue = {\n string: string;\n integer: number;\n number: number;\n boolean: boolean;\n user: APIUser & { member?: APIInteractionDataResolvedGuildMember };\n channel: APIInteractionDataResolvedChannel;\n role: APIRole;\n mentionable: (APIUser & { member?: APIInteractionDataResolvedGuildMember }) | APIRole;\n attachment: APIAttachment;\n};\n\nexport type ChannelType =\n | 'text'\n | 'voice'\n | 'category'\n | 'announcement'\n | 'announcement_thread'\n | 'public_thread'\n | 'private_thread'\n | 'stage'\n | 'directory'\n | 'forum';\n\nexport const CHANNEL_TYPES: Record<ChannelType, Schemas['ChannelTypes']> = {\n text: 0,\n voice: 2,\n category: 4,\n announcement: 5,\n announcement_thread: 10,\n public_thread: 11,\n private_thread: 12,\n stage: 13,\n directory: 14,\n forum: 15,\n};\n\nexport type Option<\n T extends keyof OptionValue = keyof OptionValue,\n R extends boolean = boolean,\n V = OptionValue[T],\n> = {\n type: T;\n description: string;\n required: R;\n choices?: Record<string, V>;\n min?: number;\n max?: number;\n minLength?: number;\n maxLength?: number;\n channelTypes?: ChannelType[];\n locales?: Locales;\n autocomplete?: (c: AutocompleteContext) => Suggestion<V>[] | Promise<Suggestion<V>[]>;\n};\n\nexport type AutocompleteContext = {\n interaction: APIApplicationCommandAutocompleteInteraction;\n value: string;\n};\n\nexport type Suggestion<V> = V | { name: string; value: V };\n\nexport type Options = Record<string, Option<keyof OptionValue, boolean, unknown>>;\n\nconst OPTION_TYPE: Record<keyof OptionValue, ApplicationCommandOptionType> = {\n string: 3,\n integer: 4,\n number: 10,\n boolean: 5,\n user: 6,\n channel: 7,\n role: 8,\n mentionable: 9,\n attachment: 11,\n};\n\nconst USER: ApplicationCommandOptionType.User = 6;\nconst CHANNEL: ApplicationCommandOptionType.Channel = 7;\nconst ROLE: ApplicationCommandOptionType.Role = 8;\nconst MENTIONABLE: ApplicationCommandOptionType.Mentionable = 9;\nconst ATTACHMENT: ApplicationCommandOptionType.Attachment = 11;\n\nexport type CommandOption = NonNullable<\n Schemas['ApplicationCommandUpdateRequest']['options']\n>[number];\n\nexport function optionsPayload(options: Options): CommandOption[] {\n return Object.entries(options)\n .map(([name, o]) => {\n const built: Record<string, unknown> = {\n name,\n description: o.description,\n type: OPTION_TYPE[o.type],\n required: o.required,\n };\n\n if (o.choices) {\n built.choices = Object.entries(o.choices).map(([label, value]) => ({ name: label, value }));\n }\n if (o.min !== undefined) built.min_value = o.min;\n if (o.max !== undefined) built.max_value = o.max;\n if (o.minLength !== undefined) built.min_length = o.minLength;\n if (o.maxLength !== undefined) built.max_length = o.maxLength;\n if (o.channelTypes) built.channel_types = o.channelTypes.map((t) => CHANNEL_TYPES[t]);\n if (o.autocomplete) built.autocomplete = true;\n Object.assign(built, localePayload(o.locales));\n\n return built as CommandOption;\n })\n .toSorted((a, b) => Number(b.required) - Number(a.required));\n}\n\nfunction userWithMember(\n resolved: APIChatInputApplicationCommandInteractionData['resolved'],\n id: string,\n): OptionValue['user'] | undefined {\n const user = resolved?.users?.[id];\n if (!user) return undefined;\n\n const member = resolved?.members?.[id];\n\n return member ? { ...user, member } : user;\n}\n\nexport function optionValues(\n options: APIApplicationCommandInteractionDataOption[],\n resolved: APIChatInputApplicationCommandInteractionData['resolved'],\n): Record<string, OptionValue[keyof OptionValue] | undefined> {\n const values: Record<string, OptionValue[keyof OptionValue] | undefined> = {};\n\n for (const o of options) {\n if (!('value' in o)) continue;\n\n const id = String(o.value);\n if (o.type === USER) values[o.name] = userWithMember(resolved, id);\n else if (o.type === CHANNEL) values[o.name] = resolved?.channels?.[id];\n else if (o.type === ROLE) values[o.name] = resolved?.roles?.[id];\n else if (o.type === MENTIONABLE)\n values[o.name] = userWithMember(resolved, id) ?? resolved?.roles?.[id];\n else if (o.type === ATTACHMENT) values[o.name] = resolved?.attachments?.[id];\n else values[o.name] = o.value;\n }\n\n return values;\n}\n","export type Upload = {\n name: string;\n data: Blob | ArrayBuffer | Uint8Array<ArrayBuffer> | string;\n description?: string;\n};\n\nexport function attachmentsOf(\n files: Upload[],\n): { id: string; filename: string; description?: string }[] {\n return files.map((file, i) => ({\n id: String(i),\n filename: file.name,\n ...(file.description !== undefined && { description: file.description }),\n }));\n}\n\nexport function formOf(payload: unknown, files: Upload[]): FormData {\n const form = new FormData();\n form.append('payload_json', JSON.stringify(payload));\n files.forEach((file, i) => {\n const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);\n form.append(`files[${i}]`, blob, file.name);\n });\n\n return form;\n}\n\nexport async function uploadsOf(form: FormData): Promise<{ payload: unknown; files: Upload[] }> {\n const files: Upload[] = [];\n for (const [key, value] of form.entries()) {\n if (key.startsWith('files[') && typeof value !== 'string') {\n files.push({ name: value.name, data: value });\n }\n }\n\n const payload = form.get('payload_json');\n\n return { payload: typeof payload === 'string' ? (JSON.parse(payload) as unknown) : null, files };\n}\n","import type { Routes } from '#lib/routes';\nimport { formOf, type Upload } from '#utils/upload';\n\nconst API = 'https://discord.com/api/v10';\nconst TRIES = 3;\n\ntype Method = 'get' | 'post' | 'put' | 'patch' | 'delete';\n\ntype Init = {\n path?: Record<string, string>;\n query?: object;\n body?: unknown;\n files?: Upload[];\n reason?: string;\n};\n\ntype Route<M extends Method> = {\n [K in keyof Routes]: M extends keyof Routes[K] ? K : never;\n}[keyof Routes];\n\ntype Operation<P extends keyof Routes, M extends Method> =\n Routes[P] extends Record<M, infer O extends { args: unknown[]; result: unknown }> ? O : never;\n\ntype Result<P extends keyof Routes, M extends Method> = Promise<\n [Error, null] | [null, Operation<P, M>['result']]\n>;\n\n/** Calls the Discord REST API. Each method takes a route from the Discord API spec. */\nexport type Rest = {\n [M in Method]: <P extends Route<M>>(path: P, ...args: Operation<P, M>['args']) => Result<P, M>;\n};\n\nexport function callDiscord<M extends Method, P extends Route<M>>(\n token: string | null,\n method: M,\n route: P,\n ...args: Operation<P, M>['args']\n): Result<P, M> {\n return request(token, method, route, args[0] as Init | undefined);\n}\n\nexport function createRest(token: () => unknown): Rest {\n const call =\n (method: Method) =>\n async (route: string, init?: Init): Promise<[Error, null] | [null, unknown]> => {\n const value = token();\n if (typeof value !== 'string') return [new Error('DISCORD_TOKEN is not set'), null];\n\n return request(value, method, route, init);\n };\n\n return {\n get: call('get'),\n post: call('post'),\n put: call('put'),\n patch: call('patch'),\n delete: call('delete'),\n } as Rest;\n}\n\nasync function request<T>(\n token: string | null,\n method: Method,\n route: string,\n init: Init = {},\n): Promise<[Error, null] | [null, T]> {\n const path = route.replace(/\\{(\\w+)\\}/g, (_, name: string) =>\n encodeURIComponent(init.path?.[name] ?? ''),\n );\n\n const query = new URLSearchParams();\n for (const [name, value] of Object.entries(init.query ?? {})) {\n if (value !== undefined) query.set(name, String(value));\n }\n const url = query.size > 0 ? `${API}${path}?${query.toString()}` : `${API}${path}`;\n\n const headers: Record<string, string> = {};\n if (token) headers.Authorization = `Bot ${token}`;\n if (init.reason) headers['X-Audit-Log-Reason'] = encodeURIComponent(init.reason);\n let body: string | FormData | undefined;\n if (init.files?.length) {\n body = formOf(init.body, init.files);\n } else if (init.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(init.body);\n }\n\n const send = async (attempt: number): Promise<[Error, null] | [null, T]> => {\n const res = await fetch(url, { method: method.toUpperCase(), headers, body });\n\n if (res.status === 429 && attempt < TRIES) {\n const { retry_after } = (await res.json()) as { retry_after: number };\n await new Promise((resolve) => setTimeout(resolve, retry_after * 1000));\n return send(attempt + 1);\n }\n\n if (!res.ok) return [new Error(`Discord answered ${res.status}: ${await res.text()}`), null];\n const data: unknown = res.status === 204 ? null : await res.json();\n\n return [null, data as T];\n };\n\n return send(1);\n}\n","import type { Schemas } from '#lib/routes';\n\nexport type Button = (\n | { id: string; style?: ButtonStyle; url?: never }\n | { url: string; id?: never; style?: never }\n) &\n ({ label: string; emoji?: string } | { label?: string; emoji: string }) & { disabled?: boolean };\n\ntype ButtonStyle = 'primary' | 'secondary' | 'success' | 'danger';\n\nconst STYLE: Record<ButtonStyle, Schemas['ButtonStyleTypes']> = {\n primary: 1,\n secondary: 2,\n success: 3,\n danger: 4,\n};\nconst LINK: Schemas['ButtonStyleTypes'] = 5;\nconst CUSTOM_EMOJI = /^<a?:(\\w+):(\\d+)>$/;\n\nfunction emojiOf(text: string): Schemas['ComponentEmojiForRequest'] {\n const custom = CUSTOM_EMOJI.exec(text);\n if (!custom?.[1]) return { name: text };\n\n return { name: custom[1], id: custom[2] };\n}\n\nexport function buttonComponent(button: Button): Schemas['ButtonComponentForMessageRequest'] {\n const component: Schemas['ButtonComponentForMessageRequest'] = {\n type: 2,\n style: 'url' in button ? LINK : STYLE[button.style ?? 'primary'],\n };\n\n if (button.label !== undefined) component.label = button.label;\n if ('url' in button) component.url = button.url;\n else component.custom_id = button.id;\n if (button.emoji) component.emoji = emojiOf(button.emoji);\n if (button.disabled) component.disabled = true;\n\n return component;\n}\n","import type { APIMessageComponentSelectMenuInteraction } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { CHANNEL_TYPES, type ChannelType } from '#utils/option';\n\nexport type SelectType = 'string' | 'user' | 'role' | 'channel' | 'mentionable';\n\nexport type SelectMenu = {\n type: SelectType;\n id: string;\n placeholder?: string;\n min?: number;\n max?: number;\n options?: Record<string, string>;\n channelTypes?: ChannelType[];\n};\n\nconst COMPONENT: Record<SelectType, 3 | 5 | 6 | 7 | 8> = {\n string: 3,\n user: 5,\n role: 6,\n mentionable: 7,\n channel: 8,\n};\n\nexport function selectRow(menu: SelectMenu): Schemas['ActionRowComponentForMessageRequest'] {\n const component: Record<string, unknown> = { type: COMPONENT[menu.type], custom_id: menu.id };\n\n if (menu.placeholder !== undefined) component.placeholder = menu.placeholder;\n if (menu.min !== undefined) component.min_values = menu.min;\n if (menu.max !== undefined) component.max_values = menu.max;\n if (menu.options) {\n component.options = Object.entries(menu.options).map(([label, value]) => ({ label, value }));\n }\n if (menu.channelTypes) component.channel_types = menu.channelTypes.map((t) => CHANNEL_TYPES[t]);\n\n return {\n type: 1,\n components: [component as Schemas['ActionRowComponentForMessageRequest']['components'][number]],\n };\n}\n\nexport function selectValues(interaction: APIMessageComponentSelectMenuInteraction): unknown[] {\n const { data } = interaction;\n const resolved = 'resolved' in data ? data.resolved : undefined;\n\n return data.values.map((id) => {\n if (!resolved) return id;\n if ('users' in resolved && resolved.users?.[id]) return resolved.users[id];\n if ('roles' in resolved && resolved.roles?.[id]) return resolved.roles[id];\n if ('channels' in resolved && resolved.channels?.[id]) return resolved.channels[id];\n\n return id;\n });\n}\n","import type { MessageFlags } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { type Button, buttonComponent } from '#utils/button';\nimport { type SelectMenu, selectRow } from '#utils/select';\nimport { attachmentsOf, type Upload } from '#utils/upload';\n\nexport type MessageOptions = {\n buttons?: Button[];\n selects?: SelectMenu[];\n embeds?: Schemas['RichEmbed'][];\n allowedMentions?: Schemas['MessageAllowedMentionsRequest'];\n files?: Upload[];\n};\n\nexport type ReplyOptions = MessageOptions & { ephemeral?: boolean };\n\nexport type FollowUps = {\n after(work: () => Promise<void>): void;\n followUp(content: string, options?: ReplyOptions): Promise<Error | null>;\n editReply(content: string, options?: MessageOptions): Promise<Error | null>;\n deleteReply(): Promise<Error | null>;\n};\n\nconst PER_ROW = 5;\nconst EPHEMERAL: MessageFlags.Ephemeral = 64;\nconst NO_PINGS: Schemas['MessageAllowedMentionsRequest'] = { parse: [] };\n\nexport function message(\n content: string,\n options: ReplyOptions = {},\n): Schemas['IncomingWebhookInteractionRequest'] {\n const data: Schemas['IncomingWebhookInteractionRequest'] = {\n content,\n allowed_mentions: options.allowedMentions ?? NO_PINGS,\n };\n if (options.ephemeral) data.flags = EPHEMERAL;\n if (options.embeds) data.embeds = options.embeds;\n if (options.files?.length) data.attachments = attachmentsOf(options.files);\n if (!options.buttons && !options.selects) return data;\n\n const rows = (options.selects ?? []).map(selectRow);\n const buttons = options.buttons ?? [];\n for (let i = 0; i < buttons.length; i += PER_ROW) {\n rows.push({\n type: 1,\n components: buttons.slice(i, i + PER_ROW).map(buttonComponent),\n });\n }\n data.components = rows;\n\n return data;\n}\n","import { callDiscord } from '#lib/rest';\nimport type { Schemas } from '#lib/routes';\nimport type { SyncEnv } from '#utils/env';\nimport { message, type MessageOptions } from '#utils/message';\nimport { type Upload, uploadsOf } from '#utils/upload';\n\nexport async function putCommands(\n env: SyncEnv,\n body: Schemas['ApplicationCommandUpdateRequest'][],\n): Promise<[Error, null] | [null, number]> {\n const [error, saved] = env.guildId\n ? await callDiscord(\n env.token,\n 'put',\n '/applications/{application_id}/guilds/{guild_id}/commands',\n {\n path: { application_id: env.applicationId, guild_id: env.guildId },\n body,\n },\n )\n : await callDiscord(env.token, 'put', '/applications/{application_id}/commands', {\n path: { application_id: env.applicationId },\n body,\n });\n if (error) return [error, null];\n\n return [null, saved?.length ?? 0];\n}\n\nexport async function editOriginal(\n applicationId: string,\n token: string,\n body: Schemas['IncomingWebhookUpdateRequestPartial'],\n files?: Upload[],\n): Promise<Error | null> {\n const [error] = await callDiscord(\n null,\n 'patch',\n '/webhooks/{webhook_id}/{webhook_token}/messages/@original',\n {\n path: { webhook_id: applicationId, webhook_token: token },\n body,\n files,\n },\n );\n\n return error;\n}\n\nexport type Send = (\n channelId: string,\n content: string,\n options?: MessageOptions,\n) => Promise<Error | null>;\n\nexport async function sendMessage(\n token: string,\n channelId: string,\n content: string,\n options: MessageOptions = {},\n): Promise<Error | null> {\n const [error] = await callDiscord(token, 'post', '/channels/{channel_id}/messages', {\n path: { channel_id: channelId },\n body: message(content, {\n buttons: options.buttons,\n selects: options.selects,\n embeds: options.embeds,\n allowedMentions: options.allowedMentions,\n files: options.files,\n }),\n files: options.files,\n });\n\n return error;\n}\n\nexport async function interactionCallback(\n id: string,\n token: string,\n response: Response,\n): Promise<Error | null> {\n const multipart = response.headers.get('content-type')?.startsWith('multipart/form-data');\n const { payload, files } = multipart\n ? await uploadsOf(await response.formData())\n : { payload: JSON.parse(await response.text()) as unknown, files: [] };\n\n const [error] = await callDiscord(\n null,\n 'post',\n '/interactions/{interaction_id}/{interaction_token}/callback',\n {\n path: { interaction_id: id, interaction_token: token },\n body: payload as Schemas['create_interaction_response_body'],\n files,\n },\n );\n\n return error;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"discord-DEZe1asd.mjs","names":[],"sources":["../src/utils/locale.ts","../src/utils/option.ts","../src/utils/upload.ts","../src/lib/rest.ts","../src/utils/button.ts","../src/utils/select.ts","../src/utils/message.ts","../src/lib/discord.ts"],"sourcesContent":["import type { Locale } from 'discord-api-types/v10';\n\nexport type Locales = Partial<Record<`${Locale}`, { name?: string; description?: string }>>;\n\nexport function localePayload(locales: Locales | undefined): {\n name_localizations?: Record<string, string>;\n description_localizations?: Record<string, string>;\n} {\n const names: Record<string, string> = {};\n const descriptions: Record<string, string> = {};\n for (const [locale, text] of Object.entries(locales ?? {})) {\n if (text.name !== undefined) names[locale] = text.name;\n if (text.description !== undefined) descriptions[locale] = text.description;\n }\n\n const fields: ReturnType<typeof localePayload> = {};\n if (Object.keys(names).length > 0) fields.name_localizations = names;\n if (Object.keys(descriptions).length > 0) fields.description_localizations = descriptions;\n\n return fields;\n}\n","import type {\n APIApplicationCommandAutocompleteInteraction,\n APIApplicationCommandInteractionDataOption,\n APIChatInputApplicationCommandInteractionData,\n APIAttachment,\n APIInteractionDataResolvedChannel,\n APIInteractionDataResolvedGuildMember,\n APIRole,\n APIUser,\n ApplicationCommandOptionType,\n} from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { type Locales, localePayload } from '#utils/locale';\n\nexport type OptionValue = {\n string: string;\n integer: number;\n number: number;\n boolean: boolean;\n user: APIUser & { member?: APIInteractionDataResolvedGuildMember };\n channel: APIInteractionDataResolvedChannel;\n role: APIRole;\n mentionable: (APIUser & { member?: APIInteractionDataResolvedGuildMember }) | APIRole;\n attachment: APIAttachment;\n};\n\nexport type ChannelType =\n | 'text'\n | 'voice'\n | 'category'\n | 'announcement'\n | 'announcement_thread'\n | 'public_thread'\n | 'private_thread'\n | 'stage'\n | 'directory'\n | 'forum';\n\nexport const CHANNEL_TYPES: Record<ChannelType, Schemas['ChannelTypes']> = {\n text: 0,\n voice: 2,\n category: 4,\n announcement: 5,\n announcement_thread: 10,\n public_thread: 11,\n private_thread: 12,\n stage: 13,\n directory: 14,\n forum: 15,\n};\n\nexport type Option<\n T extends keyof OptionValue = keyof OptionValue,\n R extends boolean = boolean,\n V = OptionValue[T],\n> = {\n type: T;\n description: string;\n required: R;\n choices?: Record<string, V>;\n min?: number;\n max?: number;\n minLength?: number;\n maxLength?: number;\n channelTypes?: ChannelType[];\n locales?: Locales;\n autocomplete?: (c: AutocompleteContext) => Suggestion<V>[] | Promise<Suggestion<V>[]>;\n};\n\nexport type AutocompleteContext = {\n interaction: APIApplicationCommandAutocompleteInteraction;\n value: string;\n};\n\nexport type Suggestion<V> = V | { name: string; value: V };\n\nexport type Options = Record<string, Option<keyof OptionValue, boolean, unknown>>;\n\nconst OPTION_TYPE: Record<keyof OptionValue, ApplicationCommandOptionType> = {\n string: 3,\n integer: 4,\n number: 10,\n boolean: 5,\n user: 6,\n channel: 7,\n role: 8,\n mentionable: 9,\n attachment: 11,\n};\n\nconst USER: ApplicationCommandOptionType.User = 6;\nconst CHANNEL: ApplicationCommandOptionType.Channel = 7;\nconst ROLE: ApplicationCommandOptionType.Role = 8;\nconst MENTIONABLE: ApplicationCommandOptionType.Mentionable = 9;\nconst ATTACHMENT: ApplicationCommandOptionType.Attachment = 11;\n\nexport type CommandOption = NonNullable<\n Schemas['ApplicationCommandUpdateRequest']['options']\n>[number];\n\nexport function optionsPayload(options: Options): CommandOption[] {\n return Object.entries(options)\n .map(([name, o]) => {\n const built: Record<string, unknown> = {\n name,\n description: o.description,\n type: OPTION_TYPE[o.type],\n required: o.required,\n };\n\n if (o.choices) {\n built.choices = Object.entries(o.choices).map(([label, value]) => ({ name: label, value }));\n }\n if (o.min !== undefined) built.min_value = o.min;\n if (o.max !== undefined) built.max_value = o.max;\n if (o.minLength !== undefined) built.min_length = o.minLength;\n if (o.maxLength !== undefined) built.max_length = o.maxLength;\n if (o.channelTypes) built.channel_types = o.channelTypes.map((t) => CHANNEL_TYPES[t]);\n if (o.autocomplete) built.autocomplete = true;\n Object.assign(built, localePayload(o.locales));\n\n return built as CommandOption;\n })\n .toSorted((a, b) => Number(b.required) - Number(a.required));\n}\n\nfunction userWithMember(\n resolved: APIChatInputApplicationCommandInteractionData['resolved'],\n id: string,\n): OptionValue['user'] | undefined {\n const user = resolved?.users?.[id];\n if (!user) return undefined;\n\n const member = resolved?.members?.[id];\n\n return member ? { ...user, member } : user;\n}\n\nexport function optionValues(\n options: APIApplicationCommandInteractionDataOption[],\n resolved: APIChatInputApplicationCommandInteractionData['resolved'],\n): Record<string, OptionValue[keyof OptionValue] | undefined> {\n const values: Record<string, OptionValue[keyof OptionValue] | undefined> = {};\n\n for (const o of options) {\n if (!('value' in o)) continue;\n\n const id = String(o.value);\n if (o.type === USER) values[o.name] = userWithMember(resolved, id);\n else if (o.type === CHANNEL) values[o.name] = resolved?.channels?.[id];\n else if (o.type === ROLE) values[o.name] = resolved?.roles?.[id];\n else if (o.type === MENTIONABLE)\n values[o.name] = userWithMember(resolved, id) ?? resolved?.roles?.[id];\n else if (o.type === ATTACHMENT) values[o.name] = resolved?.attachments?.[id];\n else values[o.name] = o.value;\n }\n\n return values;\n}\n","export type Upload = {\n name: string;\n data: Blob | ArrayBuffer | Uint8Array<ArrayBuffer> | string;\n description?: string;\n};\n\nexport function attachmentsOf(\n files: Upload[],\n): { id: string; filename: string; description?: string }[] {\n return files.map((file, i) => ({\n id: String(i),\n filename: file.name,\n ...(file.description !== undefined && { description: file.description }),\n }));\n}\n\nexport function formOf(payload: unknown, files: Upload[]): FormData {\n const form = new FormData();\n form.append('payload_json', JSON.stringify(payload));\n files.forEach((file, i) => {\n const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);\n form.append(`files[${i}]`, blob, file.name);\n });\n\n return form;\n}\n\nexport async function uploadsOf(form: FormData): Promise<{ payload: unknown; files: Upload[] }> {\n const files: Upload[] = [];\n for (const [key, value] of form.entries()) {\n if (key.startsWith('files[') && typeof value !== 'string') {\n files.push({ name: value.name, data: value });\n }\n }\n\n const payload = form.get('payload_json');\n\n return { payload: typeof payload === 'string' ? (JSON.parse(payload) as unknown) : null, files };\n}\n","import type { Routes } from '#lib/routes';\nimport { formOf, type Upload } from '#utils/upload';\n\nconst API = 'https://discord.com/api/v10';\nconst TRIES = 3;\n\ntype Method = 'get' | 'post' | 'put' | 'patch' | 'delete';\n\ntype Init = {\n path?: Record<string, string>;\n query?: object;\n body?: unknown;\n files?: Upload[];\n reason?: string;\n};\n\ntype Route<M extends Method> = {\n [K in keyof Routes]: M extends keyof Routes[K] ? K : never;\n}[keyof Routes];\n\ntype Operation<P extends keyof Routes, M extends Method> =\n Routes[P] extends Record<M, infer O extends { args: unknown[]; result: unknown }> ? O : never;\n\ntype Result<P extends keyof Routes, M extends Method> = Promise<\n [Error, null] | [null, Operation<P, M>['result']]\n>;\n\n/** Calls the Discord REST API. Each method takes a route from the Discord API spec. */\nexport type Rest = {\n [M in Method]: <P extends Route<M>>(path: P, ...args: Operation<P, M>['args']) => Result<P, M>;\n};\n\nexport function callDiscord<M extends Method, P extends Route<M>>(\n token: string | null,\n method: M,\n route: P,\n ...args: Operation<P, M>['args']\n): Result<P, M> {\n return request(token, method, route, args[0] as Init | undefined);\n}\n\nexport function createRest(token: () => unknown): Rest {\n const call =\n (method: Method) =>\n async (route: string, init?: Init): Promise<[Error, null] | [null, unknown]> => {\n const value = token();\n if (typeof value !== 'string') return [new Error('DISCORD_TOKEN is not set'), null];\n\n return request(value, method, route, init);\n };\n\n return {\n get: call('get'),\n post: call('post'),\n put: call('put'),\n patch: call('patch'),\n delete: call('delete'),\n } as Rest;\n}\n\nasync function request<T>(\n token: string | null,\n method: Method,\n route: string,\n init: Init = {},\n): Promise<[Error, null] | [null, T]> {\n const path = route.replace(/\\{(\\w+)\\}/g, (_, name: string) =>\n encodeURIComponent(init.path?.[name] ?? ''),\n );\n\n const query = new URLSearchParams();\n for (const [name, value] of Object.entries(init.query ?? {})) {\n if (value !== undefined) query.set(name, String(value));\n }\n const url = query.size > 0 ? `${API}${path}?${query.toString()}` : `${API}${path}`;\n\n const headers: Record<string, string> = {};\n if (token) headers.Authorization = `Bot ${token}`;\n if (init.reason) headers['X-Audit-Log-Reason'] = encodeURIComponent(init.reason);\n let body: string | FormData | undefined;\n if (init.files?.length) {\n body = formOf(init.body, init.files);\n } else if (init.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(init.body);\n }\n\n const send = async (attempt: number): Promise<[Error, null] | [null, T]> => {\n const res = await fetch(url, { method: method.toUpperCase(), headers, body });\n\n if (res.status === 429 && attempt < TRIES) {\n const { retry_after } = (await res.json()) as { retry_after: number };\n await new Promise((resolve) => setTimeout(resolve, retry_after * 1000));\n return send(attempt + 1);\n }\n\n if (!res.ok) return [new Error(`Discord answered ${res.status}: ${await res.text()}`), null];\n const data: unknown = res.status === 204 ? null : await res.json();\n\n return [null, data as T];\n };\n\n return send(1);\n}\n","import type { Schemas } from '#lib/routes';\n\n/**\n * A button on a message. A button with an `id` runs the handler with that id in the `buttons` of\n * `bot()`, and takes a `style`: `primary` (default), `secondary`, `success` or `danger`. A button\n * with a `url` opens the link and needs no handler. A button needs a `label`, an `emoji` or both.\n * An `emoji` is a Unicode emoji, or a custom emoji such as `<:name:id>`.\n */\nexport type Button = (\n | {\n /** The handler name, then an optional `:` and a value, such as `page:2`. */\n id: string;\n /** `primary` (default), `secondary`, `success` or `danger`. */\n style?: ButtonStyle;\n url?: never;\n }\n | {\n /** The link the button opens. */\n url: string;\n id?: never;\n style?: never;\n }\n) &\n (\n | {\n /** The text on the button. */\n label: string;\n /** A Unicode emoji, or a custom emoji such as `<:name:id>`. */\n emoji?: string;\n }\n | { label?: string; emoji: string }\n ) & {\n /** `true` greys the button out. */\n disabled?: boolean;\n };\n\ntype ButtonStyle = 'primary' | 'secondary' | 'success' | 'danger';\n\nconst STYLE: Record<ButtonStyle, Schemas['ButtonStyleTypes']> = {\n primary: 1,\n secondary: 2,\n success: 3,\n danger: 4,\n};\nconst LINK: Schemas['ButtonStyleTypes'] = 5;\nconst CUSTOM_EMOJI = /^<a?:(\\w+):(\\d+)>$/;\n\nfunction emojiOf(text: string): Schemas['ComponentEmojiForRequest'] {\n const custom = CUSTOM_EMOJI.exec(text);\n if (!custom?.[1]) return { name: text };\n\n return { name: custom[1], id: custom[2] };\n}\n\nexport function buttonComponent(button: Button): Schemas['ButtonComponentForMessageRequest'] {\n const component: Schemas['ButtonComponentForMessageRequest'] = {\n type: 2,\n style: 'url' in button ? LINK : STYLE[button.style ?? 'primary'],\n };\n\n if (button.label !== undefined) component.label = button.label;\n if ('url' in button) component.url = button.url;\n else component.custom_id = button.id;\n if (button.emoji) component.emoji = emojiOf(button.emoji);\n if (button.disabled) component.disabled = true;\n\n return component;\n}\n","import type { APIMessageComponentSelectMenuInteraction } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { CHANNEL_TYPES, type ChannelType } from '#utils/option';\n\nexport type SelectType = 'string' | 'user' | 'role' | 'channel' | 'mentionable';\n\nexport type SelectMenu = {\n type: SelectType;\n id: string;\n placeholder?: string;\n min?: number;\n max?: number;\n options?: Record<string, string>;\n channelTypes?: ChannelType[];\n};\n\nconst COMPONENT: Record<SelectType, 3 | 5 | 6 | 7 | 8> = {\n string: 3,\n user: 5,\n role: 6,\n mentionable: 7,\n channel: 8,\n};\n\nexport function selectRow(menu: SelectMenu): Schemas['ActionRowComponentForMessageRequest'] {\n const component: Record<string, unknown> = { type: COMPONENT[menu.type], custom_id: menu.id };\n\n if (menu.placeholder !== undefined) component.placeholder = menu.placeholder;\n if (menu.min !== undefined) component.min_values = menu.min;\n if (menu.max !== undefined) component.max_values = menu.max;\n if (menu.options) {\n component.options = Object.entries(menu.options).map(([label, value]) => ({ label, value }));\n }\n if (menu.channelTypes) component.channel_types = menu.channelTypes.map((t) => CHANNEL_TYPES[t]);\n\n return {\n type: 1,\n components: [component as Schemas['ActionRowComponentForMessageRequest']['components'][number]],\n };\n}\n\nexport function selectValues(interaction: APIMessageComponentSelectMenuInteraction): unknown[] {\n const { data } = interaction;\n const resolved = 'resolved' in data ? data.resolved : undefined;\n\n return data.values.map((id) => {\n if (!resolved) return id;\n if ('users' in resolved && resolved.users?.[id]) return resolved.users[id];\n if ('roles' in resolved && resolved.roles?.[id]) return resolved.roles[id];\n if ('channels' in resolved && resolved.channels?.[id]) return resolved.channels[id];\n\n return id;\n });\n}\n","import type { MessageFlags } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport { type Button, buttonComponent } from '#utils/button';\nimport { type SelectMenu, selectRow } from '#utils/select';\nimport { attachmentsOf, type Upload } from '#utils/upload';\n\n/** What a message holds besides its text. */\nexport type MessageOptions = {\n /** Buttons, five to a row. Pass `[]` to remove the buttons of the message. */\n buttons?: Button[];\n /** Select menus made with `select()`, one to a row, above the buttons. */\n selects?: SelectMenu[];\n /** Embeds, as in the Discord API, such as `{ title, description }`. */\n embeds?: Schemas['RichEmbed'][];\n /**\n * Who the message pings. By default it pings nobody, even for `@everyone` or `<@id>` in the\n * text. Pass `{ users: [id] }` or `{ parse: ['roles'] }` to ping.\n */\n allowedMentions?: Schemas['MessageAllowedMentionsRequest'];\n /** Files to attach. */\n files?: Upload[];\n};\n\n/** {@link MessageOptions} for the first answer or a follow-up. */\nexport type ReplyOptions = MessageOptions & {\n /** `true` shows the message to the user who ran the interaction only. */\n ephemeral?: boolean;\n};\n\n/** Calls that send more after the first answer. Each returns an `Error` if Discord refuses. */\nexport type FollowUps = {\n /**\n * Runs `work` once Discord has the first answer. Call it before you return the answer. `work`\n * can call the other follow-up calls.\n */\n after(work: () => Promise<void>): void;\n /** Sends a new message. */\n followUp(content: string, options?: ReplyOptions): Promise<Error | null>;\n /** Edits the first answer. */\n editReply(content: string, options?: MessageOptions): Promise<Error | null>;\n /** Deletes the first answer. */\n deleteReply(): Promise<Error | null>;\n};\n\nconst PER_ROW = 5;\nconst EPHEMERAL: MessageFlags.Ephemeral = 64;\nconst NO_PINGS: Schemas['MessageAllowedMentionsRequest'] = { parse: [] };\n\nexport function message(\n content: string,\n options: ReplyOptions = {},\n): Schemas['IncomingWebhookInteractionRequest'] {\n const data: Schemas['IncomingWebhookInteractionRequest'] = {\n content,\n allowed_mentions: options.allowedMentions ?? NO_PINGS,\n };\n if (options.ephemeral) data.flags = EPHEMERAL;\n if (options.embeds) data.embeds = options.embeds;\n if (options.files?.length) data.attachments = attachmentsOf(options.files);\n if (!options.buttons && !options.selects) return data;\n\n const rows = (options.selects ?? []).map(selectRow);\n const buttons = options.buttons ?? [];\n for (let i = 0; i < buttons.length; i += PER_ROW) {\n rows.push({\n type: 1,\n components: buttons.slice(i, i + PER_ROW).map(buttonComponent),\n });\n }\n data.components = rows;\n\n return data;\n}\n","import { callDiscord } from '#lib/rest';\nimport type { Schemas } from '#lib/routes';\nimport type { SyncEnv } from '#utils/env';\nimport { message, type MessageOptions } from '#utils/message';\nimport { type Upload, uploadsOf } from '#utils/upload';\n\nexport async function putCommands(\n env: SyncEnv,\n body: Schemas['ApplicationCommandUpdateRequest'][],\n): Promise<[Error, null] | [null, number]> {\n const [error, saved] = env.guildId\n ? await callDiscord(\n env.token,\n 'put',\n '/applications/{application_id}/guilds/{guild_id}/commands',\n {\n path: { application_id: env.applicationId, guild_id: env.guildId },\n body,\n },\n )\n : await callDiscord(env.token, 'put', '/applications/{application_id}/commands', {\n path: { application_id: env.applicationId },\n body,\n });\n if (error) return [error, null];\n\n return [null, saved?.length ?? 0];\n}\n\nexport async function editOriginal(\n applicationId: string,\n token: string,\n body: Schemas['IncomingWebhookUpdateRequestPartial'],\n files?: Upload[],\n): Promise<Error | null> {\n const [error] = await callDiscord(\n null,\n 'patch',\n '/webhooks/{webhook_id}/{webhook_token}/messages/@original',\n {\n path: { webhook_id: applicationId, webhook_token: token },\n body,\n files,\n },\n );\n\n return error;\n}\n\nexport type Send = (\n channelId: string,\n content: string,\n options?: MessageOptions,\n) => Promise<Error | null>;\n\nexport async function sendMessage(\n token: string,\n channelId: string,\n content: string,\n options: MessageOptions = {},\n): Promise<Error | null> {\n const [error] = await callDiscord(token, 'post', '/channels/{channel_id}/messages', {\n path: { channel_id: channelId },\n body: message(content, {\n buttons: options.buttons,\n selects: options.selects,\n embeds: options.embeds,\n allowedMentions: options.allowedMentions,\n files: options.files,\n }),\n files: options.files,\n });\n\n return error;\n}\n\nexport async function interactionCallback(\n id: string,\n token: string,\n response: Response,\n): Promise<Error | null> {\n const multipart = response.headers.get('content-type')?.startsWith('multipart/form-data');\n const { payload, files } = multipart\n ? await uploadsOf(await response.formData())\n : { payload: JSON.parse(await response.text()) as unknown, files: [] };\n\n const [error] = await callDiscord(\n null,\n 'post',\n '/interactions/{interaction_id}/{interaction_token}/callback',\n {\n path: { interaction_id: id, interaction_token: token },\n body: payload as Schemas['create_interaction_response_body'],\n files,\n },\n );\n\n return error;\n}\n"],"mappings":"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"}
@@ -1,15 +1,37 @@
1
- import { i as ErrorHandler, r as Send, t as Bot } from "./bot-icKel3r9.mjs";
1
+ import { i as ErrorHandler, r as Send, t as Bot } from "./bot-CW_jhC5U.mjs";
2
2
  import { DurableObject } from "cloudflare:workers";
3
3
  import { GatewayDispatchPayload, GatewayIntentBits, GatewayPresenceUpdateData } from "discord-api-types/v10";
4
4
  type Intent = Exclude<keyof typeof GatewayIntentBits, "GuildBans" | "GuildEmojisAndStickers">;
5
+ /** What a gateway event handler gets besides the event data. */
6
+ export type EventContext = {
7
+ /** Posts a message to a channel, with optional `buttons` and `embeds`. */
8
+ send: Send;
9
+ /**
10
+ * The storage of the Durable Object that holds the connection, such as `storage.kv.get(key)`.
11
+ * The connection keeps its own state under the `session` key, so leave that key alone.
12
+ */
13
+ storage: DurableObjectStorage;
14
+ };
5
15
  type GatewayConfig = {
16
+ /**
17
+ * The names of the gateway intents the bot needs, such as `'GuildMessages'`. Turn on privileged
18
+ * ones, such as `'MessageContent'`, in the Developer Portal first.
19
+ */
6
20
  intents: Intent[];
21
+ /** The status the bot shows while it is connected. */
7
22
  presence?: GatewayPresenceUpdateData;
23
+ /**
24
+ * The bot from `bot()`. Its handlers answer the commands, buttons and modals that arrive over the
25
+ * gateway, so the application needs no Interactions Endpoint URL.
26
+ */
8
27
  bot?: Pick<Bot, "answer">;
28
+ /** Gets errors from event handlers and from answering interactions. */
9
29
  onError?: ErrorHandler;
10
- events: { [P in GatewayDispatchPayload as P["t"]]?: (data: P["d"], c: {
11
- send: Send;
12
- }) => unknown; };
30
+ /**
31
+ * A handler per dispatch event name, such as `MESSAGE_CREATE`. `c.send` posts a message to a
32
+ * channel, with optional `buttons` and `embeds`. `c.storage` keeps state between events.
33
+ */
34
+ events: { [P in GatewayDispatchPayload as P["t"]]?: (data: P["d"], c: EventContext) => unknown; };
13
35
  };
14
36
  /**
15
37
  * Creates a Durable Object class that keeps one connection to the Discord Gateway open and runs your
@@ -19,14 +41,6 @@ type GatewayConfig = {
19
41
  * It reads `DISCORD_TOKEN` from the Worker environment.
20
42
  *
21
43
  * @param config What the bot listens to.
22
- * @param config.intents The names of the gateway intents the bot needs, such as `'GuildMessages'`.
23
- * Turn on privileged ones, such as `'MessageContent'`, in the Developer Portal first.
24
- * @param config.presence The status the bot shows while it is connected.
25
- * @param config.events A handler per dispatch event name. `c.send` posts a message to a channel,
26
- * with optional `buttons` and `embeds`.
27
- * @param config.onError Gets errors from event handlers and from answering interactions.
28
- * @param config.bot The bot from `bot()`. Its handlers answer the commands, buttons and modals that
29
- * arrive over the gateway, so the application needs no Interactions Endpoint URL.
30
44
  * @returns The Durable Object class.
31
45
  * @example
32
46
  * import { bot } from 'botflare';
package/dist/gateway.mjs CHANGED
@@ -151,6 +151,7 @@ var Shard = class {
151
151
  }
152
152
  };
153
153
  // src/gateway.ts
154
+ /** @module botflare/gateway */
154
155
  const GATEWAY = "wss://gateway.discord.gg";
155
156
  const WATCH_MS = 6e4;
156
157
  const INTERACTION_CREATE = "INTERACTION_CREATE";
@@ -162,14 +163,6 @@ const INTERACTION_CREATE = "INTERACTION_CREATE";
162
163
  * It reads `DISCORD_TOKEN` from the Worker environment.
163
164
  *
164
165
  * @param config What the bot listens to.
165
- * @param config.intents The names of the gateway intents the bot needs, such as `'GuildMessages'`.
166
- * Turn on privileged ones, such as `'MessageContent'`, in the Developer Portal first.
167
- * @param config.presence The status the bot shows while it is connected.
168
- * @param config.events A handler per dispatch event name. `c.send` posts a message to a channel,
169
- * with optional `buttons` and `embeds`.
170
- * @param config.onError Gets errors from event handlers and from answering interactions.
171
- * @param config.bot The bot from `bot()`. Its handlers answer the commands, buttons and modals that
172
- * arrive over the gateway, so the application needs no Interactions Endpoint URL.
173
166
  * @returns The Durable Object class.
174
167
  * @example
175
168
  * import { bot } from 'botflare';
@@ -239,7 +232,10 @@ function gateway(config) {
239
232
  if (!handler) return;
240
233
  const run = async () => {
241
234
  try {
242
- await handler(event.d, { send });
235
+ await handler(event.d, {
236
+ send,
237
+ storage: this.ctx.storage
238
+ });
243
239
  } catch (error) {
244
240
  await config.onError?.(error, event.t);
245
241
  }
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.mjs","names":[],"sources":["../src/utils/intents.ts","../src/utils/shard.ts","../src/gateway.ts"],"sourcesContent":["import type { GatewayIntentBits } from 'discord-api-types/v10';\n\nexport type Intent = Exclude<\n keyof typeof GatewayIntentBits,\n 'GuildBans' | 'GuildEmojisAndStickers'\n>;\n\nconst BITS: Record<Intent, GatewayIntentBits> = {\n Guilds: 1,\n GuildMembers: 2,\n GuildModeration: 4,\n GuildExpressions: 8,\n GuildIntegrations: 16,\n GuildWebhooks: 32,\n GuildInvites: 64,\n GuildVoiceStates: 128,\n GuildPresences: 256,\n GuildMessages: 512,\n GuildMessageReactions: 1024,\n GuildMessageTyping: 2048,\n DirectMessages: 4096,\n DirectMessageReactions: 8192,\n DirectMessageTyping: 16384,\n MessageContent: 32768,\n GuildScheduledEvents: 65536,\n AutoModerationConfiguration: 1048576,\n AutoModerationExecution: 2097152,\n GuildMessagePolls: 16777216,\n DirectMessagePolls: 33554432,\n};\n\nexport function intentBits(names: Intent[]): number {\n return names.reduce((bits, name) => bits | BITS[name], 0);\n}\n","import type {\n GatewayCloseCodes,\n GatewayDispatchEvents,\n GatewayDispatchPayload,\n GatewayOpcodes,\n GatewayPresenceUpdateData,\n GatewayReceivePayload,\n GatewaySendPayload,\n} from 'discord-api-types/v10';\n\nexport type Session = { id: string; url: string; seq: number | null; intents: number };\n\nconst DISPATCH: GatewayOpcodes.Dispatch = 0;\nconst HEARTBEAT: GatewayOpcodes.Heartbeat = 1;\nconst IDENTIFY: GatewayOpcodes.Identify = 2;\nconst PRESENCE_UPDATE: GatewayOpcodes.PresenceUpdate = 3;\nconst RESUME: GatewayOpcodes.Resume = 6;\nconst RECONNECT: GatewayOpcodes.Reconnect = 7;\nconst INVALID_SESSION: GatewayOpcodes.InvalidSession = 9;\nconst HELLO: GatewayOpcodes.Hello = 10;\nconst HEARTBEAT_ACK: GatewayOpcodes.HeartbeatAck = 11;\n\nconst READY = 'READY' as GatewayDispatchEvents.Ready;\nconst RESUMED = 'RESUMED' as GatewayDispatchEvents.Resumed;\n\nconst FATAL = new Set<GatewayCloseCodes>([4004, 4010, 4011, 4012, 4013, 4014]);\nconst FRESH = new Set<GatewayCloseCodes>([4007, 4009]);\n\nexport function afterClose(code: number): 'stop' | 'identify' | 'resume' {\n if (FATAL.has(code)) return 'stop';\n\n return FRESH.has(code) ? 'identify' : 'resume';\n}\n\ntype Identity = { intents: number; presence?: GatewayPresenceUpdateData };\n\ntype Hooks = {\n send: (payload: GatewaySendPayload) => void;\n reconnect: (resume: boolean) => void;\n save: (session: Session | null) => void;\n dispatch: (event: GatewayDispatchPayload) => void;\n every: (ms: number, fn: () => void) => () => void;\n};\n\nexport class Shard {\n private readonly token: string;\n private readonly identity: Identity;\n private readonly hooks: Hooks;\n private session: Session | null;\n private acked = true;\n private stopBeat: (() => void) | undefined;\n\n constructor(token: string, identity: Identity, session: Session | null, hooks: Hooks) {\n this.token = token;\n this.identity = identity;\n this.session = session;\n this.hooks = hooks;\n }\n\n handle(payload: GatewayReceivePayload): void {\n switch (payload.op) {\n case HELLO: {\n this.acked = true;\n this.stopBeat?.();\n this.stopBeat = this.hooks.every(payload.d.heartbeat_interval, () => this.beat());\n\n if (this.session) {\n this.hooks.send({\n op: RESUME,\n d: { token: this.token, session_id: this.session.id, seq: this.session.seq ?? 0 },\n });\n } else {\n this.identify();\n }\n return;\n }\n case HEARTBEAT_ACK:\n this.acked = true;\n return;\n case HEARTBEAT:\n this.hooks.send({ op: HEARTBEAT, d: this.session?.seq ?? null });\n return;\n case RECONNECT:\n this.hooks.reconnect(true);\n return;\n case INVALID_SESSION:\n if (!payload.d) this.forget();\n this.hooks.reconnect(payload.d);\n return;\n case DISPATCH:\n this.onDispatch(payload);\n }\n }\n\n stop(): void {\n this.stopBeat?.();\n this.stopBeat = undefined;\n }\n\n private forget(): void {\n this.session = null;\n this.hooks.save(null);\n }\n\n private onDispatch(payload: GatewayDispatchPayload): void {\n if (payload.t === READY) {\n this.session = {\n id: payload.d.session_id,\n url: payload.d.resume_gateway_url,\n seq: payload.s,\n intents: this.identity.intents,\n };\n } else if (this.session) {\n this.session.seq = payload.s;\n }\n if (this.session) this.hooks.save(this.session);\n\n if (payload.t === RESUMED && this.identity.presence) {\n this.hooks.send({ op: PRESENCE_UPDATE, d: this.identity.presence });\n }\n\n this.hooks.dispatch(payload);\n }\n\n private beat(): void {\n if (!this.acked) {\n this.hooks.reconnect(true);\n return;\n }\n\n this.acked = false;\n this.hooks.send({ op: HEARTBEAT, d: this.session?.seq ?? null });\n }\n\n private identify(): void {\n this.hooks.send({\n op: IDENTIFY,\n d: {\n token: this.token,\n intents: this.identity.intents,\n properties: { os: 'cloudflare-workers', browser: 'botflare', device: 'botflare' },\n ...(this.identity.presence && { presence: this.identity.presence }),\n },\n });\n }\n}\n","import { DurableObject } from 'cloudflare:workers';\nimport type {\n GatewayDispatchEvents,\n GatewayDispatchPayload,\n GatewayPresenceUpdateData,\n GatewayReceivePayload,\n} from 'discord-api-types/v10';\n\nimport type { Bot } from '#bot';\nimport { interactionCallback, type Send, sendMessage } from '#lib/discord';\nimport type { ErrorHandler } from '#types';\nimport { type Intent, intentBits } from '#utils/intents';\nimport { afterClose, type Session, Shard } from '#utils/shard';\n\nconst GATEWAY = 'wss://gateway.discord.gg';\nconst WATCH_MS = 60_000;\nconst INTERACTION_CREATE = 'INTERACTION_CREATE' as GatewayDispatchEvents.InteractionCreate;\n\ntype GatewayConfig = {\n intents: Intent[];\n presence?: GatewayPresenceUpdateData;\n bot?: Pick<Bot, 'answer'>;\n onError?: ErrorHandler;\n events: {\n [P in GatewayDispatchPayload as P['t']]?: (data: P['d'], c: { send: Send }) => unknown;\n };\n};\n\n/**\n * Creates a Durable Object class that keeps one connection to the Discord Gateway open and runs your\n * handler for each event. Export it under the `class_name` of a SQLite Durable Object binding in your\n * Wrangler config, and give that binding name to `bot({ gateway })` so a Cron Trigger starts and\n * reconnects it.\n * It reads `DISCORD_TOKEN` from the Worker environment.\n *\n * @param config What the bot listens to.\n * @param config.intents The names of the gateway intents the bot needs, such as `'GuildMessages'`.\n * Turn on privileged ones, such as `'MessageContent'`, in the Developer Portal first.\n * @param config.presence The status the bot shows while it is connected.\n * @param config.events A handler per dispatch event name. `c.send` posts a message to a channel,\n * with optional `buttons` and `embeds`.\n * @param config.onError Gets errors from event handlers and from answering interactions.\n * @param config.bot The bot from `bot()`. Its handlers answer the commands, buttons and modals that\n * arrive over the gateway, so the application needs no Interactions Endpoint URL.\n * @returns The Durable Object class.\n * @example\n * import { bot } from 'botflare';\n * import { gateway } from 'botflare/gateway';\n *\n * const app = bot({ commands: [], gateway: 'GATEWAY' });\n *\n * export const Gateway = gateway({\n * intents: ['Guilds', 'GuildMessages'],\n * bot: app,\n * events: { MESSAGE_CREATE: (message, c) => c.send(message.channel_id, 'heard you') },\n * });\n *\n * export default app;\n */\nexport function gateway(\n config: GatewayConfig,\n): new (\n ctx: DurableObjectState,\n env: { DISCORD_TOKEN: string },\n) => DurableObject<{ DISCORD_TOKEN: string }> & { ensure(): Promise<void> } {\n return class Gateway extends DurableObject<{ DISCORD_TOKEN: string }> {\n private ws: WebSocket | undefined;\n private shard: Shard | undefined;\n private stopped = false;\n\n async ensure(): Promise<void> {\n if (!(await this.ctx.storage.getAlarm()))\n await this.ctx.storage.setAlarm(Date.now() + WATCH_MS);\n\n if (!this.ws && !this.stopped) this.dial();\n }\n\n override async alarm(): Promise<void> {\n await this.ensure();\n }\n\n private dial(): void {\n const identity = { intents: intentBits(config.intents), presence: config.presence };\n const stored = this.ctx.storage.kv.get<Session>('session');\n const saved = stored?.intents === identity.intents ? stored : null;\n\n const ws = new WebSocket(`${saved?.url ?? GATEWAY}/?v=10&encoding=json`);\n const send: Send = (channelId, content, options) =>\n sendMessage(this.env.DISCORD_TOKEN, channelId, content, options);\n\n const shard = new Shard(this.env.DISCORD_TOKEN, identity, saved, {\n send: (payload) => ws.send(JSON.stringify(payload)),\n reconnect: (resume) => {\n if (this.ws !== ws) return;\n\n this.drop();\n ws.close(4000);\n this.after(resume ? 'resume' : 'identify');\n },\n save: (session) =>\n session\n ? this.ctx.storage.kv.put('session', session)\n : this.ctx.storage.kv.delete('session'),\n dispatch: (event) => {\n if (event.t === INTERACTION_CREATE && config.bot) {\n const { answer } = config.bot;\n const respond = async (): Promise<void> => {\n try {\n const after: (() => Promise<void>)[] = [];\n const response = await answer(event.d, {\n waitUntil: (promise) => this.ctx.waitUntil(promise),\n afterAnswer: (task) => after.push(task),\n });\n if (!response.ok) return;\n\n const failed = await interactionCallback(event.d.id, event.d.token, response);\n if (failed) await config.onError?.(failed, event.t);\n else for (const task of after) this.ctx.waitUntil(task());\n } catch (error) {\n await config.onError?.(error, event.t);\n }\n };\n this.ctx.waitUntil(respond());\n }\n\n const handler = config.events[event.t] as\n | ((data: unknown, c: { send: typeof send }) => unknown)\n | undefined;\n if (!handler) return;\n\n const run = async (): Promise<void> => {\n try {\n await handler(event.d, { send });\n } catch (error) {\n await config.onError?.(error, event.t);\n }\n };\n this.ctx.waitUntil(run());\n },\n every: (ms, fn) => {\n let interval: ReturnType<typeof setInterval> | null = null;\n const first = setTimeout(() => {\n fn();\n interval = setInterval(fn, ms);\n }, ms * Math.random());\n\n return () => {\n clearTimeout(first);\n clearInterval(interval);\n };\n },\n });\n\n this.ws = ws;\n this.shard = shard;\n\n ws.addEventListener('message', (event) => {\n if (this.ws === ws) shard.handle(JSON.parse(String(event.data)) as GatewayReceivePayload);\n });\n ws.addEventListener('close', (event) => {\n if (this.ws !== ws) return;\n\n this.drop();\n this.after(afterClose(event.code));\n });\n }\n\n private drop(): void {\n this.shard?.stop();\n this.shard = undefined;\n this.ws = undefined;\n }\n\n private after(next: 'stop' | 'identify' | 'resume'): void {\n if (next === 'stop') {\n this.stopped = true;\n return;\n }\n\n if (next === 'identify') this.ctx.storage.kv.delete('session');\n setTimeout(() => this.dial(), 1000 + Math.random() * 4000);\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gateway.mjs","names":[],"sources":["../src/utils/intents.ts","../src/utils/shard.ts","../src/gateway.ts"],"sourcesContent":["import type { GatewayIntentBits } from 'discord-api-types/v10';\n\nexport type Intent = Exclude<\n keyof typeof GatewayIntentBits,\n 'GuildBans' | 'GuildEmojisAndStickers'\n>;\n\nconst BITS: Record<Intent, GatewayIntentBits> = {\n Guilds: 1,\n GuildMembers: 2,\n GuildModeration: 4,\n GuildExpressions: 8,\n GuildIntegrations: 16,\n GuildWebhooks: 32,\n GuildInvites: 64,\n GuildVoiceStates: 128,\n GuildPresences: 256,\n GuildMessages: 512,\n GuildMessageReactions: 1024,\n GuildMessageTyping: 2048,\n DirectMessages: 4096,\n DirectMessageReactions: 8192,\n DirectMessageTyping: 16384,\n MessageContent: 32768,\n GuildScheduledEvents: 65536,\n AutoModerationConfiguration: 1048576,\n AutoModerationExecution: 2097152,\n GuildMessagePolls: 16777216,\n DirectMessagePolls: 33554432,\n};\n\nexport function intentBits(names: Intent[]): number {\n return names.reduce((bits, name) => bits | BITS[name], 0);\n}\n","import type {\n GatewayCloseCodes,\n GatewayDispatchEvents,\n GatewayDispatchPayload,\n GatewayOpcodes,\n GatewayPresenceUpdateData,\n GatewayReceivePayload,\n GatewaySendPayload,\n} from 'discord-api-types/v10';\n\nexport type Session = { id: string; url: string; seq: number | null; intents: number };\n\nconst DISPATCH: GatewayOpcodes.Dispatch = 0;\nconst HEARTBEAT: GatewayOpcodes.Heartbeat = 1;\nconst IDENTIFY: GatewayOpcodes.Identify = 2;\nconst PRESENCE_UPDATE: GatewayOpcodes.PresenceUpdate = 3;\nconst RESUME: GatewayOpcodes.Resume = 6;\nconst RECONNECT: GatewayOpcodes.Reconnect = 7;\nconst INVALID_SESSION: GatewayOpcodes.InvalidSession = 9;\nconst HELLO: GatewayOpcodes.Hello = 10;\nconst HEARTBEAT_ACK: GatewayOpcodes.HeartbeatAck = 11;\n\nconst READY = 'READY' as GatewayDispatchEvents.Ready;\nconst RESUMED = 'RESUMED' as GatewayDispatchEvents.Resumed;\n\nconst FATAL = new Set<GatewayCloseCodes>([4004, 4010, 4011, 4012, 4013, 4014]);\nconst FRESH = new Set<GatewayCloseCodes>([4007, 4009]);\n\nexport function afterClose(code: number): 'stop' | 'identify' | 'resume' {\n if (FATAL.has(code)) return 'stop';\n\n return FRESH.has(code) ? 'identify' : 'resume';\n}\n\ntype Identity = { intents: number; presence?: GatewayPresenceUpdateData };\n\ntype Hooks = {\n send: (payload: GatewaySendPayload) => void;\n reconnect: (resume: boolean) => void;\n save: (session: Session | null) => void;\n dispatch: (event: GatewayDispatchPayload) => void;\n every: (ms: number, fn: () => void) => () => void;\n};\n\nexport class Shard {\n private readonly token: string;\n private readonly identity: Identity;\n private readonly hooks: Hooks;\n private session: Session | null;\n private acked = true;\n private stopBeat: (() => void) | undefined;\n\n constructor(token: string, identity: Identity, session: Session | null, hooks: Hooks) {\n this.token = token;\n this.identity = identity;\n this.session = session;\n this.hooks = hooks;\n }\n\n handle(payload: GatewayReceivePayload): void {\n switch (payload.op) {\n case HELLO: {\n this.acked = true;\n this.stopBeat?.();\n this.stopBeat = this.hooks.every(payload.d.heartbeat_interval, () => this.beat());\n\n if (this.session) {\n this.hooks.send({\n op: RESUME,\n d: { token: this.token, session_id: this.session.id, seq: this.session.seq ?? 0 },\n });\n } else {\n this.identify();\n }\n return;\n }\n case HEARTBEAT_ACK:\n this.acked = true;\n return;\n case HEARTBEAT:\n this.hooks.send({ op: HEARTBEAT, d: this.session?.seq ?? null });\n return;\n case RECONNECT:\n this.hooks.reconnect(true);\n return;\n case INVALID_SESSION:\n if (!payload.d) this.forget();\n this.hooks.reconnect(payload.d);\n return;\n case DISPATCH:\n this.onDispatch(payload);\n }\n }\n\n stop(): void {\n this.stopBeat?.();\n this.stopBeat = undefined;\n }\n\n private forget(): void {\n this.session = null;\n this.hooks.save(null);\n }\n\n private onDispatch(payload: GatewayDispatchPayload): void {\n if (payload.t === READY) {\n this.session = {\n id: payload.d.session_id,\n url: payload.d.resume_gateway_url,\n seq: payload.s,\n intents: this.identity.intents,\n };\n } else if (this.session) {\n this.session.seq = payload.s;\n }\n if (this.session) this.hooks.save(this.session);\n\n if (payload.t === RESUMED && this.identity.presence) {\n this.hooks.send({ op: PRESENCE_UPDATE, d: this.identity.presence });\n }\n\n this.hooks.dispatch(payload);\n }\n\n private beat(): void {\n if (!this.acked) {\n this.hooks.reconnect(true);\n return;\n }\n\n this.acked = false;\n this.hooks.send({ op: HEARTBEAT, d: this.session?.seq ?? null });\n }\n\n private identify(): void {\n this.hooks.send({\n op: IDENTIFY,\n d: {\n token: this.token,\n intents: this.identity.intents,\n properties: { os: 'cloudflare-workers', browser: 'botflare', device: 'botflare' },\n ...(this.identity.presence && { presence: this.identity.presence }),\n },\n });\n }\n}\n","/** @module botflare/gateway */\nimport { DurableObject } from 'cloudflare:workers';\nimport type {\n GatewayDispatchEvents,\n GatewayDispatchPayload,\n GatewayPresenceUpdateData,\n GatewayReceivePayload,\n} from 'discord-api-types/v10';\n\nimport type { Bot } from '#bot';\nimport { interactionCallback, type Send, sendMessage } from '#lib/discord';\nimport type { ErrorHandler } from '#types';\nimport { type Intent, intentBits } from '#utils/intents';\nimport { afterClose, type Session, Shard } from '#utils/shard';\n\nconst GATEWAY = 'wss://gateway.discord.gg';\nconst WATCH_MS = 60_000;\nconst INTERACTION_CREATE = 'INTERACTION_CREATE' as GatewayDispatchEvents.InteractionCreate;\n\n/** What a gateway event handler gets besides the event data. */\nexport type EventContext = {\n /** Posts a message to a channel, with optional `buttons` and `embeds`. */\n send: Send;\n /**\n * The storage of the Durable Object that holds the connection, such as `storage.kv.get(key)`.\n * The connection keeps its own state under the `session` key, so leave that key alone.\n */\n storage: DurableObjectStorage;\n};\n\ntype GatewayConfig = {\n /**\n * The names of the gateway intents the bot needs, such as `'GuildMessages'`. Turn on privileged\n * ones, such as `'MessageContent'`, in the Developer Portal first.\n */\n intents: Intent[];\n /** The status the bot shows while it is connected. */\n presence?: GatewayPresenceUpdateData;\n /**\n * The bot from `bot()`. Its handlers answer the commands, buttons and modals that arrive over the\n * gateway, so the application needs no Interactions Endpoint URL.\n */\n bot?: Pick<Bot, 'answer'>;\n /** Gets errors from event handlers and from answering interactions. */\n onError?: ErrorHandler;\n /**\n * A handler per dispatch event name, such as `MESSAGE_CREATE`. `c.send` posts a message to a\n * channel, with optional `buttons` and `embeds`. `c.storage` keeps state between events.\n */\n events: {\n [P in GatewayDispatchPayload as P['t']]?: (data: P['d'], c: EventContext) => unknown;\n };\n};\n\n/**\n * Creates a Durable Object class that keeps one connection to the Discord Gateway open and runs your\n * handler for each event. Export it under the `class_name` of a SQLite Durable Object binding in your\n * Wrangler config, and give that binding name to `bot({ gateway })` so a Cron Trigger starts and\n * reconnects it.\n * It reads `DISCORD_TOKEN` from the Worker environment.\n *\n * @param config What the bot listens to.\n * @returns The Durable Object class.\n * @example\n * import { bot } from 'botflare';\n * import { gateway } from 'botflare/gateway';\n *\n * const app = bot({ commands: [], gateway: 'GATEWAY' });\n *\n * export const Gateway = gateway({\n * intents: ['Guilds', 'GuildMessages'],\n * bot: app,\n * events: { MESSAGE_CREATE: (message, c) => c.send(message.channel_id, 'heard you') },\n * });\n *\n * export default app;\n */\nexport function gateway(\n config: GatewayConfig,\n): new (\n ctx: DurableObjectState,\n env: { DISCORD_TOKEN: string },\n) => DurableObject<{ DISCORD_TOKEN: string }> & { ensure(): Promise<void> } {\n return class Gateway extends DurableObject<{ DISCORD_TOKEN: string }> {\n private ws: WebSocket | undefined;\n private shard: Shard | undefined;\n private stopped = false;\n\n async ensure(): Promise<void> {\n if (!(await this.ctx.storage.getAlarm()))\n await this.ctx.storage.setAlarm(Date.now() + WATCH_MS);\n\n if (!this.ws && !this.stopped) this.dial();\n }\n\n override async alarm(): Promise<void> {\n await this.ensure();\n }\n\n private dial(): void {\n const identity = { intents: intentBits(config.intents), presence: config.presence };\n const stored = this.ctx.storage.kv.get<Session>('session');\n const saved = stored?.intents === identity.intents ? stored : null;\n\n const ws = new WebSocket(`${saved?.url ?? GATEWAY}/?v=10&encoding=json`);\n const send: Send = (channelId, content, options) =>\n sendMessage(this.env.DISCORD_TOKEN, channelId, content, options);\n\n const shard = new Shard(this.env.DISCORD_TOKEN, identity, saved, {\n send: (payload) => ws.send(JSON.stringify(payload)),\n reconnect: (resume) => {\n if (this.ws !== ws) return;\n\n this.drop();\n ws.close(4000);\n this.after(resume ? 'resume' : 'identify');\n },\n save: (session) =>\n session\n ? this.ctx.storage.kv.put('session', session)\n : this.ctx.storage.kv.delete('session'),\n dispatch: (event) => {\n if (event.t === INTERACTION_CREATE && config.bot) {\n const { answer } = config.bot;\n const respond = async (): Promise<void> => {\n try {\n const after: (() => Promise<void>)[] = [];\n const response = await answer(event.d, {\n waitUntil: (promise) => this.ctx.waitUntil(promise),\n afterAnswer: (task) => after.push(task),\n });\n if (!response.ok) return;\n\n const failed = await interactionCallback(event.d.id, event.d.token, response);\n if (failed) await config.onError?.(failed, event.t);\n else for (const task of after) this.ctx.waitUntil(task());\n } catch (error) {\n await config.onError?.(error, event.t);\n }\n };\n this.ctx.waitUntil(respond());\n }\n\n const handler = config.events[event.t] as\n | ((data: unknown, c: EventContext) => unknown)\n | undefined;\n if (!handler) return;\n\n const run = async (): Promise<void> => {\n try {\n await handler(event.d, { send, storage: this.ctx.storage });\n } catch (error) {\n await config.onError?.(error, event.t);\n }\n };\n this.ctx.waitUntil(run());\n },\n every: (ms, fn) => {\n let interval: ReturnType<typeof setInterval> | null = null;\n const first = setTimeout(() => {\n fn();\n interval = setInterval(fn, ms);\n }, ms * Math.random());\n\n return () => {\n clearTimeout(first);\n clearInterval(interval);\n };\n },\n });\n\n this.ws = ws;\n this.shard = shard;\n\n ws.addEventListener('message', (event) => {\n if (this.ws === ws) shard.handle(JSON.parse(String(event.data)) as GatewayReceivePayload);\n });\n ws.addEventListener('close', (event) => {\n if (this.ws !== ws) return;\n\n this.drop();\n this.after(afterClose(event.code));\n });\n }\n\n private drop(): void {\n this.shard?.stop();\n this.shard = undefined;\n this.ws = undefined;\n }\n\n private after(next: 'stop' | 'identify' | 'resume'): void {\n if (next === 'stop') {\n this.stopped = true;\n return;\n }\n\n if (next === 'identify') this.ctx.storage.kv.delete('session');\n setTimeout(() => this.dial(), 1000 + Math.random() * 4000);\n }\n };\n}\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Locales, S as Suggestion, _ as AutocompleteContext, a as Select, b as OptionValue, c as Access, d as ContextMenu, f as MenuContext, g as SelectType, h as Modal, i as ErrorHandler, l as Command, m as Fields, n as bot, o as SelectContext, p as MenuType, s as SelectValue, t as Bot, u as Context, v as ChannelType, w as Routes, x as Options, y as Option } from "./bot-icKel3r9.mjs";
1
+ import { C as Locales, S as Suggestion, _ as AutocompleteContext, a as Select, b as OptionValue, c as Access, d as ContextMenu, f as MenuContext, g as SelectType, h as Modal, i as ErrorHandler, l as Command, m as Fields, n as bot, o as SelectContext, p as MenuType, s as SelectValue, t as Bot, u as Context, v as ChannelType, w as Routes, x as Options, y as Option } from "./bot-CW_jhC5U.mjs";
2
2
  import { Locale } from "discord-api-types/v10";
3
3
  /**
4
4
  * Declares a slash command and the handler that answers it. To split a command into subcommands,
@@ -10,14 +10,6 @@ import { Locale } from "discord-api-types/v10";
10
10
  * @param config.name The name the user types after `/`.
11
11
  * @param config.description The text Discord shows under the name.
12
12
  * @param config.options The options, keyed by name, made with {@link option}.
13
- * @param config.permissions Who sees the command by default: members with all of these
14
- * permissions, such as `['ManageGuild']`. Pass `[]` to show it to admins only. Server admins can
15
- * change this in Server Settings. Applies to a top-level command only.
16
- * @param config.contexts Where the command shows: `guild`, `bot_dm` or `private_channel`.
17
- * @param config.installs How the app must be installed for the command: `guild` or `user`.
18
- * @param config.locales The name and description in other languages, keyed by Discord locale,
19
- * such as `{ th: { name: 'ทอย', description: 'ทอยลูกเต๋า' } }`. The handler still gets the
20
- * default name.
21
13
  * @param run Answers the command. It gets the interaction, the typed option values, `reply`
22
14
  * for an answer within 3 seconds and `defer` for work that takes longer. Pass
23
15
  * `{ ephemeral: true }` to either so only the user who ran the command sees the answer, `buttons`
@@ -46,6 +38,14 @@ export declare function command(config: {
46
38
  description: string;
47
39
  subcommands: Command[];
48
40
  } & Access): Command;
41
+ type MenuConfig = {
42
+ /** The name of the item, such as `Report message`. */
43
+ name: string;
44
+ /** The name in other languages, keyed by Discord locale. */
45
+ locales?: Partial<Record<`${Locale}`, {
46
+ name: string;
47
+ }>>;
48
+ } & Omit<Access, "locales">;
49
49
  /**
50
50
  * Declares a context menu command: an item in the Apps menu when the user right-clicks a user or a
51
51
  * message. Add it to the `commands` of {@link bot}, next to the slash commands. It has no options
@@ -53,22 +53,12 @@ export declare function command(config: {
53
53
  *
54
54
  * @param menu Whether the item shows on a `user` or on a `message`.
55
55
  * @param config What Discord shows.
56
- * @param config.name The name of the item, such as `Report message`.
57
- * @param config.permissions Who sees the item by default, as in {@link command}.
58
- * @param config.contexts Where the item shows, as in {@link command}.
59
- * @param config.installs How the app must be installed for the item, as in {@link command}.
60
- * @param config.locales The name in other languages, keyed by Discord locale.
61
56
  * @param run Answers the click, with `reply`, `defer` and `modal` as in a slash command.
62
57
  * @returns The context menu command, for the `commands` of {@link bot}.
63
58
  * @example
64
59
  * const avatar = contextMenu('user', { name: 'Show avatar' }, (c) => c.reply(c.target.username));
65
60
  */
66
- export declare function contextMenu<const T extends MenuType>(menu: T, config: {
67
- name: string;
68
- locales?: Partial<Record<`${Locale}`, {
69
- name: string;
70
- }>>;
71
- } & Omit<Access, "locales">, run: (c: MenuContext<T>) => Response | Promise<Response>): ContextMenu<T>;
61
+ export declare function contextMenu<const T extends MenuType>(menu: T, config: MenuConfig, run: (c: MenuContext<T>) => Response | Promise<Response>): ContextMenu<T>;
72
62
  type Id = string | {
73
63
  id: string;
74
64
  };
@@ -140,6 +130,25 @@ type OptionLimits<T extends keyof OptionValue> = T extends "string" ? {
140
130
  channelTypes?: ChannelType[];
141
131
  } : unknown;
142
132
  type Choice<T extends keyof OptionValue> = T extends "string" ? string : T extends "integer" | "number" ? number : never;
133
+ type OptionConfig<T extends keyof OptionValue, R extends boolean, C> = {
134
+ /** The text Discord shows under the option name. */
135
+ description: string;
136
+ /** Whether the user must fill it in. Defaults to `false`. */
137
+ required?: R;
138
+ /**
139
+ * The only values the user can pick, keyed by the label Discord shows. For a `string`, `integer`
140
+ * or `number` option.
141
+ */
142
+ choices?: C;
143
+ /** The name and description in other languages, keyed by Discord locale. */
144
+ locales?: Locales;
145
+ } & OptionLimits<T> & {
146
+ /**
147
+ * Suggests values as the user types, instead of fixed `choices`. Gets what the user has typed
148
+ * so far as `c.value` and returns up to 25 suggestions, as plain values or as `{ name, value }`.
149
+ */
150
+ autocomplete?: [Choice<T>] extends [never] ? never : [C] extends [never] ? (c: AutocompleteContext) => Suggestion<Choice<T>>[] | Promise<Suggestion<Choice<T>>[]> : never;
151
+ };
143
152
  /**
144
153
  * Declares one option of a slash command. The handler reads its value from `c.options`,
145
154
  * typed from `type`, and `undefined` when the option is not `required`.
@@ -157,22 +166,12 @@ type Choice<T extends keyof OptionValue> = T extends "string" ? string : T exten
157
166
  * @param type The value type Discord asks the user for. `user`, `channel` and `role` give the full
158
167
  * object Discord sends with the command, not only its id.
159
168
  * @param config How Discord shows the option.
160
- * @param config.description The text Discord shows under the option name.
161
- * @param config.required Whether the user must fill it in. Defaults to `false`.
162
- * @param config.locales The name and description in other languages, keyed by Discord locale.
163
169
  * @returns The option, for the `options` of {@link command}.
164
170
  * @example
165
171
  * option('integer', { description: 'How many dice', required: true, min: 1, max: 6 });
166
172
  * option('string', { description: 'Color', choices: { Red: 'red', Blue: 'blue' } });
167
173
  */
168
- export declare function option<const T extends keyof OptionValue, const R extends boolean = false, const C extends Record<string, Choice<T>> = never>(type: T, config: {
169
- description: string;
170
- required?: R;
171
- choices?: C;
172
- locales?: Locales;
173
- } & OptionLimits<T> & {
174
- autocomplete?: [Choice<T>] extends [never] ? never : [C] extends [never] ? (c: AutocompleteContext) => Suggestion<Choice<T>>[] | Promise<Suggestion<Choice<T>>[]> : never;
175
- }): Option<T, R, [C] extends [never] ? OptionValue[T] : C[keyof C]>;
174
+ export declare function option<const T extends keyof OptionValue, const R extends boolean = false, const C extends Record<string, Choice<T>> = never>(type: T, config: OptionConfig<T, R, C>): Option<T, R, [C] extends [never] ? OptionValue[T] : C[keyof C]>;
176
175
  type Method = "get" | "post" | "put" | "patch" | "delete";
177
176
  type Route<M extends Method> = { [K in keyof Routes]: M extends keyof Routes[K] ? K : never; }[keyof Routes];
178
177
  type Operation<P extends keyof Routes, M extends Method> = Routes[P] extends Record<M, infer O extends {
@@ -202,6 +201,23 @@ type Rest = { [M in Method]: <P extends Route<M>>(path: P, ...args: Operation<P,
202
201
  * });
203
202
  */
204
203
  export declare const rest: Rest;
204
+ type SelectConfig<T extends SelectType, O> = {
205
+ /** The id Discord sends back with the pick. */
206
+ id: string;
207
+ /** The text the menu shows before a pick. */
208
+ placeholder?: string;
209
+ /** The fewest items the user must pick. Defaults to 1. */
210
+ min?: number;
211
+ /** The most items the user can pick. Defaults to 1. */
212
+ max?: number;
213
+ /** The items of a `string` menu, keyed by the label Discord shows. */
214
+ options?: O;
215
+ } & (T extends "string" ? {
216
+ options: unknown;
217
+ } : unknown) & (T extends "channel" ? {
218
+ /** The channel types the user can pick, such as `['text', 'forum']`. */
219
+ channelTypes?: ChannelType[];
220
+ } : unknown);
205
221
  /**
206
222
  * Declares a select menu and the handler that runs when the user picks from it. Put it in the
207
223
  * `selects` of a message, such as `c.reply('Pick one', { selects: [menu] })`, and in the `selects` of
@@ -213,10 +229,6 @@ export declare const rest: Rest;
213
229
  *
214
230
  * @param type What the user picks from.
215
231
  * @param config How Discord shows the menu.
216
- * @param config.id The id Discord sends back with the pick.
217
- * @param config.placeholder The text the menu shows before a pick.
218
- * @param config.min The fewest items the user must pick. Defaults to 1.
219
- * @param config.max The most items the user can pick. Defaults to 1.
220
232
  * @param run Answers the pick. `c.values` holds the picked items.
221
233
  * @returns The select menu.
222
234
  * @example
@@ -224,16 +236,6 @@ export declare const rest: Rest;
224
236
  * c.update(`picked ${c.values.join(', ')}`),
225
237
  * );
226
238
  */
227
- export declare function select<const T extends SelectType, const O extends Record<string, T extends "string" ? string : never> = never>(type: T, config: {
228
- id: string;
229
- placeholder?: string;
230
- min?: number;
231
- max?: number;
232
- options?: O;
233
- } & (T extends "string" ? {
234
- options: unknown;
235
- } : unknown) & (T extends "channel" ? {
236
- channelTypes?: ChannelType[];
237
- } : unknown), run: (c: SelectContext<[O] extends [never] ? SelectValue[T] : O[keyof O]>) => Response | Promise<Response>): Select<T, [O] extends [never] ? SelectValue[T] : O[keyof O]>;
239
+ export declare function select<const T extends SelectType, const O extends Record<string, T extends "string" ? string : never> = never>(type: T, config: SelectConfig<T, O>, run: (c: SelectContext<[O] extends [never] ? SelectValue[T] : O[keyof O]>) => Response | Promise<Response>): Select<T, [O] extends [never] ? SelectValue[T] : O[keyof O]>;
238
240
  export { type Bot, type ErrorHandler, type Select, type SelectContext, type SelectValue, bot };
239
241
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -274,24 +274,6 @@ async function startGateway(env, name) {
274
274
  * the hex public key of your application in the Discord Developer Portal.
275
275
  *
276
276
  * @param config What the bot answers.
277
- * @param config.commands The commands, made with {@link command} or `contextMenu()`.
278
- * @param config.modals The modals the bot answers, made with {@link modal}.
279
- * @param config.onError Gets errors from deferred work. If that work fails, the bot also edits the
280
- * answer to "Something went wrong." so it does not stay on "thinking…".
281
- * @param config.gateway The name of the Durable Object binding that holds the class from `gateway()`.
282
- * Open the Worker URL once after the first deploy to start the connection. A GET request or any
283
- * Cron Trigger starts it again if it is down.
284
- * @param config.cron Runs on a Cron Trigger, keyed by the cron expression from your Wrangler config.
285
- * `c.send` posts a message to a channel with `DISCORD_TOKEN`, with optional `buttons` and `embeds`,
286
- * and returns an `Error` if Discord refuses.
287
- * @param config.buttons Answers a click, keyed by the button id given to `reply`. An id such as
288
- * `page:2` runs the `page` handler with `c.value` set to `'2'`. `update` edits the message that
289
- * holds the button. Pass `{ buttons: [] }` to remove them.
290
- * A button takes a `label`, an `emoji` or both, and a `style`: `primary` (default), `secondary`,
291
- * `success` or `danger`. A button with a `url` instead of an `id` opens the link and needs no
292
- * handler. `disabled: true` greys a button out. `c.deferUpdate(work)` keeps the message as it is
293
- * and edits it with what `work` returns, for a click that takes longer than 3 seconds.
294
- * @param config.selects The select menus the bot answers, made with `select()`.
295
277
  * @returns The Worker handler. Export it as the default export of your Worker. Pass it to
296
278
  * `gateway({ bot })` to answer interactions that arrive over the gateway too.
297
279
  * @example
@@ -365,11 +347,6 @@ function command(config, run) {
365
347
  *
366
348
  * @param menu Whether the item shows on a `user` or on a `message`.
367
349
  * @param config What Discord shows.
368
- * @param config.name The name of the item, such as `Report message`.
369
- * @param config.permissions Who sees the item by default, as in {@link command}.
370
- * @param config.contexts Where the item shows, as in {@link command}.
371
- * @param config.installs How the app must be installed for the item, as in {@link command}.
372
- * @param config.locales The name in other languages, keyed by Discord locale.
373
350
  * @param run Answers the click, with `reply`, `defer` and `modal` as in a slash command.
374
351
  * @returns The context menu command, for the `commands` of {@link bot}.
375
352
  * @example
@@ -466,9 +443,6 @@ function modal(config, run) {
466
443
  * @param type The value type Discord asks the user for. `user`, `channel` and `role` give the full
467
444
  * object Discord sends with the command, not only its id.
468
445
  * @param config How Discord shows the option.
469
- * @param config.description The text Discord shows under the option name.
470
- * @param config.required Whether the user must fill it in. Defaults to `false`.
471
- * @param config.locales The name and description in other languages, keyed by Discord locale.
472
446
  * @returns The option, for the `options` of {@link command}.
473
447
  * @example
474
448
  * option('integer', { description: 'How many dice', required: true, min: 1, max: 6 });
@@ -514,10 +488,6 @@ const rest = createRest(() => env.DISCORD_TOKEN);
514
488
  *
515
489
  * @param type What the user picks from.
516
490
  * @param config How Discord shows the menu.
517
- * @param config.id The id Discord sends back with the pick.
518
- * @param config.placeholder The text the menu shows before a pick.
519
- * @param config.min The fewest items the user must pick. Defaults to 1.
520
- * @param config.max The most items the user can pick. Defaults to 1.
521
491
  * @param run Answers the pick. `c.values` holds the picked items.
522
492
  * @returns The select menu.
523
493
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["workerEnv"],"sources":["../src/answers/autocomplete.ts","../src/utils/modal.ts","../src/utils/response.ts","../src/answers/defer.ts","../src/answers/follow.ts","../src/utils/id.ts","../src/answers/button.ts","../src/answers/command.ts","../src/answers/menu.ts","../src/answers/modal.ts","../src/answers/select.ts","../src/utils/verify.ts","../src/bot.ts","../src/command.ts","../src/context-menu.ts","../src/format.ts","../src/modal.ts","../src/option.ts","../src/rest.ts","../src/select.ts"],"sourcesContent":["import type { APIApplicationCommandAutocompleteInteraction } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport type { ErrorHandler } from '#types';\nimport { type Command, findCommand } from '#utils/command';\nimport type { Suggestion } from '#utils/option';\n\nconst MAX_CHOICES = 25;\n\nexport async function answerAutocomplete(\n interaction: APIApplicationCommandAutocompleteInteraction,\n commands: Map<string, Command>,\n onError?: ErrorHandler,\n): Promise<Response> {\n const { found, given, path } = findCommand(commands, interaction.data);\n const focused = given.find((o) => 'focused' in o && o.focused);\n const suggest =\n found && 'options' in found && focused ? found.options[focused.name]?.autocomplete : undefined;\n if (!suggest || !focused || !('value' in focused)) return new Response(null, { status: 404 });\n\n let suggestions: Suggestion<unknown>[] = [];\n try {\n suggestions = await suggest({ interaction, value: String(focused.value) });\n } catch (error) {\n await onError?.(error, `${path} autocomplete`);\n }\n\n const choices = suggestions\n .slice(0, MAX_CHOICES)\n .map((s) => (typeof s === 'object' && s !== null ? s : { name: String(s), value: s }));\n const response = {\n type: 8,\n data: { choices },\n } as Schemas['ApplicationCommandAutocompleteCallbackRequest'];\n\n return Response.json(response);\n}\n","import type { APIModalSubmissionComponent, APIModalSubmitInteraction } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport type { FollowUps, ReplyOptions } from '#utils/message';\n\nexport type Fields = Record<\n string,\n { label: string; style?: 'short' | 'paragraph'; required?: boolean }\n>;\n\nexport type Modal<F extends Fields = Fields> = {\n id: string;\n title: string;\n fields: F;\n run(\n c: {\n interaction: APIModalSubmitInteraction;\n value: string | undefined;\n fields: {\n [K in keyof F]: F[K] extends { label: string; required?: true }\n ? string\n : string | undefined;\n };\n reply: (content: string, options?: ReplyOptions) => Response;\n } & FollowUps,\n ): Response | Promise<Response>;\n};\n\nexport function modalData(\n modal: Modal,\n value?: string,\n): Schemas['ModalInteractionCallbackRequestData'] {\n return {\n custom_id: value === undefined ? modal.id : `${modal.id}:${value}`,\n title: modal.title,\n components: Object.entries(modal.fields).map(([name, field]) => ({\n type: 18,\n label: field.label,\n component: {\n type: 4,\n custom_id: name,\n style: field.style === 'paragraph' ? 2 : 1,\n required: field.required ?? true,\n },\n })),\n };\n}\n\nexport function fieldValues(\n components: APIModalSubmissionComponent[],\n): Record<string, string | undefined> {\n const inputs = components.flatMap((row) => {\n if ('component' in row) return [row.component];\n if ('components' in row) return row.components;\n return [];\n });\n\n const values: Record<string, string | undefined> = {};\n for (const input of inputs) {\n if ('value' in input && typeof input.value === 'string')\n values[input.custom_id] = input.value || undefined;\n }\n\n return values;\n}\n","import type { Schemas } from '#lib/routes';\nimport { message, type MessageOptions, type ReplyOptions } from '#utils/message';\nimport { type Modal, modalData } from '#utils/modal';\nimport { formOf } from '#utils/upload';\n\nexport const PONG: Schemas['PongInteractionCallbackRequest'] = { type: 1 };\nexport const DEFERRED: Schemas['CreateMessageInteractionCallbackRequest'] = { type: 5 };\nexport const DEFERRED_EPHEMERAL: Schemas['CreateMessageInteractionCallbackRequest'] = {\n type: 5,\n data: { flags: 64 },\n};\n\nexport const DEFERRED_UPDATE: Schemas['UpdateMessageInteractionCallbackRequest'] = { type: 6 };\n\nfunction answer(response: object, options?: MessageOptions): Response {\n if (!options?.files?.length) return Response.json(response);\n\n return new Response(formOf(response, options.files));\n}\n\nexport function replyResponse(content: string, options?: ReplyOptions): Response {\n const response: Schemas['CreateMessageInteractionCallbackRequest'] = {\n type: 4,\n data: message(content, options),\n };\n\n return answer(response, options);\n}\n\nexport function updateResponse(content: string, options?: MessageOptions): Response {\n const response: Schemas['UpdateMessageInteractionCallbackRequest'] = {\n type: 7,\n data: message(content, options),\n };\n\n return answer(response, options);\n}\n\nexport function modalResponse(form: Modal, value?: string): Response {\n const response: Schemas['ModalInteractionCallbackRequest'] = {\n type: 9,\n data: modalData(form, value),\n };\n\n return Response.json(response);\n}\n","import type { AnswerContext } from '#answers/follow';\nimport { editOriginal } from '#lib/discord';\nimport type { ErrorHandler } from '#types';\nimport type { Context } from '#utils/command';\nimport { message } from '#utils/message';\nimport type { Options } from '#utils/option';\nimport { DEFERRED, DEFERRED_EPHEMERAL, DEFERRED_UPDATE } from '#utils/response';\nimport type { Upload } from '#utils/upload';\n\ntype Work = Parameters<Context<Options>['defer']>[0];\n\nfunction later(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n work: Work,\n onError?: ErrorHandler,\n): void {\n const finish = async (): Promise<void> => {\n let edit = message('Something went wrong.');\n let files: Upload[] | undefined;\n try {\n const done = await work();\n edit =\n typeof done === 'string'\n ? message(done)\n : message(done.content, {\n buttons: done.buttons,\n selects: done.selects,\n embeds: done.embeds,\n allowedMentions: done.allowedMentions,\n files: done.files,\n });\n if (typeof done !== 'string') files = done.files;\n } catch (error) {\n await onError?.(error, where);\n }\n\n const failed = await editOriginal(interaction.application_id, interaction.token, edit, files);\n if (failed) await onError?.(failed, where);\n };\n ctx.waitUntil(finish());\n}\n\nexport function deferAnswer(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n onError?: ErrorHandler,\n): Context<Options>['defer'] {\n return (work, options) => {\n later(interaction, ctx, where, work, onError);\n\n return Response.json(options?.ephemeral ? DEFERRED_EPHEMERAL : DEFERRED);\n };\n}\n\nexport function deferUpdate(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n onError?: ErrorHandler,\n): (work: Work) => Response {\n return (work) => {\n later(interaction, ctx, where, work, onError);\n\n return Response.json(DEFERRED_UPDATE);\n };\n}\n","import { callDiscord } from '#lib/rest';\nimport type { ErrorHandler } from '#types';\nimport { type FollowUps, message } from '#utils/message';\n\nexport type AnswerContext = Pick<ExecutionContext, 'waitUntil'> & {\n afterAnswer?: (task: () => Promise<void>) => void;\n};\n\nexport function followUps(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n onError?: ErrorHandler,\n): FollowUps {\n const path = { webhook_id: interaction.application_id, webhook_token: interaction.token };\n const schedule = ctx.afterAnswer ?? ((task) => ctx.waitUntil(task()));\n\n return {\n after: (work) =>\n schedule(async () => {\n try {\n await work();\n } catch (error) {\n await onError?.(error, `${where} after`);\n }\n }),\n async followUp(content, options = {}) {\n const [error] = await callDiscord(null, 'post', '/webhooks/{webhook_id}/{webhook_token}', {\n path,\n body: message(content, options),\n files: options.files,\n });\n\n return error;\n },\n async editReply(content, options = {}) {\n const [error] = await callDiscord(\n null,\n 'patch',\n '/webhooks/{webhook_id}/{webhook_token}/messages/@original',\n { path, body: message(content, options), files: options.files },\n );\n\n return error;\n },\n async deleteReply() {\n const [error] = await callDiscord(\n null,\n 'delete',\n '/webhooks/{webhook_id}/{webhook_token}/messages/@original',\n { path },\n );\n\n return error;\n },\n };\n}\n","export function splitId(id: string): { name: string; value: string | undefined } {\n const colon = id.indexOf(':');\n if (colon === -1) return { name: id, value: undefined };\n\n return { name: id.slice(0, colon), value: id.slice(colon + 1) };\n}\n","import type { APIMessageComponentButtonInteraction } from 'discord-api-types/v10';\n\nimport { deferUpdate } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ButtonContext, ErrorHandler } from '#types';\nimport { splitId } from '#utils/id';\nimport { modalResponse, replyResponse, updateResponse } from '#utils/response';\n\nexport function answerButton(\n interaction: APIMessageComponentButtonInteraction,\n buttons: Record<string, (c: ButtonContext) => Response | Promise<Response>> | undefined,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { name, value } = splitId(interaction.data.custom_id);\n const clicked = buttons?.[name];\n if (!clicked) return new Response(null, { status: 404 });\n\n return clicked({\n interaction,\n value,\n reply: replyResponse,\n update: updateResponse,\n modal: modalResponse,\n ...followUps(interaction, ctx, `button ${name}`, onError),\n deferUpdate: deferUpdate(interaction, ctx, `button ${name}`, onError),\n });\n}\n","import type { APIChatInputApplicationCommandInteraction } from 'discord-api-types/v10';\n\nimport { deferAnswer } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler } from '#types';\nimport { type Command, findCommand } from '#utils/command';\nimport { optionValues } from '#utils/option';\nimport { modalResponse, replyResponse } from '#utils/response';\n\nexport function answerCommand(\n interaction: APIChatInputApplicationCommandInteraction,\n commands: Map<string, Command>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { found, given, path } = findCommand(commands, interaction.data);\n if (!found || !('run' in found)) return new Response(null, { status: 404 });\n\n return found.run({\n interaction,\n options: optionValues(given, interaction.data.resolved),\n reply: replyResponse,\n defer: deferAnswer(interaction, ctx, path, onError),\n modal: modalResponse,\n ...followUps(interaction, ctx, path, onError),\n });\n}\n","import type {\n APIContextMenuInteraction,\n APIMessageApplicationCommandInteraction,\n APIUserApplicationCommandInteraction,\n ApplicationCommandType,\n} from 'discord-api-types/v10';\n\nimport { deferAnswer } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler } from '#types';\nimport type { ContextMenu, MenuType } from '#utils/command';\nimport { modalResponse, replyResponse } from '#utils/response';\n\nconst USER: ApplicationCommandType.User = 2;\n\nexport function answerMenu(\n interaction: APIContextMenuInteraction,\n menus: Map<string, ContextMenu>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { data } = interaction;\n const menu: MenuType = data.type === USER ? 'user' : 'message';\n const found = menus.get(`${menu}:${data.name}`);\n if (!found) return new Response(null, { status: 404 });\n\n const target =\n data.type === USER\n ? (interaction as APIUserApplicationCommandInteraction).data.resolved.users[data.target_id]\n : (interaction as APIMessageApplicationCommandInteraction).data.resolved.messages[\n data.target_id\n ];\n\n return found.run({\n interaction,\n target: target as never,\n reply: replyResponse,\n defer: deferAnswer(interaction, ctx, `${data.name} (${menu} menu)`, onError),\n modal: modalResponse,\n ...followUps(interaction, ctx, `${data.name} (${menu} menu)`, onError),\n });\n}\n","import type { APIModalSubmitInteraction } from 'discord-api-types/v10';\n\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler } from '#types';\nimport { splitId } from '#utils/id';\nimport { fieldValues, type Modal } from '#utils/modal';\nimport { replyResponse } from '#utils/response';\n\nexport function answerModal(\n interaction: APIModalSubmitInteraction,\n modals: Map<string, Modal>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { name, value } = splitId(interaction.data.custom_id);\n const submitted = modals.get(name);\n if (!submitted) return new Response(null, { status: 404 });\n\n return submitted.run({\n interaction,\n value,\n fields: fieldValues(interaction.data.components),\n reply: replyResponse,\n ...followUps(interaction, ctx, `modal ${name}`, onError),\n });\n}\n","import type { APIMessageComponentSelectMenuInteraction } from 'discord-api-types/v10';\n\nimport { deferUpdate } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler, Select } from '#types';\nimport { modalResponse, replyResponse, updateResponse } from '#utils/response';\nimport { type SelectType, selectValues } from '#utils/select';\n\nexport function answerSelect(\n interaction: APIMessageComponentSelectMenuInteraction,\n selects: Map<string, Select<SelectType, unknown>>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const picked = selects.get(interaction.data.custom_id);\n if (!picked) return new Response(null, { status: 404 });\n\n return picked.run({\n interaction,\n values: selectValues(interaction),\n reply: replyResponse,\n update: updateResponse,\n modal: modalResponse,\n ...followUps(interaction, ctx, `select ${interaction.data.custom_id}`, onError),\n deferUpdate: deferUpdate(interaction, ctx, `select ${interaction.data.custom_id}`, onError),\n });\n}\n","const encoder = new TextEncoder();\n\nfunction bytes(hex: string): Uint8Array<ArrayBuffer> | null {\n if (!/^(?:[\\da-f]{2})+$/i.test(hex)) return null;\n return Uint8Array.from(hex.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));\n}\n\nexport async function verify(\n publicKey: string,\n signature: string,\n timestamp: string,\n body: string,\n): Promise<boolean> {\n const key = bytes(publicKey);\n const sig = bytes(signature);\n\n if (key?.length !== 32 || sig?.length !== 64) return false;\n\n const imported = await crypto.subtle.importKey('raw', key, { name: 'Ed25519' }, false, [\n 'verify',\n ]);\n\n return crypto.subtle.verify({ name: 'Ed25519' }, imported, sig, encoder.encode(timestamp + body));\n}\n","import type {\n APIChatInputApplicationCommandInteraction,\n APIContextMenuInteraction,\n APIInteraction,\n APIMessageComponentButtonInteraction,\n APIMessageComponentSelectMenuInteraction,\n ApplicationCommandType,\n ComponentType,\n InteractionType,\n} from 'discord-api-types/v10';\n\nimport { answerAutocomplete } from '#answers/autocomplete';\nimport { answerButton } from '#answers/button';\nimport { answerCommand } from '#answers/command';\nimport type { AnswerContext } from '#answers/follow';\nimport { answerMenu } from '#answers/menu';\nimport { answerModal } from '#answers/modal';\nimport { answerSelect } from '#answers/select';\nimport { type Send, sendMessage } from '#lib/discord';\nimport type { ButtonContext, ErrorHandler, Select } from '#types';\nimport type { Command, ContextMenu } from '#utils/command';\nimport type { Modal } from '#utils/modal';\nimport { PONG } from '#utils/response';\nimport type { SelectType } from '#utils/select';\nimport { verify } from '#utils/verify';\n\nconst PING: InteractionType.Ping = 1;\nconst APPLICATION_COMMAND: InteractionType.ApplicationCommand = 2;\nconst MESSAGE_COMPONENT: InteractionType.MessageComponent = 3;\nconst AUTOCOMPLETE: InteractionType.ApplicationCommandAutocomplete = 4;\nconst MODAL_SUBMIT: InteractionType.ModalSubmit = 5;\nconst CHAT_INPUT: ApplicationCommandType.ChatInput = 1;\nconst USER_MENU: ApplicationCommandType.User = 2;\nconst MESSAGE_MENU: ApplicationCommandType.Message = 3;\nconst BUTTON: ComponentType.Button = 2;\nconst PUBLIC_KEY = /^[\\da-f]{64}$/i;\n\ntype CronContext = { controller: ScheduledController; send: Send };\n\ntype BotConfig = {\n commands: (Command | ContextMenu)[];\n buttons?: Record<string, (c: ButtonContext) => Response | Promise<Response>>;\n selects?: Select<SelectType, unknown>[];\n modals?: Modal[];\n gateway?: string;\n onError?: ErrorHandler;\n cron?: Record<string, (c: CronContext) => void | Promise<void>>;\n};\n\n/** The Worker handler from {@link bot}. */\nexport type Bot = {\n commands: (Command | ContextMenu)[];\n fetch: (\n request: Request,\n env: { DISCORD_PUBLIC_KEY: string } & Record<string, unknown>,\n ctx: ExecutionContext,\n ) => Promise<Response>;\n scheduled: (\n controller: ScheduledController,\n env: { DISCORD_TOKEN: string } & Record<string, unknown>,\n ) => Promise<void>;\n answer: (interaction: APIInteraction, ctx: AnswerContext) => Promise<Response>;\n};\n\nasync function startGateway(env: Record<string, unknown>, name: string): Promise<void> {\n const namespace = env[name] as DurableObjectNamespace;\n const stub = namespace.getByName('main') as unknown as { ensure(): Promise<void> };\n await stub.ensure();\n}\n\n/**\n * Creates a Worker handler for the Interactions Endpoint URL of a Discord application.\n * It rejects requests without a valid Ed25519 signature, answers Discord's PING check\n * and runs the matching command. It reads `DISCORD_PUBLIC_KEY` from the Worker environment:\n * the hex public key of your application in the Discord Developer Portal.\n *\n * @param config What the bot answers.\n * @param config.commands The commands, made with {@link command} or `contextMenu()`.\n * @param config.modals The modals the bot answers, made with {@link modal}.\n * @param config.onError Gets errors from deferred work. If that work fails, the bot also edits the\n * answer to \"Something went wrong.\" so it does not stay on \"thinking…\".\n * @param config.gateway The name of the Durable Object binding that holds the class from `gateway()`.\n * Open the Worker URL once after the first deploy to start the connection. A GET request or any\n * Cron Trigger starts it again if it is down.\n * @param config.cron Runs on a Cron Trigger, keyed by the cron expression from your Wrangler config.\n * `c.send` posts a message to a channel with `DISCORD_TOKEN`, with optional `buttons` and `embeds`,\n * and returns an `Error` if Discord refuses.\n * @param config.buttons Answers a click, keyed by the button id given to `reply`. An id such as\n * `page:2` runs the `page` handler with `c.value` set to `'2'`. `update` edits the message that\n * holds the button. Pass `{ buttons: [] }` to remove them.\n * A button takes a `label`, an `emoji` or both, and a `style`: `primary` (default), `secondary`,\n * `success` or `danger`. A button with a `url` instead of an `id` opens the link and needs no\n * handler. `disabled: true` greys a button out. `c.deferUpdate(work)` keeps the message as it is\n * and edits it with what `work` returns, for a click that takes longer than 3 seconds.\n * @param config.selects The select menus the bot answers, made with `select()`.\n * @returns The Worker handler. Export it as the default export of your Worker. Pass it to\n * `gateway({ bot })` to answer interactions that arrive over the gateway too.\n * @example\n * import { bot, command } from 'botflare';\n *\n * export default bot({ commands: [command({ name: 'ping', description: 'Pong' }, (c) => c.reply('pong'))] });\n */\nexport function bot(config: BotConfig): Bot {\n const commands = new Map<string, Command>();\n const menus = new Map<string, ContextMenu>();\n for (const c of config.commands) {\n if ('menu' in c) menus.set(`${c.menu}:${c.name}`, c);\n else commands.set(c.name, c);\n }\n const modals = new Map((config.modals ?? []).map((m) => [m.id, m]));\n const selects = new Map((config.selects ?? []).map((s) => [s.id, s]));\n\n const answer = async (interaction: APIInteraction, ctx: AnswerContext): Promise<Response> => {\n if (interaction.type === MESSAGE_COMPONENT && interaction.data.component_type !== BUTTON) {\n return answerSelect(\n interaction as APIMessageComponentSelectMenuInteraction,\n selects,\n ctx,\n config.onError,\n );\n }\n if (interaction.type === MESSAGE_COMPONENT && interaction.data.component_type === BUTTON) {\n return answerButton(\n interaction as APIMessageComponentButtonInteraction,\n config.buttons,\n ctx,\n config.onError,\n );\n }\n if (interaction.type === MODAL_SUBMIT)\n return answerModal(interaction, modals, ctx, config.onError);\n if (interaction.type === AUTOCOMPLETE) {\n return answerAutocomplete(interaction, commands, config.onError);\n }\n if (\n interaction.type === APPLICATION_COMMAND &&\n (interaction.data.type === USER_MENU || interaction.data.type === MESSAGE_MENU)\n ) {\n return answerMenu(interaction as APIContextMenuInteraction, menus, ctx, config.onError);\n }\n if (interaction.type === APPLICATION_COMMAND && interaction.data.type === CHAT_INPUT) {\n return answerCommand(\n interaction as APIChatInputApplicationCommandInteraction,\n commands,\n ctx,\n config.onError,\n );\n }\n\n return new Response(null, { status: 400 });\n };\n\n return {\n commands: config.commands,\n answer,\n async scheduled(controller, env) {\n if (config.gateway) await startGateway(env, config.gateway);\n\n const job = config.cron?.[controller.cron];\n if (!job) return;\n\n await job({\n controller,\n send: (channelId, content, options) =>\n sendMessage(env.DISCORD_TOKEN, channelId, content, options),\n });\n },\n async fetch(request, env, ctx) {\n if (request.method === 'GET' && config.gateway) {\n await startGateway(env, config.gateway);\n return new Response('gateway started');\n }\n\n if (request.method !== 'POST') return new Response(null, { status: 405 });\n\n if (!PUBLIC_KEY.test(env.DISCORD_PUBLIC_KEY)) {\n return new Response(\n 'DISCORD_PUBLIC_KEY must be the 64 hex characters from the Developer Portal',\n { status: 500 },\n );\n }\n\n const signature = request.headers.get('X-Signature-Ed25519');\n const timestamp = request.headers.get('X-Signature-Timestamp');\n const body = await request.text();\n\n const valid =\n signature !== null &&\n timestamp !== null &&\n (await verify(env.DISCORD_PUBLIC_KEY, signature, timestamp, body));\n if (!valid) return new Response('invalid request signature', { status: 401 });\n\n const interaction = JSON.parse(body) as APIInteraction;\n if (interaction.type === PING) return Response.json(PONG);\n\n return answer(interaction, ctx);\n },\n };\n}\n","import type { Access, Command, Context } from '#utils/command';\nimport type { Options } from '#utils/option';\n\n/**\n * Declares a slash command and the handler that answers it. To split a command into subcommands,\n * such as `/config get` and `/config set`, pass `subcommands` instead of `options` and `run`: a list\n * of commands made with `command()`. A subcommand that has `subcommands` of its own becomes a group,\n * such as `/config role add`. Discord allows one group level only.\n *\n * @param config What Discord shows for the command.\n * @param config.name The name the user types after `/`.\n * @param config.description The text Discord shows under the name.\n * @param config.options The options, keyed by name, made with {@link option}.\n * @param config.permissions Who sees the command by default: members with all of these\n * permissions, such as `['ManageGuild']`. Pass `[]` to show it to admins only. Server admins can\n * change this in Server Settings. Applies to a top-level command only.\n * @param config.contexts Where the command shows: `guild`, `bot_dm` or `private_channel`.\n * @param config.installs How the app must be installed for the command: `guild` or `user`.\n * @param config.locales The name and description in other languages, keyed by Discord locale,\n * such as `{ th: { name: 'ทอย', description: 'ทอยลูกเต๋า' } }`. The handler still gets the\n * default name.\n * @param run Answers the command. It gets the interaction, the typed option values, `reply`\n * for an answer within 3 seconds and `defer` for work that takes longer. Pass\n * `{ ephemeral: true }` to either so only the user who ran the command sees the answer, `buttons`\n * to add buttons and `embeds` to add embeds. Messages ping nobody, even for `@everyone` or `<@id>`\n * in the text. To ping, pass `allowedMentions`, such as `{ users: [id] }` or `{ parse: ['roles'] }`. The work of `defer` returns the text, or\n * `{ content, buttons, embeds }`.\n *\n * To send more after the first answer, call `c.after(work)` before you return it. `work` runs once\n * Discord has the answer, and can call `c.followUp(text, options)` for a new message,\n * `c.editReply(text, options)` and `c.deleteReply()`. Each returns an `Error` if Discord refuses.\n * The work of `defer` can call them too. Buttons, select menus, modals and context menus have them.\n * @returns The command, for the `commands` of {@link bot}.\n * @example\n * const greet = command(\n * { name: 'greet', description: 'Say hi', options: { name: option('string', { description: 'Who', required: true }) } },\n * (c) => c.reply(`hi ${c.options.name}`),\n * );\n */\nexport function command<const O extends Options = never>(\n config: { name: string; description: string; options?: O } & Access,\n run: (c: Context<O>) => Response | Promise<Response>,\n): Command<O>;\nexport function command(\n config: { name: string; description: string; subcommands: Command[] } & Access,\n): Command;\nexport function command(\n config: {\n name: string;\n description: string;\n options?: Options;\n subcommands?: Command[];\n } & Access,\n run?: (c: Context<Options>) => Response | Promise<Response>,\n): Command {\n const { options, subcommands, ...rest } = config;\n if (subcommands) return { ...rest, subcommands };\n\n return { ...rest, options: options ?? {}, run: run as NonNullable<typeof run> };\n}\n","import type { Locale } from 'discord-api-types/v10';\n\nimport type { Access, ContextMenu, MenuContext, MenuType } from '#utils/command';\n\n/**\n * Declares a context menu command: an item in the Apps menu when the user right-clicks a user or a\n * message. Add it to the `commands` of {@link bot}, next to the slash commands. It has no options\n * and no description. The handler gets the user or the message as `c.target`.\n *\n * @param menu Whether the item shows on a `user` or on a `message`.\n * @param config What Discord shows.\n * @param config.name The name of the item, such as `Report message`.\n * @param config.permissions Who sees the item by default, as in {@link command}.\n * @param config.contexts Where the item shows, as in {@link command}.\n * @param config.installs How the app must be installed for the item, as in {@link command}.\n * @param config.locales The name in other languages, keyed by Discord locale.\n * @param run Answers the click, with `reply`, `defer` and `modal` as in a slash command.\n * @returns The context menu command, for the `commands` of {@link bot}.\n * @example\n * const avatar = contextMenu('user', { name: 'Show avatar' }, (c) => c.reply(c.target.username));\n */\nexport function contextMenu<const T extends MenuType>(\n menu: T,\n config: { name: string; locales?: Partial<Record<`${Locale}`, { name: string }>> } & Omit<\n Access,\n 'locales'\n >,\n run: (c: MenuContext<T>) => Response | Promise<Response>,\n): ContextMenu<T> {\n return { ...config, menu, run };\n}\n","type Id = string | { id: string };\n\nconst idOf = (target: Id): string => (typeof target === 'string' ? target : target.id);\n\n/**\n * Writes a mention for message text. A mention shows as a link to the user, role, channel or\n * command. It pings nobody unless the message allows it with `allowedMentions`.\n *\n * @example\n * c.reply(`${mention.user(c.interaction.member.user)} joined ${mention.channel('123')}`);\n */\nexport const mention: {\n user: (user: Id) => string;\n role: (role: Id) => string;\n channel: (channel: Id) => string;\n command: (name: string, id: string) => string;\n} = {\n user: (user) => `<@${idOf(user)}>`,\n role: (role) => `<@&${idOf(role)}>`,\n channel: (channel) => `<#${idOf(channel)}>`,\n command: (name, id) => `</${name}:${id}>`,\n};\n\nconst STYLE = {\n 'short-time': 't',\n 'long-time': 'T',\n 'short-date': 'd',\n 'long-date': 'D',\n short: 'f',\n long: 'F',\n relative: 'R',\n} as const;\n\n/**\n * Writes a timestamp for message text. Discord shows it in the time zone and language of each\n * reader.\n *\n * @param date The moment, as a `Date` or as milliseconds since 1970, such as `Date.now()`.\n * @param style How Discord shows it: `short` (default, such as \"5 November 2026 12:30\"), `long`,\n * `short-date`, `long-date`, `short-time`, `long-time` or `relative` (such as \"in 2 hours\").\n * @returns The timestamp markup, such as `<t:1762338600:R>`.\n * @example\n * c.reply(`The event starts ${time(start, 'relative')}`);\n */\nexport function time(date: Date | number, style: keyof typeof STYLE = 'short'): string {\n const seconds = Math.floor(Number(date) / 1000);\n\n return `<t:${seconds}:${STYLE[style]}>`;\n}\n","import type { Fields, Modal } from '#utils/modal';\n\n/**\n * Declares a modal: a form Discord shows in a popup. Open it with `c.modal()` from a command or a\n * button, and add it to the `modals` of {@link bot} so the bot gets what the user typed.\n *\n * @param config What Discord shows in the popup.\n * @param config.id The id Discord sends back when the user submits the form.\n * @param config.title The title of the popup.\n * @param config.fields The text fields, keyed by name. A field is required unless `required` is `false`.\n * @param run Answers the submitted form. `c.fields` holds the text of each field, keyed by name.\n * A field that is not required and left empty is `undefined`.\n * To carry a value through the form, open it with `c.modal(form, value)`. The handler gets that\n * value as `c.value`, such as the id of the item the form edits.\n * @returns The modal, for `c.modal()` and the `modals` of {@link bot}.\n * @example\n * const feedback = modal(\n * { id: 'feedback', title: 'Feedback', fields: { text: { label: 'Your idea', style: 'paragraph' } } },\n * (c) => c.reply(`Thanks: ${c.fields.text}`),\n * );\n */\nexport function modal<const F extends Fields>(\n config: { id: string; title: string; fields: F },\n run: Modal<F>['run'],\n): Modal<F> {\n return { id: config.id, title: config.title, fields: config.fields, run };\n}\n","import type { Locales } from '#utils/locale';\nimport type {\n AutocompleteContext,\n ChannelType,\n Option,\n OptionValue,\n Suggestion,\n} from '#utils/option';\n\ntype OptionLimits<T extends keyof OptionValue> = T extends 'string'\n ? { minLength?: number; maxLength?: number }\n : T extends 'integer' | 'number'\n ? { min?: number; max?: number }\n : T extends 'channel'\n ? { channelTypes?: ChannelType[] }\n : unknown;\n\ntype Choice<T extends keyof OptionValue> = T extends 'string'\n ? string\n : T extends 'integer' | 'number'\n ? number\n : never;\n\n/**\n * Declares one option of a slash command. The handler reads its value from `c.options`,\n * typed from `type`, and `undefined` when the option is not `required`.\n *\n * A `string`, `integer` or `number` option also takes `choices`, keyed by the label Discord shows.\n * The user can pick only those, and the handler gets one of their values. A `string` option takes\n * `minLength` and `maxLength`, an `integer` or `number` option takes `min` and `max`, and a\n * `channel` option takes `channelTypes`, such as `['text', 'forum']`.\n *\n * Instead of fixed `choices`, a `string`, `integer` or `number` option can take `autocomplete`: a\n * function that gets what the user has typed so far as `c.value` and returns up to 25 suggestions,\n * as plain values or as `{ name, value }`. If it throws, `onError` gets the error and Discord shows\n * no suggestions.\n *\n * @param type The value type Discord asks the user for. `user`, `channel` and `role` give the full\n * object Discord sends with the command, not only its id.\n * @param config How Discord shows the option.\n * @param config.description The text Discord shows under the option name.\n * @param config.required Whether the user must fill it in. Defaults to `false`.\n * @param config.locales The name and description in other languages, keyed by Discord locale.\n * @returns The option, for the `options` of {@link command}.\n * @example\n * option('integer', { description: 'How many dice', required: true, min: 1, max: 6 });\n * option('string', { description: 'Color', choices: { Red: 'red', Blue: 'blue' } });\n */\nexport function option<\n const T extends keyof OptionValue,\n const R extends boolean = false,\n const C extends Record<string, Choice<T>> = never,\n>(\n type: T,\n config: {\n description: string;\n required?: R;\n choices?: C;\n locales?: Locales;\n } & OptionLimits<T> & {\n autocomplete?: [Choice<T>] extends [never]\n ? never\n : [C] extends [never]\n ? (c: AutocompleteContext) => Suggestion<Choice<T>>[] | Promise<Suggestion<Choice<T>>[]>\n : never;\n },\n): Option<T, R, [C] extends [never] ? OptionValue[T] : C[keyof C]> {\n const built: Option = { ...config, type, required: config.required ?? false };\n\n return built as Option<T, R, [C] extends [never] ? OptionValue[T] : C[keyof C]>;\n}\n","import { env as workerEnv } from 'cloudflare:workers';\n\nimport { createRest, type Rest } from '#lib/rest';\n\n/**\n * Calls the Discord REST API as the bot, with `DISCORD_TOKEN` from the Worker environment. Each\n * method takes a route from the Discord API spec, such as `'/guilds/{guild_id}/roles'`, and\n * types its `path`, `query`, `body` and result from that route. It returns `[error, data]` and\n * does not throw. When Discord answers 429, it waits for `retry_after` and tries again, up to\n * three tries in all.\n *\n * A request that is not a GET takes `reason`, the text that shows in the audit log of the server.\n *\n * @example\n * import { rest } from 'botflare';\n *\n * const [error, member] = await rest.get('/guilds/{guild_id}/members/{user_id}', {\n * path: { guild_id: guildId, user_id: userId },\n * });\n * if (!error) await rest.put('/guilds/{guild_id}/members/{user_id}/roles/{role_id}', {\n * path: { guild_id: guildId, user_id: member.user.id, role_id: roleId },\n * });\n */\nexport const rest: Rest = createRest(\n () => (workerEnv as { DISCORD_TOKEN?: unknown }).DISCORD_TOKEN,\n);\n","import type { Select, SelectContext, SelectValue } from '#types';\nimport type { ChannelType } from '#utils/option';\nimport type { SelectType } from '#utils/select';\n\n/**\n * Declares a select menu and the handler that runs when the user picks from it. Put it in the\n * `selects` of a message, such as `c.reply('Pick one', { selects: [menu] })`, and in the `selects` of\n * {@link bot} so the bot gets the pick. Each menu takes a row of its own, above the buttons.\n *\n * A `string` menu lists `options`, keyed by the label Discord shows, and the handler gets the picked\n * values. A `user`, `role`, `channel` or `mentionable` menu lets the user pick from the server, and\n * the handler gets the full objects Discord resolved. A `channel` menu takes `channelTypes`.\n *\n * @param type What the user picks from.\n * @param config How Discord shows the menu.\n * @param config.id The id Discord sends back with the pick.\n * @param config.placeholder The text the menu shows before a pick.\n * @param config.min The fewest items the user must pick. Defaults to 1.\n * @param config.max The most items the user can pick. Defaults to 1.\n * @param run Answers the pick. `c.values` holds the picked items.\n * @returns The select menu.\n * @example\n * const fruit = select('string', { id: 'fruit', options: { Apple: 'apple', Mango: 'mango' } }, (c) =>\n * c.update(`picked ${c.values.join(', ')}`),\n * );\n */\nexport function select<\n const T extends SelectType,\n const O extends Record<string, T extends 'string' ? string : never> = never,\n>(\n type: T,\n config: {\n id: string;\n placeholder?: string;\n min?: number;\n max?: number;\n options?: O;\n } & (T extends 'string' ? { options: unknown } : unknown) &\n (T extends 'channel' ? { channelTypes?: ChannelType[] } : unknown),\n run: (\n c: SelectContext<[O] extends [never] ? SelectValue[T] : O[keyof O]>,\n ) => Response | Promise<Response>,\n): Select<T, [O] extends [never] ? SelectValue[T] : O[keyof O]> {\n return { ...config, type, run };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["workerEnv"],"sources":["../src/answers/autocomplete.ts","../src/utils/modal.ts","../src/utils/response.ts","../src/answers/defer.ts","../src/answers/follow.ts","../src/utils/id.ts","../src/answers/button.ts","../src/answers/command.ts","../src/answers/menu.ts","../src/answers/modal.ts","../src/answers/select.ts","../src/utils/verify.ts","../src/bot.ts","../src/command.ts","../src/context-menu.ts","../src/format.ts","../src/modal.ts","../src/option.ts","../src/rest.ts","../src/select.ts"],"sourcesContent":["import type { APIApplicationCommandAutocompleteInteraction } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport type { ErrorHandler } from '#types';\nimport { type Command, findCommand } from '#utils/command';\nimport type { Suggestion } from '#utils/option';\n\nconst MAX_CHOICES = 25;\n\nexport async function answerAutocomplete(\n interaction: APIApplicationCommandAutocompleteInteraction,\n commands: Map<string, Command>,\n onError?: ErrorHandler,\n): Promise<Response> {\n const { found, given, path } = findCommand(commands, interaction.data);\n const focused = given.find((o) => 'focused' in o && o.focused);\n const suggest =\n found && 'options' in found && focused ? found.options[focused.name]?.autocomplete : undefined;\n if (!suggest || !focused || !('value' in focused)) return new Response(null, { status: 404 });\n\n let suggestions: Suggestion<unknown>[] = [];\n try {\n suggestions = await suggest({ interaction, value: String(focused.value) });\n } catch (error) {\n await onError?.(error, `${path} autocomplete`);\n }\n\n const choices = suggestions\n .slice(0, MAX_CHOICES)\n .map((s) => (typeof s === 'object' && s !== null ? s : { name: String(s), value: s }));\n const response = {\n type: 8,\n data: { choices },\n } as Schemas['ApplicationCommandAutocompleteCallbackRequest'];\n\n return Response.json(response);\n}\n","import type { APIModalSubmissionComponent, APIModalSubmitInteraction } from 'discord-api-types/v10';\n\nimport type { Schemas } from '#lib/routes';\nimport type { FollowUps, ReplyOptions } from '#utils/message';\n\nexport type Fields = Record<\n string,\n { label: string; style?: 'short' | 'paragraph'; required?: boolean }\n>;\n\nexport type Modal<F extends Fields = Fields> = {\n id: string;\n title: string;\n fields: F;\n run(\n c: {\n interaction: APIModalSubmitInteraction;\n value: string | undefined;\n fields: {\n [K in keyof F]: F[K] extends { label: string; required?: true }\n ? string\n : string | undefined;\n };\n reply: (content: string, options?: ReplyOptions) => Response;\n } & FollowUps,\n ): Response | Promise<Response>;\n};\n\nexport function modalData(\n modal: Modal,\n value?: string,\n): Schemas['ModalInteractionCallbackRequestData'] {\n return {\n custom_id: value === undefined ? modal.id : `${modal.id}:${value}`,\n title: modal.title,\n components: Object.entries(modal.fields).map(([name, field]) => ({\n type: 18,\n label: field.label,\n component: {\n type: 4,\n custom_id: name,\n style: field.style === 'paragraph' ? 2 : 1,\n required: field.required ?? true,\n },\n })),\n };\n}\n\nexport function fieldValues(\n components: APIModalSubmissionComponent[],\n): Record<string, string | undefined> {\n const inputs = components.flatMap((row) => {\n if ('component' in row) return [row.component];\n if ('components' in row) return row.components;\n return [];\n });\n\n const values: Record<string, string | undefined> = {};\n for (const input of inputs) {\n if ('value' in input && typeof input.value === 'string')\n values[input.custom_id] = input.value || undefined;\n }\n\n return values;\n}\n","import type { Schemas } from '#lib/routes';\nimport { message, type MessageOptions, type ReplyOptions } from '#utils/message';\nimport { type Modal, modalData } from '#utils/modal';\nimport { formOf } from '#utils/upload';\n\nexport const PONG: Schemas['PongInteractionCallbackRequest'] = { type: 1 };\nexport const DEFERRED: Schemas['CreateMessageInteractionCallbackRequest'] = { type: 5 };\nexport const DEFERRED_EPHEMERAL: Schemas['CreateMessageInteractionCallbackRequest'] = {\n type: 5,\n data: { flags: 64 },\n};\n\nexport const DEFERRED_UPDATE: Schemas['UpdateMessageInteractionCallbackRequest'] = { type: 6 };\n\nfunction answer(response: object, options?: MessageOptions): Response {\n if (!options?.files?.length) return Response.json(response);\n\n return new Response(formOf(response, options.files));\n}\n\nexport function replyResponse(content: string, options?: ReplyOptions): Response {\n const response: Schemas['CreateMessageInteractionCallbackRequest'] = {\n type: 4,\n data: message(content, options),\n };\n\n return answer(response, options);\n}\n\nexport function updateResponse(content: string, options?: MessageOptions): Response {\n const response: Schemas['UpdateMessageInteractionCallbackRequest'] = {\n type: 7,\n data: message(content, options),\n };\n\n return answer(response, options);\n}\n\nexport function modalResponse(form: Modal, value?: string): Response {\n const response: Schemas['ModalInteractionCallbackRequest'] = {\n type: 9,\n data: modalData(form, value),\n };\n\n return Response.json(response);\n}\n","import type { AnswerContext } from '#answers/follow';\nimport { editOriginal } from '#lib/discord';\nimport type { ErrorHandler } from '#types';\nimport type { Context } from '#utils/command';\nimport { message } from '#utils/message';\nimport type { Options } from '#utils/option';\nimport { DEFERRED, DEFERRED_EPHEMERAL, DEFERRED_UPDATE } from '#utils/response';\nimport type { Upload } from '#utils/upload';\n\ntype Work = Parameters<Context<Options>['defer']>[0];\n\nfunction later(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n work: Work,\n onError?: ErrorHandler,\n): void {\n const finish = async (): Promise<void> => {\n let edit = message('Something went wrong.');\n let files: Upload[] | undefined;\n try {\n const done = await work();\n edit =\n typeof done === 'string'\n ? message(done)\n : message(done.content, {\n buttons: done.buttons,\n selects: done.selects,\n embeds: done.embeds,\n allowedMentions: done.allowedMentions,\n files: done.files,\n });\n if (typeof done !== 'string') files = done.files;\n } catch (error) {\n await onError?.(error, where);\n }\n\n const failed = await editOriginal(interaction.application_id, interaction.token, edit, files);\n if (failed) await onError?.(failed, where);\n };\n ctx.waitUntil(finish());\n}\n\nexport function deferAnswer(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n onError?: ErrorHandler,\n): Context<Options>['defer'] {\n return (work, options) => {\n later(interaction, ctx, where, work, onError);\n\n return Response.json(options?.ephemeral ? DEFERRED_EPHEMERAL : DEFERRED);\n };\n}\n\nexport function deferUpdate(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n onError?: ErrorHandler,\n): (work: Work) => Response {\n return (work) => {\n later(interaction, ctx, where, work, onError);\n\n return Response.json(DEFERRED_UPDATE);\n };\n}\n","import { callDiscord } from '#lib/rest';\nimport type { ErrorHandler } from '#types';\nimport { type FollowUps, message } from '#utils/message';\n\nexport type AnswerContext = Pick<ExecutionContext, 'waitUntil'> & {\n afterAnswer?: (task: () => Promise<void>) => void;\n};\n\nexport function followUps(\n interaction: { application_id: string; token: string },\n ctx: AnswerContext,\n where: string,\n onError?: ErrorHandler,\n): FollowUps {\n const path = { webhook_id: interaction.application_id, webhook_token: interaction.token };\n const schedule = ctx.afterAnswer ?? ((task) => ctx.waitUntil(task()));\n\n return {\n after: (work) =>\n schedule(async () => {\n try {\n await work();\n } catch (error) {\n await onError?.(error, `${where} after`);\n }\n }),\n async followUp(content, options = {}) {\n const [error] = await callDiscord(null, 'post', '/webhooks/{webhook_id}/{webhook_token}', {\n path,\n body: message(content, options),\n files: options.files,\n });\n\n return error;\n },\n async editReply(content, options = {}) {\n const [error] = await callDiscord(\n null,\n 'patch',\n '/webhooks/{webhook_id}/{webhook_token}/messages/@original',\n { path, body: message(content, options), files: options.files },\n );\n\n return error;\n },\n async deleteReply() {\n const [error] = await callDiscord(\n null,\n 'delete',\n '/webhooks/{webhook_id}/{webhook_token}/messages/@original',\n { path },\n );\n\n return error;\n },\n };\n}\n","export function splitId(id: string): { name: string; value: string | undefined } {\n const colon = id.indexOf(':');\n if (colon === -1) return { name: id, value: undefined };\n\n return { name: id.slice(0, colon), value: id.slice(colon + 1) };\n}\n","import type { APIMessageComponentButtonInteraction } from 'discord-api-types/v10';\n\nimport { deferUpdate } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ButtonContext, ErrorHandler } from '#types';\nimport { splitId } from '#utils/id';\nimport { modalResponse, replyResponse, updateResponse } from '#utils/response';\n\nexport function answerButton(\n interaction: APIMessageComponentButtonInteraction,\n buttons: Record<string, (c: ButtonContext) => Response | Promise<Response>> | undefined,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { name, value } = splitId(interaction.data.custom_id);\n const clicked = buttons?.[name];\n if (!clicked) return new Response(null, { status: 404 });\n\n return clicked({\n interaction,\n value,\n reply: replyResponse,\n update: updateResponse,\n modal: modalResponse,\n ...followUps(interaction, ctx, `button ${name}`, onError),\n deferUpdate: deferUpdate(interaction, ctx, `button ${name}`, onError),\n });\n}\n","import type { APIChatInputApplicationCommandInteraction } from 'discord-api-types/v10';\n\nimport { deferAnswer } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler } from '#types';\nimport { type Command, findCommand } from '#utils/command';\nimport { optionValues } from '#utils/option';\nimport { modalResponse, replyResponse } from '#utils/response';\n\nexport function answerCommand(\n interaction: APIChatInputApplicationCommandInteraction,\n commands: Map<string, Command>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { found, given, path } = findCommand(commands, interaction.data);\n if (!found || !('run' in found)) return new Response(null, { status: 404 });\n\n return found.run({\n interaction,\n options: optionValues(given, interaction.data.resolved),\n reply: replyResponse,\n defer: deferAnswer(interaction, ctx, path, onError),\n modal: modalResponse,\n ...followUps(interaction, ctx, path, onError),\n });\n}\n","import type {\n APIContextMenuInteraction,\n APIMessageApplicationCommandInteraction,\n APIUserApplicationCommandInteraction,\n ApplicationCommandType,\n} from 'discord-api-types/v10';\n\nimport { deferAnswer } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler } from '#types';\nimport type { ContextMenu, MenuType } from '#utils/command';\nimport { modalResponse, replyResponse } from '#utils/response';\n\nconst USER: ApplicationCommandType.User = 2;\n\nexport function answerMenu(\n interaction: APIContextMenuInteraction,\n menus: Map<string, ContextMenu>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { data } = interaction;\n const menu: MenuType = data.type === USER ? 'user' : 'message';\n const found = menus.get(`${menu}:${data.name}`);\n if (!found) return new Response(null, { status: 404 });\n\n const target =\n data.type === USER\n ? (interaction as APIUserApplicationCommandInteraction).data.resolved.users[data.target_id]\n : (interaction as APIMessageApplicationCommandInteraction).data.resolved.messages[\n data.target_id\n ];\n\n return found.run({\n interaction,\n target: target as never,\n reply: replyResponse,\n defer: deferAnswer(interaction, ctx, `${data.name} (${menu} menu)`, onError),\n modal: modalResponse,\n ...followUps(interaction, ctx, `${data.name} (${menu} menu)`, onError),\n });\n}\n","import type { APIModalSubmitInteraction } from 'discord-api-types/v10';\n\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler } from '#types';\nimport { splitId } from '#utils/id';\nimport { fieldValues, type Modal } from '#utils/modal';\nimport { replyResponse } from '#utils/response';\n\nexport function answerModal(\n interaction: APIModalSubmitInteraction,\n modals: Map<string, Modal>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const { name, value } = splitId(interaction.data.custom_id);\n const submitted = modals.get(name);\n if (!submitted) return new Response(null, { status: 404 });\n\n return submitted.run({\n interaction,\n value,\n fields: fieldValues(interaction.data.components),\n reply: replyResponse,\n ...followUps(interaction, ctx, `modal ${name}`, onError),\n });\n}\n","import type { APIMessageComponentSelectMenuInteraction } from 'discord-api-types/v10';\n\nimport { deferUpdate } from '#answers/defer';\nimport { type AnswerContext, followUps } from '#answers/follow';\nimport type { ErrorHandler, Select } from '#types';\nimport { modalResponse, replyResponse, updateResponse } from '#utils/response';\nimport { type SelectType, selectValues } from '#utils/select';\n\nexport function answerSelect(\n interaction: APIMessageComponentSelectMenuInteraction,\n selects: Map<string, Select<SelectType, unknown>>,\n ctx: AnswerContext,\n onError?: ErrorHandler,\n): Response | Promise<Response> {\n const picked = selects.get(interaction.data.custom_id);\n if (!picked) return new Response(null, { status: 404 });\n\n return picked.run({\n interaction,\n values: selectValues(interaction),\n reply: replyResponse,\n update: updateResponse,\n modal: modalResponse,\n ...followUps(interaction, ctx, `select ${interaction.data.custom_id}`, onError),\n deferUpdate: deferUpdate(interaction, ctx, `select ${interaction.data.custom_id}`, onError),\n });\n}\n","const encoder = new TextEncoder();\n\nfunction bytes(hex: string): Uint8Array<ArrayBuffer> | null {\n if (!/^(?:[\\da-f]{2})+$/i.test(hex)) return null;\n return Uint8Array.from(hex.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));\n}\n\nexport async function verify(\n publicKey: string,\n signature: string,\n timestamp: string,\n body: string,\n): Promise<boolean> {\n const key = bytes(publicKey);\n const sig = bytes(signature);\n\n if (key?.length !== 32 || sig?.length !== 64) return false;\n\n const imported = await crypto.subtle.importKey('raw', key, { name: 'Ed25519' }, false, [\n 'verify',\n ]);\n\n return crypto.subtle.verify({ name: 'Ed25519' }, imported, sig, encoder.encode(timestamp + body));\n}\n","import type {\n APIChatInputApplicationCommandInteraction,\n APIContextMenuInteraction,\n APIInteraction,\n APIMessageComponentButtonInteraction,\n APIMessageComponentSelectMenuInteraction,\n ApplicationCommandType,\n ComponentType,\n InteractionType,\n} from 'discord-api-types/v10';\n\nimport { answerAutocomplete } from '#answers/autocomplete';\nimport { answerButton } from '#answers/button';\nimport { answerCommand } from '#answers/command';\nimport type { AnswerContext } from '#answers/follow';\nimport { answerMenu } from '#answers/menu';\nimport { answerModal } from '#answers/modal';\nimport { answerSelect } from '#answers/select';\nimport { type Send, sendMessage } from '#lib/discord';\nimport type { ButtonContext, ErrorHandler, Select } from '#types';\nimport type { Command, ContextMenu } from '#utils/command';\nimport type { Modal } from '#utils/modal';\nimport { PONG } from '#utils/response';\nimport type { SelectType } from '#utils/select';\nimport { verify } from '#utils/verify';\n\nconst PING: InteractionType.Ping = 1;\nconst APPLICATION_COMMAND: InteractionType.ApplicationCommand = 2;\nconst MESSAGE_COMPONENT: InteractionType.MessageComponent = 3;\nconst AUTOCOMPLETE: InteractionType.ApplicationCommandAutocomplete = 4;\nconst MODAL_SUBMIT: InteractionType.ModalSubmit = 5;\nconst CHAT_INPUT: ApplicationCommandType.ChatInput = 1;\nconst USER_MENU: ApplicationCommandType.User = 2;\nconst MESSAGE_MENU: ApplicationCommandType.Message = 3;\nconst BUTTON: ComponentType.Button = 2;\nconst PUBLIC_KEY = /^[\\da-f]{64}$/i;\n\ntype CronContext = { controller: ScheduledController; send: Send };\n\ntype BotConfig = {\n /** The commands, made with {@link command} or `contextMenu()`. */\n commands: (Command | ContextMenu)[];\n /**\n * Answers a click, keyed by the button id given to `reply`. An id such as `page:2` runs the\n * `page` handler with `c.value` set to `'2'`. `c.update` edits the message that holds the button.\n * Pass `{ buttons: [] }` to remove them. `c.deferUpdate(work)` keeps the message as it is and\n * edits it with what `work` returns, for a click that takes longer than 3 seconds.\n */\n buttons?: Record<string, (c: ButtonContext) => Response | Promise<Response>>;\n /** The select menus the bot answers, made with `select()`. */\n selects?: Select<SelectType, unknown>[];\n /** The modals the bot answers, made with {@link modal}. */\n modals?: Modal[];\n /**\n * The name of the Durable Object binding that holds the class from `gateway()`. Open the Worker\n * URL once after the first deploy to start the connection. A GET request or any Cron Trigger\n * starts it again if it is down.\n */\n gateway?: string;\n /**\n * Gets errors from deferred work. If that work fails, the bot also edits the answer to\n * \"Something went wrong.\" so it does not stay on \"thinking…\".\n */\n onError?: ErrorHandler;\n /**\n * Runs on a Cron Trigger, keyed by the cron expression from your Wrangler config. `c.send` posts\n * a message to a channel with `DISCORD_TOKEN`, and returns an `Error` if Discord refuses.\n */\n cron?: Record<string, (c: CronContext) => void | Promise<void>>;\n};\n\n/** The Worker handler from {@link bot}. */\nexport type Bot = {\n commands: (Command | ContextMenu)[];\n fetch: (\n request: Request,\n env: { DISCORD_PUBLIC_KEY: string } & Record<string, unknown>,\n ctx: ExecutionContext,\n ) => Promise<Response>;\n scheduled: (\n controller: ScheduledController,\n env: { DISCORD_TOKEN: string } & Record<string, unknown>,\n ) => Promise<void>;\n answer: (interaction: APIInteraction, ctx: AnswerContext) => Promise<Response>;\n};\n\nasync function startGateway(env: Record<string, unknown>, name: string): Promise<void> {\n const namespace = env[name] as DurableObjectNamespace;\n const stub = namespace.getByName('main') as unknown as { ensure(): Promise<void> };\n await stub.ensure();\n}\n\n/**\n * Creates a Worker handler for the Interactions Endpoint URL of a Discord application.\n * It rejects requests without a valid Ed25519 signature, answers Discord's PING check\n * and runs the matching command. It reads `DISCORD_PUBLIC_KEY` from the Worker environment:\n * the hex public key of your application in the Discord Developer Portal.\n *\n * @param config What the bot answers.\n * @returns The Worker handler. Export it as the default export of your Worker. Pass it to\n * `gateway({ bot })` to answer interactions that arrive over the gateway too.\n * @example\n * import { bot, command } from 'botflare';\n *\n * export default bot({ commands: [command({ name: 'ping', description: 'Pong' }, (c) => c.reply('pong'))] });\n */\nexport function bot(config: BotConfig): Bot {\n const commands = new Map<string, Command>();\n const menus = new Map<string, ContextMenu>();\n for (const c of config.commands) {\n if ('menu' in c) menus.set(`${c.menu}:${c.name}`, c);\n else commands.set(c.name, c);\n }\n const modals = new Map((config.modals ?? []).map((m) => [m.id, m]));\n const selects = new Map((config.selects ?? []).map((s) => [s.id, s]));\n\n const answer = async (interaction: APIInteraction, ctx: AnswerContext): Promise<Response> => {\n if (interaction.type === MESSAGE_COMPONENT && interaction.data.component_type !== BUTTON) {\n return answerSelect(\n interaction as APIMessageComponentSelectMenuInteraction,\n selects,\n ctx,\n config.onError,\n );\n }\n if (interaction.type === MESSAGE_COMPONENT && interaction.data.component_type === BUTTON) {\n return answerButton(\n interaction as APIMessageComponentButtonInteraction,\n config.buttons,\n ctx,\n config.onError,\n );\n }\n if (interaction.type === MODAL_SUBMIT)\n return answerModal(interaction, modals, ctx, config.onError);\n if (interaction.type === AUTOCOMPLETE) {\n return answerAutocomplete(interaction, commands, config.onError);\n }\n if (\n interaction.type === APPLICATION_COMMAND &&\n (interaction.data.type === USER_MENU || interaction.data.type === MESSAGE_MENU)\n ) {\n return answerMenu(interaction as APIContextMenuInteraction, menus, ctx, config.onError);\n }\n if (interaction.type === APPLICATION_COMMAND && interaction.data.type === CHAT_INPUT) {\n return answerCommand(\n interaction as APIChatInputApplicationCommandInteraction,\n commands,\n ctx,\n config.onError,\n );\n }\n\n return new Response(null, { status: 400 });\n };\n\n return {\n commands: config.commands,\n answer,\n async scheduled(controller, env) {\n if (config.gateway) await startGateway(env, config.gateway);\n\n const job = config.cron?.[controller.cron];\n if (!job) return;\n\n await job({\n controller,\n send: (channelId, content, options) =>\n sendMessage(env.DISCORD_TOKEN, channelId, content, options),\n });\n },\n async fetch(request, env, ctx) {\n if (request.method === 'GET' && config.gateway) {\n await startGateway(env, config.gateway);\n return new Response('gateway started');\n }\n\n if (request.method !== 'POST') return new Response(null, { status: 405 });\n\n if (!PUBLIC_KEY.test(env.DISCORD_PUBLIC_KEY)) {\n return new Response(\n 'DISCORD_PUBLIC_KEY must be the 64 hex characters from the Developer Portal',\n { status: 500 },\n );\n }\n\n const signature = request.headers.get('X-Signature-Ed25519');\n const timestamp = request.headers.get('X-Signature-Timestamp');\n const body = await request.text();\n\n const valid =\n signature !== null &&\n timestamp !== null &&\n (await verify(env.DISCORD_PUBLIC_KEY, signature, timestamp, body));\n if (!valid) return new Response('invalid request signature', { status: 401 });\n\n const interaction = JSON.parse(body) as APIInteraction;\n if (interaction.type === PING) return Response.json(PONG);\n\n return answer(interaction, ctx);\n },\n };\n}\n","import type { Access, Command, Context } from '#utils/command';\nimport type { Options } from '#utils/option';\n\n/**\n * Declares a slash command and the handler that answers it. To split a command into subcommands,\n * such as `/config get` and `/config set`, pass `subcommands` instead of `options` and `run`: a list\n * of commands made with `command()`. A subcommand that has `subcommands` of its own becomes a group,\n * such as `/config role add`. Discord allows one group level only.\n *\n * @param config What Discord shows for the command.\n * @param config.name The name the user types after `/`.\n * @param config.description The text Discord shows under the name.\n * @param config.options The options, keyed by name, made with {@link option}.\n * @param run Answers the command. It gets the interaction, the typed option values, `reply`\n * for an answer within 3 seconds and `defer` for work that takes longer. Pass\n * `{ ephemeral: true }` to either so only the user who ran the command sees the answer, `buttons`\n * to add buttons and `embeds` to add embeds. Messages ping nobody, even for `@everyone` or `<@id>`\n * in the text. To ping, pass `allowedMentions`, such as `{ users: [id] }` or `{ parse: ['roles'] }`. The work of `defer` returns the text, or\n * `{ content, buttons, embeds }`.\n *\n * To send more after the first answer, call `c.after(work)` before you return it. `work` runs once\n * Discord has the answer, and can call `c.followUp(text, options)` for a new message,\n * `c.editReply(text, options)` and `c.deleteReply()`. Each returns an `Error` if Discord refuses.\n * The work of `defer` can call them too. Buttons, select menus, modals and context menus have them.\n * @returns The command, for the `commands` of {@link bot}.\n * @example\n * const greet = command(\n * { name: 'greet', description: 'Say hi', options: { name: option('string', { description: 'Who', required: true }) } },\n * (c) => c.reply(`hi ${c.options.name}`),\n * );\n */\nexport function command<const O extends Options = never>(\n config: { name: string; description: string; options?: O } & Access,\n run: (c: Context<O>) => Response | Promise<Response>,\n): Command<O>;\nexport function command(\n config: { name: string; description: string; subcommands: Command[] } & Access,\n): Command;\nexport function command(\n config: {\n name: string;\n description: string;\n options?: Options;\n subcommands?: Command[];\n } & Access,\n run?: (c: Context<Options>) => Response | Promise<Response>,\n): Command {\n const { options, subcommands, ...rest } = config;\n if (subcommands) return { ...rest, subcommands };\n\n return { ...rest, options: options ?? {}, run: run as NonNullable<typeof run> };\n}\n","import type { Locale } from 'discord-api-types/v10';\n\nimport type { Access, ContextMenu, MenuContext, MenuType } from '#utils/command';\n\ntype MenuConfig = {\n /** The name of the item, such as `Report message`. */\n name: string;\n /** The name in other languages, keyed by Discord locale. */\n locales?: Partial<Record<`${Locale}`, { name: string }>>;\n} & Omit<Access, 'locales'>;\n\n/**\n * Declares a context menu command: an item in the Apps menu when the user right-clicks a user or a\n * message. Add it to the `commands` of {@link bot}, next to the slash commands. It has no options\n * and no description. The handler gets the user or the message as `c.target`.\n *\n * @param menu Whether the item shows on a `user` or on a `message`.\n * @param config What Discord shows.\n * @param run Answers the click, with `reply`, `defer` and `modal` as in a slash command.\n * @returns The context menu command, for the `commands` of {@link bot}.\n * @example\n * const avatar = contextMenu('user', { name: 'Show avatar' }, (c) => c.reply(c.target.username));\n */\nexport function contextMenu<const T extends MenuType>(\n menu: T,\n config: MenuConfig,\n run: (c: MenuContext<T>) => Response | Promise<Response>,\n): ContextMenu<T> {\n return { ...config, menu, run };\n}\n","type Id = string | { id: string };\n\nconst idOf = (target: Id): string => (typeof target === 'string' ? target : target.id);\n\n/**\n * Writes a mention for message text. A mention shows as a link to the user, role, channel or\n * command. It pings nobody unless the message allows it with `allowedMentions`.\n *\n * @example\n * c.reply(`${mention.user(c.interaction.member.user)} joined ${mention.channel('123')}`);\n */\nexport const mention: {\n user: (user: Id) => string;\n role: (role: Id) => string;\n channel: (channel: Id) => string;\n command: (name: string, id: string) => string;\n} = {\n user: (user) => `<@${idOf(user)}>`,\n role: (role) => `<@&${idOf(role)}>`,\n channel: (channel) => `<#${idOf(channel)}>`,\n command: (name, id) => `</${name}:${id}>`,\n};\n\nconst STYLE = {\n 'short-time': 't',\n 'long-time': 'T',\n 'short-date': 'd',\n 'long-date': 'D',\n short: 'f',\n long: 'F',\n relative: 'R',\n} as const;\n\n/**\n * Writes a timestamp for message text. Discord shows it in the time zone and language of each\n * reader.\n *\n * @param date The moment, as a `Date` or as milliseconds since 1970, such as `Date.now()`.\n * @param style How Discord shows it: `short` (default, such as \"5 November 2026 12:30\"), `long`,\n * `short-date`, `long-date`, `short-time`, `long-time` or `relative` (such as \"in 2 hours\").\n * @returns The timestamp markup, such as `<t:1762338600:R>`.\n * @example\n * c.reply(`The event starts ${time(start, 'relative')}`);\n */\nexport function time(date: Date | number, style: keyof typeof STYLE = 'short'): string {\n const seconds = Math.floor(Number(date) / 1000);\n\n return `<t:${seconds}:${STYLE[style]}>`;\n}\n","import type { Fields, Modal } from '#utils/modal';\n\n/**\n * Declares a modal: a form Discord shows in a popup. Open it with `c.modal()` from a command or a\n * button, and add it to the `modals` of {@link bot} so the bot gets what the user typed.\n *\n * @param config What Discord shows in the popup.\n * @param config.id The id Discord sends back when the user submits the form.\n * @param config.title The title of the popup.\n * @param config.fields The text fields, keyed by name. A field is required unless `required` is `false`.\n * @param run Answers the submitted form. `c.fields` holds the text of each field, keyed by name.\n * A field that is not required and left empty is `undefined`.\n * To carry a value through the form, open it with `c.modal(form, value)`. The handler gets that\n * value as `c.value`, such as the id of the item the form edits.\n * @returns The modal, for `c.modal()` and the `modals` of {@link bot}.\n * @example\n * const feedback = modal(\n * { id: 'feedback', title: 'Feedback', fields: { text: { label: 'Your idea', style: 'paragraph' } } },\n * (c) => c.reply(`Thanks: ${c.fields.text}`),\n * );\n */\nexport function modal<const F extends Fields>(\n config: { id: string; title: string; fields: F },\n run: Modal<F>['run'],\n): Modal<F> {\n return { id: config.id, title: config.title, fields: config.fields, run };\n}\n","import type { Locales } from '#utils/locale';\nimport type {\n AutocompleteContext,\n ChannelType,\n Option,\n OptionValue,\n Suggestion,\n} from '#utils/option';\n\ntype OptionLimits<T extends keyof OptionValue> = T extends 'string'\n ? { minLength?: number; maxLength?: number }\n : T extends 'integer' | 'number'\n ? { min?: number; max?: number }\n : T extends 'channel'\n ? { channelTypes?: ChannelType[] }\n : unknown;\n\ntype Choice<T extends keyof OptionValue> = T extends 'string'\n ? string\n : T extends 'integer' | 'number'\n ? number\n : never;\n\ntype OptionConfig<T extends keyof OptionValue, R extends boolean, C> = {\n /** The text Discord shows under the option name. */\n description: string;\n /** Whether the user must fill it in. Defaults to `false`. */\n required?: R;\n /**\n * The only values the user can pick, keyed by the label Discord shows. For a `string`, `integer`\n * or `number` option.\n */\n choices?: C;\n /** The name and description in other languages, keyed by Discord locale. */\n locales?: Locales;\n} & OptionLimits<T> & {\n /**\n * Suggests values as the user types, instead of fixed `choices`. Gets what the user has typed\n * so far as `c.value` and returns up to 25 suggestions, as plain values or as `{ name, value }`.\n */\n autocomplete?: [Choice<T>] extends [never]\n ? never\n : [C] extends [never]\n ? (c: AutocompleteContext) => Suggestion<Choice<T>>[] | Promise<Suggestion<Choice<T>>[]>\n : never;\n };\n\n/**\n * Declares one option of a slash command. The handler reads its value from `c.options`,\n * typed from `type`, and `undefined` when the option is not `required`.\n *\n * A `string`, `integer` or `number` option also takes `choices`, keyed by the label Discord shows.\n * The user can pick only those, and the handler gets one of their values. A `string` option takes\n * `minLength` and `maxLength`, an `integer` or `number` option takes `min` and `max`, and a\n * `channel` option takes `channelTypes`, such as `['text', 'forum']`.\n *\n * Instead of fixed `choices`, a `string`, `integer` or `number` option can take `autocomplete`: a\n * function that gets what the user has typed so far as `c.value` and returns up to 25 suggestions,\n * as plain values or as `{ name, value }`. If it throws, `onError` gets the error and Discord shows\n * no suggestions.\n *\n * @param type The value type Discord asks the user for. `user`, `channel` and `role` give the full\n * object Discord sends with the command, not only its id.\n * @param config How Discord shows the option.\n * @returns The option, for the `options` of {@link command}.\n * @example\n * option('integer', { description: 'How many dice', required: true, min: 1, max: 6 });\n * option('string', { description: 'Color', choices: { Red: 'red', Blue: 'blue' } });\n */\nexport function option<\n const T extends keyof OptionValue,\n const R extends boolean = false,\n const C extends Record<string, Choice<T>> = never,\n>(\n type: T,\n config: OptionConfig<T, R, C>,\n): Option<T, R, [C] extends [never] ? OptionValue[T] : C[keyof C]> {\n const built: Option = { ...config, type, required: config.required ?? false };\n\n return built as Option<T, R, [C] extends [never] ? OptionValue[T] : C[keyof C]>;\n}\n","import { env as workerEnv } from 'cloudflare:workers';\n\nimport { createRest, type Rest } from '#lib/rest';\n\n/**\n * Calls the Discord REST API as the bot, with `DISCORD_TOKEN` from the Worker environment. Each\n * method takes a route from the Discord API spec, such as `'/guilds/{guild_id}/roles'`, and\n * types its `path`, `query`, `body` and result from that route. It returns `[error, data]` and\n * does not throw. When Discord answers 429, it waits for `retry_after` and tries again, up to\n * three tries in all.\n *\n * A request that is not a GET takes `reason`, the text that shows in the audit log of the server.\n *\n * @example\n * import { rest } from 'botflare';\n *\n * const [error, member] = await rest.get('/guilds/{guild_id}/members/{user_id}', {\n * path: { guild_id: guildId, user_id: userId },\n * });\n * if (!error) await rest.put('/guilds/{guild_id}/members/{user_id}/roles/{role_id}', {\n * path: { guild_id: guildId, user_id: member.user.id, role_id: roleId },\n * });\n */\nexport const rest: Rest = createRest(\n () => (workerEnv as { DISCORD_TOKEN?: unknown }).DISCORD_TOKEN,\n);\n","import type { Select, SelectContext, SelectValue } from '#types';\nimport type { ChannelType } from '#utils/option';\nimport type { SelectType } from '#utils/select';\n\ntype SelectConfig<T extends SelectType, O> = {\n /** The id Discord sends back with the pick. */\n id: string;\n /** The text the menu shows before a pick. */\n placeholder?: string;\n /** The fewest items the user must pick. Defaults to 1. */\n min?: number;\n /** The most items the user can pick. Defaults to 1. */\n max?: number;\n /** The items of a `string` menu, keyed by the label Discord shows. */\n options?: O;\n} & (T extends 'string' ? { options: unknown } : unknown) &\n (T extends 'channel'\n ? {\n /** The channel types the user can pick, such as `['text', 'forum']`. */\n channelTypes?: ChannelType[];\n }\n : unknown);\n\n/**\n * Declares a select menu and the handler that runs when the user picks from it. Put it in the\n * `selects` of a message, such as `c.reply('Pick one', { selects: [menu] })`, and in the `selects` of\n * {@link bot} so the bot gets the pick. Each menu takes a row of its own, above the buttons.\n *\n * A `string` menu lists `options`, keyed by the label Discord shows, and the handler gets the picked\n * values. A `user`, `role`, `channel` or `mentionable` menu lets the user pick from the server, and\n * the handler gets the full objects Discord resolved. A `channel` menu takes `channelTypes`.\n *\n * @param type What the user picks from.\n * @param config How Discord shows the menu.\n * @param run Answers the pick. `c.values` holds the picked items.\n * @returns The select menu.\n * @example\n * const fruit = select('string', { id: 'fruit', options: { Apple: 'apple', Mango: 'mango' } }, (c) =>\n * c.update(`picked ${c.values.join(', ')}`),\n * );\n */\nexport function select<\n const T extends SelectType,\n const O extends Record<string, T extends 'string' ? string : never> = never,\n>(\n type: T,\n config: SelectConfig<T, O>,\n run: (\n c: SelectContext<[O] extends [never] ? SelectValue[T] : O[keyof O]>,\n ) => Response | Promise<Response>,\n): Select<T, [O] extends [never] ? SelectValue[T] : O[keyof O]> {\n return { ...config, type, run };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botflare",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Discord bots on Cloudflare Workers, with gateway events, typed REST and Durable Objects",
5
5
  "keywords": [
6
6
  "cloudflare",