botflare 0.1.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.
@@ -0,0 +1,289 @@
1
+ import { i as sendMessage, n as interactionCallback } from "./discord-DEZe1asd.mjs";
2
+ import { DurableObject } from "cloudflare:workers";
3
+ // src/utils/intents.ts
4
+ const BITS = {
5
+ Guilds: 1,
6
+ GuildMembers: 2,
7
+ GuildModeration: 4,
8
+ GuildExpressions: 8,
9
+ GuildIntegrations: 16,
10
+ GuildWebhooks: 32,
11
+ GuildInvites: 64,
12
+ GuildVoiceStates: 128,
13
+ GuildPresences: 256,
14
+ GuildMessages: 512,
15
+ GuildMessageReactions: 1024,
16
+ GuildMessageTyping: 2048,
17
+ DirectMessages: 4096,
18
+ DirectMessageReactions: 8192,
19
+ DirectMessageTyping: 16384,
20
+ MessageContent: 32768,
21
+ GuildScheduledEvents: 65536,
22
+ AutoModerationConfiguration: 1048576,
23
+ AutoModerationExecution: 2097152,
24
+ GuildMessagePolls: 16777216,
25
+ DirectMessagePolls: 33554432
26
+ };
27
+ function intentBits(names) {
28
+ return names.reduce((bits, name) => bits | BITS[name], 0);
29
+ }
30
+ // src/utils/shard.ts
31
+ const DISPATCH = 0;
32
+ const HEARTBEAT = 1;
33
+ const IDENTIFY = 2;
34
+ const PRESENCE_UPDATE = 3;
35
+ const RESUME = 6;
36
+ const RECONNECT = 7;
37
+ const INVALID_SESSION = 9;
38
+ const HELLO = 10;
39
+ const HEARTBEAT_ACK = 11;
40
+ const READY = "READY";
41
+ const RESUMED = "RESUMED";
42
+ const FATAL = /* @__PURE__ */ new Set([
43
+ 4004,
44
+ 4010,
45
+ 4011,
46
+ 4012,
47
+ 4013,
48
+ 4014
49
+ ]);
50
+ const FRESH = /* @__PURE__ */ new Set([4007, 4009]);
51
+ function afterClose(code) {
52
+ if (FATAL.has(code)) return "stop";
53
+ return FRESH.has(code) ? "identify" : "resume";
54
+ }
55
+ var Shard = class {
56
+ token;
57
+ identity;
58
+ hooks;
59
+ session;
60
+ acked = true;
61
+ stopBeat;
62
+ constructor(token, identity, session, hooks) {
63
+ this.token = token;
64
+ this.identity = identity;
65
+ this.session = session;
66
+ this.hooks = hooks;
67
+ }
68
+ handle(payload) {
69
+ switch (payload.op) {
70
+ case HELLO:
71
+ this.acked = true;
72
+ this.stopBeat?.();
73
+ this.stopBeat = this.hooks.every(payload.d.heartbeat_interval, () => this.beat());
74
+ if (this.session) this.hooks.send({
75
+ op: RESUME,
76
+ d: {
77
+ token: this.token,
78
+ session_id: this.session.id,
79
+ seq: this.session.seq ?? 0
80
+ }
81
+ });
82
+ else this.identify();
83
+ return;
84
+ case HEARTBEAT_ACK:
85
+ this.acked = true;
86
+ return;
87
+ case HEARTBEAT:
88
+ this.hooks.send({
89
+ op: HEARTBEAT,
90
+ d: this.session?.seq ?? null
91
+ });
92
+ return;
93
+ case RECONNECT:
94
+ this.hooks.reconnect(true);
95
+ return;
96
+ case INVALID_SESSION:
97
+ if (!payload.d) this.forget();
98
+ this.hooks.reconnect(payload.d);
99
+ return;
100
+ case DISPATCH: this.onDispatch(payload);
101
+ }
102
+ }
103
+ stop() {
104
+ this.stopBeat?.();
105
+ this.stopBeat = void 0;
106
+ }
107
+ forget() {
108
+ this.session = null;
109
+ this.hooks.save(null);
110
+ }
111
+ onDispatch(payload) {
112
+ if (payload.t === READY) this.session = {
113
+ id: payload.d.session_id,
114
+ url: payload.d.resume_gateway_url,
115
+ seq: payload.s,
116
+ intents: this.identity.intents
117
+ };
118
+ else if (this.session) this.session.seq = payload.s;
119
+ if (this.session) this.hooks.save(this.session);
120
+ if (payload.t === RESUMED && this.identity.presence) this.hooks.send({
121
+ op: PRESENCE_UPDATE,
122
+ d: this.identity.presence
123
+ });
124
+ this.hooks.dispatch(payload);
125
+ }
126
+ beat() {
127
+ if (!this.acked) {
128
+ this.hooks.reconnect(true);
129
+ return;
130
+ }
131
+ this.acked = false;
132
+ this.hooks.send({
133
+ op: HEARTBEAT,
134
+ d: this.session?.seq ?? null
135
+ });
136
+ }
137
+ identify() {
138
+ this.hooks.send({
139
+ op: IDENTIFY,
140
+ d: {
141
+ token: this.token,
142
+ intents: this.identity.intents,
143
+ properties: {
144
+ os: "cloudflare-workers",
145
+ browser: "botflare",
146
+ device: "botflare"
147
+ },
148
+ ...this.identity.presence && { presence: this.identity.presence }
149
+ }
150
+ });
151
+ }
152
+ };
153
+ // src/gateway.ts
154
+ const GATEWAY = "wss://gateway.discord.gg";
155
+ const WATCH_MS = 6e4;
156
+ const INTERACTION_CREATE = "INTERACTION_CREATE";
157
+ /**
158
+ * Creates a Durable Object class that keeps one connection to the Discord Gateway open and runs your
159
+ * handler for each event. Export it under the `class_name` of a SQLite Durable Object binding in your
160
+ * Wrangler config, and give that binding name to `bot({ gateway })` so a Cron Trigger starts and
161
+ * reconnects it.
162
+ * It reads `DISCORD_TOKEN` from the Worker environment.
163
+ *
164
+ * @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
+ * @returns The Durable Object class.
174
+ * @example
175
+ * import { bot } from 'botflare';
176
+ * import { gateway } from 'botflare/gateway';
177
+ *
178
+ * const app = bot({ commands: [], gateway: 'GATEWAY' });
179
+ *
180
+ * export const Gateway = gateway({
181
+ * intents: ['Guilds', 'GuildMessages'],
182
+ * bot: app,
183
+ * events: { MESSAGE_CREATE: (message, c) => c.send(message.channel_id, 'heard you') },
184
+ * });
185
+ *
186
+ * export default app;
187
+ */
188
+ function gateway(config) {
189
+ return class Gateway extends DurableObject {
190
+ ws;
191
+ shard;
192
+ stopped = false;
193
+ async ensure() {
194
+ if (!await this.ctx.storage.getAlarm()) await this.ctx.storage.setAlarm(Date.now() + WATCH_MS);
195
+ if (!this.ws && !this.stopped) this.dial();
196
+ }
197
+ async alarm() {
198
+ await this.ensure();
199
+ }
200
+ dial() {
201
+ const identity = {
202
+ intents: intentBits(config.intents),
203
+ presence: config.presence
204
+ };
205
+ const stored = this.ctx.storage.kv.get("session");
206
+ const saved = stored?.intents === identity.intents ? stored : null;
207
+ const ws = new WebSocket(`${saved?.url ?? GATEWAY}/?v=10&encoding=json`);
208
+ const send = (channelId, content, options) => sendMessage(this.env.DISCORD_TOKEN, channelId, content, options);
209
+ const shard = new Shard(this.env.DISCORD_TOKEN, identity, saved, {
210
+ 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
+ },
217
+ save: (session) => session ? this.ctx.storage.kv.put("session", session) : this.ctx.storage.kv.delete("session"),
218
+ dispatch: (event) => {
219
+ if (event.t === INTERACTION_CREATE && config.bot) {
220
+ const { answer } = config.bot;
221
+ const respond = async () => {
222
+ try {
223
+ const after = [];
224
+ const response = await answer(event.d, {
225
+ waitUntil: (promise) => this.ctx.waitUntil(promise),
226
+ afterAnswer: (task) => after.push(task)
227
+ });
228
+ if (!response.ok) return;
229
+ const failed = await interactionCallback(event.d.id, event.d.token, response);
230
+ if (failed) await config.onError?.(failed, event.t);
231
+ else for (const task of after) this.ctx.waitUntil(task());
232
+ } catch (error) {
233
+ await config.onError?.(error, event.t);
234
+ }
235
+ };
236
+ this.ctx.waitUntil(respond());
237
+ }
238
+ const handler = config.events[event.t];
239
+ if (!handler) return;
240
+ const run = async () => {
241
+ try {
242
+ await handler(event.d, { send });
243
+ } catch (error) {
244
+ await config.onError?.(error, event.t);
245
+ }
246
+ };
247
+ this.ctx.waitUntil(run());
248
+ },
249
+ every: (ms, fn) => {
250
+ let interval = null;
251
+ const first = setTimeout(() => {
252
+ fn();
253
+ interval = setInterval(fn, ms);
254
+ }, ms * Math.random());
255
+ return () => {
256
+ clearTimeout(first);
257
+ clearInterval(interval);
258
+ };
259
+ }
260
+ });
261
+ this.ws = ws;
262
+ this.shard = shard;
263
+ ws.addEventListener("message", (event) => {
264
+ if (this.ws === ws) shard.handle(JSON.parse(String(event.data)));
265
+ });
266
+ ws.addEventListener("close", (event) => {
267
+ if (this.ws !== ws) return;
268
+ this.drop();
269
+ this.after(afterClose(event.code));
270
+ });
271
+ }
272
+ drop() {
273
+ this.shard?.stop();
274
+ this.shard = void 0;
275
+ this.ws = void 0;
276
+ }
277
+ after(next) {
278
+ if (next === "stop") {
279
+ this.stopped = true;
280
+ return;
281
+ }
282
+ if (next === "identify") this.ctx.storage.kv.delete("session");
283
+ setTimeout(() => this.dial(), 1e3 + Math.random() * 4e3);
284
+ }
285
+ };
286
+ }
287
+ export { gateway };
288
+
289
+ //# sourceMappingURL=gateway.mjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,239 @@
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";
2
+ import { Locale } from "discord-api-types/v10";
3
+ /**
4
+ * Declares a slash command and the handler that answers it. To split a command into subcommands,
5
+ * such as `/config get` and `/config set`, pass `subcommands` instead of `options` and `run`: a list
6
+ * of commands made with `command()`. A subcommand that has `subcommands` of its own becomes a group,
7
+ * such as `/config role add`. Discord allows one group level only.
8
+ *
9
+ * @param config What Discord shows for the command.
10
+ * @param config.name The name the user types after `/`.
11
+ * @param config.description The text Discord shows under the name.
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
+ * @param run Answers the command. It gets the interaction, the typed option values, `reply`
22
+ * for an answer within 3 seconds and `defer` for work that takes longer. Pass
23
+ * `{ ephemeral: true }` to either so only the user who ran the command sees the answer, `buttons`
24
+ * to add buttons and `embeds` to add embeds. Messages ping nobody, even for `@everyone` or `<@id>`
25
+ * in the text. To ping, pass `allowedMentions`, such as `{ users: [id] }` or `{ parse: ['roles'] }`. The work of `defer` returns the text, or
26
+ * `{ content, buttons, embeds }`.
27
+ *
28
+ * To send more after the first answer, call `c.after(work)` before you return it. `work` runs once
29
+ * Discord has the answer, and can call `c.followUp(text, options)` for a new message,
30
+ * `c.editReply(text, options)` and `c.deleteReply()`. Each returns an `Error` if Discord refuses.
31
+ * The work of `defer` can call them too. Buttons, select menus, modals and context menus have them.
32
+ * @returns The command, for the `commands` of {@link bot}.
33
+ * @example
34
+ * const greet = command(
35
+ * { name: 'greet', description: 'Say hi', options: { name: option('string', { description: 'Who', required: true }) } },
36
+ * (c) => c.reply(`hi ${c.options.name}`),
37
+ * );
38
+ */
39
+ export declare function command<const O extends Options = never>(config: {
40
+ name: string;
41
+ description: string;
42
+ options?: O;
43
+ } & Access, run: (c: Context<O>) => Response | Promise<Response>): Command<O>;
44
+ export declare function command(config: {
45
+ name: string;
46
+ description: string;
47
+ subcommands: Command[];
48
+ } & Access): Command;
49
+ /**
50
+ * Declares a context menu command: an item in the Apps menu when the user right-clicks a user or a
51
+ * message. Add it to the `commands` of {@link bot}, next to the slash commands. It has no options
52
+ * and no description. The handler gets the user or the message as `c.target`.
53
+ *
54
+ * @param menu Whether the item shows on a `user` or on a `message`.
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
+ * @param run Answers the click, with `reply`, `defer` and `modal` as in a slash command.
62
+ * @returns The context menu command, for the `commands` of {@link bot}.
63
+ * @example
64
+ * const avatar = contextMenu('user', { name: 'Show avatar' }, (c) => c.reply(c.target.username));
65
+ */
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>;
72
+ type Id = string | {
73
+ id: string;
74
+ };
75
+ /**
76
+ * Writes a mention for message text. A mention shows as a link to the user, role, channel or
77
+ * command. It pings nobody unless the message allows it with `allowedMentions`.
78
+ *
79
+ * @example
80
+ * c.reply(`${mention.user(c.interaction.member.user)} joined ${mention.channel('123')}`);
81
+ */
82
+ export declare const mention: {
83
+ user: (user: Id) => string;
84
+ role: (role: Id) => string;
85
+ channel: (channel: Id) => string;
86
+ command: (name: string, id: string) => string;
87
+ };
88
+ declare const STYLE: {
89
+ readonly "short-time": "t";
90
+ readonly "long-time": "T";
91
+ readonly "short-date": "d";
92
+ readonly "long-date": "D";
93
+ readonly short: "f";
94
+ readonly long: "F";
95
+ readonly relative: "R";
96
+ };
97
+ /**
98
+ * Writes a timestamp for message text. Discord shows it in the time zone and language of each
99
+ * reader.
100
+ *
101
+ * @param date The moment, as a `Date` or as milliseconds since 1970, such as `Date.now()`.
102
+ * @param style How Discord shows it: `short` (default, such as "5 November 2026 12:30"), `long`,
103
+ * `short-date`, `long-date`, `short-time`, `long-time` or `relative` (such as "in 2 hours").
104
+ * @returns The timestamp markup, such as `<t:1762338600:R>`.
105
+ * @example
106
+ * c.reply(`The event starts ${time(start, 'relative')}`);
107
+ */
108
+ export declare function time(date: Date | number, style?: keyof typeof STYLE): string;
109
+ /**
110
+ * Declares a modal: a form Discord shows in a popup. Open it with `c.modal()` from a command or a
111
+ * button, and add it to the `modals` of {@link bot} so the bot gets what the user typed.
112
+ *
113
+ * @param config What Discord shows in the popup.
114
+ * @param config.id The id Discord sends back when the user submits the form.
115
+ * @param config.title The title of the popup.
116
+ * @param config.fields The text fields, keyed by name. A field is required unless `required` is `false`.
117
+ * @param run Answers the submitted form. `c.fields` holds the text of each field, keyed by name.
118
+ * A field that is not required and left empty is `undefined`.
119
+ * To carry a value through the form, open it with `c.modal(form, value)`. The handler gets that
120
+ * value as `c.value`, such as the id of the item the form edits.
121
+ * @returns The modal, for `c.modal()` and the `modals` of {@link bot}.
122
+ * @example
123
+ * const feedback = modal(
124
+ * { id: 'feedback', title: 'Feedback', fields: { text: { label: 'Your idea', style: 'paragraph' } } },
125
+ * (c) => c.reply(`Thanks: ${c.fields.text}`),
126
+ * );
127
+ */
128
+ export declare function modal<const F extends Fields>(config: {
129
+ id: string;
130
+ title: string;
131
+ fields: F;
132
+ }, run: Modal<F>["run"]): Modal<F>;
133
+ type OptionLimits<T extends keyof OptionValue> = T extends "string" ? {
134
+ minLength?: number;
135
+ maxLength?: number;
136
+ } : T extends "integer" | "number" ? {
137
+ min?: number;
138
+ max?: number;
139
+ } : T extends "channel" ? {
140
+ channelTypes?: ChannelType[];
141
+ } : unknown;
142
+ type Choice<T extends keyof OptionValue> = T extends "string" ? string : T extends "integer" | "number" ? number : never;
143
+ /**
144
+ * Declares one option of a slash command. The handler reads its value from `c.options`,
145
+ * typed from `type`, and `undefined` when the option is not `required`.
146
+ *
147
+ * A `string`, `integer` or `number` option also takes `choices`, keyed by the label Discord shows.
148
+ * The user can pick only those, and the handler gets one of their values. A `string` option takes
149
+ * `minLength` and `maxLength`, an `integer` or `number` option takes `min` and `max`, and a
150
+ * `channel` option takes `channelTypes`, such as `['text', 'forum']`.
151
+ *
152
+ * Instead of fixed `choices`, a `string`, `integer` or `number` option can take `autocomplete`: a
153
+ * function that gets what the user has typed so far as `c.value` and returns up to 25 suggestions,
154
+ * as plain values or as `{ name, value }`. If it throws, `onError` gets the error and Discord shows
155
+ * no suggestions.
156
+ *
157
+ * @param type The value type Discord asks the user for. `user`, `channel` and `role` give the full
158
+ * object Discord sends with the command, not only its id.
159
+ * @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
+ * @returns The option, for the `options` of {@link command}.
164
+ * @example
165
+ * option('integer', { description: 'How many dice', required: true, min: 1, max: 6 });
166
+ * option('string', { description: 'Color', choices: { Red: 'red', Blue: 'blue' } });
167
+ */
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]>;
176
+ type Method = "get" | "post" | "put" | "patch" | "delete";
177
+ type Route<M extends Method> = { [K in keyof Routes]: M extends keyof Routes[K] ? K : never; }[keyof Routes];
178
+ type Operation<P extends keyof Routes, M extends Method> = Routes[P] extends Record<M, infer O extends {
179
+ args: unknown[];
180
+ result: unknown;
181
+ }> ? O : never;
182
+ type Result<P extends keyof Routes, M extends Method> = Promise<[Error, null] | [null, Operation<P, M>["result"]]>;
183
+ /** Calls the Discord REST API. Each method takes a route from the Discord API spec. */
184
+ type Rest = { [M in Method]: <P extends Route<M>>(path: P, ...args: Operation<P, M>["args"]) => Result<P, M>; };
185
+ /**
186
+ * Calls the Discord REST API as the bot, with `DISCORD_TOKEN` from the Worker environment. Each
187
+ * method takes a route from the Discord API spec, such as `'/guilds/{guild_id}/roles'`, and
188
+ * types its `path`, `query`, `body` and result from that route. It returns `[error, data]` and
189
+ * does not throw. When Discord answers 429, it waits for `retry_after` and tries again, up to
190
+ * three tries in all.
191
+ *
192
+ * A request that is not a GET takes `reason`, the text that shows in the audit log of the server.
193
+ *
194
+ * @example
195
+ * import { rest } from 'botflare';
196
+ *
197
+ * const [error, member] = await rest.get('/guilds/{guild_id}/members/{user_id}', {
198
+ * path: { guild_id: guildId, user_id: userId },
199
+ * });
200
+ * if (!error) await rest.put('/guilds/{guild_id}/members/{user_id}/roles/{role_id}', {
201
+ * path: { guild_id: guildId, user_id: member.user.id, role_id: roleId },
202
+ * });
203
+ */
204
+ export declare const rest: Rest;
205
+ /**
206
+ * Declares a select menu and the handler that runs when the user picks from it. Put it in the
207
+ * `selects` of a message, such as `c.reply('Pick one', { selects: [menu] })`, and in the `selects` of
208
+ * {@link bot} so the bot gets the pick. Each menu takes a row of its own, above the buttons.
209
+ *
210
+ * A `string` menu lists `options`, keyed by the label Discord shows, and the handler gets the picked
211
+ * values. A `user`, `role`, `channel` or `mentionable` menu lets the user pick from the server, and
212
+ * the handler gets the full objects Discord resolved. A `channel` menu takes `channelTypes`.
213
+ *
214
+ * @param type What the user picks from.
215
+ * @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
+ * @param run Answers the pick. `c.values` holds the picked items.
221
+ * @returns The select menu.
222
+ * @example
223
+ * const fruit = select('string', { id: 'fruit', options: { Apple: 'apple', Mango: 'mango' } }, (c) =>
224
+ * c.update(`picked ${c.values.join(', ')}`),
225
+ * );
226
+ */
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]>;
238
+ export { type Bot, type ErrorHandler, type Select, type SelectContext, type SelectValue, bot };
239
+ //# sourceMappingURL=index.d.mts.map