botflare 0.1.1 → 0.2.1

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.
package/dist/gateway.mjs CHANGED
@@ -1,5 +1,52 @@
1
1
  import { i as sendMessage, n as interactionCallback } from "./discord-DEZe1asd.mjs";
2
2
  import { DurableObject } from "cloudflare:workers";
3
+ // src/utils/inflate.ts
4
+ async function quietly(promise) {
5
+ try {
6
+ await promise;
7
+ } catch {}
8
+ }
9
+ function inflater(onPayload, onError) {
10
+ const stream = new DecompressionStream("deflate");
11
+ const writer = stream.writable.getWriter();
12
+ let open = true;
13
+ let buffer = "";
14
+ let depth = 0;
15
+ let inString = false;
16
+ let escaped = false;
17
+ const split = (text) => {
18
+ let from = 0;
19
+ for (let i = 0; i < text.length; i++) {
20
+ const char = text[i];
21
+ if (inString) {
22
+ if (escaped) escaped = false;
23
+ else if (char === "\\") escaped = true;
24
+ else if (char === "\"") inString = false;
25
+ } else if (char === "\"") inString = true;
26
+ else if (char === "{") depth++;
27
+ else if (char === "}" && --depth === 0) {
28
+ onPayload(buffer + text.slice(from, i + 1));
29
+ buffer = "";
30
+ from = i + 1;
31
+ }
32
+ }
33
+ buffer += text.slice(from);
34
+ };
35
+ (async () => {
36
+ try {
37
+ for await (const text of stream.readable.pipeThrough(new TextDecoderStream())) split(text);
38
+ } catch (error) {
39
+ if (open) onError(error);
40
+ }
41
+ })();
42
+ return {
43
+ push: (data) => void quietly(writer.write(new Uint8Array(data))),
44
+ close: () => {
45
+ open = false;
46
+ quietly(writer.abort());
47
+ }
48
+ };
49
+ }
3
50
  // src/utils/intents.ts
4
51
  const BITS = {
5
52
  Guilds: 1,
@@ -151,6 +198,7 @@ var Shard = class {
151
198
  }
152
199
  };
153
200
  // src/gateway.ts
201
+ /** @module botflare/gateway */
154
202
  const GATEWAY = "wss://gateway.discord.gg";
155
203
  const WATCH_MS = 6e4;
156
204
  const INTERACTION_CREATE = "INTERACTION_CREATE";
@@ -162,14 +210,6 @@ const INTERACTION_CREATE = "INTERACTION_CREATE";
162
210
  * It reads `DISCORD_TOKEN` from the Worker environment.
163
211
  *
164
212
  * @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
213
  * @returns The Durable Object class.
174
214
  * @example
175
215
  * import { bot } from 'botflare';
@@ -189,6 +229,7 @@ function gateway(config) {
189
229
  return class Gateway extends DurableObject {
190
230
  ws;
191
231
  shard;
232
+ inflate;
192
233
  stopped = false;
193
234
  async ensure() {
194
235
  if (!await this.ctx.storage.getAlarm()) await this.ctx.storage.setAlarm(Date.now() + WATCH_MS);
@@ -204,16 +245,17 @@ function gateway(config) {
204
245
  };
205
246
  const stored = this.ctx.storage.kv.get("session");
206
247
  const saved = stored?.intents === identity.intents ? stored : null;
207
- const ws = new WebSocket(`${saved?.url ?? GATEWAY}/?v=10&encoding=json`);
248
+ const ws = new WebSocket(`${saved?.url ?? GATEWAY}/?v=10&encoding=json&compress=zlib-stream`);
208
249
  const send = (channelId, content, options) => sendMessage(this.env.DISCORD_TOKEN, channelId, content, options);
250
+ const reconnect = (resume) => {
251
+ if (this.ws !== ws) return;
252
+ this.drop();
253
+ ws.close(4e3);
254
+ this.after(resume ? "resume" : "identify");
255
+ };
209
256
  const shard = new Shard(this.env.DISCORD_TOKEN, identity, saved, {
210
257
  send: (payload) => ws.send(JSON.stringify(payload)),
211
- reconnect: (resume) => {
212
- if (this.ws !== ws) return;
213
- this.drop();
214
- ws.close(4e3);
215
- this.after(resume ? "resume" : "identify");
216
- },
258
+ reconnect,
217
259
  save: (session) => session ? this.ctx.storage.kv.put("session", session) : this.ctx.storage.kv.delete("session"),
218
260
  dispatch: (event) => {
219
261
  if (event.t === INTERACTION_CREATE && config.bot) {
@@ -239,7 +281,10 @@ function gateway(config) {
239
281
  if (!handler) return;
240
282
  const run = async () => {
241
283
  try {
242
- await handler(event.d, { send });
284
+ await handler(event.d, {
285
+ send,
286
+ storage: this.ctx.storage
287
+ });
243
288
  } catch (error) {
244
289
  await config.onError?.(error, event.t);
245
290
  }
@@ -258,10 +303,15 @@ function gateway(config) {
258
303
  };
259
304
  }
260
305
  });
306
+ const inflate = inflater((json) => {
307
+ if (this.ws === ws) shard.handle(JSON.parse(json));
308
+ }, () => reconnect(true));
261
309
  this.ws = ws;
262
310
  this.shard = shard;
311
+ this.inflate = inflate;
312
+ ws.binaryType = "arraybuffer";
263
313
  ws.addEventListener("message", (event) => {
264
- if (this.ws === ws) shard.handle(JSON.parse(String(event.data)));
314
+ if (this.ws === ws) inflate.push(event.data);
265
315
  });
266
316
  ws.addEventListener("close", (event) => {
267
317
  if (this.ws !== ws) return;
@@ -272,6 +322,8 @@ function gateway(config) {
272
322
  drop() {
273
323
  this.shard?.stop();
274
324
  this.shard = void 0;
325
+ this.inflate?.close();
326
+ this.inflate = void 0;
275
327
  this.ws = void 0;
276
328
  }
277
329
  after(next) {
@@ -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/inflate.ts","../src/utils/intents.ts","../src/utils/shard.ts","../src/gateway.ts"],"sourcesContent":["// The reader in `inflater` already reports the error.\nasync function quietly(promise: Promise<unknown>): Promise<void> {\n try {\n await promise;\n } catch {}\n}\n\n// zlib-stream shares one context per connection, and decoded chunks do not line up with payloads.\nexport function inflater(\n onPayload: (json: string) => void,\n onError: (error: unknown) => void,\n): { push: (data: ArrayBuffer) => void; close: () => void } {\n const stream = new DecompressionStream('deflate');\n const writer = stream.writable.getWriter();\n let open = true;\n let buffer = '';\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n const split = (text: string): void => {\n let from = 0;\n for (let i = 0; i < text.length; i++) {\n const char = text[i];\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n } else if (char === '\"') inString = true;\n else if (char === '{') depth++;\n else if (char === '}' && --depth === 0) {\n onPayload(buffer + text.slice(from, i + 1));\n buffer = '';\n from = i + 1;\n }\n }\n buffer += text.slice(from);\n };\n\n void (async () => {\n try {\n for await (const text of stream.readable.pipeThrough(new TextDecoderStream())) split(text);\n } catch (error) {\n if (open) onError(error);\n }\n })();\n\n return {\n push: (data) => void quietly(writer.write(new Uint8Array(data))),\n close: () => {\n open = false;\n void quietly(writer.abort());\n },\n };\n}\n","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 { inflater } from '#utils/inflate';\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 inflate: ReturnType<typeof inflater> | 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&compress=zlib-stream`);\n const send: Send = (channelId, content, options) =>\n sendMessage(this.env.DISCORD_TOKEN, channelId, content, options);\n\n const reconnect = (resume: boolean): void => {\n if (this.ws !== ws) return;\n\n this.drop();\n ws.close(4000);\n this.after(resume ? 'resume' : 'identify');\n };\n\n const shard = new Shard(this.env.DISCORD_TOKEN, identity, saved, {\n send: (payload) => ws.send(JSON.stringify(payload)),\n reconnect,\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 const inflate = inflater(\n (json) => {\n if (this.ws === ws) shard.handle(JSON.parse(json) as GatewayReceivePayload);\n },\n () => reconnect(true),\n );\n\n this.ws = ws;\n this.shard = shard;\n this.inflate = inflate;\n\n ws.binaryType = 'arraybuffer';\n ws.addEventListener('message', (event) => {\n if (this.ws === ws) inflate.push(event.data as ArrayBuffer);\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.inflate?.close();\n this.inflate = 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