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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/bot-icKel3r9.d.mts +7512 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +79 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/command-CBeslrzu.mjs +133 -0
- package/dist/command-CBeslrzu.mjs.map +1 -0
- package/dist/discord-DEZe1asd.mjs +312 -0
- package/dist/discord-DEZe1asd.mjs.map +1 -0
- package/dist/gateway.d.mts +53 -0
- package/dist/gateway.mjs +289 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/index.d.mts +239 -0
- package/dist/index.mjs +537 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { t as findCommand } from "./command-CBeslrzu.mjs";
|
|
2
|
+
import { a as message, c as createRest, i as sendMessage, l as formOf, o as selectValues, s as callDiscord, t as editOriginal, u as optionValues } from "./discord-DEZe1asd.mjs";
|
|
3
|
+
import { env } from "cloudflare:workers";
|
|
4
|
+
// src/answers/autocomplete.ts
|
|
5
|
+
const MAX_CHOICES = 25;
|
|
6
|
+
async function answerAutocomplete(interaction, commands, onError) {
|
|
7
|
+
const { found, given, path } = findCommand(commands, interaction.data);
|
|
8
|
+
const focused = given.find((o) => "focused" in o && o.focused);
|
|
9
|
+
const suggest = found && "options" in found && focused ? found.options[focused.name]?.autocomplete : void 0;
|
|
10
|
+
if (!suggest || !focused || !("value" in focused)) return new Response(null, { status: 404 });
|
|
11
|
+
let suggestions = [];
|
|
12
|
+
try {
|
|
13
|
+
suggestions = await suggest({
|
|
14
|
+
interaction,
|
|
15
|
+
value: String(focused.value)
|
|
16
|
+
});
|
|
17
|
+
} catch (error) {
|
|
18
|
+
await onError?.(error, `${path} autocomplete`);
|
|
19
|
+
}
|
|
20
|
+
const response = {
|
|
21
|
+
type: 8,
|
|
22
|
+
data: { choices: suggestions.slice(0, MAX_CHOICES).map((s) => typeof s === "object" && s !== null ? s : {
|
|
23
|
+
name: String(s),
|
|
24
|
+
value: s
|
|
25
|
+
}) }
|
|
26
|
+
};
|
|
27
|
+
return Response.json(response);
|
|
28
|
+
}
|
|
29
|
+
// src/utils/modal.ts
|
|
30
|
+
function modalData(modal, value) {
|
|
31
|
+
return {
|
|
32
|
+
custom_id: value === void 0 ? modal.id : `${modal.id}:${value}`,
|
|
33
|
+
title: modal.title,
|
|
34
|
+
components: Object.entries(modal.fields).map(([name, field]) => ({
|
|
35
|
+
type: 18,
|
|
36
|
+
label: field.label,
|
|
37
|
+
component: {
|
|
38
|
+
type: 4,
|
|
39
|
+
custom_id: name,
|
|
40
|
+
style: field.style === "paragraph" ? 2 : 1,
|
|
41
|
+
required: field.required ?? true
|
|
42
|
+
}
|
|
43
|
+
}))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function fieldValues(components) {
|
|
47
|
+
const inputs = components.flatMap((row) => {
|
|
48
|
+
if ("component" in row) return [row.component];
|
|
49
|
+
if ("components" in row) return row.components;
|
|
50
|
+
return [];
|
|
51
|
+
});
|
|
52
|
+
const values = {};
|
|
53
|
+
for (const input of inputs) if ("value" in input && typeof input.value === "string") values[input.custom_id] = input.value || void 0;
|
|
54
|
+
return values;
|
|
55
|
+
}
|
|
56
|
+
// src/utils/response.ts
|
|
57
|
+
const PONG = { type: 1 };
|
|
58
|
+
const DEFERRED = { type: 5 };
|
|
59
|
+
const DEFERRED_EPHEMERAL = {
|
|
60
|
+
type: 5,
|
|
61
|
+
data: { flags: 64 }
|
|
62
|
+
};
|
|
63
|
+
const DEFERRED_UPDATE = { type: 6 };
|
|
64
|
+
function answer(response, options) {
|
|
65
|
+
if (!options?.files?.length) return Response.json(response);
|
|
66
|
+
return new Response(formOf(response, options.files));
|
|
67
|
+
}
|
|
68
|
+
function replyResponse(content, options) {
|
|
69
|
+
return answer({
|
|
70
|
+
type: 4,
|
|
71
|
+
data: message(content, options)
|
|
72
|
+
}, options);
|
|
73
|
+
}
|
|
74
|
+
function updateResponse(content, options) {
|
|
75
|
+
return answer({
|
|
76
|
+
type: 7,
|
|
77
|
+
data: message(content, options)
|
|
78
|
+
}, options);
|
|
79
|
+
}
|
|
80
|
+
function modalResponse(form, value) {
|
|
81
|
+
const response = {
|
|
82
|
+
type: 9,
|
|
83
|
+
data: modalData(form, value)
|
|
84
|
+
};
|
|
85
|
+
return Response.json(response);
|
|
86
|
+
}
|
|
87
|
+
// src/answers/defer.ts
|
|
88
|
+
function later(interaction, ctx, where, work, onError) {
|
|
89
|
+
const finish = async () => {
|
|
90
|
+
let edit = message("Something went wrong.");
|
|
91
|
+
let files;
|
|
92
|
+
try {
|
|
93
|
+
const done = await work();
|
|
94
|
+
edit = typeof done === "string" ? message(done) : message(done.content, {
|
|
95
|
+
buttons: done.buttons,
|
|
96
|
+
selects: done.selects,
|
|
97
|
+
embeds: done.embeds,
|
|
98
|
+
allowedMentions: done.allowedMentions,
|
|
99
|
+
files: done.files
|
|
100
|
+
});
|
|
101
|
+
if (typeof done !== "string") files = done.files;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await onError?.(error, where);
|
|
104
|
+
}
|
|
105
|
+
const failed = await editOriginal(interaction.application_id, interaction.token, edit, files);
|
|
106
|
+
if (failed) await onError?.(failed, where);
|
|
107
|
+
};
|
|
108
|
+
ctx.waitUntil(finish());
|
|
109
|
+
}
|
|
110
|
+
function deferAnswer(interaction, ctx, where, onError) {
|
|
111
|
+
return (work, options) => {
|
|
112
|
+
later(interaction, ctx, where, work, onError);
|
|
113
|
+
return Response.json(options?.ephemeral ? DEFERRED_EPHEMERAL : DEFERRED);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function deferUpdate(interaction, ctx, where, onError) {
|
|
117
|
+
return (work) => {
|
|
118
|
+
later(interaction, ctx, where, work, onError);
|
|
119
|
+
return Response.json(DEFERRED_UPDATE);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// src/answers/follow.ts
|
|
123
|
+
function followUps(interaction, ctx, where, onError) {
|
|
124
|
+
const path = {
|
|
125
|
+
webhook_id: interaction.application_id,
|
|
126
|
+
webhook_token: interaction.token
|
|
127
|
+
};
|
|
128
|
+
const schedule = ctx.afterAnswer ?? ((task) => ctx.waitUntil(task()));
|
|
129
|
+
return {
|
|
130
|
+
after: (work) => schedule(async () => {
|
|
131
|
+
try {
|
|
132
|
+
await work();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
await onError?.(error, `${where} after`);
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
async followUp(content, options = {}) {
|
|
138
|
+
const [error] = await callDiscord(null, "post", "/webhooks/{webhook_id}/{webhook_token}", {
|
|
139
|
+
path,
|
|
140
|
+
body: message(content, options),
|
|
141
|
+
files: options.files
|
|
142
|
+
});
|
|
143
|
+
return error;
|
|
144
|
+
},
|
|
145
|
+
async editReply(content, options = {}) {
|
|
146
|
+
const [error] = await callDiscord(null, "patch", "/webhooks/{webhook_id}/{webhook_token}/messages/@original", {
|
|
147
|
+
path,
|
|
148
|
+
body: message(content, options),
|
|
149
|
+
files: options.files
|
|
150
|
+
});
|
|
151
|
+
return error;
|
|
152
|
+
},
|
|
153
|
+
async deleteReply() {
|
|
154
|
+
const [error] = await callDiscord(null, "delete", "/webhooks/{webhook_id}/{webhook_token}/messages/@original", { path });
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// src/utils/id.ts
|
|
160
|
+
function splitId(id) {
|
|
161
|
+
const colon = id.indexOf(":");
|
|
162
|
+
if (colon === -1) return {
|
|
163
|
+
name: id,
|
|
164
|
+
value: void 0
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
name: id.slice(0, colon),
|
|
168
|
+
value: id.slice(colon + 1)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// src/answers/button.ts
|
|
172
|
+
function answerButton(interaction, buttons, ctx, onError) {
|
|
173
|
+
const { name, value } = splitId(interaction.data.custom_id);
|
|
174
|
+
const clicked = buttons?.[name];
|
|
175
|
+
if (!clicked) return new Response(null, { status: 404 });
|
|
176
|
+
return clicked({
|
|
177
|
+
interaction,
|
|
178
|
+
value,
|
|
179
|
+
reply: replyResponse,
|
|
180
|
+
update: updateResponse,
|
|
181
|
+
modal: modalResponse,
|
|
182
|
+
...followUps(interaction, ctx, `button ${name}`, onError),
|
|
183
|
+
deferUpdate: deferUpdate(interaction, ctx, `button ${name}`, onError)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// src/answers/command.ts
|
|
187
|
+
function answerCommand(interaction, commands, ctx, onError) {
|
|
188
|
+
const { found, given, path } = findCommand(commands, interaction.data);
|
|
189
|
+
if (!found || !("run" in found)) return new Response(null, { status: 404 });
|
|
190
|
+
return found.run({
|
|
191
|
+
interaction,
|
|
192
|
+
options: optionValues(given, interaction.data.resolved),
|
|
193
|
+
reply: replyResponse,
|
|
194
|
+
defer: deferAnswer(interaction, ctx, path, onError),
|
|
195
|
+
modal: modalResponse,
|
|
196
|
+
...followUps(interaction, ctx, path, onError)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// src/answers/menu.ts
|
|
200
|
+
const USER = 2;
|
|
201
|
+
function answerMenu(interaction, menus, ctx, onError) {
|
|
202
|
+
const { data } = interaction;
|
|
203
|
+
const menu = data.type === USER ? "user" : "message";
|
|
204
|
+
const found = menus.get(`${menu}:${data.name}`);
|
|
205
|
+
if (!found) return new Response(null, { status: 404 });
|
|
206
|
+
const target = data.type === USER ? interaction.data.resolved.users[data.target_id] : interaction.data.resolved.messages[data.target_id];
|
|
207
|
+
return found.run({
|
|
208
|
+
interaction,
|
|
209
|
+
target,
|
|
210
|
+
reply: replyResponse,
|
|
211
|
+
defer: deferAnswer(interaction, ctx, `${data.name} (${menu} menu)`, onError),
|
|
212
|
+
modal: modalResponse,
|
|
213
|
+
...followUps(interaction, ctx, `${data.name} (${menu} menu)`, onError)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// src/answers/modal.ts
|
|
217
|
+
function answerModal(interaction, modals, ctx, onError) {
|
|
218
|
+
const { name, value } = splitId(interaction.data.custom_id);
|
|
219
|
+
const submitted = modals.get(name);
|
|
220
|
+
if (!submitted) return new Response(null, { status: 404 });
|
|
221
|
+
return submitted.run({
|
|
222
|
+
interaction,
|
|
223
|
+
value,
|
|
224
|
+
fields: fieldValues(interaction.data.components),
|
|
225
|
+
reply: replyResponse,
|
|
226
|
+
...followUps(interaction, ctx, `modal ${name}`, onError)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// src/answers/select.ts
|
|
230
|
+
function answerSelect(interaction, selects, ctx, onError) {
|
|
231
|
+
const picked = selects.get(interaction.data.custom_id);
|
|
232
|
+
if (!picked) return new Response(null, { status: 404 });
|
|
233
|
+
return picked.run({
|
|
234
|
+
interaction,
|
|
235
|
+
values: selectValues(interaction),
|
|
236
|
+
reply: replyResponse,
|
|
237
|
+
update: updateResponse,
|
|
238
|
+
modal: modalResponse,
|
|
239
|
+
...followUps(interaction, ctx, `select ${interaction.data.custom_id}`, onError),
|
|
240
|
+
deferUpdate: deferUpdate(interaction, ctx, `select ${interaction.data.custom_id}`, onError)
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// src/utils/verify.ts
|
|
244
|
+
const encoder = new TextEncoder();
|
|
245
|
+
function bytes(hex) {
|
|
246
|
+
if (!/^(?:[\da-f]{2})+$/i.test(hex)) return null;
|
|
247
|
+
return Uint8Array.from(hex.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
248
|
+
}
|
|
249
|
+
async function verify(publicKey, signature, timestamp, body) {
|
|
250
|
+
const key = bytes(publicKey);
|
|
251
|
+
const sig = bytes(signature);
|
|
252
|
+
if (key?.length !== 32 || sig?.length !== 64) return false;
|
|
253
|
+
const imported = await crypto.subtle.importKey("raw", key, { name: "Ed25519" }, false, ["verify"]);
|
|
254
|
+
return crypto.subtle.verify({ name: "Ed25519" }, imported, sig, encoder.encode(timestamp + body));
|
|
255
|
+
}
|
|
256
|
+
// src/bot.ts
|
|
257
|
+
const PING = 1;
|
|
258
|
+
const APPLICATION_COMMAND = 2;
|
|
259
|
+
const MESSAGE_COMPONENT = 3;
|
|
260
|
+
const AUTOCOMPLETE = 4;
|
|
261
|
+
const MODAL_SUBMIT = 5;
|
|
262
|
+
const CHAT_INPUT = 1;
|
|
263
|
+
const USER_MENU = 2;
|
|
264
|
+
const MESSAGE_MENU = 3;
|
|
265
|
+
const BUTTON = 2;
|
|
266
|
+
const PUBLIC_KEY = /^[\da-f]{64}$/i;
|
|
267
|
+
async function startGateway(env, name) {
|
|
268
|
+
await env[name].getByName("main").ensure();
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Creates a Worker handler for the Interactions Endpoint URL of a Discord application.
|
|
272
|
+
* It rejects requests without a valid Ed25519 signature, answers Discord's PING check
|
|
273
|
+
* and runs the matching command. It reads `DISCORD_PUBLIC_KEY` from the Worker environment:
|
|
274
|
+
* the hex public key of your application in the Discord Developer Portal.
|
|
275
|
+
*
|
|
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
|
+
* @returns The Worker handler. Export it as the default export of your Worker. Pass it to
|
|
296
|
+
* `gateway({ bot })` to answer interactions that arrive over the gateway too.
|
|
297
|
+
* @example
|
|
298
|
+
* import { bot, command } from 'botflare';
|
|
299
|
+
*
|
|
300
|
+
* export default bot({ commands: [command({ name: 'ping', description: 'Pong' }, (c) => c.reply('pong'))] });
|
|
301
|
+
*/
|
|
302
|
+
function bot(config) {
|
|
303
|
+
const commands = /* @__PURE__ */ new Map();
|
|
304
|
+
const menus = /* @__PURE__ */ new Map();
|
|
305
|
+
for (const c of config.commands) if ("menu" in c) menus.set(`${c.menu}:${c.name}`, c);
|
|
306
|
+
else commands.set(c.name, c);
|
|
307
|
+
const modals = new Map((config.modals ?? []).map((m) => [m.id, m]));
|
|
308
|
+
const selects = new Map((config.selects ?? []).map((s) => [s.id, s]));
|
|
309
|
+
const answer = async (interaction, ctx) => {
|
|
310
|
+
if (interaction.type === MESSAGE_COMPONENT && interaction.data.component_type !== BUTTON) return answerSelect(interaction, selects, ctx, config.onError);
|
|
311
|
+
if (interaction.type === MESSAGE_COMPONENT && interaction.data.component_type === BUTTON) return answerButton(interaction, config.buttons, ctx, config.onError);
|
|
312
|
+
if (interaction.type === MODAL_SUBMIT) return answerModal(interaction, modals, ctx, config.onError);
|
|
313
|
+
if (interaction.type === AUTOCOMPLETE) return answerAutocomplete(interaction, commands, config.onError);
|
|
314
|
+
if (interaction.type === APPLICATION_COMMAND && (interaction.data.type === USER_MENU || interaction.data.type === MESSAGE_MENU)) return answerMenu(interaction, menus, ctx, config.onError);
|
|
315
|
+
if (interaction.type === APPLICATION_COMMAND && interaction.data.type === CHAT_INPUT) return answerCommand(interaction, commands, ctx, config.onError);
|
|
316
|
+
return new Response(null, { status: 400 });
|
|
317
|
+
};
|
|
318
|
+
return {
|
|
319
|
+
commands: config.commands,
|
|
320
|
+
answer,
|
|
321
|
+
async scheduled(controller, env) {
|
|
322
|
+
if (config.gateway) await startGateway(env, config.gateway);
|
|
323
|
+
const job = config.cron?.[controller.cron];
|
|
324
|
+
if (!job) return;
|
|
325
|
+
await job({
|
|
326
|
+
controller,
|
|
327
|
+
send: (channelId, content, options) => sendMessage(env.DISCORD_TOKEN, channelId, content, options)
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
async fetch(request, env, ctx) {
|
|
331
|
+
if (request.method === "GET" && config.gateway) {
|
|
332
|
+
await startGateway(env, config.gateway);
|
|
333
|
+
return new Response("gateway started");
|
|
334
|
+
}
|
|
335
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
336
|
+
if (!PUBLIC_KEY.test(env.DISCORD_PUBLIC_KEY)) return new Response("DISCORD_PUBLIC_KEY must be the 64 hex characters from the Developer Portal", { status: 500 });
|
|
337
|
+
const signature = request.headers.get("X-Signature-Ed25519");
|
|
338
|
+
const timestamp = request.headers.get("X-Signature-Timestamp");
|
|
339
|
+
const body = await request.text();
|
|
340
|
+
if (!(signature !== null && timestamp !== null && await verify(env.DISCORD_PUBLIC_KEY, signature, timestamp, body))) return new Response("invalid request signature", { status: 401 });
|
|
341
|
+
const interaction = JSON.parse(body);
|
|
342
|
+
if (interaction.type === PING) return Response.json(PONG);
|
|
343
|
+
return answer(interaction, ctx);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// src/command.ts
|
|
348
|
+
function command(config, run) {
|
|
349
|
+
const { options, subcommands, ...rest } = config;
|
|
350
|
+
if (subcommands) return {
|
|
351
|
+
...rest,
|
|
352
|
+
subcommands
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
...rest,
|
|
356
|
+
options: options ?? {},
|
|
357
|
+
run
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// src/context-menu.ts
|
|
361
|
+
/**
|
|
362
|
+
* Declares a context menu command: an item in the Apps menu when the user right-clicks a user or a
|
|
363
|
+
* message. Add it to the `commands` of {@link bot}, next to the slash commands. It has no options
|
|
364
|
+
* and no description. The handler gets the user or the message as `c.target`.
|
|
365
|
+
*
|
|
366
|
+
* @param menu Whether the item shows on a `user` or on a `message`.
|
|
367
|
+
* @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
|
+
* @param run Answers the click, with `reply`, `defer` and `modal` as in a slash command.
|
|
374
|
+
* @returns The context menu command, for the `commands` of {@link bot}.
|
|
375
|
+
* @example
|
|
376
|
+
* const avatar = contextMenu('user', { name: 'Show avatar' }, (c) => c.reply(c.target.username));
|
|
377
|
+
*/
|
|
378
|
+
function contextMenu(menu, config, run) {
|
|
379
|
+
return {
|
|
380
|
+
...config,
|
|
381
|
+
menu,
|
|
382
|
+
run
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// src/format.ts
|
|
386
|
+
const idOf = (target) => typeof target === "string" ? target : target.id;
|
|
387
|
+
/**
|
|
388
|
+
* Writes a mention for message text. A mention shows as a link to the user, role, channel or
|
|
389
|
+
* command. It pings nobody unless the message allows it with `allowedMentions`.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* c.reply(`${mention.user(c.interaction.member.user)} joined ${mention.channel('123')}`);
|
|
393
|
+
*/
|
|
394
|
+
const mention = {
|
|
395
|
+
user: (user) => `<@${idOf(user)}>`,
|
|
396
|
+
role: (role) => `<@&${idOf(role)}>`,
|
|
397
|
+
channel: (channel) => `<#${idOf(channel)}>`,
|
|
398
|
+
command: (name, id) => `</${name}:${id}>`
|
|
399
|
+
};
|
|
400
|
+
const STYLE = {
|
|
401
|
+
"short-time": "t",
|
|
402
|
+
"long-time": "T",
|
|
403
|
+
"short-date": "d",
|
|
404
|
+
"long-date": "D",
|
|
405
|
+
short: "f",
|
|
406
|
+
long: "F",
|
|
407
|
+
relative: "R"
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* Writes a timestamp for message text. Discord shows it in the time zone and language of each
|
|
411
|
+
* reader.
|
|
412
|
+
*
|
|
413
|
+
* @param date The moment, as a `Date` or as milliseconds since 1970, such as `Date.now()`.
|
|
414
|
+
* @param style How Discord shows it: `short` (default, such as "5 November 2026 12:30"), `long`,
|
|
415
|
+
* `short-date`, `long-date`, `short-time`, `long-time` or `relative` (such as "in 2 hours").
|
|
416
|
+
* @returns The timestamp markup, such as `<t:1762338600:R>`.
|
|
417
|
+
* @example
|
|
418
|
+
* c.reply(`The event starts ${time(start, 'relative')}`);
|
|
419
|
+
*/
|
|
420
|
+
function time(date, style = "short") {
|
|
421
|
+
return `<t:${Math.floor(Number(date) / 1e3)}:${STYLE[style]}>`;
|
|
422
|
+
}
|
|
423
|
+
// src/modal.ts
|
|
424
|
+
/**
|
|
425
|
+
* Declares a modal: a form Discord shows in a popup. Open it with `c.modal()` from a command or a
|
|
426
|
+
* button, and add it to the `modals` of {@link bot} so the bot gets what the user typed.
|
|
427
|
+
*
|
|
428
|
+
* @param config What Discord shows in the popup.
|
|
429
|
+
* @param config.id The id Discord sends back when the user submits the form.
|
|
430
|
+
* @param config.title The title of the popup.
|
|
431
|
+
* @param config.fields The text fields, keyed by name. A field is required unless `required` is `false`.
|
|
432
|
+
* @param run Answers the submitted form. `c.fields` holds the text of each field, keyed by name.
|
|
433
|
+
* A field that is not required and left empty is `undefined`.
|
|
434
|
+
* To carry a value through the form, open it with `c.modal(form, value)`. The handler gets that
|
|
435
|
+
* value as `c.value`, such as the id of the item the form edits.
|
|
436
|
+
* @returns The modal, for `c.modal()` and the `modals` of {@link bot}.
|
|
437
|
+
* @example
|
|
438
|
+
* const feedback = modal(
|
|
439
|
+
* { id: 'feedback', title: 'Feedback', fields: { text: { label: 'Your idea', style: 'paragraph' } } },
|
|
440
|
+
* (c) => c.reply(`Thanks: ${c.fields.text}`),
|
|
441
|
+
* );
|
|
442
|
+
*/
|
|
443
|
+
function modal(config, run) {
|
|
444
|
+
return {
|
|
445
|
+
id: config.id,
|
|
446
|
+
title: config.title,
|
|
447
|
+
fields: config.fields,
|
|
448
|
+
run
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// src/option.ts
|
|
452
|
+
/**
|
|
453
|
+
* Declares one option of a slash command. The handler reads its value from `c.options`,
|
|
454
|
+
* typed from `type`, and `undefined` when the option is not `required`.
|
|
455
|
+
*
|
|
456
|
+
* A `string`, `integer` or `number` option also takes `choices`, keyed by the label Discord shows.
|
|
457
|
+
* The user can pick only those, and the handler gets one of their values. A `string` option takes
|
|
458
|
+
* `minLength` and `maxLength`, an `integer` or `number` option takes `min` and `max`, and a
|
|
459
|
+
* `channel` option takes `channelTypes`, such as `['text', 'forum']`.
|
|
460
|
+
*
|
|
461
|
+
* Instead of fixed `choices`, a `string`, `integer` or `number` option can take `autocomplete`: a
|
|
462
|
+
* function that gets what the user has typed so far as `c.value` and returns up to 25 suggestions,
|
|
463
|
+
* as plain values or as `{ name, value }`. If it throws, `onError` gets the error and Discord shows
|
|
464
|
+
* no suggestions.
|
|
465
|
+
*
|
|
466
|
+
* @param type The value type Discord asks the user for. `user`, `channel` and `role` give the full
|
|
467
|
+
* object Discord sends with the command, not only its id.
|
|
468
|
+
* @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
|
+
* @returns The option, for the `options` of {@link command}.
|
|
473
|
+
* @example
|
|
474
|
+
* option('integer', { description: 'How many dice', required: true, min: 1, max: 6 });
|
|
475
|
+
* option('string', { description: 'Color', choices: { Red: 'red', Blue: 'blue' } });
|
|
476
|
+
*/
|
|
477
|
+
function option(type, config) {
|
|
478
|
+
return {
|
|
479
|
+
...config,
|
|
480
|
+
type,
|
|
481
|
+
required: config.required ?? false
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// src/rest.ts
|
|
485
|
+
/**
|
|
486
|
+
* Calls the Discord REST API as the bot, with `DISCORD_TOKEN` from the Worker environment. Each
|
|
487
|
+
* method takes a route from the Discord API spec, such as `'/guilds/{guild_id}/roles'`, and
|
|
488
|
+
* types its `path`, `query`, `body` and result from that route. It returns `[error, data]` and
|
|
489
|
+
* does not throw. When Discord answers 429, it waits for `retry_after` and tries again, up to
|
|
490
|
+
* three tries in all.
|
|
491
|
+
*
|
|
492
|
+
* A request that is not a GET takes `reason`, the text that shows in the audit log of the server.
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* import { rest } from 'botflare';
|
|
496
|
+
*
|
|
497
|
+
* const [error, member] = await rest.get('/guilds/{guild_id}/members/{user_id}', {
|
|
498
|
+
* path: { guild_id: guildId, user_id: userId },
|
|
499
|
+
* });
|
|
500
|
+
* if (!error) await rest.put('/guilds/{guild_id}/members/{user_id}/roles/{role_id}', {
|
|
501
|
+
* path: { guild_id: guildId, user_id: member.user.id, role_id: roleId },
|
|
502
|
+
* });
|
|
503
|
+
*/
|
|
504
|
+
const rest = createRest(() => env.DISCORD_TOKEN);
|
|
505
|
+
// src/select.ts
|
|
506
|
+
/**
|
|
507
|
+
* Declares a select menu and the handler that runs when the user picks from it. Put it in the
|
|
508
|
+
* `selects` of a message, such as `c.reply('Pick one', { selects: [menu] })`, and in the `selects` of
|
|
509
|
+
* {@link bot} so the bot gets the pick. Each menu takes a row of its own, above the buttons.
|
|
510
|
+
*
|
|
511
|
+
* A `string` menu lists `options`, keyed by the label Discord shows, and the handler gets the picked
|
|
512
|
+
* values. A `user`, `role`, `channel` or `mentionable` menu lets the user pick from the server, and
|
|
513
|
+
* the handler gets the full objects Discord resolved. A `channel` menu takes `channelTypes`.
|
|
514
|
+
*
|
|
515
|
+
* @param type What the user picks from.
|
|
516
|
+
* @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
|
+
* @param run Answers the pick. `c.values` holds the picked items.
|
|
522
|
+
* @returns The select menu.
|
|
523
|
+
* @example
|
|
524
|
+
* const fruit = select('string', { id: 'fruit', options: { Apple: 'apple', Mango: 'mango' } }, (c) =>
|
|
525
|
+
* c.update(`picked ${c.values.join(', ')}`),
|
|
526
|
+
* );
|
|
527
|
+
*/
|
|
528
|
+
function select(type, config, run) {
|
|
529
|
+
return {
|
|
530
|
+
...config,
|
|
531
|
+
type,
|
|
532
|
+
run
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
export { bot, command, contextMenu, mention, modal, option, rest, select, time };
|
|
536
|
+
|
|
537
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|