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