@seedcord/http 0.10.2 → 0.11.0-next.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.
@@ -1,18 +1,242 @@
1
1
  import "reflect-metadata";
2
- import { GatedMetadataKey, MiddlewareRegistry, PublishDefault, RegisterDefaults, RegisterSubscriber, SubscribeMetadataKey, accessorStore, asError, clearStore, decodeComponentRoute, deferFlags, guardedAccessor, interactionMiddleware, interactionRoutesOf, isEmojiTuple, outcomeFor, queuedMsFor, registrationFor, reportDispatch, reportedWrite, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
3
- import { SeedcordError, SeedcordTypeError, applicationIdFromToken, validateDiscordToken } from "@seedcord/errors/internal";
4
- import { Logger } from "@seedcord/logger";
5
- import { Converters, Envapter } from "envapt";
6
- import { DiscordAPIError, REST } from "@discordjs/rest";
7
- import { ArraySourceBase, BaseHandler, BaseReplySender, Bus, CursorSourceBase, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler, ResolvedEmoji, Silence, Subscriber, WebhookLog } from "@seedcord/core";
2
+ import { COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, FixedScope, GatedMetadataKey, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, PublishDefault, SubscriberLoader, accessorStore, attachmentsOf, clearStore, currentApplication, decodeComponentRoute, deferFlags, guardedAccessor, interactionMiddleware, interactionRoutesOf, logUnknownFault, outcomeFor, queuedMsFor, readBotToken, reportDispatch, reportedWrite, resolveEmojis, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
3
+ import { Plugin } from "@seedcord/core/plugin";
8
4
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
9
- import { MemoryRateLimiter } from "@seedcord/rate-limiter";
5
+ import { SeedcordError, SeedcordTypeError } from "@seedcord/errors/internal";
10
6
  import { ApplicationCommandOptionType, ApplicationCommandType, ChannelType, ComponentType, InteractionResponseType, InteractionType, MessageFlags, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
7
+ import { Collection } from "@discordjs/collection";
8
+ import { ArraySourceBase, BaseHandler, BaseReplySender, Bus, CursorSourceBase, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler, ResolvedEmoji, Silence, StartupPhase, Subscriber, WebhookLog } from "@seedcord/core";
9
+ import { Logger } from "@seedcord/logger";
10
+ import { HmrModuleHandler, HmrModuleHandler as HmrModuleHandler$1 } from "@seedcord/core/hmr";
11
+ import { MemoryRateLimiter } from "@seedcord/rate-limiter";
12
+ import { formatFilePath, importFiles } from "@seedcord/utils";
11
13
  import { API } from "@discordjs/core/http-only";
12
- import { buildSlashRoute } from "@seedcord/utils/internal";
14
+ import { asError, buildSlashRoute } from "@seedcord/utils/internal";
15
+ import { Envapter } from "envapt";
16
+ import { DiscordAPIError, REST } from "@discordjs/rest";
13
17
  import { prefixOf } from "@seedcord/custom-id";
14
- import { Collection } from "@discordjs/collection";
15
18
 
19
+ //#region src/Plugin.ts
20
+ const HTTP_SCOPE = { transport: "http" };
21
+ /**
22
+ * Base class for an HTTP plugin, binding `this.core` to the HTTP {@link Core}.
23
+ *
24
+ * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
25
+ * `this.core` is typed to the shared members only.
26
+ *
27
+ * @typeParam Opts - The transport and runtime the plugin declares.
28
+ */
29
+ var Plugin$1 = class extends Plugin {
30
+ /** @internal */
31
+ static [FixedScope] = HTTP_SCOPE;
32
+ };
33
+
34
+ //#endregion
35
+ //#region src/inputs/pick.ts
36
+ function pick(ids, bucket) {
37
+ const found = new Collection();
38
+ if (!ids || !bucket) return found;
39
+ for (const id of ids) if (Object.hasOwn(bucket, id)) found.set(id, bucket[id]);
40
+ return found;
41
+ }
42
+
43
+ //#endregion
44
+ //#region src/inputs/ModalFields.ts
45
+ const FIELD_KINDS = {
46
+ [ComponentType.TextInput]: {
47
+ label: "a text input",
48
+ getter: "getTextInputValue"
49
+ },
50
+ [ComponentType.StringSelect]: {
51
+ label: "a string select",
52
+ getter: "getStringSelectValues"
53
+ },
54
+ [ComponentType.UserSelect]: {
55
+ label: "a user select",
56
+ getter: "getSelectedUsers"
57
+ },
58
+ [ComponentType.RoleSelect]: {
59
+ label: "a role select",
60
+ getter: "getSelectedRoles"
61
+ },
62
+ [ComponentType.MentionableSelect]: {
63
+ label: "a mentionable select",
64
+ getter: "getSelectedMentionables"
65
+ },
66
+ [ComponentType.ChannelSelect]: {
67
+ label: "a channel select",
68
+ getter: "getSelectedChannels"
69
+ },
70
+ [ComponentType.FileUpload]: {
71
+ label: "a file upload",
72
+ getter: "getUploadedFiles"
73
+ },
74
+ [ComponentType.RadioGroup]: {
75
+ label: "a radio group",
76
+ getter: "getRadioGroup"
77
+ },
78
+ [ComponentType.CheckboxGroup]: {
79
+ label: "a checkbox group",
80
+ getter: "getCheckboxGroup"
81
+ },
82
+ [ComponentType.Checkbox]: {
83
+ label: "a checkbox",
84
+ getter: "getCheckbox"
85
+ }
86
+ };
87
+ function nameOfChannelType(type) {
88
+ return ChannelType[type] ?? String(type);
89
+ }
90
+ function listOf(values) {
91
+ return values ?? [];
92
+ }
93
+ function isKind(entry, kinds) {
94
+ return kinds.includes(entry.type);
95
+ }
96
+ /**
97
+ * The fields a modal carries, read by custom id.
98
+ *
99
+ * A select getter resolves the ids its field picked against the interaction's `resolved` data and returns a
100
+ * `Collection` keyed by id. A field that picked nothing returns null.
101
+ *
102
+ * @throws A **SeedcordTypeError** from every getter when no field in the modal has that custom id.
103
+ * @throws A **SeedcordTypeError** from every getter when the field holds a kind that getter does not read. The message gives the getter that does.
104
+ * @throws A **SeedcordTypeError** from a select getter called with `required` when its field picked nothing.
105
+ */
106
+ var ModalFields = class {
107
+ entries = /* @__PURE__ */ new Map();
108
+ resolved;
109
+ constructor(data) {
110
+ this.resolved = data.resolved;
111
+ for (const component of data.components ?? []) {
112
+ if ("components" in component) for (const child of component.components) this.entries.set(child.custom_id, child);
113
+ if ("component" in component) this.entries.set(component.component.custom_id, component.component);
114
+ }
115
+ }
116
+ entry(customId) {
117
+ const found = this.entries.get(customId);
118
+ if (!found) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldNotFound, [customId]);
119
+ return found;
120
+ }
121
+ field(customId, kinds) {
122
+ const entry = this.entry(customId);
123
+ if (!isKind(entry, kinds)) {
124
+ const known = FIELD_KINDS[entry.type];
125
+ const args = known ? [
126
+ customId,
127
+ known.label,
128
+ known.getter
129
+ ] : [customId, `component type ${entry.type}`];
130
+ throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldWrongKind, [...args]);
131
+ }
132
+ return entry;
133
+ }
134
+ selected(customId, kinds, bucket, required) {
135
+ const { values } = this.field(customId, kinds);
136
+ const found = pick(values, bucket);
137
+ if (found.size > 0) return found;
138
+ if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
139
+ return null;
140
+ }
141
+ getField(customId, kind) {
142
+ return kind === void 0 ? this.entry(customId) : this.field(customId, [kind]);
143
+ }
144
+ getTextInputValue(customId) {
145
+ return this.field(customId, [ComponentType.TextInput]).value;
146
+ }
147
+ getStringSelectValues(customId) {
148
+ return listOf(this.field(customId, [ComponentType.StringSelect]).values);
149
+ }
150
+ getSelectedUsers(customId, required = false) {
151
+ const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
152
+ return this.selected(customId, kinds, this.resolved?.users, required);
153
+ }
154
+ /** The guild members behind the picked users. Discord resolves these only inside a guild. */
155
+ getSelectedMembers(customId) {
156
+ const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
157
+ return this.selected(customId, kinds, this.resolved?.members, false);
158
+ }
159
+ getSelectedRoles(customId, required = false) {
160
+ const kinds = [ComponentType.RoleSelect, ComponentType.MentionableSelect];
161
+ return this.selected(customId, kinds, this.resolved?.roles, required);
162
+ }
163
+ getSelectedChannels(customId, required = false, channelTypes = []) {
164
+ const kinds = [ComponentType.ChannelSelect];
165
+ const picked = this.selected(customId, kinds, this.resolved?.channels, required);
166
+ if (!picked || channelTypes.length === 0) return picked;
167
+ for (const found of picked.values()) {
168
+ if (channelTypes.includes(found.type)) continue;
169
+ throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldChannelType, [
170
+ customId,
171
+ nameOfChannelType(found.type),
172
+ channelTypes.map(nameOfChannelType)
173
+ ]);
174
+ }
175
+ return picked;
176
+ }
177
+ getSelectedMentionables(customId, required = false) {
178
+ const { values } = this.field(customId, [ComponentType.MentionableSelect]);
179
+ const resolved = this.resolved;
180
+ const picked = {
181
+ users: pick(values, resolved?.users),
182
+ members: pick(values, resolved?.members),
183
+ roles: pick(values, resolved?.roles)
184
+ };
185
+ if (picked.users.size + picked.members.size + picked.roles.size > 0) return picked;
186
+ if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
187
+ return null;
188
+ }
189
+ getUploadedFiles(customId, required = false) {
190
+ const kinds = [ComponentType.FileUpload];
191
+ return this.selected(customId, kinds, this.resolved?.attachments, required);
192
+ }
193
+ getRadioGroup(customId, required = false) {
194
+ const value = this.field(customId, [ComponentType.RadioGroup]).value ?? null;
195
+ if (value !== null) return value;
196
+ if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
197
+ return null;
198
+ }
199
+ getCheckboxGroup(customId) {
200
+ return listOf(this.field(customId, [ComponentType.CheckboxGroup]).values);
201
+ }
202
+ getCheckbox(customId) {
203
+ return this.field(customId, [ComponentType.Checkbox]).value;
204
+ }
205
+ };
206
+
207
+ //#endregion
208
+ //#region src/emojis/EmojiInjector.ts
209
+ const emojiStorage = accessorStore();
210
+ /**
211
+ * The bot's resolved emojis, keyed by {@link EmojiMap}. A node bot fills these during startup. An edge
212
+ * bot fills them on its first request, from the ids `seedcord build` wrote into the worker.
213
+ *
214
+ * @throws A **SeedcordError** when you read a key that was not filled.
215
+ */
216
+ const Emojis = guardedAccessor("Emojis", emojiStorage);
217
+ function installEmojis(emojis) {
218
+ clearStore(emojiStorage);
219
+ for (const [key, { name, id, animated }] of Object.entries(emojis)) emojiStorage[key] = new ResolvedEmoji(name, id, animated);
220
+ }
221
+ var EmojiInjector = class {
222
+ core;
223
+ logger = new Logger("Emojis", { channel: "bot" });
224
+ constructor(core) {
225
+ this.core = core;
226
+ }
227
+ async init() {
228
+ const configured = this.core.config.bot.emojis;
229
+ if (!configured || Object.keys(configured).length === 0) {
230
+ installEmojis({});
231
+ this.logger.debug("No emojis configured, skipping emoji injection.");
232
+ return;
233
+ }
234
+ installEmojis(await resolveEmojis(this.core.rest, this.core.applicationId, configured));
235
+ this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
236
+ }
237
+ };
238
+
239
+ //#endregion
16
240
  //#region src/reply/ReplySender.ts
17
241
  function wireAllowedMentions(mentions) {
18
242
  const { repliedUser, ...wire } = mentions;
@@ -170,484 +394,359 @@ var RepliableHandler$1 = class extends RepliableHandler {
170
394
  };
171
395
 
172
396
  //#endregion
173
- //#region src/gates/context.ts
174
- function interactionGateContext(payload, core, dispatch) {
175
- const member = payload.member ?? null;
176
- const resolved = member?.user ?? payload.user;
177
- return {
178
- kind: "interaction",
179
- core,
180
- dispatch,
181
- interaction: payload,
182
- user: resolved,
183
- member,
184
- userId: resolved?.id ?? null,
185
- guildId: payload.guild_id ?? null,
186
- channelId: payload.channel?.id ?? null,
187
- memberRoleIds: member?.roles ?? [],
188
- memberPermissions: member ? BigInt(member.permissions) : null,
189
- appPermissions: BigInt(payload.app_permissions),
190
- declaredRoute: null
191
- };
192
- }
397
+ //#region src/handlers/interaction/InteractionHandler.ts
398
+ /**
399
+ * Shared base the repliable interaction handlers extend.
400
+ *
401
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ContextMenuHandler}, {@link ButtonHandler},
402
+ * {@link ModalHandler}, or one of the select menu bases instead. This class adds `showModal` on top
403
+ * of the reply members.
404
+ *
405
+ * @typeParam Event - The repliable interaction type this handler processes
406
+ */
407
+ var InteractionHandler = class extends RepliableHandler$1 {
408
+ constructor(event, core, dispatch) {
409
+ super(event, core, dispatch);
410
+ }
411
+ /**
412
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
413
+ * compile time (Discord forbids a modal in response to a modal).
414
+ */
415
+ showModal(modal) {
416
+ return this.sender.showModal(modal);
417
+ }
418
+ };
193
419
 
194
420
  //#endregion
195
- //#region src/dispatch/slashRouteOf.ts
196
- function slashRouteOf(data) {
197
- const first = data.options?.[0];
198
- if (first?.type === ApplicationCommandOptionType.SubcommandGroup) return buildSlashRoute(data.name, first.options[0]?.name, first.name);
199
- if (first?.type === ApplicationCommandOptionType.Subcommand) return buildSlashRoute(data.name, first.name);
200
- return data.name;
201
- }
421
+ //#region src/handlers/interaction/InteractionMiddleware.ts
422
+ /**
423
+ * Base class for interaction middleware on the HTTP transport.
424
+ *
425
+ * Middleware runs after the dispatcher builds the handler and before the gates, over the handler's own
426
+ * reply surface. Name the kinds in the generic and list the same ones in `{ kinds }`. Several kinds give
427
+ * `this.event` a union. Narrow it on `type` and `data.component_type`. A catchall reads every repliable
428
+ * kind.
429
+ *
430
+ * @typeParam Kind - One or more {@link InteractionKind} members. Defaults to every repliable kind.
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * \@RegisterInteractionMiddleware({ kinds: [InteractionKind.Button] })
435
+ * class Audit extends InteractionMiddleware<InteractionKind.Button> {
436
+ * async execute() {
437
+ * this.logger.info(this.event.data.custom_id);
438
+ * }
439
+ * }
440
+ * ```
441
+ */
442
+ var InteractionMiddleware = class extends RepliableHandler$1 {
443
+ constructor(event, core, dispatch, sender) {
444
+ super(event, core, dispatch, sender);
445
+ }
446
+ };
202
447
 
203
448
  //#endregion
204
- //#region src/dispatch/reportFault.ts
205
- function reportFault(error, uuid, dispatch, payload, core) {
206
- const source = interactionSource(payload);
207
- const from = {
208
- uuid,
209
- dispatchId: dispatch.id,
210
- origin: dispatch.routeId
211
- };
212
- if (error instanceof Notice && source) core.bus[PublishDefault]("handledException", {
213
- denial: error,
214
- ...from,
215
- source
216
- });
217
- else core.bus[PublishDefault]("unknownException", {
218
- ...from,
219
- error,
220
- ...actors(payload),
221
- metadata: payload
222
- });
223
- }
224
- function actors(payload) {
225
- const user = payload.member?.user ?? payload.user;
226
- return {
227
- guild: void 0,
228
- user: user ? {
229
- id: user.id,
230
- username: user.username
231
- } : void 0
232
- };
233
- }
234
- function interactionSource(payload) {
235
- const user = payload.member?.user ?? payload.user;
236
- if (!user) return null;
237
- const common = {
238
- kind: "interaction",
239
- userId: user.id,
240
- guildId: payload.guild_id ?? null,
241
- channelId: payload.channel?.id ?? null,
242
- interactionId: payload.id,
243
- raw: payload
244
- };
245
- if (payload.type === InteractionType.ApplicationCommand) {
246
- const { data } = payload;
247
- const isSlash = data.type === ApplicationCommandType.ChatInput;
248
- return {
249
- ...common,
250
- interactionKind: isSlash ? "slash" : "context-menu",
251
- command: isSlash ? slashRouteOf(data) : data.name,
252
- customId: null
253
- };
449
+ //#region src/inputs/HttpSlashOptions.ts
450
+ var HttpSlashOptions = class {
451
+ leaves = /* @__PURE__ */ new Map();
452
+ resolved;
453
+ constructor(data) {
454
+ this.resolved = data.resolved;
455
+ let options = data.options ?? [];
456
+ const first = options[0];
457
+ if (first?.type === ApplicationCommandOptionType.SubcommandGroup) options = first.options;
458
+ const second = options[0];
459
+ if (second?.type === ApplicationCommandOptionType.Subcommand) options = second.options ?? [];
460
+ for (const option of options) if (option.type !== ApplicationCommandOptionType.Subcommand && option.type !== ApplicationCommandOptionType.SubcommandGroup) this.leaves.set(option.name, option);
254
461
  }
255
- if (payload.type === InteractionType.ModalSubmit) return {
256
- ...common,
257
- interactionKind: "modal",
258
- command: null,
259
- customId: payload.data.custom_id
260
- };
261
- if (payload.type === InteractionType.MessageComponent) {
262
- const isButton = payload.data.component_type === ComponentType.Button;
263
- return {
264
- ...common,
265
- interactionKind: isButton ? "button" : "select",
266
- command: null,
267
- customId: payload.data.custom_id
268
- };
462
+ getString(name) {
463
+ const leaf = this.leaves.get(name);
464
+ return leaf?.type === ApplicationCommandOptionType.String ? leaf.value : null;
269
465
  }
270
- return null;
271
- }
272
-
273
- //#endregion
274
- //#region src/dispatch/dispatchInteraction.ts
275
- let dispatchLogger;
276
- function logger() {
277
- dispatchLogger ??= new Logger("Dispatcher", { channel: "interactions" });
278
- return dispatchLogger;
279
- }
280
- function noLifecycle(accessor) {
281
- throw new SeedcordError(SeedcordErrorCode.CoreLifecycleUnavailable, [accessor]);
282
- }
283
- const edgeShutdown = { addTask: () => noLifecycle("shutdown") };
284
- const edgeStartup = { addTask: () => noLifecycle("startup") };
285
- const EDGE_SWEEPERS = {
286
- hashSweepInterval: 0,
287
- handlerSweepInterval: 0
288
- };
289
- function edgeRestOptions(given = {}) {
290
- for (const key of Object.keys(EDGE_SWEEPERS)) if (given[key] !== void 0) throw new SeedcordError(SeedcordErrorCode.ConfigEdgeRestSweeper, [key]);
291
- return {
292
- ...given,
293
- ...EDGE_SWEEPERS
294
- };
295
- }
296
- function createCore(config, token) {
297
- const draft = {
298
- config,
299
- rateLimiter: config.store ?? new MemoryRateLimiter(),
300
- rest: new REST(edgeRestOptions(config.bot.restOptions)).setToken(token),
301
- shutdown: edgeShutdown,
302
- startup: edgeStartup,
303
- get applicationId() {
304
- return applicationIdFromToken(token);
305
- }
306
- };
307
- draft.bus = new Bus(draft);
308
- return draft;
309
- }
310
- const HARMLESS_API_CODES = /* @__PURE__ */ new Set([
311
- RESTJSONErrorCodes.UnknownInteraction,
312
- RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
313
- RESTJSONErrorCodes.UnknownMessage
314
- ]);
315
- async function sendGuarded(routeId, send) {
316
- try {
317
- await send();
318
- } catch (error) {
319
- if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
320
- logger().debug(`boundary send hit harmless code ${paint.amber(String(error.code))}`);
321
- return;
322
- }
323
- logger().error(`boundary send failed for route ${paint.sky.bold(routeId)}`, error);
466
+ getInteger(name) {
467
+ const leaf = this.leaves.get(name);
468
+ return leaf?.type === ApplicationCommandOptionType.Integer ? numeric(leaf.value) : null;
324
469
  }
325
- }
326
- async function respondEmptyChoices(scope) {
327
- const telemetry = {
328
- bus: scope.core.bus,
329
- dispatch: scope.dispatch,
330
- interactionId: scope.payload.id
331
- };
332
- await reportedWrite(telemetry, "respond", () => scope.core.rest.post(Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
333
- type: InteractionResponseType.ApplicationCommandAutocompleteResult,
334
- data: { choices: [] }
335
- } }));
336
- }
337
- function renderContext(scope, uuid) {
338
- const { dispatch } = scope;
339
- const developerUsername = scope.core.config.notifications?.developerUsername;
340
- return developerUsername === void 0 ? {
341
- uuid,
342
- dispatch
343
- } : {
344
- uuid,
345
- developerUsername,
346
- dispatch
347
- };
348
- }
349
- async function handleNotice(notice, uuid, scope) {
350
- if (notice.report) {
351
- logger().error(`${notice.name}: ${paint.mute(uuid)}`, notice);
352
- reportFault(notice, uuid, scope.dispatch, scope.payload, scope.core);
470
+ getNumber(name) {
471
+ const leaf = this.leaves.get(name);
472
+ return leaf?.type === ApplicationCommandOptionType.Number ? numeric(leaf.value) : null;
353
473
  }
354
- const { sender } = scope;
355
- if (!sender) {
356
- await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
357
- return;
474
+ getBoolean(name) {
475
+ const leaf = this.leaves.get(name);
476
+ return leaf?.type === ApplicationCommandOptionType.Boolean ? leaf.value : null;
358
477
  }
359
- const response = notice.render(renderContext(scope, uuid));
360
- await sendGuarded(scope.routeId, () => sender.send(response, { ephemeral: notice.ephemeral }));
361
- }
362
- async function handleRawFault(error, uuid, scope) {
363
- const { core, sender } = scope;
364
- if (core.config.errors?.errorStack ?? false) logger().error(paint.mute(uuid), error);
365
- else logger().error(`${paint.mute(uuid)} | ${error.message}`);
366
- reportFault(error, uuid, scope.dispatch, scope.payload, core);
367
- if (!sender) {
368
- await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
369
- return;
478
+ getUser(name) {
479
+ const leaf = this.leaves.get(name);
480
+ if (leaf?.type !== ApplicationCommandOptionType.User) return null;
481
+ return this.resolved?.users?.[leaf.value] ?? null;
370
482
  }
371
- const Override = core.config.errors?.defaultError;
372
- const response = (Override ? new Override(uuid) : new Fault()).render(renderContext(scope, uuid));
373
- await sendGuarded(scope.routeId, () => sender.send(response, { ephemeral: true }));
374
- }
375
- async function handleFault(caught, scope) {
376
- if (caught instanceof Silence) {
377
- if (caught.reason !== void 0 && (scope.core.config.errors?.logSilences ?? true)) logger().debug(`Silence: ${caught.reason}`);
378
- return;
483
+ getMember(name) {
484
+ const leaf = this.leaves.get(name);
485
+ if (leaf?.type !== ApplicationCommandOptionType.User) return null;
486
+ return this.resolved?.members?.[leaf.value] ?? null;
379
487
  }
380
- const uuid = crypto.randomUUID();
381
- if (caught instanceof Notice) {
382
- await handleNotice(caught, uuid, scope);
383
- return;
488
+ getChannel(name) {
489
+ const leaf = this.leaves.get(name);
490
+ if (leaf?.type !== ApplicationCommandOptionType.Channel) return null;
491
+ return this.resolved?.channels?.[leaf.value] ?? null;
384
492
  }
385
- const error = asError(caught);
386
- const ignore = new Set(scope.core.config.errors?.ignoreApiCodes ?? []);
387
- if (error instanceof DiscordAPIError && ignore.has(error.code)) {
388
- logger().debug(`swallowed api code ${paint.amber(String(error.code))}`);
389
- return;
390
- }
391
- await handleRawFault(error, uuid, scope);
392
- }
393
- function unhandledRouteId(match) {
394
- if (match.routeId) return match.routeId;
395
- const key = match.attemptedKey ?? "";
396
- return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
397
- }
398
- function freshScope(match, payload, core, dispatch) {
399
- const ref = {
400
- application_id: payload.application_id,
401
- id: payload.id,
402
- token: payload.token
403
- };
404
- return {
405
- core,
406
- payload,
407
- routeId: unhandledRouteId(match),
408
- dispatch,
409
- sender: match.kind === InteractionKind.Autocomplete ? null : new ReplySender(ref, core.rest, dispatch, core.bus)
410
- };
411
- }
412
- function dispatchReporter(match, payload, core, dispatchId) {
413
- const startedAt = performance.now();
414
- const queuedMs = queuedMsFor(payload.id);
415
- return (outcome) => {
416
- reportDispatch(core.bus, {
417
- dispatchId,
418
- routeId: unhandledRouteId(match),
419
- interactionId: payload.id,
420
- kind: match.kind,
421
- outcome,
422
- fallback: match.routeId === null,
423
- userId: (payload.member?.user ?? payload.user)?.id ?? null,
424
- guildId: payload.guild_id ?? null,
425
- startedAt,
426
- queuedMs
427
- });
428
- };
429
- }
430
- async function answer(caught, scope, report, outcome = outcomeFor(caught)) {
431
- try {
432
- await handleFault(caught, scope);
433
- } finally {
434
- report(outcome);
435
- }
436
- }
437
- async function runMiddlewares(step, kind, sender) {
438
- const { args, dispatch, ran } = step;
439
- for (const Middleware of args.middlewares.chainFor(kind)) {
440
- const event = args.payload;
441
- const instance = new Middleware(event, args.core, dispatch, sender);
442
- ran.push(instance);
443
- await instance.execute();
444
- }
445
- }
446
- async function refusalBeforeHandler(step) {
447
- const { args, scope } = step;
448
- const { kind } = args.match;
449
- if (kind !== InteractionKind.Autocomplete && scope.sender) try {
450
- await runMiddlewares(step, kind, scope.sender);
451
- } catch (caught) {
452
- return { caught };
453
- }
454
- return gateRefusal(step);
455
- }
456
- async function dispatchInteraction(args) {
457
- const { match, payload, core } = args;
458
- const routeId = unhandledRouteId(match);
459
- const dispatch = new DispatchContext(routeId);
460
- const report = dispatchReporter(match, payload, core, dispatch.id);
461
- const Handler = match.ctor;
462
- logger().debug(`Processing ${paint.sky.bold(routeId)} with ${paint.mute(Handler.name)}`);
463
- let handler;
464
- try {
465
- handler = new Handler(payload, core, dispatch);
466
- } catch (caught) {
467
- await answer(caught, freshScope(match, payload, core, dispatch), report);
468
- return null;
469
- }
470
- const scope = {
471
- core,
472
- payload,
473
- routeId,
474
- dispatch,
475
- sender: handler instanceof RepliableHandler$1 ? handler.sender : null
476
- };
477
- const ran = [];
478
- const refusal = await refusalBeforeHandler({
479
- args,
480
- Handler,
481
- dispatch,
482
- scope,
483
- ran
484
- });
485
- if (refusal) {
486
- try {
487
- await answer(refusal.caught, scope, report);
488
- } finally {
489
- await runAfter(ran, resultFor(refusal.caught), logger());
490
- }
491
- return null;
492
- }
493
- return async () => {
494
- let result = { outcome: "handled" };
495
- try {
496
- await handler.execute();
497
- report("handled");
498
- } catch (caught) {
499
- result = resultFor(caught);
500
- await answer(caught, scope, report);
501
- } finally {
502
- await runAfter(ran, result, logger());
503
- }
504
- };
505
- }
506
- async function gateRefusal(step) {
507
- const { Handler, dispatch } = step;
508
- const { match, payload, core } = step.args;
509
- if (match.kind === InteractionKind.Autocomplete || payload.type === InteractionType.ApplicationCommandAutocomplete) return null;
510
- const monitor = slowGateMonitor();
511
- try {
512
- await runHandlerGates(Handler, interactionGateContext(payload, core, dispatch), match.routeId ?? void 0, monitor?.observe);
513
- return null;
514
- } catch (caught) {
515
- return { caught };
516
- } finally {
517
- monitor?.report(match.routeId);
493
+ getRole(name) {
494
+ const leaf = this.leaves.get(name);
495
+ if (leaf?.type !== ApplicationCommandOptionType.Role) return null;
496
+ return this.resolved?.roles?.[leaf.value] ?? null;
518
497
  }
498
+ getAttachment(name) {
499
+ const leaf = this.leaves.get(name);
500
+ if (leaf?.type !== ApplicationCommandOptionType.Attachment) return null;
501
+ return this.resolved?.attachments?.[leaf.value] ?? null;
502
+ }
503
+ getMentionable(name) {
504
+ const leaf = this.leaves.get(name);
505
+ if (leaf?.type !== ApplicationCommandOptionType.Mentionable) return null;
506
+ const resolved = this.resolved;
507
+ return resolved?.members?.[leaf.value] ?? resolved?.users?.[leaf.value] ?? resolved?.roles?.[leaf.value] ?? null;
508
+ }
509
+ getFocused() {
510
+ for (const leaf of this.leaves.values()) if ("focused" in leaf && leaf.focused === true) return {
511
+ name: leaf.name,
512
+ value: String(leaf.value)
513
+ };
514
+ return null;
515
+ }
516
+ };
517
+ function numeric(value) {
518
+ if (typeof value === "number") return value;
519
+ if (value.trim() === "") return null;
520
+ const parsed = Number(value);
521
+ return Number.isNaN(parsed) ? null : parsed;
519
522
  }
520
523
 
521
524
  //#endregion
522
- //#region src/inputs/HttpSlashOptions.ts
523
- var HttpSlashOptions = class {
524
- leaves = /* @__PURE__ */ new Map();
525
- resolved;
526
- constructor(data) {
527
- this.resolved = data.resolved;
528
- let options = data.options ?? [];
529
- const first = options[0];
530
- if (first?.type === ApplicationCommandOptionType.SubcommandGroup) options = first.options;
531
- const second = options[0];
532
- if (second?.type === ApplicationCommandOptionType.Subcommand) options = second.options ?? [];
533
- for (const option of options) if (option.type !== ApplicationCommandOptionType.Subcommand && option.type !== ApplicationCommandOptionType.SubcommandGroup) this.leaves.set(option.name, option);
534
- }
535
- getString(name) {
536
- const leaf = this.leaves.get(name);
537
- return leaf?.type === ApplicationCommandOptionType.String ? leaf.value : null;
538
- }
539
- getInteger(name) {
540
- const leaf = this.leaves.get(name);
541
- return leaf?.type === ApplicationCommandOptionType.Integer ? numeric(leaf.value) : null;
542
- }
543
- getNumber(name) {
544
- const leaf = this.leaves.get(name);
545
- return leaf?.type === ApplicationCommandOptionType.Number ? numeric(leaf.value) : null;
546
- }
547
- getBoolean(name) {
548
- const leaf = this.leaves.get(name);
549
- return leaf?.type === ApplicationCommandOptionType.Boolean ? leaf.value : null;
550
- }
551
- getUser(name) {
552
- const leaf = this.leaves.get(name);
553
- if (leaf?.type !== ApplicationCommandOptionType.User) return null;
554
- return this.resolved?.users?.[leaf.value] ?? null;
555
- }
556
- getMember(name) {
557
- const leaf = this.leaves.get(name);
558
- if (leaf?.type !== ApplicationCommandOptionType.User) return null;
559
- return this.resolved?.members?.[leaf.value] ?? null;
560
- }
561
- getChannel(name) {
562
- const leaf = this.leaves.get(name);
563
- if (leaf?.type !== ApplicationCommandOptionType.Channel) return null;
564
- return this.resolved?.channels?.[leaf.value] ?? null;
565
- }
566
- getRole(name) {
567
- const leaf = this.leaves.get(name);
568
- if (leaf?.type !== ApplicationCommandOptionType.Role) return null;
569
- return this.resolved?.roles?.[leaf.value] ?? null;
570
- }
571
- getAttachment(name) {
572
- const leaf = this.leaves.get(name);
573
- if (leaf?.type !== ApplicationCommandOptionType.Attachment) return null;
574
- return this.resolved?.attachments?.[leaf.value] ?? null;
575
- }
576
- getMentionable(name) {
577
- const leaf = this.leaves.get(name);
578
- if (leaf?.type !== ApplicationCommandOptionType.Mentionable) return null;
579
- const resolved = this.resolved;
580
- return resolved?.members?.[leaf.value] ?? resolved?.users?.[leaf.value] ?? resolved?.roles?.[leaf.value] ?? null;
581
- }
582
- getFocused() {
583
- for (const leaf of this.leaves.values()) if ("focused" in leaf && leaf.focused === true) return {
584
- name: leaf.name,
585
- value: String(leaf.value)
586
- };
587
- return null;
588
- }
589
- };
590
- function numeric(value) {
591
- if (typeof value === "number") return value;
592
- if (value.trim() === "") return null;
593
- const parsed = Number(value);
594
- return Number.isNaN(parsed) ? null : parsed;
525
+ //#region src/dispatch/slashRouteOf.ts
526
+ function slashRouteOf(data) {
527
+ const first = data.options?.[0];
528
+ if (first?.type === ApplicationCommandOptionType.SubcommandGroup) return buildSlashRoute(data.name, first.options[0]?.name, first.name);
529
+ if (first?.type === ApplicationCommandOptionType.Subcommand) return buildSlashRoute(data.name, first.name);
530
+ return data.name;
595
531
  }
596
532
 
597
533
  //#endregion
598
- //#region src/handlers/interaction/AutocompleteHandler.ts
534
+ //#region src/handlers/interaction/SlashHandler.ts
599
535
  /**
600
- * Base class for an autocomplete handler on the HTTP transport.
536
+ * Base class for a chat-input (slash) command handler on the HTTP transport.
601
537
  *
602
- * Pass the command route(s) from the generated registry as the generic. Branch on the focused field with
603
- * `this.match`, read already-entered sibling options with `this.options`, and find which command fired
604
- * with `this.route`.
538
+ * Pass the route from the generated registry as the generic, then read `this.options` for a single
539
+ * command or `this.match` for several. Command authoring stays plain discord.js, `seedcord codegen`
540
+ * reads its `toJSON()` to type these options.
605
541
  *
606
- * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'search'`.
542
+ * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'ban'` or `'ban' | 'kick'`.
607
543
  */
608
- var AutocompleteHandler = class extends BaseHandler {
609
- constructor(event, core, dispatch) {
610
- super(event, core, dispatch, "interactions");
611
- }
544
+ var SlashHandler = class extends InteractionHandler {
545
+ resolver;
612
546
  /**
613
- * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
547
+ * The typed options for this command's route. Only the getters for kinds this command declares appear.
548
+ * Each getter's name argument is restricted to that command's options of its kind.
549
+ *
550
+ * - A required option drops the null.
551
+ *
552
+ * - Choices narrow to their literal union.
553
+ *
554
+ * - A channel option declared with `addChannelTypes` narrows to those types.
555
+ *
556
+ * Rich kinds resolve from the interaction's `resolved` data.
614
557
  */
615
- get api() {
616
- return apiFor(this.core.rest);
617
- }
618
- resolver;
619
- get reader() {
558
+ get options() {
620
559
  this.resolver ??= new HttpSlashOptions(this.event.data);
621
560
  return this.resolver;
622
561
  }
623
562
  /**
624
- * The focused option for this interaction. `value` is always the raw partial string the user is typing,
625
- * even for an integer or number option, so coerce it yourself when you need a number.
626
- */
627
- get focused() {
628
- const focused = this.reader.getFocused();
629
- if (!focused) throw new SeedcordError(SeedcordErrorCode.AutocompleteNoFocusedOption);
630
- return focused;
631
- }
632
- /**
633
- * Run the arm for the focused field. An autocomplete always dispatches by which field is focused.
634
- * Match is how you read it.
563
+ * Run the arm for whichever command fired. Use this only when the handler is registered for several
564
+ * routes.
635
565
  *
636
- * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
637
- * arm receives the focused partial value and a `respond` typed to that field's choice value. The
638
- * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
639
- * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
566
+ * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
567
+ * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
568
+ * key is a compile error.
640
569
  *
641
- * @param arms - One handler per autocompletable field, keyed by field name.
570
+ * @param arms - One handler per registered route, keyed by route string.
642
571
  * @returns The result of the arm that ran.
643
572
  */
644
573
  async match(arms) {
645
- const { name, value } = this.focused;
646
- const respond = (choices) => this.respond(choices);
647
- const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
648
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
649
- return await arm(value, respond);
650
- }
574
+ const route = slashRouteOf(this.event.data);
575
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
576
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
577
+ this.constructor.name,
578
+ "command route",
579
+ route
580
+ ]);
581
+ return await arm(this.options);
582
+ }
583
+ };
584
+
585
+ //#endregion
586
+ //#region src/handlers/interaction/ContextMenuHandler.ts
587
+ function armFor(handler, arms, name) {
588
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
589
+ if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
590
+ handler.constructor.name,
591
+ "context menu command",
592
+ name
593
+ ]);
594
+ return arm;
595
+ }
596
+ /**
597
+ * Base class for a user context-menu command handler on the HTTP transport (right-click a user).
598
+ *
599
+ * Pass the command name(s) as the generic, the same as `@UserContextMenuRoute`. Read the right-clicked user
600
+ * from `this.target` and that same user's guild member from `this.targetMember`.
601
+ *
602
+ * @typeParam Names - One or more command names from the generated user registry.
603
+ */
604
+ var UserContextMenuHandler = class extends InteractionHandler {
605
+ get target() {
606
+ const { target_id: targetId, resolved } = this.event.data;
607
+ return resolved.users[targetId];
608
+ }
609
+ /** The right-clicked user's guild member, null outside a guild. */
610
+ get targetMember() {
611
+ const { target_id: targetId, resolved } = this.event.data;
612
+ return resolved.members?.[targetId] ?? null;
613
+ }
614
+ /** Which of the registered commands fired. */
615
+ get commandName() {
616
+ return this.event.data.name;
617
+ }
618
+ /**
619
+ * Run the arm for whichever command fired. Use this only when the handler is registered for several
620
+ * names.
621
+ *
622
+ * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
623
+ * arm receives the clicked user and that user's guild member. The arms must cover every name in the
624
+ * generic. A missing name or an unknown key is a compile error.
625
+ *
626
+ * @param arms - One handler per registered command name.
627
+ * @returns The result of the arm that ran.
628
+ *
629
+ * @example
630
+ * ```ts
631
+ * \@UserContextMenuRoute('View Profile', 'Warn')
632
+ * class Moderation extends UserContextMenuHandler<'View Profile' | 'Warn'> {
633
+ * async execute() {
634
+ * await this.match({
635
+ * 'View Profile': (user) => this.reply(`Profile for ${user.username}.`),
636
+ * Warn: (user, member) => this.reply(`Warned ${member?.nick ?? user.username}.`)
637
+ * });
638
+ * }
639
+ * }
640
+ * ```
641
+ */
642
+ async match(arms) {
643
+ return await armFor(this, arms, this.event.data.name)(this.target, this.targetMember);
644
+ }
645
+ };
646
+ /**
647
+ * Base class for a message context-menu command handler on the HTTP transport (right-click a message).
648
+ *
649
+ * Pass the command name(s) as the generic, the same as `@MessageContextMenuRoute`. Read the right-clicked
650
+ * message from `this.target`.
651
+ *
652
+ * @typeParam Names - One or more command names from the generated message registry.
653
+ */
654
+ var MessageContextMenuHandler = class extends InteractionHandler {
655
+ get target() {
656
+ const { target_id: targetId, resolved } = this.event.data;
657
+ return resolved.messages[targetId];
658
+ }
659
+ /** Which of the registered commands fired. */
660
+ get commandName() {
661
+ return this.event.data.name;
662
+ }
663
+ /**
664
+ * Run the arm for whichever command fired. Use this only when the handler is registered for several
665
+ * names.
666
+ *
667
+ * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
668
+ * arm receives the clicked message. The arms must cover every name in the generic. A missing name or
669
+ * an unknown key is a compile error.
670
+ *
671
+ * @param arms - One handler per registered command name.
672
+ * @returns The result of the arm that ran.
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * \@MessageContextMenuRoute('Report Message', 'Quote Message')
677
+ * class MessageTools extends MessageContextMenuHandler<'Report Message' | 'Quote Message'> {
678
+ * async execute() {
679
+ * await this.match({
680
+ * 'Report Message': (message) => this.report(message),
681
+ * 'Quote Message': (message) => this.reply(`> ${message.content}`)
682
+ * });
683
+ * }
684
+ * }
685
+ * ```
686
+ */
687
+ async match(arms) {
688
+ return await armFor(this, arms, this.event.data.name)(this.target);
689
+ }
690
+ };
691
+
692
+ //#endregion
693
+ //#region src/handlers/interaction/AutocompleteHandler.ts
694
+ /**
695
+ * Base class for an autocomplete handler on the HTTP transport.
696
+ *
697
+ * Pass the command route(s) from the generated registry as the generic. Branch on the focused field with
698
+ * `this.match`, read already-entered sibling options with `this.options`, and find which command fired
699
+ * with `this.route`.
700
+ *
701
+ * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'search'`.
702
+ */
703
+ var AutocompleteHandler = class extends BaseHandler {
704
+ constructor(event, core, dispatch) {
705
+ super(event, core, dispatch, "interactions");
706
+ }
707
+ /**
708
+ * Typed Discord REST API (`@discordjs/core/http-only`) over `core.rest`, one instance per core.
709
+ */
710
+ get api() {
711
+ return apiFor(this.core.rest);
712
+ }
713
+ resolver;
714
+ get reader() {
715
+ this.resolver ??= new HttpSlashOptions(this.event.data);
716
+ return this.resolver;
717
+ }
718
+ /**
719
+ * The focused option for this interaction. `value` is always the raw partial string the user is typing,
720
+ * even for an integer or number option, so coerce it yourself when you need a number.
721
+ */
722
+ get focused() {
723
+ const focused = this.reader.getFocused();
724
+ if (!focused) throw new SeedcordError(SeedcordErrorCode.AutocompleteNoFocusedOption);
725
+ return focused;
726
+ }
727
+ /**
728
+ * Run the arm for the focused field. An autocomplete always dispatches by which field is focused.
729
+ * Match is how you read it.
730
+ *
731
+ * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
732
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
733
+ * arms must cover every autocompletable field. A missing field or an unknown key is a compile error.
734
+ *
735
+ * @param arms - One handler per autocompletable field, keyed by field name.
736
+ * @returns The result of the arm that ran.
737
+ * @throws A **SeedcordTypeError** with `MatchArmMissing` when the focused field has no arm. Only a stale-deployed command can send one.
738
+ */
739
+ async match(arms) {
740
+ const { name, value } = this.focused;
741
+ const respond = (choices) => this.respond(choices);
742
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
743
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
744
+ this.constructor.name,
745
+ "focused field",
746
+ name
747
+ ]);
748
+ return await arm(value, respond);
749
+ }
651
750
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
652
751
  async respond(choices) {
653
752
  await reportedWrite({
@@ -674,158 +773,558 @@ var AutocompleteHandler = class extends BaseHandler {
674
773
  };
675
774
 
676
775
  //#endregion
677
- //#region src/handlers/interaction/InteractionHandler.ts
776
+ //#region src/handlers/interaction/components/ComponentHandler.ts
678
777
  /**
679
- * Shared base the repliable interaction handlers extend.
778
+ * Shared base the customId-routed component handlers extend on the HTTP transport.
680
779
  *
681
- * Not a public entry point. Extend {@link SlashHandler}, {@link ContextMenuHandler}, {@link ButtonHandler},
682
- * {@link ModalHandler}, or one of the select menu bases instead. This class adds `showModal` on top
683
- * of the reply members.
780
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link ModalHandler}, or one of the select menu
781
+ * bases instead. This class defines the customId decode, the route matching, and the source-message
782
+ * verbs `update` and `deferUpdate`.
684
783
  *
685
- * @typeParam Event - The repliable interaction type this handler processes
784
+ * @typeParam Event - The component or modal interaction type this handler processes
785
+ * @typeParam Defs - The customId route definitions registered on the concrete handler
686
786
  */
687
- var InteractionHandler = class extends RepliableHandler$1 {
688
- constructor(event, core, dispatch) {
689
- super(event, core, dispatch);
787
+ var ComponentHandler = class extends InteractionHandler {
788
+ /** Rewrite the source message this component interaction came from. */
789
+ update(response) {
790
+ return this.sender.update(response);
791
+ }
792
+ /** Acknowledge the component without changing the source message. */
793
+ deferUpdate() {
794
+ return this.sender.deferUpdate();
795
+ }
796
+ decoded;
797
+ get route() {
798
+ this.decoded ??= decodeComponentRoute(this.constructor, this.event.data.custom_id);
799
+ return this.decoded;
690
800
  }
691
801
  /**
692
- * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
693
- * compile time (Discord forbids a modal in response to a modal).
802
+ * The decoded params of the single route this handler is registered for.
803
+ *
804
+ * Reading this decodes `this.event.data.custom_id` once and caches the result. On a handler
805
+ * registered for several routes this is `never`. Use {@link match} instead.
806
+ *
807
+ * @throws A **StaleCustomId** when the wire was encoded under an older shape. The dispatcher boundary turns it into a reply.
808
+ * @throws An **InvalidCustomId** when the wire is corrupt. The dispatcher boundary turns it into a reply.
694
809
  */
695
- showModal(modal) {
696
- return this.sender.showModal(modal);
810
+ get params() {
811
+ return this.route.params;
812
+ }
813
+ /**
814
+ * Run the arm for whichever route the component was minted from. Use this only when the handler is
815
+ * registered for several routes. A single-route handler reads `this.params` directly. On a multi-route
816
+ * handler `this.params` is `never`, so match is the only way to read the decoded params.
817
+ *
818
+ * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
819
+ * decoded params. The compiler checks that the arms cover every registered route prefix. `match`
820
+ * decodes the wire before it runs an arm.
821
+ *
822
+ * @param arms - One callback per registered route, keyed by prefix.
823
+ * @returns The result of the arm that ran.
824
+ * @throws A **SeedcordTypeError** with `MatchArmMissing` when the wire's prefix has no arm.
825
+ * @throws A **StaleCustomId** when the wire was encoded under an older shape.
826
+ * @throws An **InvalidCustomId** when the wire is corrupt.
827
+ *
828
+ * @example
829
+ * ```ts
830
+ * \@ButtonRoute(Approve, Reject)
831
+ * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
832
+ * async execute() {
833
+ * await this.match({
834
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
835
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
836
+ * });
837
+ * }
838
+ * }
839
+ * ```
840
+ */
841
+ async match(arms) {
842
+ const { prefix, params } = this.route;
843
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
844
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
845
+ this.constructor.name,
846
+ "component route",
847
+ prefix
848
+ ]);
849
+ return await arm(params);
697
850
  }
698
851
  };
699
852
 
700
853
  //#endregion
701
- //#region src/handlers/interaction/InteractionMiddleware.ts
854
+ //#region src/handlers/interaction/components/ButtonHandler.ts
702
855
  /**
703
- * Base class for interaction middleware on the HTTP transport.
856
+ * Base class for a button interaction handler on the HTTP transport.
704
857
  *
705
- * Middleware runs after the dispatcher builds the handler and before the gates, over the handler's own
706
- * reply surface. Name the kinds in the generic and list the same ones in `{ kinds }`. Several kinds give
707
- * `this.event` a union. Narrow it on `type` and `data.component_type`. A catchall reads every repliable
708
- * kind.
858
+ * Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
859
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the
860
+ * handler members, or rewrite the source message with `this.update`. Passing different definitions to the
861
+ * decorator and the generic is a compile error.
709
862
  *
710
- * @typeParam Kind - One or more {@link InteractionKind} members. Defaults to every repliable kind.
863
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
864
+ */
865
+ var ButtonHandler = class extends ComponentHandler {};
866
+
867
+ //#endregion
868
+ //#region src/handlers/interaction/components/ModalHandler.ts
869
+ /**
870
+ * Base class for a modal submit handler on the HTTP transport.
871
+ *
872
+ * Register the customId definitions this handler decodes with `@ModalRoute` and list the same ones in the
873
+ * generic. Read the submitted inputs from `this.fields`. Reply through the handler members. `showModal`
874
+ * fails compilation on this kind, because Discord forbids opening a modal in response to a modal.
875
+ *
876
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
711
877
  *
712
878
  * @example
713
879
  * ```ts
714
- * \@RegisterInteractionMiddleware({ kinds: [InteractionKind.Button] })
715
- * class Audit extends InteractionMiddleware<InteractionKind.Button> {
880
+ * \@ModalRoute(ConfigId)
881
+ * class ConfigModal extends ModalHandler<[typeof ConfigId]> {
716
882
  * async execute() {
717
- * this.logger.info(this.event.data.custom_id);
718
- * }
883
+ * const { guildId } = this.params;
884
+ * const name = this.fields.getTextInputValue('name');
885
+ * await this.reply(`saved ${name} for ${guildId}`);
886
+ * }
719
887
  * }
720
888
  * ```
721
889
  */
722
- var InteractionMiddleware = class extends RepliableHandler$1 {
723
- constructor(event, core, dispatch, sender) {
724
- super(event, core, dispatch, sender);
890
+ var ModalHandler = class extends ComponentHandler {
891
+ reader;
892
+ /** The fields this modal submitted, read by custom id. */
893
+ get fields() {
894
+ this.reader ??= new ModalFields(this.event.data);
895
+ return this.reader;
896
+ }
897
+ /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
898
+ update(response) {
899
+ this.ensureSourceMessage("update");
900
+ return super.update(response);
901
+ }
902
+ /** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
903
+ deferUpdate() {
904
+ this.ensureSourceMessage("deferUpdate");
905
+ return super.deferUpdate();
906
+ }
907
+ ensureSourceMessage(method) {
908
+ if (this.event.message) return;
909
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
725
910
  }
726
911
  };
727
912
 
728
913
  //#endregion
729
- //#region src/manifest/entries.ts
730
- function isHandlerClass(value) {
731
- if (typeof value !== "function") return false;
732
- return value.prototype instanceof InteractionHandler || value.prototype instanceof AutocompleteHandler;
733
- }
734
- function isMiddlewareClass(value) {
735
- return typeof value === "function" && value.prototype instanceof InteractionMiddleware;
736
- }
737
- function isSubscriberClass(value) {
738
- return typeof value === "function" && value.prototype instanceof Subscriber;
739
- }
740
- function wrongClass(array, value, base) {
741
- const name = typeof value === "function" ? value.name : String(value);
742
- return new SeedcordTypeError(SeedcordErrorCode.ManifestEntryWrongClass, [
743
- array,
744
- name,
745
- base
746
- ]);
747
- }
748
- function noRoutes(array, className, decorator) {
749
- return new SeedcordError(SeedcordErrorCode.ManifestEntryNoRoutes, [
750
- array,
751
- className,
752
- decorator
753
- ]);
914
+ //#region src/handlers/interaction/components/SelectMenuHandler.ts
915
+ function resolvedOf(data) {
916
+ return data.resolved ?? {};
754
917
  }
755
-
756
- //#endregion
757
- //#region src/dispatch/registerSubscribers.ts
758
- function registerSubscribers(bus, subscribers) {
759
- for (const ctor of subscribers) {
760
- if (!isSubscriberClass(ctor)) throw wrongClass("subscribers", ctor, "Subscriber");
761
- if (!Reflect.hasMetadata(SubscribeMetadataKey, ctor)) throw noRoutes("subscribers", ctor.name, "@Subscribe");
762
- bus[RegisterSubscriber](registrationFor(ctor));
918
+ /**
919
+ * Shared base the select menu handlers extend on the HTTP transport.
920
+ *
921
+ * Not a public entry point. Extend {@link StringMenuHandler}, {@link UserMenuHandler},
922
+ * {@link RoleMenuHandler}, {@link ChannelMenuHandler}, or {@link MentionableMenuHandler} instead. This
923
+ * class defines the `values` getter those bases share.
924
+ *
925
+ * @typeParam Event - The select menu interaction type this handler processes
926
+ * @typeParam Defs - The customId route definitions registered on the concrete handler
927
+ */
928
+ var SelectMenuHandler = class extends ComponentHandler {
929
+ /** The values this select picked. On every kind but the string menu they are snowflake ids. */
930
+ get values() {
931
+ return this.event.data.values ?? [];
763
932
  }
764
- }
765
-
766
- //#endregion
767
- //#region src/handlers/defaults/UnhandledAutocomplete.ts
768
- var UnhandledAutocomplete = class extends AutocompleteHandler {
769
- async execute() {
770
- await this.respond([]);
933
+ };
934
+ /**
935
+ * Base class for a string select menu handler on the HTTP transport.
936
+ *
937
+ * Register the customId definitions with `@StringMenuRoute`, list the same ones in the generic, then read
938
+ * the chosen option values from `this.values`. Read `this.params` for a single route or `this.match` for
939
+ * several. Reply through the handler members, or rewrite the source message with `this.update`.
940
+ *
941
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof TopicsId]`.
942
+ *
943
+ * @example
944
+ * ```ts
945
+ * \@StringMenuRoute(TopicsId)
946
+ * class Topics extends StringMenuHandler<[typeof TopicsId]> {
947
+ * async execute() {
948
+ * const { userId } = this.params;
949
+ * await this.update(`<@${userId}> now follows ${this.values.join(', ')}`);
950
+ * }
951
+ * }
952
+ * ```
953
+ */
954
+ var StringMenuHandler = class extends SelectMenuHandler {};
955
+ /**
956
+ * Base class for a user select menu handler on the HTTP transport.
957
+ *
958
+ * Beside `this.values` this handler declares `this.users` and `this.members`. Register the customId
959
+ * definitions with `@UserMenuRoute` and list the same ones in the generic.
960
+ *
961
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
962
+ *
963
+ * @example
964
+ * ```ts
965
+ * \@UserMenuRoute(AssignId)
966
+ * class Assign extends UserMenuHandler<[typeof AssignId]> {
967
+ * async execute() {
968
+ * const { roleId } = this.params;
969
+ * await this.reply(`assigning ${this.users.size} member(s) to <@&${roleId}>`);
970
+ * }
971
+ * }
972
+ * ```
973
+ */
974
+ var UserMenuHandler = class extends SelectMenuHandler {
975
+ /** The users this select picked. */
976
+ get users() {
977
+ return pick(this.values, resolvedOf(this.event.data).users);
978
+ }
979
+ /** The guild members behind the picked users. Discord resolves these only inside a guild. */
980
+ get members() {
981
+ return pick(this.values, resolvedOf(this.event.data).members);
771
982
  }
772
983
  };
773
-
774
- //#endregion
775
- //#region src/handlers/defaults/UnhandledRepliable.ts
776
- var UnhandledRepliable = class extends InteractionHandler {
777
- async execute() {
778
- await this.reply("Feature not implemented yet.");
984
+ /**
985
+ * Base class for a role select menu handler on the HTTP transport.
986
+ *
987
+ * Beside `this.values` this handler declares `this.roles`. Register the customId definitions with
988
+ * `@RoleMenuRoute` and list the same ones in the generic.
989
+ *
990
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof GrantId]`.
991
+ *
992
+ * @example
993
+ * ```ts
994
+ * \@RoleMenuRoute(GrantId)
995
+ * class Grant extends RoleMenuHandler<[typeof GrantId]> {
996
+ * async execute() {
997
+ * await this.update(`granting ${this.roles.size} role(s)`);
998
+ * }
999
+ * }
1000
+ * ```
1001
+ */
1002
+ var RoleMenuHandler = class extends SelectMenuHandler {
1003
+ /** The roles this select picked. */
1004
+ get roles() {
1005
+ return pick(this.values, resolvedOf(this.event.data).roles);
779
1006
  }
780
1007
  };
781
-
782
- //#endregion
783
- //#region src/dispatch/resolve.ts
784
- function commandKind(type) {
785
- switch (type) {
786
- case ApplicationCommandType.ChatInput: return InteractionKind.Slash;
787
- case ApplicationCommandType.User: return InteractionKind.UserContextMenu;
788
- case ApplicationCommandType.Message: return InteractionKind.MessageContextMenu;
789
- default: return null;
1008
+ /**
1009
+ * Base class for a channel select menu handler on the HTTP transport.
1010
+ *
1011
+ * Beside `this.values` this handler declares `this.channels`. Register the customId definitions with
1012
+ * `@ChannelMenuRoute` and list the same ones in the generic.
1013
+ *
1014
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof LogTargetId]`.
1015
+ *
1016
+ * @example
1017
+ * ```ts
1018
+ * \@ChannelMenuRoute(LogTargetId)
1019
+ * class LogTarget extends ChannelMenuHandler<[typeof LogTargetId]> {
1020
+ * async execute() {
1021
+ * await this.update(`logging to ${this.channels.size} channel(s)`);
1022
+ * }
1023
+ * }
1024
+ * ```
1025
+ */
1026
+ var ChannelMenuHandler = class extends SelectMenuHandler {
1027
+ /** The channels this select picked. */
1028
+ get channels() {
1029
+ return pick(this.values, resolvedOf(this.event.data).channels);
790
1030
  }
791
- }
792
- function componentMapKey(type) {
793
- switch (type) {
794
- case ComponentType.Button: return InteractionKind.Button;
795
- case ComponentType.StringSelect: return InteractionKind.StringMenu;
796
- case ComponentType.UserSelect: return InteractionKind.UserMenu;
797
- case ComponentType.RoleSelect: return InteractionKind.RoleMenu;
798
- case ComponentType.ChannelSelect: return InteractionKind.ChannelMenu;
799
- case ComponentType.MentionableSelect: return InteractionKind.MentionableMenu;
800
- default: return null;
1031
+ };
1032
+ /**
1033
+ * Base class for a mentionable select menu handler on the HTTP transport.
1034
+ *
1035
+ * A mentionable menu accepts users and roles together. `this.users`, `this.members`, and `this.roles`
1036
+ * can each come back empty.
1037
+ *
1038
+ * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof InviteId]`.
1039
+ *
1040
+ * @example
1041
+ * ```ts
1042
+ * \@MentionableMenuRoute(InviteId)
1043
+ * class Invite extends MentionableMenuHandler<[typeof InviteId]> {
1044
+ * async execute() {
1045
+ * await this.update(`inviting ${this.users.size} user(s) and ${this.roles.size} role(s)`);
1046
+ * }
1047
+ * }
1048
+ * ```
1049
+ */
1050
+ var MentionableMenuHandler = class extends SelectMenuHandler {
1051
+ /** The users this select picked. */
1052
+ get users() {
1053
+ return pick(this.values, resolvedOf(this.event.data).users);
801
1054
  }
802
- }
803
- function emptyRouteMaps() {
804
- return {
805
- [InteractionKind.Slash]: /* @__PURE__ */ new Map(),
806
- [InteractionKind.UserContextMenu]: /* @__PURE__ */ new Map(),
807
- [InteractionKind.MessageContextMenu]: /* @__PURE__ */ new Map(),
808
- [InteractionKind.Autocomplete]: /* @__PURE__ */ new Map(),
809
- [InteractionKind.Button]: /* @__PURE__ */ new Map(),
810
- [InteractionKind.StringMenu]: /* @__PURE__ */ new Map(),
811
- [InteractionKind.UserMenu]: /* @__PURE__ */ new Map(),
812
- [InteractionKind.RoleMenu]: /* @__PURE__ */ new Map(),
813
- [InteractionKind.ChannelMenu]: /* @__PURE__ */ new Map(),
814
- [InteractionKind.MentionableMenu]: /* @__PURE__ */ new Map(),
815
- [InteractionKind.Modal]: /* @__PURE__ */ new Map()
816
- };
817
- }
818
- function unhandled(kind, attemptedKey) {
1055
+ /** The guild members behind the picked users. Discord resolves these only inside a guild. */
1056
+ get members() {
1057
+ return pick(this.values, resolvedOf(this.event.data).members);
1058
+ }
1059
+ /** The roles this select picked. */
1060
+ get roles() {
1061
+ return pick(this.values, resolvedOf(this.event.data).roles);
1062
+ }
1063
+ };
1064
+
1065
+ //#endregion
1066
+ //#region src/subscribers/index.ts
1067
+ /**
1068
+ * Base class for an HTTP subscriber, binding `this.core` to the HTTP {@link Core}.
1069
+ *
1070
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
1071
+ */
1072
+ var Subscriber$1 = class extends Subscriber {};
1073
+ /**
1074
+ * Base class for an HTTP subscriber that delivers its event to a Discord webhook, binding
1075
+ * `this.core` to the HTTP {@link Core}.
1076
+ *
1077
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1078
+ */
1079
+ var WebhookLog$1 = class extends WebhookLog {};
1080
+
1081
+ //#endregion
1082
+ //#region src/pagination/Paginator.ts
1083
+ function contextOf(interaction, core) {
819
1084
  return {
820
- kind,
821
- routeId: null,
822
- attemptedKey,
823
- ctor: kind === InteractionKind.Autocomplete ? UnhandledAutocomplete : UnhandledRepliable
1085
+ interaction,
1086
+ user: interaction.member?.user ?? interaction.user,
1087
+ guildId: interaction.guild_id ?? null,
1088
+ core
824
1089
  };
825
1090
  }
826
1091
  /**
827
- * Matches a verified non-PING interaction to a registered handler. A known kind with no handler resolves
828
- * to the unhandled default. Null is an unrecognized payload shape, which the engine acks with a 202
1092
+ * A restart-proof paginator. Each nav button's customId encodes its full target page, so clicks are
1093
+ * idempotent and survive a restart. A persistent `@ButtonRoute` on `Handler` dispatches the clicks.
1094
+ *
1095
+ * @typeParam Item - The item type, inferred from the source.
1096
+ * @typeParam Prefix - The route prefix, inferred from `config.prefix`.
1097
+ *
1098
+ * @example
1099
+ * ```ts
1100
+ * export const Bans = new Paginator({
1101
+ * prefix: 'bans',
1102
+ * source: new ArraySource((ctx) => loadBans(ctx.guildId), { perPage: 10 }),
1103
+ * renderItem: (ban) => ban.user.username
1104
+ * });
1105
+ *
1106
+ * \@ButtonRoute(Bans.cursor)
1107
+ * export class BansNav extends Bans.Handler {}
1108
+ * ```
1109
+ */
1110
+ var Paginator = class extends PaginatorBase {
1111
+ /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
1112
+ Handler;
1113
+ constructor(config) {
1114
+ super(config);
1115
+ const pageFor = (handler, n) => this.page(handler, n);
1116
+ this.Handler = class Nav extends ButtonHandler {
1117
+ async execute() {
1118
+ await this.deferUpdate();
1119
+ const response = await pageFor(this, this.params.page);
1120
+ await this.update(response);
1121
+ }
1122
+ };
1123
+ }
1124
+ /**
1125
+ * Render a page as a {@link ReplyResponse} without sending anything.
1126
+ *
1127
+ * @param handler - The handler rendering the page, normally `this`.
1128
+ * @param n - The zero-based page.
1129
+ */
1130
+ async page(handler, n) {
1131
+ return this.buildPage(contextOf(handler.getEvent(), handler.core), n);
1132
+ }
1133
+ /**
1134
+ * Send a page through the handler's sender. The sender picks reply, edit, or followUp from its ack state.
1135
+ *
1136
+ * @param handler - The handler starting the paginator, normally `this`.
1137
+ * @param n - The zero-based page to open on.
1138
+ */
1139
+ async start(handler, n = 0) {
1140
+ const response = await this.page(handler, n);
1141
+ return handler.sender.send(response, { ephemeral: this.config.ephemeral ?? false });
1142
+ }
1143
+ };
1144
+
1145
+ //#endregion
1146
+ //#region src/pagination/sources.ts
1147
+ /**
1148
+ * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
1149
+ * page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
1150
+ * load is expensive.
1151
+ *
1152
+ * @typeParam Item - The item type, inferred from the loader's return.
1153
+ */
1154
+ var ArraySource = class extends ArraySourceBase {};
1155
+ /**
1156
+ * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1157
+ * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
1158
+ * source has no cheap total, so `totalPages` is undefined, the last button is omitted, and the indicator
1159
+ * reads "Page X".
1160
+ *
1161
+ * @typeParam Item - The item type, inferred from the fetcher's slice.
1162
+ */
1163
+ var CursorSource = class extends CursorSourceBase {};
1164
+
1165
+ //#endregion
1166
+ //#region src/gates/Gated.ts
1167
+ /**
1168
+ * Attaches gates to an HTTP interaction handler. The gates run before `execute`, and a gate refusing stops
1169
+ * the handler with the reply or drop the gate threw. Multiple gates are ANDed. A gate
1170
+ * that requires a context the handler does not provide (a button gate on a slash handler, a gate on an
1171
+ * autocomplete handler, or a `RequirePermissions([...], { in: 'guild' })` gate the http transport cannot
1172
+ * satisfy) is a compile error at this line, and the error names the gate and the handler kind. Place it
1173
+ * above the route decorator.
1174
+ *
1175
+ * Combine gates with {@link and} or {@link or} to build a single gate from multiple arms.
1176
+ *
1177
+ * @example
1178
+ * ```ts
1179
+ * import { PermissionFlagsBits } from 'discord-api-types/v10';
1180
+ *
1181
+ * \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.ManageMessages]))
1182
+ * \@SlashRoute('purge')
1183
+ * class Purge extends SlashHandler<'purge'> {
1184
+ * async execute() {
1185
+ * // both gates passed, so this runs in a guild with a caller who can moderate this channel
1186
+ * }
1187
+ * }
1188
+ * ```
1189
+ *
1190
+ * @typeParam Gates - The non-empty tuple of gates to run, in order, against the handler's context.
1191
+ * @typeParam TCtor - The decorated handler's constructor, checked against every gate's required context.
1192
+ * @param gates - One or more gates to attach, each ANDed so the handler runs only when all pass.
1193
+ * @param ctor - The handler class being decorated, rejected at compile time when a gate requires a context it lacks.
1194
+ *
1195
+ * @see {@link defineGate}
1196
+ * @see {@link defineEffectGate}
1197
+ * @decorator
1198
+ */
1199
+ function Gated(...gates) {
1200
+ return function(ctor) {
1201
+ const existing = Reflect.getMetadata(GatedMetadataKey, ctor) ?? [];
1202
+ Reflect.defineMetadata(GatedMetadataKey, [...existing, ...gates], ctor);
1203
+ };
1204
+ }
1205
+
1206
+ //#endregion
1207
+ //#region src/version.ts
1208
+ const version = "0.11.0-next.0";
1209
+
1210
+ //#endregion
1211
+ //#region src/publicKey.ts
1212
+ async function publicKeyOf(rest) {
1213
+ const fromEnv = Envapter.get("DISCORD_PUBLIC_KEY");
1214
+ if (fromEnv !== void 0) return fromEnv;
1215
+ try {
1216
+ return (await currentApplication(rest)).verify_key;
1217
+ } catch (error) {
1218
+ throw new SeedcordError(SeedcordErrorCode.CoreDiscordReadFailed, [
1219
+ "the public key",
1220
+ asError(error).message,
1221
+ `The bot reads it there because ${paint.bold("DISCORD_PUBLIC_KEY")} is unset. Check ${paint.bold("DISCORD_BOT_TOKEN")}, or set ${paint.bold("DISCORD_PUBLIC_KEY")} to the key from your application's General Information page.`
1222
+ ], { cause: error });
1223
+ }
1224
+ }
1225
+
1226
+ //#endregion
1227
+ //#region src/HttpStartupTasks.ts
1228
+ var HttpStartupTasks = class {
1229
+ host;
1230
+ service;
1231
+ commands;
1232
+ emojis;
1233
+ constructor(host, service) {
1234
+ this.host = host;
1235
+ this.service = service;
1236
+ this.commands = host.config.bot.commands.path ? new CommandRegistry(host) : void 0;
1237
+ this.emojis = new EmojiInjector(host);
1238
+ }
1239
+ registerHmr(hmr) {
1240
+ this.host.startup.addTask(StartupPhase.Configuration, "hmr-registration", () => {
1241
+ if (this.service.interactions) hmr.register(this.service.interactions);
1242
+ if (this.commands) hmr.register(this.commands);
1243
+ hmr.register(this.service.subscribers);
1244
+ for (const { instance } of attachmentsOf(this.host)) hmr.register(instance);
1245
+ return Promise.resolve();
1246
+ });
1247
+ }
1248
+ addDiscordTasks() {
1249
+ const { startup } = this.host;
1250
+ startup.addTask(StartupPhase.Login, "interactions-engine", async () => {
1251
+ this.service.prepareEngine(await publicKeyOf(this.service.rest));
1252
+ });
1253
+ const { commands } = this;
1254
+ if (!commands) {
1255
+ startup.addTask(StartupPhase.Login, "emoji-injection", () => this.emojis.init());
1256
+ return;
1257
+ }
1258
+ startup.addTask(StartupPhase.Login, "command-deploy", async (signal) => {
1259
+ await this.emojis.init();
1260
+ await commands.deploy(signal, { routes: this.service.interactions });
1261
+ }, COMMAND_DEPLOY_TIMEOUT_MS);
1262
+ }
1263
+ };
1264
+
1265
+ //#endregion
1266
+ //#region src/handlers/defaults/UnhandledAutocomplete.ts
1267
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
1268
+ async execute() {
1269
+ await this.respond([]);
1270
+ }
1271
+ };
1272
+
1273
+ //#endregion
1274
+ //#region src/handlers/defaults/UnhandledRepliable.ts
1275
+ var UnhandledRepliable = class extends InteractionHandler {
1276
+ async execute() {
1277
+ await this.reply("Feature not implemented yet.");
1278
+ }
1279
+ };
1280
+
1281
+ //#endregion
1282
+ //#region src/dispatch/resolve.ts
1283
+ function commandKind(type) {
1284
+ switch (type) {
1285
+ case ApplicationCommandType.ChatInput: return InteractionKind.Slash;
1286
+ case ApplicationCommandType.User: return InteractionKind.UserContextMenu;
1287
+ case ApplicationCommandType.Message: return InteractionKind.MessageContextMenu;
1288
+ default: return null;
1289
+ }
1290
+ }
1291
+ function componentMapKey(type) {
1292
+ switch (type) {
1293
+ case ComponentType.Button: return InteractionKind.Button;
1294
+ case ComponentType.StringSelect: return InteractionKind.StringMenu;
1295
+ case ComponentType.UserSelect: return InteractionKind.UserMenu;
1296
+ case ComponentType.RoleSelect: return InteractionKind.RoleMenu;
1297
+ case ComponentType.ChannelSelect: return InteractionKind.ChannelMenu;
1298
+ case ComponentType.MentionableSelect: return InteractionKind.MentionableMenu;
1299
+ default: return null;
1300
+ }
1301
+ }
1302
+ function emptyRouteMaps() {
1303
+ return {
1304
+ [InteractionKind.Slash]: /* @__PURE__ */ new Map(),
1305
+ [InteractionKind.UserContextMenu]: /* @__PURE__ */ new Map(),
1306
+ [InteractionKind.MessageContextMenu]: /* @__PURE__ */ new Map(),
1307
+ [InteractionKind.Autocomplete]: /* @__PURE__ */ new Map(),
1308
+ [InteractionKind.Button]: /* @__PURE__ */ new Map(),
1309
+ [InteractionKind.StringMenu]: /* @__PURE__ */ new Map(),
1310
+ [InteractionKind.UserMenu]: /* @__PURE__ */ new Map(),
1311
+ [InteractionKind.RoleMenu]: /* @__PURE__ */ new Map(),
1312
+ [InteractionKind.ChannelMenu]: /* @__PURE__ */ new Map(),
1313
+ [InteractionKind.MentionableMenu]: /* @__PURE__ */ new Map(),
1314
+ [InteractionKind.Modal]: /* @__PURE__ */ new Map()
1315
+ };
1316
+ }
1317
+ function unhandled(kind, attemptedKey) {
1318
+ return {
1319
+ kind,
1320
+ routeId: null,
1321
+ attemptedKey,
1322
+ ctor: kind === InteractionKind.Autocomplete ? UnhandledAutocomplete : UnhandledRepliable
1323
+ };
1324
+ }
1325
+ /**
1326
+ * Matches a verified non-PING interaction to a registered handler. A known kind with no handler resolves
1327
+ * to the unhandled default. Null is an unrecognized payload shape, which the engine acks with a 202
829
1328
  * without dispatching. Components and modals route by the stable customId prefix. A wire whose layout
830
1329
  * hash drifted still reaches its handler, where decode refuses with `StaleCustomId`.
831
1330
  */
@@ -868,16 +1367,16 @@ var RouteRegistry = class {
868
1367
  /**
869
1368
  * Writes every route the class declares. A class declaring none returns false.
870
1369
  *
871
- * @throws A **SeedcordError** when another class already holds one of them.
1370
+ * @throws A **SeedcordTypeError** when another class already holds one of them.
872
1371
  */
873
1372
  register(ctor, from) {
874
1373
  const writes = [];
875
1374
  for (const [kind, keys] of interactionRoutesOf(ctor)) for (const key of keys) {
876
1375
  const existing = this.owners.get(`${kind}:${key}`);
877
- if (existing && existing.ctor !== ctor) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
1376
+ if (existing && existing.ctor !== ctor) throw new SeedcordTypeError(SeedcordErrorCode.CoreDuplicateName, [
1377
+ "route",
878
1378
  `${kind}:${key}`,
879
- `${existing.ctor.name} (${existing.from})`,
880
- `${ctor.name} (${from})`
1379
+ [`${paint.bold(existing.ctor.name)} (${paint.path(existing.from)})`, `${paint.bold(ctor.name)} (${paint.path(from)})`]
881
1380
  ]);
882
1381
  writes.push({
883
1382
  kind,
@@ -917,1008 +1416,624 @@ var RouteRegistry = class {
917
1416
  };
918
1417
 
919
1418
  //#endregion
920
- //#region src/receiver/Ed25519Verifier.ts
921
- const PUBLIC_KEY_BYTES = 32;
922
- const SIGNATURE_BYTES = 64;
923
- const HEX_RADIX = 16;
924
- const HEX_PATTERN = /^[0-9a-f]+$/i;
925
- function hexToBytes(hex, byteLength) {
926
- if (hex.length !== byteLength * 2 || !HEX_PATTERN.test(hex)) return null;
927
- const bytes = new Uint8Array(byteLength);
928
- for (let index = 0; index < byteLength; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), HEX_RADIX);
929
- return bytes;
930
- }
931
- const encoder = new TextEncoder();
932
- var Ed25519Verifier = class {
933
- key;
934
- constructor(publicKeyHex) {
935
- const publicKey = hexToBytes(publicKeyHex, PUBLIC_KEY_BYTES);
936
- if (!publicKey) throw new SeedcordTypeError(SeedcordErrorCode.ConfigInvalidEnv, ["DISCORD_PUBLIC_KEY", "64 hex characters"]);
937
- this.key = crypto.subtle.importKey("raw", publicKey, "Ed25519", false, ["verify"]);
1419
+ //#region src/dispatch/InteractionDispatcher.ts
1420
+ var InteractionDispatcher = class {
1421
+ handlersDir;
1422
+ middlewaresDir;
1423
+ /** @internal */
1424
+ middlewares = new MiddlewareRegistry(interactionMiddleware);
1425
+ /** @internal */
1426
+ logger = new Logger("Interactions", { channel: "interactions" });
1427
+ routes = new RouteRegistry();
1428
+ get maps() {
1429
+ return this.routes.maps;
1430
+ }
1431
+ isInitialized = false;
1432
+ hmrHandler;
1433
+ loading = false;
1434
+ loadedHandlers = [];
1435
+ loadedMiddlewares = [];
1436
+ constructor(handlersDir, middlewaresDir) {
1437
+ this.handlersDir = handlersDir;
1438
+ this.middlewaresDir = middlewaresDir;
1439
+ if (!Envapter.isDevelopment && !Envapter.isTest) return;
1440
+ this.hmrHandler = new HmrModuleHandler({
1441
+ handlersDir,
1442
+ ...middlewaresDir && { middlewaresDir },
1443
+ isHandler: this.isHandler.bind(this),
1444
+ isMiddleware: this.isMiddleware.bind(this),
1445
+ registerHandler: this.registerHandler.bind(this),
1446
+ registerMiddleware: this.registerMiddleware.bind(this),
1447
+ unregisterHandler: this.routes.unregister.bind(this.routes),
1448
+ unregisterMiddleware: this.unregisterMiddleware.bind(this),
1449
+ getArtifacts: this.routes.routesOf.bind(this.routes),
1450
+ logger: this.logger
1451
+ });
938
1452
  }
939
- async verify(signatureHex, timestamp, body) {
940
- const signature = hexToBytes(signatureHex, SIGNATURE_BYTES);
941
- if (!signature) return false;
942
- const timestampBytes = encoder.encode(timestamp);
943
- const message = new Uint8Array(timestampBytes.length + body.length);
944
- message.set(timestampBytes, 0);
945
- message.set(body, timestampBytes.length);
946
- return crypto.subtle.verify("Ed25519", await this.key, signature, message);
1453
+ /** @internal */
1454
+ async init() {
1455
+ if (this.isInitialized) return;
1456
+ this.isInitialized = true;
1457
+ this.loading = true;
1458
+ this.loadedHandlers.length = 0;
1459
+ this.loadedMiddlewares.length = 0;
1460
+ try {
1461
+ if (this.middlewaresDir) await this.loadMiddlewares(this.middlewaresDir);
1462
+ await this.loadHandlers(this.handlersDir);
1463
+ } finally {
1464
+ this.loading = false;
1465
+ }
1466
+ this.reportLoad();
947
1467
  }
948
- };
949
-
950
- //#endregion
951
- //#region src/receiver/ReplayGuard.ts
952
- const REPLAY_WINDOW_MS = 3e5;
953
- const SWEEP_GAP_MS = 6e4;
954
- const MS_PER_SECOND = 1e3;
955
- const TIMESTAMP_PATTERN = /^\d+$/;
956
- var ReplayGuard = class {
957
- seen = /* @__PURE__ */ new Map();
958
- lastSweep = Date.now();
959
- fresh(timestampHeader) {
960
- if (!TIMESTAMP_PATTERN.test(timestampHeader)) return null;
961
- const timestampMs = Number(timestampHeader) * MS_PER_SECOND;
962
- return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS ? timestampMs : null;
1468
+ async loadHandlers(dir) {
1469
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const value of Object.values(imported)) {
1470
+ if (!this.isHandler(value)) continue;
1471
+ this.registerHandler(value, relativePath);
1472
+ this.hmrHandler?.trackHandler(fullPath, value);
1473
+ }
963
1474
  }
964
- unseen(signatureHex, timestampMs) {
965
- const now = Date.now();
966
- this.maybeSweep(now);
967
- const rejectUntil = this.seen.get(signatureHex);
968
- if (rejectUntil !== void 0 && rejectUntil >= now) return false;
969
- this.seen.set(signatureHex, timestampMs + REPLAY_WINDOW_MS);
970
- return true;
1475
+ async loadMiddlewares(dir) {
1476
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const value of Object.values(imported)) {
1477
+ if (!this.isMiddleware(value)) continue;
1478
+ this.registerMiddleware(value, relativePath);
1479
+ this.hmrHandler?.trackMiddleware(fullPath, value);
1480
+ }
971
1481
  }
972
- maybeSweep(now) {
973
- if (now - this.lastSweep < SWEEP_GAP_MS) return;
974
- this.lastSweep = now;
975
- for (const [signature, rejectUntil] of this.seen) if (rejectUntil < now) this.seen.delete(signature);
1482
+ reportLoad() {
1483
+ const { utils } = this.logger;
1484
+ if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares), "debug");
1485
+ utils.block("Loaded handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({ routes: this.routes.size })], "debug");
976
1486
  }
977
- };
978
-
979
- //#endregion
980
- //#region src/engine.ts
981
- const SIGNATURE_HEADER = "x-signature-ed25519";
982
- const TIMESTAMP_HEADER = "x-signature-timestamp";
983
- const ACCEPTED = 202;
984
- const BAD_REQUEST = 400;
985
- const UNAUTHORIZED = 401;
986
- const METHOD_NOT_ALLOWED = 405;
987
- const PING = InteractionType.Ping;
988
- const decoder = new TextDecoder();
989
- function hasInteractionType(payload) {
990
- return typeof payload === "object" && payload !== null && "type" in payload && typeof payload.type === "number";
991
- }
992
- function buildEngine(core, maps, middlewares) {
993
- if (!Envapter.has("DISCORD_PUBLIC_KEY")) throw new SeedcordError(SeedcordErrorCode.ConfigMissingEnv, ["DISCORD_PUBLIC_KEY"]);
994
- const publicKey = Envapter.getRequired("DISCORD_PUBLIC_KEY", Converters.String);
995
- const verifier = new Ed25519Verifier(publicKey);
996
- const replays = new ReplayGuard();
997
- const inFlight = /* @__PURE__ */ new Set();
998
- const logger = new Logger("Engine", { channel: "bot" });
999
- async function dispatchMatched(match, payload, ctx) {
1000
- const start = await dispatchInteraction({
1001
- match,
1002
- payload,
1003
- core,
1004
- middlewares
1005
- });
1006
- if (!start) return;
1007
- const work = start().catch(rootFault);
1008
- if (ctx) ctx.waitUntil(work);
1009
- else {
1010
- inFlight.add(work);
1011
- work.finally(() => inFlight.delete(work));
1487
+ /** @internal */
1488
+ async onHmr(event) {
1489
+ await this.hmrHandler?.handle(event);
1490
+ }
1491
+ warnUnhandledRoutes(commandLeaves) {
1492
+ this.warnMissing(commandLeaves, this.maps[InteractionKind.Slash], "Slash route", "@SlashRoute");
1493
+ }
1494
+ warnUnhandledContextMenuRoutes(leaves) {
1495
+ const user = this.maps[InteractionKind.UserContextMenu];
1496
+ const message = this.maps[InteractionKind.MessageContextMenu];
1497
+ this.warnMissing(leaves.user, user, "User context menu", "@ContextMenuRoute");
1498
+ this.warnMissing(leaves.message, message, "Message context menu", "@ContextMenuRoute");
1499
+ }
1500
+ warnMissing(names, map, label, decorator) {
1501
+ for (const name of names) {
1502
+ if (map.has(name)) continue;
1503
+ this.logger.warn(`${label} ${paint.sky.bold(name)} has no registered ${paint.bold(decorator)} handler.`);
1012
1504
  }
1013
1505
  }
1014
- function rootFault(caught) {
1015
- const error = asError(caught);
1016
- logger.error("dispatch failed past the boundary", error);
1017
- core.bus[PublishDefault]("unhandledInteractionError", { error });
1506
+ isHandler(value) {
1507
+ if (typeof value !== "function" || !Reflect.hasMetadata(InteractionMetadataKey, value)) return false;
1508
+ return value.prototype instanceof InteractionHandler || value.prototype instanceof AutocompleteHandler;
1509
+ }
1510
+ isMiddleware(value) {
1511
+ if (typeof value !== "function") return false;
1512
+ return value.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(InteractionMiddlewareMetadataKey, value);
1513
+ }
1514
+ registerMiddleware(ctor, relativePath) {
1515
+ const metadata = this.middlewares.register(ctor);
1516
+ if (!metadata || !this.loading) return;
1517
+ const scope = metadata.keys ? `${metadata.priority}, ${metadata.keys.join(", ")}` : metadata.priority;
1518
+ this.loadedMiddlewares.push({
1519
+ name: `${ctor.name} (${String(scope)})`,
1520
+ from: formatFilePath(relativePath)
1521
+ });
1018
1522
  }
1019
- const handle = async (request, ctx) => {
1020
- const [response, outcome] = await respond(request, ctx);
1021
- logger.debug(`${paint.sky.bold(request.method)} ${outcome} ${paint.amber(String(response.status))}`);
1022
- return response;
1023
- };
1024
- const respond = async (request, ctx) => {
1025
- if (request.method !== "POST") return [new Response(null, {
1026
- status: METHOD_NOT_ALLOWED,
1027
- headers: { allow: "POST" }
1028
- }), "not-post"];
1029
- const signature = request.headers.get(SIGNATURE_HEADER);
1030
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1031
- if (signature === null || timestamp === null) return [new Response(null, { status: UNAUTHORIZED }), "unsigned"];
1032
- const timestampMs = replays.fresh(timestamp);
1033
- if (timestampMs === null) return [new Response(null, { status: UNAUTHORIZED }), "stale"];
1034
- const body = new Uint8Array(await request.arrayBuffer());
1035
- if (!await verifier.verify(signature, timestamp, body)) return [new Response(null, { status: UNAUTHORIZED }), "bad-signature"];
1036
- let payload;
1037
- try {
1038
- payload = JSON.parse(decoder.decode(body));
1039
- } catch {
1040
- return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
1041
- }
1042
- if (!hasInteractionType(payload)) return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
1043
- if (payload.type === PING) return [Response.json({ type: InteractionResponseType.Pong }), "ping"];
1044
- if (!replays.unseen(signature, timestampMs)) return [new Response(null, { status: UNAUTHORIZED }), "replay"];
1045
- try {
1046
- core.bus[PublishDefault]("anyInteraction", { interaction: payload });
1047
- const match = resolve(maps, payload);
1048
- if (match) await dispatchMatched(match, payload, ctx);
1049
- } catch (caught) {
1050
- rootFault(caught);
1051
- }
1052
- return [new Response(null, { status: ACCEPTED }), "dispatched"];
1053
- };
1523
+ unregisterMiddleware(ctor) {
1524
+ this.middlewares.unregister(ctor);
1525
+ }
1526
+ registerHandler(ctor, relativePath) {
1527
+ const from = formatFilePath(relativePath);
1528
+ if (!this.routes.register(ctor, from)) return;
1529
+ if (this.loading) this.loadedHandlers.push({
1530
+ name: ctor.name,
1531
+ from
1532
+ });
1533
+ }
1534
+ };
1535
+
1536
+ //#endregion
1537
+ //#region src/gates/context.ts
1538
+ function interactionGateContext(payload, core, dispatch) {
1539
+ const member = payload.member ?? null;
1540
+ const resolved = member?.user ?? payload.user;
1054
1541
  return {
1055
- handle,
1056
- inFlight
1542
+ kind: "interaction",
1543
+ core,
1544
+ dispatch,
1545
+ interaction: payload,
1546
+ user: resolved,
1547
+ member,
1548
+ userId: resolved?.id ?? null,
1549
+ guildId: payload.guild_id ?? null,
1550
+ channelId: payload.channel?.id ?? null,
1551
+ memberRoleIds: member?.roles ?? [],
1552
+ memberPermissions: member ? BigInt(member.permissions) : null,
1553
+ appPermissions: BigInt(payload.app_permissions),
1554
+ declaredRoute: null
1057
1555
  };
1058
1556
  }
1059
1557
 
1060
1558
  //#endregion
1061
- //#region src/createSeedcord.ts
1062
- const MANIFEST_ORIGIN = "the manifest";
1063
- /**
1064
- * Builds the HTTP-interactions engine, a `(request, ctx?) => Promise<Response>` handler.
1065
- *
1066
- * The handler verifies the Ed25519 signature over the raw request bytes, rejects stale or replayed
1067
- * requests, answers a PING with an in-body PONG, and acks every other interaction with an empty 202.
1068
- * The engine dispatches a matched interaction through the manifest before the 202 goes out. Gates run
1069
- * first and the sender posts any refusal over the REST callback. `execute()` continues past the 202 under
1070
- * `ctx.waitUntil` when the caller passes one (the edge entry), and an engine-held in-flight set tracks
1071
- * it otherwise (node paths). It reads only the method, headers, and body. Mount it at any path.
1072
- *
1073
- * Reads `DISCORD_PUBLIC_KEY` and `DISCORD_BOT_TOKEN` from the environment through envapt and throws a
1074
- * `SeedcordError` when either is missing or malformed.
1075
- *
1076
- * Applies `config.logger`. An omitted `logger` resets the level and the sinks to their defaults, the
1077
- * way `installNodeDefaults` does on the node hosts.
1078
- */
1079
- function createSeedcord(config, manifest) {
1080
- Logger.configure(config.logger ?? {});
1081
- const token = validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
1082
- const core = createCore(config, token);
1083
- core.bus[RegisterDefaults]();
1084
- registerSubscribers(core.bus, manifest.subscribers);
1085
- const routes = new RouteRegistry();
1086
- for (const handler of manifest.handlers) {
1087
- if (!isHandlerClass(handler)) throw wrongClass("handlers", handler, "InteractionHandler or AutocompleteHandler");
1088
- if (!routes.register(handler, MANIFEST_ORIGIN)) throw noRoutes("handlers", handler.name, "route decorator");
1089
- }
1090
- const middlewares = new MiddlewareRegistry(interactionMiddleware);
1091
- for (const middleware of manifest.middleware) {
1092
- if (!isMiddlewareClass(middleware)) throw wrongClass("middleware", middleware, "InteractionMiddleware");
1093
- if (!middlewares.register(middleware)) throw noRoutes("middleware", middleware.name, "@RegisterInteractionMiddleware");
1094
- }
1095
- return buildEngine(core, routes.maps, middlewares).handle;
1559
+ //#region src/dispatch/reportFault.ts
1560
+ function reportFault(error, uuid, dispatch, payload, core) {
1561
+ const source = interactionSource(payload);
1562
+ const from = {
1563
+ uuid,
1564
+ dispatchId: dispatch.id,
1565
+ origin: dispatch.routeId
1566
+ };
1567
+ if (error instanceof Notice && source) core.bus[PublishDefault]("handledException", {
1568
+ denial: error,
1569
+ ...from,
1570
+ source
1571
+ });
1572
+ else core.bus[PublishDefault]("unknownException", {
1573
+ ...from,
1574
+ error,
1575
+ ...actors(payload),
1576
+ metadata: payload
1577
+ });
1096
1578
  }
1097
-
1098
- //#endregion
1099
- //#region src/inputs/pick.ts
1100
- function pick(ids, bucket) {
1101
- const found = new Collection();
1102
- if (!ids || !bucket) return found;
1103
- for (const id of ids) if (Object.hasOwn(bucket, id)) found.set(id, bucket[id]);
1104
- return found;
1579
+ function actors(payload) {
1580
+ const user = payload.member?.user ?? payload.user;
1581
+ return {
1582
+ guild: void 0,
1583
+ user: user ? {
1584
+ id: user.id,
1585
+ username: user.username
1586
+ } : void 0
1587
+ };
1588
+ }
1589
+ function interactionSource(payload) {
1590
+ const user = payload.member?.user ?? payload.user;
1591
+ if (!user) return null;
1592
+ const common = {
1593
+ kind: "interaction",
1594
+ userId: user.id,
1595
+ guildId: payload.guild_id ?? null,
1596
+ channelId: payload.channel?.id ?? null,
1597
+ interactionId: payload.id,
1598
+ raw: payload
1599
+ };
1600
+ if (payload.type === InteractionType.ApplicationCommand) {
1601
+ const { data } = payload;
1602
+ const isSlash = data.type === ApplicationCommandType.ChatInput;
1603
+ return {
1604
+ ...common,
1605
+ interactionKind: isSlash ? "slash" : "context-menu",
1606
+ command: isSlash ? slashRouteOf(data) : data.name,
1607
+ customId: null
1608
+ };
1609
+ }
1610
+ if (payload.type === InteractionType.ModalSubmit) return {
1611
+ ...common,
1612
+ interactionKind: "modal",
1613
+ command: null,
1614
+ customId: payload.data.custom_id
1615
+ };
1616
+ if (payload.type === InteractionType.MessageComponent) {
1617
+ const isButton = payload.data.component_type === ComponentType.Button;
1618
+ return {
1619
+ ...common,
1620
+ interactionKind: isButton ? "button" : "select",
1621
+ command: null,
1622
+ customId: payload.data.custom_id
1623
+ };
1624
+ }
1625
+ return null;
1105
1626
  }
1106
1627
 
1107
1628
  //#endregion
1108
- //#region src/inputs/ModalFields.ts
1109
- const FIELD_KINDS = {
1110
- [ComponentType.TextInput]: {
1111
- label: "a text input",
1112
- getter: "getTextInputValue"
1113
- },
1114
- [ComponentType.StringSelect]: {
1115
- label: "a string select",
1116
- getter: "getStringSelectValues"
1117
- },
1118
- [ComponentType.UserSelect]: {
1119
- label: "a user select",
1120
- getter: "getSelectedUsers"
1121
- },
1122
- [ComponentType.RoleSelect]: {
1123
- label: "a role select",
1124
- getter: "getSelectedRoles"
1125
- },
1126
- [ComponentType.MentionableSelect]: {
1127
- label: "a mentionable select",
1128
- getter: "getSelectedMentionables"
1129
- },
1130
- [ComponentType.ChannelSelect]: {
1131
- label: "a channel select",
1132
- getter: "getSelectedChannels"
1133
- },
1134
- [ComponentType.FileUpload]: {
1135
- label: "a file upload",
1136
- getter: "getUploadedFiles"
1137
- },
1138
- [ComponentType.RadioGroup]: {
1139
- label: "a radio group",
1140
- getter: "getRadioGroup"
1141
- },
1142
- [ComponentType.CheckboxGroup]: {
1143
- label: "a checkbox group",
1144
- getter: "getCheckboxGroup"
1145
- },
1146
- [ComponentType.Checkbox]: {
1147
- label: "a checkbox",
1148
- getter: "getCheckbox"
1629
+ //#region src/dispatch/dispatchInteraction.ts
1630
+ let dispatchLogger;
1631
+ function logger() {
1632
+ dispatchLogger ??= new Logger("Dispatcher", { channel: "interactions" });
1633
+ return dispatchLogger;
1634
+ }
1635
+ const HARMLESS_API_CODES = /* @__PURE__ */ new Set([
1636
+ RESTJSONErrorCodes.UnknownInteraction,
1637
+ RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1638
+ RESTJSONErrorCodes.UnknownMessage
1639
+ ]);
1640
+ async function sendGuarded(routeId, send) {
1641
+ try {
1642
+ await send();
1643
+ } catch (error) {
1644
+ if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
1645
+ logger().debug(`boundary send hit harmless code ${paint.amber(String(error.code))}`);
1646
+ return;
1647
+ }
1648
+ logger().error(`boundary send failed for route ${paint.sky.bold(routeId)}`, error);
1149
1649
  }
1150
- };
1151
- function nameOfChannelType(type) {
1152
- return ChannelType[type] ?? String(type);
1153
1650
  }
1154
- function listOf(values) {
1155
- return values ?? [];
1651
+ async function respondEmptyChoices(scope) {
1652
+ const telemetry = {
1653
+ bus: scope.core.bus,
1654
+ dispatch: scope.dispatch,
1655
+ interactionId: scope.payload.id
1656
+ };
1657
+ await reportedWrite(telemetry, "respond", () => scope.core.rest.post(Routes.interactionCallback(scope.payload.id, scope.payload.token), { body: {
1658
+ type: InteractionResponseType.ApplicationCommandAutocompleteResult,
1659
+ data: { choices: [] }
1660
+ } }));
1156
1661
  }
1157
- function isKind(entry, kinds) {
1158
- return kinds.includes(entry.type);
1662
+ function renderContext(scope, uuid) {
1663
+ const { dispatch } = scope;
1664
+ const developerUsername = scope.core.config.notifications?.developerUsername;
1665
+ return developerUsername === void 0 ? {
1666
+ uuid,
1667
+ dispatch
1668
+ } : {
1669
+ uuid,
1670
+ developerUsername,
1671
+ dispatch
1672
+ };
1159
1673
  }
1160
- /**
1161
- * The fields a modal carries, read by custom id.
1162
- *
1163
- * Every getter throws when no field in the modal has that custom id. It also throws when the field holds a
1164
- * kind that getter does not read. The message names the getter that does.
1165
- *
1166
- * A select getter resolves the ids its field picked against the interaction's `resolved` data and returns a
1167
- * `Collection` keyed by id. A field that picked nothing returns null. Pass `required` to throw there instead.
1168
- */
1169
- var ModalFields = class {
1170
- entries = /* @__PURE__ */ new Map();
1171
- resolved;
1172
- constructor(data) {
1173
- this.resolved = data.resolved;
1174
- for (const component of data.components ?? []) {
1175
- if ("components" in component) for (const child of component.components) this.entries.set(child.custom_id, child);
1176
- if ("component" in component) this.entries.set(component.component.custom_id, component.component);
1177
- }
1674
+ async function handleNotice(notice, uuid, scope) {
1675
+ if (notice.report) {
1676
+ logger().error(`${notice.name}: ${paint.mute(uuid)}`, notice);
1677
+ reportFault(notice, uuid, scope.dispatch, scope.payload, scope.core);
1178
1678
  }
1179
- entry(customId) {
1180
- const found = this.entries.get(customId);
1181
- if (!found) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldNotFound, [customId]);
1182
- return found;
1679
+ const { sender } = scope;
1680
+ if (!sender) {
1681
+ await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
1682
+ return;
1183
1683
  }
1184
- field(customId, kinds) {
1185
- const entry = this.entry(customId);
1186
- if (!isKind(entry, kinds)) {
1187
- const known = FIELD_KINDS[entry.type];
1188
- const args = known ? [
1189
- customId,
1190
- known.label,
1191
- known.getter
1192
- ] : [customId, `component type ${entry.type}`];
1193
- throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldWrongKind, [...args]);
1194
- }
1195
- return entry;
1684
+ const response = notice.render(renderContext(scope, uuid));
1685
+ await sendGuarded(scope.routeId, () => sender.send(response, { ephemeral: notice.ephemeral }));
1686
+ }
1687
+ async function handleRawFault(error, uuid, scope) {
1688
+ const { core, sender } = scope;
1689
+ logUnknownFault(logger(), paint.mute(uuid), error, core.config.errors);
1690
+ reportFault(error, uuid, scope.dispatch, scope.payload, core);
1691
+ if (!sender) {
1692
+ await sendGuarded(scope.routeId, () => respondEmptyChoices(scope));
1693
+ return;
1196
1694
  }
1197
- selected(customId, kinds, bucket, required) {
1198
- const { values } = this.field(customId, kinds);
1199
- const found = pick(values, bucket);
1200
- if (found.size > 0) return found;
1201
- if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
1202
- return null;
1695
+ const Override = core.config.errors?.defaultError;
1696
+ const response = (Override ? new Override(uuid) : new Fault()).render(renderContext(scope, uuid));
1697
+ await sendGuarded(scope.routeId, () => sender.send(response, { ephemeral: true }));
1698
+ }
1699
+ async function handleFault(caught, scope) {
1700
+ if (caught instanceof Silence) {
1701
+ if (caught.reason !== void 0 && (scope.core.config.errors?.logSilences ?? true)) logger().debug(`Silence: ${caught.reason}`);
1702
+ return;
1203
1703
  }
1204
- getField(customId, kind) {
1205
- return kind === void 0 ? this.entry(customId) : this.field(customId, [kind]);
1704
+ const uuid = crypto.randomUUID();
1705
+ if (caught instanceof Notice) {
1706
+ await handleNotice(caught, uuid, scope);
1707
+ return;
1206
1708
  }
1207
- getTextInputValue(customId) {
1208
- return this.field(customId, [ComponentType.TextInput]).value;
1709
+ const error = asError(caught);
1710
+ const ignore = new Set(scope.core.config.errors?.ignoreApiCodes ?? []);
1711
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
1712
+ logger().debug(`swallowed api code ${paint.amber(String(error.code))}`);
1713
+ return;
1209
1714
  }
1210
- getStringSelectValues(customId) {
1211
- return listOf(this.field(customId, [ComponentType.StringSelect]).values);
1715
+ await handleRawFault(error, uuid, scope);
1716
+ }
1717
+ function unhandledRouteId(match) {
1718
+ if (match.routeId) return match.routeId;
1719
+ const key = match.attemptedKey ?? "";
1720
+ return `${match.kind}:${key.length > 0 ? key : "unrouted"}`;
1721
+ }
1722
+ function freshScope(match, payload, core, dispatch) {
1723
+ const ref = {
1724
+ application_id: payload.application_id,
1725
+ id: payload.id,
1726
+ token: payload.token
1727
+ };
1728
+ return {
1729
+ core,
1730
+ payload,
1731
+ routeId: unhandledRouteId(match),
1732
+ dispatch,
1733
+ sender: match.kind === InteractionKind.Autocomplete ? null : new ReplySender(ref, core.rest, dispatch, core.bus)
1734
+ };
1735
+ }
1736
+ function dispatchReporter(match, payload, core, dispatchId) {
1737
+ const startedAt = performance.now();
1738
+ const queuedMs = queuedMsFor(payload.id);
1739
+ return (outcome) => {
1740
+ reportDispatch(core.bus, {
1741
+ dispatchId,
1742
+ routeId: unhandledRouteId(match),
1743
+ interactionId: payload.id,
1744
+ kind: match.kind,
1745
+ outcome,
1746
+ fallback: match.routeId === null,
1747
+ userId: (payload.member?.user ?? payload.user)?.id ?? null,
1748
+ guildId: payload.guild_id ?? null,
1749
+ startedAt,
1750
+ queuedMs
1751
+ });
1752
+ };
1753
+ }
1754
+ async function answer(caught, scope, report, outcome = outcomeFor(caught)) {
1755
+ try {
1756
+ await handleFault(caught, scope);
1757
+ } finally {
1758
+ report(outcome);
1212
1759
  }
1213
- getSelectedUsers(customId, required = false) {
1214
- const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
1215
- return this.selected(customId, kinds, this.resolved?.users, required);
1760
+ }
1761
+ async function runMiddlewares(step, kind, sender) {
1762
+ const { args, dispatch, ran } = step;
1763
+ for (const Middleware of args.middlewares.chainFor(kind)) {
1764
+ const event = args.payload;
1765
+ const instance = new Middleware(event, args.core, dispatch, sender);
1766
+ ran.push(instance);
1767
+ await instance.execute();
1216
1768
  }
1217
- /** The guild members behind the picked users. Discord resolves these only inside a guild. */
1218
- getSelectedMembers(customId) {
1219
- const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
1220
- return this.selected(customId, kinds, this.resolved?.members, false);
1769
+ }
1770
+ async function refusalBeforeHandler(step) {
1771
+ const { args, scope } = step;
1772
+ const { kind } = args.match;
1773
+ if (kind !== InteractionKind.Autocomplete && scope.sender) try {
1774
+ await runMiddlewares(step, kind, scope.sender);
1775
+ } catch (caught) {
1776
+ return { caught };
1221
1777
  }
1222
- getSelectedRoles(customId, required = false) {
1223
- const kinds = [ComponentType.RoleSelect, ComponentType.MentionableSelect];
1224
- return this.selected(customId, kinds, this.resolved?.roles, required);
1778
+ return gateRefusal(step);
1779
+ }
1780
+ async function dispatchInteraction(args) {
1781
+ const { match, payload, core } = args;
1782
+ const routeId = unhandledRouteId(match);
1783
+ const dispatch = new DispatchContext(routeId);
1784
+ const report = dispatchReporter(match, payload, core, dispatch.id);
1785
+ const Handler = match.ctor;
1786
+ logger().debug(`Processing ${paint.sky.bold(routeId)} with ${paint.mute(Handler.name)}`);
1787
+ let handler;
1788
+ try {
1789
+ handler = new Handler(payload, core, dispatch);
1790
+ } catch (caught) {
1791
+ await answer(caught, freshScope(match, payload, core, dispatch), report);
1792
+ return null;
1225
1793
  }
1226
- getSelectedChannels(customId, required = false, channelTypes = []) {
1227
- const kinds = [ComponentType.ChannelSelect];
1228
- const picked = this.selected(customId, kinds, this.resolved?.channels, required);
1229
- if (!picked || channelTypes.length === 0) return picked;
1230
- for (const found of picked.values()) {
1231
- if (channelTypes.includes(found.type)) continue;
1232
- const allowed = channelTypes.map(nameOfChannelType).join(", ");
1233
- throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldChannelType, [
1234
- customId,
1235
- nameOfChannelType(found.type),
1236
- allowed
1237
- ]);
1794
+ const scope = {
1795
+ core,
1796
+ payload,
1797
+ routeId,
1798
+ dispatch,
1799
+ sender: handler instanceof RepliableHandler$1 ? handler.sender : null
1800
+ };
1801
+ const ran = [];
1802
+ const refusal = await refusalBeforeHandler({
1803
+ args,
1804
+ Handler,
1805
+ dispatch,
1806
+ scope,
1807
+ ran
1808
+ });
1809
+ if (refusal) {
1810
+ try {
1811
+ await answer(refusal.caught, scope, report);
1812
+ } finally {
1813
+ await runAfter(ran, resultFor(refusal.caught), logger());
1238
1814
  }
1239
- return picked;
1240
- }
1241
- getSelectedMentionables(customId, required = false) {
1242
- const { values } = this.field(customId, [ComponentType.MentionableSelect]);
1243
- const resolved = this.resolved;
1244
- const picked = {
1245
- users: pick(values, resolved?.users),
1246
- members: pick(values, resolved?.members),
1247
- roles: pick(values, resolved?.roles)
1248
- };
1249
- if (picked.users.size + picked.members.size + picked.roles.size > 0) return picked;
1250
- if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
1251
1815
  return null;
1252
1816
  }
1253
- getUploadedFiles(customId, required = false) {
1254
- const kinds = [ComponentType.FileUpload];
1255
- return this.selected(customId, kinds, this.resolved?.attachments, required);
1256
- }
1257
- getRadioGroup(customId, required = false) {
1258
- const value = this.field(customId, [ComponentType.RadioGroup]).value ?? null;
1259
- if (value !== null) return value;
1260
- if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
1817
+ return async () => {
1818
+ let result = { outcome: "handled" };
1819
+ try {
1820
+ await handler.execute();
1821
+ report("handled");
1822
+ } catch (caught) {
1823
+ result = resultFor(caught);
1824
+ await answer(caught, scope, report);
1825
+ } finally {
1826
+ await runAfter(ran, result, logger());
1827
+ }
1828
+ };
1829
+ }
1830
+ async function gateRefusal(step) {
1831
+ const { Handler, dispatch } = step;
1832
+ const { match, payload, core } = step.args;
1833
+ if (match.kind === InteractionKind.Autocomplete || payload.type === InteractionType.ApplicationCommandAutocomplete) return null;
1834
+ const monitor = slowGateMonitor();
1835
+ try {
1836
+ await runHandlerGates(Handler, interactionGateContext(payload, core, dispatch), match.routeId ?? void 0, monitor?.observe);
1261
1837
  return null;
1838
+ } catch (caught) {
1839
+ return { caught };
1840
+ } finally {
1841
+ monitor?.report(match.routeId);
1262
1842
  }
1263
- getCheckboxGroup(customId) {
1264
- return listOf(this.field(customId, [ComponentType.CheckboxGroup]).values);
1265
- }
1266
- getCheckbox(customId) {
1267
- return this.field(customId, [ComponentType.Checkbox]).value;
1268
- }
1269
- };
1843
+ }
1270
1844
 
1271
1845
  //#endregion
1272
- //#region src/emojis/EmojiInjector.ts
1273
- const emojiStorage = accessorStore();
1274
- function reasonOf(error) {
1275
- return Error.isError(error) ? error.message : String(error);
1276
- }
1277
- function byName(list) {
1278
- const map = /* @__PURE__ */ new Map();
1279
- for (const entry of list) if (entry.name) map.set(entry.name, entry);
1280
- return map;
1846
+ //#region src/receiver/Ed25519Verifier.ts
1847
+ const PUBLIC_KEY_BYTES = 32;
1848
+ const SIGNATURE_BYTES = 64;
1849
+ const HEX_RADIX = 16;
1850
+ const HEX_PATTERN = /^[0-9a-f]+$/i;
1851
+ function hexToBytes(hex, byteLength) {
1852
+ if (hex.length !== byteLength * 2 || !HEX_PATTERN.test(hex)) return null;
1853
+ const bytes = new Uint8Array(byteLength);
1854
+ for (let index = 0; index < byteLength; index++) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), HEX_RADIX);
1855
+ return bytes;
1281
1856
  }
1282
- /**
1283
- * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
1284
- * Throws if you read a key the injector did not fill.
1285
- */
1286
- const Emojis = guardedAccessor("Emojis", emojiStorage);
1287
- var EmojiInjector = class {
1288
- core;
1289
- logger = new Logger("Emojis", { channel: "bot" });
1290
- constructor(core) {
1291
- this.core = core;
1292
- }
1293
- async init() {
1294
- clearStore(emojiStorage);
1295
- const configured = this.core.config.bot.emojis;
1296
- if (!configured || Object.keys(configured).length === 0) {
1297
- this.logger.debug("No emojis configured, skipping emoji injection.");
1298
- return;
1299
- }
1300
- const failures = [];
1301
- const guilds = /* @__PURE__ */ new Map();
1302
- let application;
1303
- for (const [key, value] of Object.entries(configured)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, guilds, failures);
1304
- else if (typeof value === "string") {
1305
- application ??= await this.applicationEmojis(failures);
1306
- this.resolve(key, value, application, failures, "the application");
1307
- } else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
1308
- if (failures.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
1309
- this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
1310
- }
1311
- async applicationEmojis(failures) {
1312
- try {
1313
- const appId = this.core.applicationId;
1314
- return byName((await this.core.rest.get(Routes.applicationEmojis(appId))).items);
1315
- } catch (error) {
1316
- failures.push(` - the application emojis could not be read (${reasonOf(error)})`);
1317
- return /* @__PURE__ */ new Map();
1318
- }
1319
- }
1320
- async resolveTuple(key, [name, guildId], guilds, failures) {
1321
- let guild = guilds.get(guildId);
1322
- if (!guild) {
1323
- try {
1324
- guild = byName(await this.core.rest.get(Routes.guildEmojis(guildId)));
1325
- } catch (error) {
1326
- failures.push(` - "${name}" for "${key}" targets guild ${guildId}, which could not be read (${reasonOf(error)})`);
1327
- return;
1328
- }
1329
- guilds.set(guildId, guild);
1330
- }
1331
- this.resolve(key, name, guild, failures, `guild ${guildId}`);
1332
- }
1333
- resolve(key, name, source, failures, where) {
1334
- const found = source.get(name);
1335
- if (!found?.id) {
1336
- failures.push(` - "${name}" for "${key}" was not found in ${where}`);
1337
- return;
1338
- }
1339
- emojiStorage[key] = new ResolvedEmoji(name, found.id, found.animated ?? false);
1340
- }
1341
- };
1342
-
1343
- //#endregion
1344
- //#region src/handlers/interaction/SlashHandler.ts
1345
- /**
1346
- * Base class for a chat-input (slash) command handler on the HTTP transport.
1347
- *
1348
- * Pass the route from the generated registry as the generic, then read `this.options` for a single
1349
- * command or `this.match` for several. Command authoring stays plain discord.js, `seedcord codegen`
1350
- * reads its `toJSON()` to type these options.
1351
- *
1352
- * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'ban'` or `'ban' | 'kick'`.
1353
- */
1354
- var SlashHandler = class extends InteractionHandler {
1355
- resolver;
1356
- /**
1357
- * The typed options for this command's route. Only the getters for kinds this command declares appear.
1358
- * Each getter's name argument is restricted to that command's options of its kind.
1359
- *
1360
- * - A required option drops the null.
1361
- *
1362
- * - Choices narrow to their literal union.
1363
- *
1364
- * - A channel option declared with `addChannelTypes` narrows to those types.
1365
- *
1366
- * Rich kinds resolve from the interaction's `resolved` data.
1367
- */
1368
- get options() {
1369
- this.resolver ??= new HttpSlashOptions(this.event.data);
1370
- return this.resolver;
1371
- }
1372
- /**
1373
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
1374
- * routes.
1375
- *
1376
- * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1377
- * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
1378
- * key is a compile error.
1379
- *
1380
- * @param arms - One handler per registered route, keyed by route string.
1381
- * @returns The result of the arm that ran.
1382
- */
1383
- async match(arms) {
1384
- const route = slashRouteOf(this.event.data);
1385
- const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1386
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1387
- return await arm(this.options);
1388
- }
1389
- };
1390
-
1391
- //#endregion
1392
- //#region src/handlers/interaction/ContextMenuHandler.ts
1393
- function armFor(arms, name) {
1394
- const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1395
- if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.ContextMenuMatchArmMissing, [name]);
1396
- return arm;
1397
- }
1398
- /**
1399
- * Base class for a user context-menu command handler on the HTTP transport (right-click a user).
1400
- *
1401
- * Pass the command name(s) as the generic, the same as `@UserContextMenuRoute`. Read the right-clicked user
1402
- * from `this.target` and that same user's guild member from `this.targetMember`.
1403
- *
1404
- * @typeParam Names - One or more command names from the generated user registry.
1405
- */
1406
- var UserContextMenuHandler = class extends InteractionHandler {
1407
- get target() {
1408
- const { target_id: targetId, resolved } = this.event.data;
1409
- return resolved.users[targetId];
1410
- }
1411
- /** The right-clicked user's guild member, null outside a guild. */
1412
- get targetMember() {
1413
- const { target_id: targetId, resolved } = this.event.data;
1414
- return resolved.members?.[targetId] ?? null;
1415
- }
1416
- /** Which of the registered commands fired. */
1417
- get commandName() {
1418
- return this.event.data.name;
1419
- }
1420
- /**
1421
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
1422
- * names.
1423
- *
1424
- * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
1425
- * arm receives the clicked user and that user's guild member. The arms must cover every name in the
1426
- * generic. A missing name or an unknown key is a compile error.
1427
- *
1428
- * @param arms - One handler per registered command name.
1429
- * @returns The result of the arm that ran.
1430
- *
1431
- * @example
1432
- * ```ts
1433
- * \@UserContextMenuRoute('View Profile', 'Warn')
1434
- * class Moderation extends UserContextMenuHandler<'View Profile' | 'Warn'> {
1435
- * async execute() {
1436
- * await this.match({
1437
- * 'View Profile': (user) => this.reply(`Profile for ${user.username}.`),
1438
- * Warn: (user, member) => this.reply(`Warned ${member?.nick ?? user.username}.`)
1439
- * });
1440
- * }
1441
- * }
1442
- * ```
1443
- */
1444
- async match(arms) {
1445
- return await armFor(arms, this.event.data.name)(this.target, this.targetMember);
1446
- }
1447
- };
1448
- /**
1449
- * Base class for a message context-menu command handler on the HTTP transport (right-click a message).
1450
- *
1451
- * Pass the command name(s) as the generic, the same as `@MessageContextMenuRoute`. Read the right-clicked
1452
- * message from `this.target`.
1453
- *
1454
- * @typeParam Names - One or more command names from the generated message registry.
1455
- */
1456
- var MessageContextMenuHandler = class extends InteractionHandler {
1457
- get target() {
1458
- const { target_id: targetId, resolved } = this.event.data;
1459
- return resolved.messages[targetId];
1460
- }
1461
- /** Which of the registered commands fired. */
1462
- get commandName() {
1463
- return this.event.data.name;
1464
- }
1465
- /**
1466
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
1467
- * names.
1468
- *
1469
- * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
1470
- * arm receives the clicked message. The arms must cover every name in the generic. A missing name or
1471
- * an unknown key is a compile error.
1472
- *
1473
- * @param arms - One handler per registered command name.
1474
- * @returns The result of the arm that ran.
1475
- *
1476
- * @example
1477
- * ```ts
1478
- * \@MessageContextMenuRoute('Report Message', 'Quote Message')
1479
- * class MessageTools extends MessageContextMenuHandler<'Report Message' | 'Quote Message'> {
1480
- * async execute() {
1481
- * await this.match({
1482
- * 'Report Message': (message) => this.report(message),
1483
- * 'Quote Message': (message) => this.reply(`> ${message.content}`)
1484
- * });
1485
- * }
1486
- * }
1487
- * ```
1488
- */
1489
- async match(arms) {
1490
- return await armFor(arms, this.event.data.name)(this.target);
1491
- }
1492
- };
1493
-
1494
- //#endregion
1495
- //#region src/handlers/interaction/components/ComponentHandler.ts
1496
- /**
1497
- * Shared base the customId-routed component handlers extend on the HTTP transport.
1498
- *
1499
- * Not a public entry point. Extend {@link ButtonHandler}, {@link ModalHandler}, or one of the select menu
1500
- * bases instead. This class defines the customId decode, the route matching, and the source-message
1501
- * verbs `update` and `deferUpdate`.
1502
- *
1503
- * @typeParam Event - The component or modal interaction type this handler processes
1504
- * @typeParam Defs - The customId route definitions registered on the concrete handler
1505
- */
1506
- var ComponentHandler = class extends InteractionHandler {
1507
- /** Rewrite the source message this component interaction came from. */
1508
- update(response) {
1509
- return this.sender.update(response);
1510
- }
1511
- /** Acknowledge the component without changing the source message. */
1512
- deferUpdate() {
1513
- return this.sender.deferUpdate();
1514
- }
1515
- decoded;
1516
- get route() {
1517
- this.decoded ??= decodeComponentRoute(this.constructor, this.event.data.custom_id);
1518
- return this.decoded;
1519
- }
1520
- /**
1521
- * The decoded params of the single route this handler is registered for.
1522
- *
1523
- * Reading this decodes `this.event.data.custom_id` once (cached after the first read) and throws
1524
- * `StaleCustomId` or `InvalidCustomId` when the wire no longer matches the current shape, which the
1525
- * dispatcher boundary turns into a reply. On a handler registered for several routes this is
1526
- * `never`, so use {@link match} instead.
1527
- */
1528
- get params() {
1529
- return this.route.params;
1530
- }
1531
- /**
1532
- * Run the arm for whichever route the component was minted from. Use this only when the handler is
1533
- * registered for several routes. A single-route handler reads `this.params` directly. On a multi-route
1534
- * handler `this.params` is `never`, so match is the only way to read the decoded params.
1535
- *
1536
- * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1537
- * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1538
- * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1539
- * corrupt wire throws before an arm body executes.
1540
- *
1541
- * @param arms - One callback per registered route, keyed by prefix.
1542
- * @returns The result of the arm that ran.
1543
- *
1544
- * @example
1545
- * ```ts
1546
- * \@ButtonRoute(Approve, Reject)
1547
- * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1548
- * async execute() {
1549
- * await this.match({
1550
- * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1551
- * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1552
- * });
1553
- * }
1554
- * }
1555
- * ```
1556
- */
1557
- async match(arms) {
1558
- const { prefix, params } = this.route;
1559
- const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1560
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1561
- return await arm(params);
1562
- }
1563
- };
1564
-
1565
- //#endregion
1566
- //#region src/handlers/interaction/components/ButtonHandler.ts
1567
- /**
1568
- * Base class for a button interaction handler on the HTTP transport.
1569
- *
1570
- * Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
1571
- * generic, then read `this.params` for a single route or `this.match` for several. Reply through the
1572
- * handler members, or rewrite the source message with `this.update`. Passing different definitions to the
1573
- * decorator and the generic is a compile error.
1574
- *
1575
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1576
- */
1577
- var ButtonHandler = class extends ComponentHandler {};
1578
-
1579
- //#endregion
1580
- //#region src/handlers/interaction/components/ModalHandler.ts
1581
- /**
1582
- * Base class for a modal submit handler on the HTTP transport.
1583
- *
1584
- * Register the customId definitions this handler decodes with `@ModalRoute` and list the same ones in the
1585
- * generic. Read the submitted inputs from `this.fields`. Reply through the handler members. `showModal`
1586
- * fails compilation on this kind, because Discord forbids opening a modal in response to a modal.
1587
- *
1588
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1589
- *
1590
- * @example
1591
- * ```ts
1592
- * \@ModalRoute(ConfigId)
1593
- * class ConfigModal extends ModalHandler<[typeof ConfigId]> {
1594
- * async execute() {
1595
- * const { guildId } = this.params;
1596
- * const name = this.fields.getTextInputValue('name');
1597
- * await this.reply(`saved ${name} for ${guildId}`);
1598
- * }
1599
- * }
1600
- * ```
1601
- */
1602
- var ModalHandler = class extends ComponentHandler {
1603
- reader;
1604
- /** The fields this modal submitted, read by custom id. */
1605
- get fields() {
1606
- this.reader ??= new ModalFields(this.event.data);
1607
- return this.reader;
1608
- }
1609
- /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1610
- update(response) {
1611
- this.ensureSourceMessage("update");
1612
- return super.update(response);
1613
- }
1614
- /** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
1615
- deferUpdate() {
1616
- this.ensureSourceMessage("deferUpdate");
1617
- return super.deferUpdate();
1618
- }
1619
- ensureSourceMessage(method) {
1620
- if (this.event.message) return;
1621
- throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1622
- }
1623
- };
1624
-
1625
- //#endregion
1626
- //#region src/handlers/interaction/components/SelectMenuHandler.ts
1627
- function resolvedOf(data) {
1628
- return data.resolved ?? {};
1629
- }
1630
- /**
1631
- * Shared base the select menu handlers extend on the HTTP transport.
1632
- *
1633
- * Not a public entry point. Extend {@link StringMenuHandler}, {@link UserMenuHandler},
1634
- * {@link RoleMenuHandler}, {@link ChannelMenuHandler}, or {@link MentionableMenuHandler} instead. This
1635
- * class defines the `values` getter those bases share.
1636
- *
1637
- * @typeParam Event - The select menu interaction type this handler processes
1638
- * @typeParam Defs - The customId route definitions registered on the concrete handler
1639
- */
1640
- var SelectMenuHandler = class extends ComponentHandler {
1641
- /** The values this select picked. On every kind but the string menu they are snowflake ids. */
1642
- get values() {
1643
- return this.event.data.values ?? [];
1644
- }
1645
- };
1646
- /**
1647
- * Base class for a string select menu handler on the HTTP transport.
1648
- *
1649
- * Register the customId definitions with `@StringMenuRoute`, list the same ones in the generic, then read
1650
- * the chosen option values from `this.values`. Read `this.params` for a single route or `this.match` for
1651
- * several. Reply through the handler members, or rewrite the source message with `this.update`.
1652
- *
1653
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof TopicsId]`.
1654
- *
1655
- * @example
1656
- * ```ts
1657
- * \@StringMenuRoute(TopicsId)
1658
- * class Topics extends StringMenuHandler<[typeof TopicsId]> {
1659
- * async execute() {
1660
- * const { userId } = this.params;
1661
- * await this.update(`<@${userId}> now follows ${this.values.join(', ')}`);
1662
- * }
1663
- * }
1664
- * ```
1665
- */
1666
- var StringMenuHandler = class extends SelectMenuHandler {};
1667
- /**
1668
- * Base class for a user select menu handler on the HTTP transport.
1669
- *
1670
- * Beside `this.values` this handler declares `this.users` and `this.members`. Register the customId
1671
- * definitions with `@UserMenuRoute` and list the same ones in the generic.
1672
- *
1673
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
1674
- *
1675
- * @example
1676
- * ```ts
1677
- * \@UserMenuRoute(AssignId)
1678
- * class Assign extends UserMenuHandler<[typeof AssignId]> {
1679
- * async execute() {
1680
- * const { roleId } = this.params;
1681
- * await this.reply(`assigning ${this.users.size} member(s) to <@&${roleId}>`);
1682
- * }
1683
- * }
1684
- * ```
1685
- */
1686
- var UserMenuHandler = class extends SelectMenuHandler {
1687
- /** The users this select picked. */
1688
- get users() {
1689
- return pick(this.values, resolvedOf(this.event.data).users);
1690
- }
1691
- /** The guild members behind the picked users. Discord resolves these only inside a guild. */
1692
- get members() {
1693
- return pick(this.values, resolvedOf(this.event.data).members);
1694
- }
1695
- };
1696
- /**
1697
- * Base class for a role select menu handler on the HTTP transport.
1698
- *
1699
- * Beside `this.values` this handler declares `this.roles`. Register the customId definitions with
1700
- * `@RoleMenuRoute` and list the same ones in the generic.
1701
- *
1702
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof GrantId]`.
1703
- *
1704
- * @example
1705
- * ```ts
1706
- * \@RoleMenuRoute(GrantId)
1707
- * class Grant extends RoleMenuHandler<[typeof GrantId]> {
1708
- * async execute() {
1709
- * await this.update(`granting ${this.roles.size} role(s)`);
1710
- * }
1711
- * }
1712
- * ```
1713
- */
1714
- var RoleMenuHandler = class extends SelectMenuHandler {
1715
- /** The roles this select picked. */
1716
- get roles() {
1717
- return pick(this.values, resolvedOf(this.event.data).roles);
1857
+ const encoder = new TextEncoder();
1858
+ var Ed25519Verifier = class {
1859
+ key;
1860
+ constructor(publicKeyHex) {
1861
+ const publicKey = hexToBytes(publicKeyHex, PUBLIC_KEY_BYTES);
1862
+ if (!publicKey) throw new SeedcordError(SeedcordErrorCode.ConfigInvalidEnv, ["DISCORD_PUBLIC_KEY", "64 hex characters"]);
1863
+ this.key = crypto.subtle.importKey("raw", publicKey, "Ed25519", false, ["verify"]);
1718
1864
  }
1719
- };
1720
- /**
1721
- * Base class for a channel select menu handler on the HTTP transport.
1722
- *
1723
- * Beside `this.values` this handler declares `this.channels`. Register the customId definitions with
1724
- * `@ChannelMenuRoute` and list the same ones in the generic.
1725
- *
1726
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof LogTargetId]`.
1727
- *
1728
- * @example
1729
- * ```ts
1730
- * \@ChannelMenuRoute(LogTargetId)
1731
- * class LogTarget extends ChannelMenuHandler<[typeof LogTargetId]> {
1732
- * async execute() {
1733
- * await this.update(`logging to ${this.channels.size} channel(s)`);
1734
- * }
1735
- * }
1736
- * ```
1737
- */
1738
- var ChannelMenuHandler = class extends SelectMenuHandler {
1739
- /** The channels this select picked. */
1740
- get channels() {
1741
- return pick(this.values, resolvedOf(this.event.data).channels);
1865
+ async verify(signatureHex, timestamp, body) {
1866
+ const signature = hexToBytes(signatureHex, SIGNATURE_BYTES);
1867
+ if (!signature) return false;
1868
+ const timestampBytes = encoder.encode(timestamp);
1869
+ const message = new Uint8Array(timestampBytes.length + body.length);
1870
+ message.set(timestampBytes, 0);
1871
+ message.set(body, timestampBytes.length);
1872
+ return crypto.subtle.verify("Ed25519", await this.key, signature, message);
1742
1873
  }
1743
1874
  };
1744
- /**
1745
- * Base class for a mentionable select menu handler on the HTTP transport.
1746
- *
1747
- * A mentionable menu accepts users and roles together. `this.users`, `this.members`, and `this.roles`
1748
- * can each come back empty.
1749
- *
1750
- * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof InviteId]`.
1751
- *
1752
- * @example
1753
- * ```ts
1754
- * \@MentionableMenuRoute(InviteId)
1755
- * class Invite extends MentionableMenuHandler<[typeof InviteId]> {
1756
- * async execute() {
1757
- * await this.update(`inviting ${this.users.size} user(s) and ${this.roles.size} role(s)`);
1758
- * }
1759
- * }
1760
- * ```
1761
- */
1762
- var MentionableMenuHandler = class extends SelectMenuHandler {
1763
- /** The users this select picked. */
1764
- get users() {
1765
- return pick(this.values, resolvedOf(this.event.data).users);
1875
+
1876
+ //#endregion
1877
+ //#region src/receiver/ReplayGuard.ts
1878
+ const REPLAY_WINDOW_MS = 3e5;
1879
+ const SWEEP_GAP_MS = 6e4;
1880
+ const MS_PER_SECOND = 1e3;
1881
+ const TIMESTAMP_PATTERN = /^\d+$/;
1882
+ var ReplayGuard = class {
1883
+ seen = /* @__PURE__ */ new Map();
1884
+ lastSweep = Date.now();
1885
+ fresh(timestampHeader) {
1886
+ if (!TIMESTAMP_PATTERN.test(timestampHeader)) return null;
1887
+ const timestampMs = Number(timestampHeader) * MS_PER_SECOND;
1888
+ return Math.abs(Date.now() - timestampMs) <= REPLAY_WINDOW_MS ? timestampMs : null;
1766
1889
  }
1767
- /** The guild members behind the picked users. Discord resolves these only inside a guild. */
1768
- get members() {
1769
- return pick(this.values, resolvedOf(this.event.data).members);
1890
+ unseen(signatureHex, timestampMs) {
1891
+ const now = Date.now();
1892
+ this.maybeSweep(now);
1893
+ const rejectUntil = this.seen.get(signatureHex);
1894
+ if (rejectUntil !== void 0 && rejectUntil >= now) return false;
1895
+ this.seen.set(signatureHex, timestampMs + REPLAY_WINDOW_MS);
1896
+ return true;
1770
1897
  }
1771
- /** The roles this select picked. */
1772
- get roles() {
1773
- return pick(this.values, resolvedOf(this.event.data).roles);
1898
+ maybeSweep(now) {
1899
+ if (now - this.lastSweep < SWEEP_GAP_MS) return;
1900
+ this.lastSweep = now;
1901
+ for (const [signature, rejectUntil] of this.seen) if (rejectUntil < now) this.seen.delete(signature);
1774
1902
  }
1775
1903
  };
1776
1904
 
1777
1905
  //#endregion
1778
- //#region src/subscribers/index.ts
1779
- /**
1780
- * Base class for an HTTP subscriber, binding `this.core` to the HTTP {@link Core}.
1781
- *
1782
- * @typeParam KeyOfSubscribers - The subscription key this handler receives
1783
- */
1784
- var Subscriber$1 = class extends Subscriber {};
1785
- /**
1786
- * Base class for an HTTP subscriber that delivers its event to a Discord webhook, binding
1787
- * `this.core` to the HTTP {@link Core}.
1788
- *
1789
- * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1790
- */
1791
- var WebhookLog$1 = class extends WebhookLog {};
1792
-
1793
- //#endregion
1794
- //#region src/pagination/Paginator.ts
1795
- function contextOf(interaction, core) {
1796
- return {
1797
- interaction,
1798
- user: interaction.member?.user ?? interaction.user,
1799
- guildId: interaction.guild_id ?? null,
1800
- core
1801
- };
1906
+ //#region src/engine.ts
1907
+ const SIGNATURE_HEADER = "x-signature-ed25519";
1908
+ const TIMESTAMP_HEADER = "x-signature-timestamp";
1909
+ const ACCEPTED = 202;
1910
+ const BAD_REQUEST = 400;
1911
+ const UNAUTHORIZED = 401;
1912
+ const METHOD_NOT_ALLOWED = 405;
1913
+ const PING = InteractionType.Ping;
1914
+ const decoder = new TextDecoder();
1915
+ function rejectNonPost(request) {
1916
+ if (request.method === "POST") return void 0;
1917
+ return new Response(null, {
1918
+ status: METHOD_NOT_ALLOWED,
1919
+ headers: { allow: "POST" }
1920
+ });
1802
1921
  }
1803
- /**
1804
- * A restart-proof paginator. Each nav button's customId encodes its full target page, so clicks are
1805
- * idempotent and survive a restart. A persistent `@ButtonRoute` on `Handler` dispatches the clicks.
1806
- *
1807
- * @typeParam Item - The item type, inferred from the source.
1808
- * @typeParam Prefix - The route prefix, inferred from `config.prefix`.
1809
- *
1810
- * @example
1811
- * ```ts
1812
- * export const Bans = new Paginator({
1813
- * prefix: 'bans',
1814
- * source: new ArraySource((ctx) => loadBans(ctx.guildId), { perPage: 10 }),
1815
- * renderItem: (ban) => ban.user.username
1816
- * });
1817
- *
1818
- * \@ButtonRoute(Bans.cursor)
1819
- * export class BansNav extends Bans.Handler {}
1820
- * ```
1821
- */
1822
- var Paginator = class extends PaginatorBase {
1823
- /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
1824
- Handler;
1825
- constructor(config) {
1826
- super(config);
1827
- const pageFor = (handler, n) => this.page(handler, n);
1828
- this.Handler = class Nav extends ButtonHandler {
1829
- async execute() {
1830
- await this.deferUpdate();
1831
- const response = await pageFor(this, this.params.page);
1832
- await this.update(response);
1833
- }
1834
- };
1835
- }
1836
- /**
1837
- * Render a page as a {@link ReplyResponse} without sending anything.
1838
- *
1839
- * @param handler - The handler rendering the page, normally `this`.
1840
- * @param n - The zero-based page.
1841
- */
1842
- async page(handler, n) {
1843
- return this.buildPage(contextOf(handler.getEvent(), handler.core), n);
1922
+ function hasInteractionType(payload) {
1923
+ return typeof payload === "object" && payload !== null && "type" in payload && typeof payload.type === "number";
1924
+ }
1925
+ function buildEngine(core, maps, middlewares, publicKey) {
1926
+ const verifier = new Ed25519Verifier(publicKey);
1927
+ const replays = new ReplayGuard();
1928
+ const logger = new Logger("Engine", { channel: "bot" });
1929
+ async function dispatchMatched(match, payload, ctx) {
1930
+ const start = await dispatchInteraction({
1931
+ match,
1932
+ payload,
1933
+ core,
1934
+ middlewares
1935
+ });
1936
+ if (!start) return;
1937
+ const work = start().catch(rootFault);
1938
+ ctx?.waitUntil(work);
1844
1939
  }
1845
- /**
1846
- * Send a page through the handler's sender. The sender picks reply, edit, or followUp from its ack state.
1847
- *
1848
- * @param handler - The handler starting the paginator, normally `this`.
1849
- * @param n - The zero-based page to open on.
1850
- */
1851
- async start(handler, n = 0) {
1852
- const response = await this.page(handler, n);
1853
- return handler.sender.send(response, { ephemeral: this.config.ephemeral ?? false });
1940
+ function rootFault(caught) {
1941
+ const error = asError(caught);
1942
+ logger.error("dispatch failed past the boundary", error);
1943
+ core.bus[PublishDefault]("unhandledInteractionError", { error });
1854
1944
  }
1855
- };
1945
+ const handle = async (request, ctx) => {
1946
+ const [response, outcome] = await respond(request, ctx);
1947
+ logger.debug(`${paint.sky.bold(request.method)} ${outcome} ${paint.amber(String(response.status))}`);
1948
+ return response;
1949
+ };
1950
+ const respond = async (request, ctx) => {
1951
+ const notPost = rejectNonPost(request);
1952
+ if (notPost) return [notPost, "not-post"];
1953
+ const signature = request.headers.get(SIGNATURE_HEADER);
1954
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1955
+ if (signature === null || timestamp === null) return [new Response(null, { status: UNAUTHORIZED }), "unsigned"];
1956
+ const timestampMs = replays.fresh(timestamp);
1957
+ if (timestampMs === null) return [new Response(null, { status: UNAUTHORIZED }), "stale"];
1958
+ const body = new Uint8Array(await request.arrayBuffer());
1959
+ if (!await verifier.verify(signature, timestamp, body)) return [new Response(null, { status: UNAUTHORIZED }), "bad-signature"];
1960
+ let payload;
1961
+ try {
1962
+ payload = JSON.parse(decoder.decode(body));
1963
+ } catch {
1964
+ return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
1965
+ }
1966
+ if (!hasInteractionType(payload)) return [new Response(null, { status: BAD_REQUEST }), "bad-body"];
1967
+ if (payload.type === PING) return [Response.json({ type: InteractionResponseType.Pong }), "ping"];
1968
+ if (!replays.unseen(signature, timestampMs)) return [new Response(null, { status: UNAUTHORIZED }), "replay"];
1969
+ try {
1970
+ core.bus[PublishDefault]("anyInteraction", { interaction: payload });
1971
+ const match = resolve(maps, payload);
1972
+ if (match) await dispatchMatched(match, payload, ctx);
1973
+ } catch (caught) {
1974
+ rootFault(caught);
1975
+ }
1976
+ return [new Response(null, { status: ACCEPTED }), "dispatched"];
1977
+ };
1978
+ return handle;
1979
+ }
1856
1980
 
1857
1981
  //#endregion
1858
- //#region src/pagination/sources.ts
1859
- /**
1860
- * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
1861
- * page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
1862
- * load is expensive.
1863
- *
1864
- * @typeParam Item - The item type, inferred from the loader's return.
1865
- */
1866
- var ArraySource = class extends ArraySourceBase {};
1867
- /**
1868
- * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1869
- * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
1870
- * source has no cheap total, so `totalPages` is undefined, the last button is omitted, and the indicator
1871
- * reads "Page X".
1872
- *
1873
- * @typeParam Item - The item type, inferred from the fetcher's slice.
1874
- */
1875
- var CursorSource = class extends CursorSourceBase {};
1982
+ //#region src/InteractionsService.ts
1983
+ var InteractionsService = class {
1984
+ host;
1985
+ rest;
1986
+ rateLimiter;
1987
+ bus;
1988
+ subscribers;
1989
+ interactions;
1990
+ token;
1991
+ builtEngine;
1992
+ constructor(host, config, restOptions) {
1993
+ this.host = host;
1994
+ this.rest = new REST(restOptions);
1995
+ this.rateLimiter = config.store ?? new MemoryRateLimiter();
1996
+ this.bus = new Bus(host);
1997
+ this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
1998
+ const interactions = config.bot.interactions;
1999
+ this.interactions = interactions.path ? new InteractionDispatcher(interactions.path, interactions.middlewares) : void 0;
2000
+ }
2001
+ get applicationId() {
2002
+ if (!this.token) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
2003
+ return this.token.applicationId;
2004
+ }
2005
+ authenticate() {
2006
+ this.token = readBotToken();
2007
+ this.rest.setToken(this.token.value);
2008
+ }
2009
+ get engine() {
2010
+ if (!this.builtEngine) throw new SeedcordError(SeedcordErrorCode.CoreFetchBeforeStart);
2011
+ return this.builtEngine;
2012
+ }
2013
+ prepareEngine(publicKey) {
2014
+ this.builtEngine ??= this.createEngine(publicKey);
2015
+ return this.builtEngine;
2016
+ }
2017
+ createEngine(publicKey) {
2018
+ const maps = this.interactions?.maps ?? emptyRouteMaps();
2019
+ const middlewares = this.interactions?.middlewares ?? new MiddlewareRegistry(interactionMiddleware);
2020
+ return buildEngine(this.host, maps, middlewares, publicKey);
2021
+ }
2022
+ };
1876
2023
 
1877
2024
  //#endregion
1878
- //#region src/gates/Gated.ts
1879
- /**
1880
- * Attaches gates to an HTTP interaction handler. The gates run before `execute`, and a gate refusing stops
1881
- * the handler with the reply or drop the gate threw. Multiple gates are ANDed. A gate
1882
- * that requires a context the handler does not provide (a button gate on a slash handler, a gate on an
1883
- * autocomplete handler, or a `RequirePermissions([...], { in: 'guild' })` gate the http transport cannot
1884
- * satisfy) is a compile error at this line, and the error names the gate and the handler kind. Place it
1885
- * above the route decorator.
1886
- *
1887
- * Combine gates with {@link and} or {@link or} to build a single gate from multiple arms.
1888
- *
1889
- * @example
1890
- * ```ts
1891
- * import { PermissionFlagsBits } from 'discord-api-types/v10';
1892
- *
1893
- * \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.ManageMessages]))
1894
- * \@SlashRoute('purge')
1895
- * class Purge extends SlashHandler<'purge'> {
1896
- * async execute() {
1897
- * // both gates passed, so this runs in a guild with a caller who can moderate this channel
1898
- * }
1899
- * }
1900
- * ```
1901
- *
1902
- * @typeParam Gates - The non-empty tuple of gates to run, in order, against the handler's context.
1903
- * @typeParam TCtor - The decorated handler's constructor, checked against every gate's required context.
1904
- * @param gates - One or more gates to attach, each ANDed so the handler runs only when all pass.
1905
- * @param ctor - The handler class being decorated, rejected at compile time when a gate requires a context it lacks.
1906
- *
1907
- * @see {@link defineGate}
1908
- * @see {@link defineEffectGate}
1909
- * @decorator
1910
- */
1911
- function Gated(...gates) {
1912
- return function(ctor) {
1913
- const existing = Reflect.getMetadata(GatedMetadataKey, ctor) ?? [];
1914
- Reflect.defineMetadata(GatedMetadataKey, [...existing, ...gates], ctor);
1915
- };
2025
+ //#region src/username.ts
2026
+ async function usernameOf(rest, logger) {
2027
+ try {
2028
+ const me = await rest.get(Routes.user("@me"));
2029
+ if (me.username) logger.info(`Running as ${paint.sky.bold(me.username)}`);
2030
+ return me.username;
2031
+ } catch (caught) {
2032
+ logger.warn("could not fetch the bot identity", caught);
2033
+ return;
2034
+ }
1916
2035
  }
1917
2036
 
1918
2037
  //#endregion
1919
- //#region src/version.ts
1920
- const version = "0.10.2";
1921
-
1922
- //#endregion
1923
- export { InteractionHandler as A, createSeedcord as C, isHandlerClass as D, emptyRouteMaps as E, RepliableHandler$1 as M, ReplySender as N, isMiddlewareClass as O, ModalFields as S, RouteRegistry as T, MessageContextMenuHandler as _, Paginator as a, EmojiInjector as b, ChannelMenuHandler as c, SelectMenuHandler as d, StringMenuHandler as f, ComponentHandler as g, ButtonHandler as h, CursorSource as i, AutocompleteHandler as j, InteractionMiddleware as k, MentionableMenuHandler as l, ModalHandler as m, Gated as n, Subscriber$1 as o, UserMenuHandler as p, ArraySource as r, WebhookLog$1 as s, version as t, RoleMenuHandler as u, UserContextMenuHandler as v, buildEngine as w, Emojis as x, SlashHandler as y };
1924
- //# sourceMappingURL=edge.index-DCP1BD6f.mjs.map
2038
+ export { Emojis as A, MessageContextMenuHandler as C, InteractionHandler as D, InteractionMiddleware as E, ModalFields as M, Plugin$1 as N, RepliableHandler$1 as O, AutocompleteHandler as S, SlashHandler as T, StringMenuHandler as _, HmrModuleHandler$1 as a, ButtonHandler as b, ArraySource as c, Subscriber$1 as d, WebhookLog$1 as f, SelectMenuHandler as g, RoleMenuHandler as h, HttpStartupTasks as i, installEmojis as j, ReplySender as k, CursorSource as l, MentionableMenuHandler as m, InteractionsService as n, version as o, ChannelMenuHandler as p, rejectNonPost as r, Gated as s, usernameOf as t, Paginator as u, UserMenuHandler as v, UserContextMenuHandler as w, ComponentHandler as x, ModalHandler as y };
2039
+ //# sourceMappingURL=username-3UsqzTSd.mjs.map