@seedcord/gateway 0.8.0 → 0.9.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,23 +1,21 @@
1
1
  import "reflect-metadata";
2
2
  import { DRAIN_TASK_TIMEOUT_MS, DRAIN_WINDOW_MS, HealthCheck, InFlight, ShutdownPhase, StartupPhase, setEnvaptBaseDir, shutdownOf } from "@seedcord/core/node/internal";
3
3
  import { WinstonConsoleSink, WinstonFileSink, installNodeConsole } from "@seedcord/logger/node";
4
- import { COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, MiddlewareRegistry, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, SubscriberLoader, accessorStore, attachmentsOf, bindBotColor, busLoggerOf, clearStore, connectDevChannel, decodeComponentRoute, deferFlags, eventResultFor, guardedAccessor, interactionMiddleware, interactionRoutesOf, isEmojiTuple, logUnknownFault, outcomeFor, pickNotice, queuedMsFor, readBotToken, reportDispatch, reportedWrite, resultFor, runAfter, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
4
+ import { AutocompleteResponder, COMMAND_DEPLOY_TIMEOUT_MS, CommandRegistry, DispatchContext, EventMetadataKey, EventMiddlewareMetadataKey, FaultBoundary, FixedScope, GatedMetadataKey, HandlerLoader, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, InteractionRun, MiddlewareRegistry, NoticeCard, OnceTracker, PublishDefault, RouteRegistry, SubscriberLoader, accessorStore, assertUniqueKeys, attachmentsOf, bindBotColor, busLoggerOf, clearStore, componentKindOf, connectDevChannel, decodeComponentRoute, deferFlags, eventResultFor, guardedAccessor, interactionFaultSource, interactionMiddleware, isEmojiTuple, matchRoute, readBotToken, resultFor, runAfter, runCase, runHandlerGates, sendFlags } from "@seedcord/core/internal";
5
5
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
6
6
  import { SeedcordError, SeedcordTypeError, throwSingleOrAggregate } from "@seedcord/errors/internal";
7
- import { ArraySourceBase, BaseHandler as BaseHandler$1, BaseReplySender, BuilderComponent, Bus, CursorSourceBase, CustomId, DispatchContext, Fault, InteractionKind, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, ResolvedEmoji, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
7
+ import { ArraySourceBase, BaseHandler as BaseHandler$1, BaseReplySender, BuilderComponent, CursorSourceBase, CustomId, InteractionKind, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, ResolvedEmoji, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
8
+ import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, FormattingPatterns, GatewayIntentBits, Guild, GuildMember, IntentsBitField, InteractionContextType, Message, MessageFlags, PermissionFlagsBits, Role, User } from "discord.js";
8
9
  import { Logger } from "@seedcord/logger";
9
- import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, GatewayIntentBits, Guild, GuildMember, IntentsBitField, InteractionContextType, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, User } from "discord.js";
10
10
  import { ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
11
- import { asError, buildSlashRoute, isStringArray } from "@seedcord/utils/internal";
12
11
  import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
13
12
  import { CoordinatedShutdown, CoordinatedStartup, ServerHost } from "@seedcord/core/node";
14
- import { MemoryRateLimiter } from "@seedcord/rate-limiter";
15
13
  import { HostAugmentTarget, HostVersion, SeedcordBrand } from "@seedcord/types/internal";
16
14
  import { Envapter } from "envapt";
17
- import { HmrModuleHandler, HmrModuleHandler as HmrModuleHandler$1 } from "@seedcord/core/hmr";
18
- import { formatFilePath, hasKeys, importFiles } from "@seedcord/utils";
19
- import * as crypto from "node:crypto";
15
+ import { hasKeys } from "@seedcord/utils";
16
+ import { asError, buildSlashRoute } from "@seedcord/utils/internal";
20
17
  import { prefixOf } from "@seedcord/custom-id";
18
+ import { HmrModuleHandler } from "@seedcord/core/hmr";
21
19
 
22
20
  export * from "@seedcord/errors"
23
21
 
@@ -25,8 +23,6 @@ export * from "@seedcord/core"
25
23
 
26
24
  export * from "@seedcord/logger"
27
25
 
28
- export * from "@seedcord/rate-limiter"
29
-
30
26
  export * from "@seedcord/utils"
31
27
 
32
28
  export * from "@seedcord/event-emitter"
@@ -82,12 +78,12 @@ installNodeConsole();
82
78
  */
83
79
  function RegisterEvent(...defs) {
84
80
  return function(constructor) {
85
- const existing = Reflect.getMetadata(EventMetadataKey, constructor) ?? [];
86
81
  const entries = defs.map(([event, options]) => ({
87
82
  event,
88
83
  frequency: options?.frequency ?? "on"
89
84
  }));
90
- Reflect.defineMetadata(EventMetadataKey, [...existing, ...entries], constructor);
85
+ assertUniqueKeys("@RegisterEvent", constructor, entries.map((entry) => entry.event));
86
+ Reflect.defineMetadata(EventMetadataKey, entries, constructor);
91
87
  };
92
88
  }
93
89
 
@@ -97,10 +93,10 @@ function RegisterEvent(...defs) {
97
93
  * Attaches gates to a handler. The gates run before `execute`, and a gate refusing stops the handler with
98
94
  * the reply or drop the gate threw. Multiple gates are ANDed, so each must pass. A gate that requires a
99
95
  * context the handler does not provide (a button gate on a slash handler, an interaction gate on an event
100
- * handler) is a compile error at this line, and the error names the gate and the handler kind. Place it
96
+ * handler) is a compile error at this line, and the error message shows the gate and the handler kind. Place it
101
97
  * above the route decorator.
102
98
  *
103
- * Combine gates with {@link and} or {@link or} to build a single gate from multiple arms.
99
+ * Combine gates into a single gate with {@link and} or {@link or}.
104
100
  *
105
101
  * @example
106
102
  * ```ts
@@ -193,19 +189,55 @@ const IgnoreBots = defineGate("IgnoreBots", (ctx) => {
193
189
  if (ctx.user?.bot) throw new Silence("actor is a bot");
194
190
  });
195
191
 
192
+ //#endregion
193
+ //#region src/bot/gates/catalog/MentionPrefix.ts
194
+ function startsWithBotMention(message) {
195
+ const match = FormattingPatterns.User.exec(message.content?.trimStart() ?? "");
196
+ return match?.index === 0 && match.groups?.id === message.client.user.id;
197
+ }
198
+ /**
199
+ * Passes only a message that starts with a mention of the bot, like `@bot ping`. It ignores every other message
200
+ * with a {@link Silence}, including a reply that pings the bot, a role mention, and a mention later in the text.
201
+ *
202
+ * On `messageUpdate` it passes only when the edit adds the mention. An edit that keeps the mention doesn't run the
203
+ * handler again. With `Partials.Message` on, discord.js also emits an edit to a message it didn't cache. That edit
204
+ * runs the handler again. Attach the gate without calling it, since it doesn't take options.
205
+ *
206
+ * A handler behind this gate can read `content` without the Message Content intent, because Discord sends the text of
207
+ * a message that mentions the bot.
208
+ *
209
+ * @see {@link Gated}
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * import { Events } from 'discord.js';
214
+ *
215
+ * \@Gated(IgnoreBots, MentionPrefix)
216
+ * \@RegisterEvent([Events.MessageCreate], [Events.MessageUpdate])
217
+ * class MentionCommands extends EventHandler<Events.MessageCreate | Events.MessageUpdate> {
218
+ * async execute() {
219
+ * // ...
220
+ * }
221
+ * }
222
+ * ```
223
+ */
224
+ const MentionPrefix = defineGate("MentionPrefix", ({ payload }) => {
225
+ if (!(payload.length === 1 ? startsWithBotMention(payload[0]) : !startsWithBotMention(payload[0]) && startsWithBotMention(payload[1]))) throw new Silence("message does not start with a mention of the bot");
226
+ });
227
+
196
228
  //#endregion
197
229
  //#region src/bot/notices/index.ts
198
- var NotNsfw = class extends GateNotice {
199
- constructor(message = "This can only be used in an age-restricted channel.") {
200
- super(message);
230
+ var NotNsfw = class extends Notice {
231
+ constructor() {
232
+ super("This can only be used in an age-restricted channel.");
201
233
  this.summary = "be in an age-restricted channel";
202
234
  }
203
235
  };
204
236
  var RoleHigherThanMe = class extends Notice {
205
237
  role;
206
238
  botRole;
207
- constructor(message, role, botRole) {
208
- super(message);
239
+ constructor(role, botRole) {
240
+ super("Role is higher than me");
209
241
  this.role = role;
210
242
  this.botRole = botRole;
211
243
  }
@@ -214,11 +246,13 @@ var RoleHigherThanMe = class extends Notice {
214
246
  }
215
247
  };
216
248
  var CannotAssignBotRole = class extends Notice {
217
- constructor(message = "I cannot assign a managed role.") {
218
- super(message);
249
+ role;
250
+ constructor(role) {
251
+ super(`Cannot assign managed role ${role.name}`);
252
+ this.role = role;
219
253
  }
220
254
  render() {
221
- return { components: [new NoticeCard("I cannot assign a managed role.").component] };
255
+ return { components: [new NoticeCard(`I cannot assign <@&${this.role.id}>, a managed role.`).component] };
222
256
  }
223
257
  };
224
258
 
@@ -230,11 +264,11 @@ function channelIsNsfw(channel) {
230
264
  return "nsfw" in channel ? channel.nsfw : false;
231
265
  }
232
266
  /**
233
- * Requires an age-restricted channel, else refuses. Pass {@link GateNoticeOptions} to reword or replace the refusal.
267
+ * Requires an age-restricted channel, else refuses. Pass a Notice class as `notice` to replace the refusal.
234
268
  *
235
269
  * A thread inherits its parent channel's nsfw flag. ModalSubmit has no reliable channel, so it is excluded.
236
270
  *
237
- * @param options - Reword the default refusal with `message`, or replace it with `notice`.
271
+ * @param options - Replace the refusal with a Notice class as `notice`.
238
272
  *
239
273
  * @see {@link Gated}
240
274
  *
@@ -252,7 +286,7 @@ function channelIsNsfw(channel) {
252
286
  function Nsfw(options) {
253
287
  return defineGate("Nsfw", (ctx) => {
254
288
  if (channelIsNsfw(ctx.interaction.channel)) return;
255
- throw pickNotice(options, (message) => new NotNsfw(message));
289
+ throw new ((options?.notice) ?? NotNsfw)();
256
290
  });
257
291
  }
258
292
 
@@ -353,57 +387,68 @@ function mentionFor(subject) {
353
387
 
354
388
  //#endregion
355
389
  //#region src/bot/utilities/permissions/checkPermissions.ts
356
- function checkPermissions(a, b, c, d, e) {
357
- const { for: pFor, in: pIn, scope, inverse = false, missingNotice, dangerousNotice } = a instanceof Role || a instanceof GuildMember ? {
358
- for: a,
359
- in: b,
360
- scope: c,
361
- inverse: d ?? false,
362
- ...e
363
- } : a;
364
- const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
390
+ /**
391
+ * Checks a {@link Role} or a {@link GuildMember} against the base permission set from roles, or against a
392
+ * channel's set when `in` is given. Refuses when a permission in `scope` is missing, or with `inverse` when
393
+ * one is present.
394
+ *
395
+ * @throws A {@link Notice} when the check refuses. It lists the missing or present permissions.
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * import { PermissionFlagsBits } from 'discord.js';
400
+ * import { checkPermissions } from '@seedcord/gateway';
401
+ *
402
+ * checkPermissions({ for: member, scope: [PermissionFlagsBits.KickMembers] });
403
+ * checkPermissions({ for: role, in: channel, scope: [PermissionFlagsBits.SendMessages] });
404
+ * ```
405
+ *
406
+ * @example
407
+ * ```ts
408
+ * // refuse when the member holds Administrator, with your own Notice class
409
+ * checkPermissions({ for: member, scope: [PermissionFlagsBits.Administrator], inverse: true, notice: TooPowerful });
410
+ * ```
411
+ */
412
+ function checkPermissions({ for: target, in: channel, scope, inverse = false, notice }) {
413
+ const held = channel ? channel.permissionsFor(target, true) : target.permissions;
365
414
  assertPermissions({
366
- subject: mentionFor(pFor),
367
- permissions: perms.bitfield,
415
+ subject: mentionFor(target),
416
+ permissions: held.bitfield,
368
417
  scope,
369
418
  inverse,
370
- ...missingNotice && { missingNotice },
371
- ...dangerousNotice && { dangerousNotice }
419
+ notice
372
420
  });
373
421
  }
374
422
 
375
423
  //#endregion
376
424
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
425
+ function botMemberOf(guild) {
426
+ const me = guild.members.me;
427
+ if (!me) throw new SeedcordError(SeedcordErrorCode.CoreBotMemberMissing, [guild.id]);
428
+ return me;
429
+ }
377
430
  /**
378
- * Checks the bot's own permissions in a {@link Guild} or {@link TextChannel}. Refuses when a permission in
379
- * `scope` is missing, or with `inverse` when one is present. When the bot member is uncached, the refusal
380
- * reports every permission in `scope` as missing.
431
+ * Checks the bot's own permissions in a {@link Guild}, or in a channel or thread. Refuses when a permission
432
+ * in `scope` is missing, or with `inverse` when one is present.
433
+ *
434
+ * @throws A {@link Notice} when the check refuses. It lists the missing or present permissions.
435
+ * @throws A **SeedcordError** when the guild's member cache doesn't hold the bot's member.
381
436
  *
382
437
  * @example
383
438
  * ```ts
384
439
  * import { PermissionFlagsBits } from 'discord.js';
385
440
  * import { checkBotPermissions } from '@seedcord/gateway';
386
441
  *
387
- * checkBotPermissions(textChannel, [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel]);
442
+ * checkBotPermissions({ in: channel, scope: [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel] });
388
443
  * ```
389
444
  */
390
- function checkBotPermissions(target, scope, inverse = false, errors) {
391
- if (target instanceof Guild) {
392
- const me = target.members.me;
393
- if (!me) {
394
- const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
395
- throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
396
- }
397
- checkPermissions(me, target, scope, inverse, errors);
398
- return;
399
- }
400
- const me = target.guild.members.me;
401
- if (!me) {
402
- new Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
403
- const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
404
- throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
405
- }
406
- checkPermissions(me, target, scope, inverse, errors);
445
+ function checkBotPermissions({ in: where, ...check }) {
446
+ const [guild, channel] = where instanceof Guild ? [where, void 0] : [where.guild, where];
447
+ checkPermissions({
448
+ for: botMemberOf(guild),
449
+ in: channel,
450
+ ...check
451
+ });
407
452
  }
408
453
 
409
454
  //#endregion
@@ -414,42 +459,31 @@ function botRoleOf(guild) {
414
459
  return botRole;
415
460
  }
416
461
  /**
417
- * Validates that the bot can assign a target role. Refuses when the target role is above the bot's, is managed, or the bot lacks Manage Roles.
462
+ * Validates that the bot can assign a role. Refuses when the role is at or above the bot's, when an
463
+ * integration manages it, or when the bot lacks Manage Roles.
418
464
  *
419
- * @param roleOrOptions - Target role or complete options for the check
420
465
  * @throws A {@link Notice} when the check refuses.
421
- * @throws A **SeedcordError** when the bot has no role in the guild's role cache.
466
+ * @throws A **SeedcordError** when the guild's caches don't hold the bot's role or member.
422
467
  *
423
468
  * @example
424
469
  * ```ts
425
470
  * import { hasPermsToAssign } from '@seedcord/gateway';
426
471
  *
427
- * // Check if the bot can assign a specific role
428
- * hasPermsToAssign(role);
472
+ * hasPermsToAssign({ role });
429
473
  *
430
- * // Check with custom error handling
431
- * hasPermsToAssign({
432
- * targetRole: role,
433
- * noticeOverrides: {
434
- * higherNotice: CustomHigherNotice,
435
- * managedNotice: CustomManagedNotice,
436
- * missingNotice: CustomMissingPermissionsNotice
437
- * }
438
- * });
474
+ * // with your own Notice classes
475
+ * hasPermsToAssign({ role, higher: RoleTooHigh, managed: RoleIsManaged, missing: CannotManageRoles });
439
476
  * ```
440
477
  */
441
- function hasPermsToAssign(roleOrOptions) {
442
- const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof Role ? {
443
- targetRole: roleOrOptions,
444
- noticeOverrides: void 0
445
- } : roleOrOptions;
446
- const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
447
- const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
448
- const MissingNotice = errors?.missingNotice ?? MissingPermissions;
449
- const botRole = botRoleOf(targetRole.guild);
450
- if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
451
- if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
452
- checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
478
+ function hasPermsToAssign({ role, higher, managed, missing }) {
479
+ const botRole = botRoleOf(role.guild);
480
+ if (role.comparePositionTo(botRole) >= 0) throw new (higher ?? RoleHigherThanMe)(role, botRole);
481
+ if (role.managed) throw new (managed ?? CannotAssignBotRole)(role);
482
+ checkBotPermissions({
483
+ in: role.guild,
484
+ scope: [PermissionFlagsBits.ManageRoles],
485
+ notice: missing
486
+ });
453
487
  }
454
488
 
455
489
  //#endregion
@@ -514,126 +548,6 @@ async function getConfirmation(handler, prompt, options) {
514
548
  return confirmed;
515
549
  }
516
550
 
517
- //#endregion
518
- //#region src/bot/ReplySender.ts
519
- function isReplyFile(file) {
520
- return typeof file === "object" && "data" in file && ArrayBuffer.isView(file.data);
521
- }
522
- function toDjsFile(file) {
523
- if (!isReplyFile(file)) return file;
524
- return {
525
- attachment: Buffer.from(file.data),
526
- name: file.name,
527
- ...file.description !== void 0 && { description: file.description },
528
- ...file.title !== void 0 && { title: file.title }
529
- };
530
- }
531
- function seedState(interaction) {
532
- if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
533
- if (interaction.replied) return "replied";
534
- if (interaction.deferred) return "deferred-reply";
535
- return "unacked";
536
- }
537
- /** Writes interaction responses through discord.js. The repliable handler bases and the dispatcher construct it. */
538
- var ReplySender = class extends BaseReplySender {
539
- interaction;
540
- constructor(interaction, dispatch, bus) {
541
- super({
542
- bus,
543
- dispatch,
544
- interactionId: interaction.id
545
- }, seedState(interaction));
546
- this.interaction = interaction;
547
- }
548
- async writeReply(response, opts) {
549
- const result = await this.interaction.reply({
550
- ...this.replyOptions(response, sendFlags(opts)),
551
- withResponse: true
552
- });
553
- return this.createdMessage(result);
554
- }
555
- async writeDefer(opts) {
556
- await this.interaction.deferReply({ flags: deferFlags(opts) });
557
- }
558
- async writeDeferUpdate() {
559
- await this.sourceInteraction("deferUpdate").deferUpdate();
560
- }
561
- async writeUpdate(response) {
562
- const result = await this.sourceInteraction("update").update({
563
- ...this.updateOptions(response),
564
- withResponse: true
565
- });
566
- return this.createdMessage(result);
567
- }
568
- async writeFollowUp(response, opts) {
569
- return await this.interaction.followUp(this.replyOptions(response, sendFlags(opts)));
570
- }
571
- async writeEditOriginal(response) {
572
- return await this.interaction.editReply(this.editOptions(response));
573
- }
574
- async writeEditTarget(targetId, response) {
575
- return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
576
- }
577
- async writeDeleteOriginal() {
578
- await this.interaction.deleteReply();
579
- }
580
- async writeDeleteTarget(targetId) {
581
- await this.interaction.deleteReply(targetId);
582
- }
583
- async writeModal(data) {
584
- const { interaction } = this;
585
- if (interaction.isModalSubmit()) this.rejectModalSubmit();
586
- await interaction.showModal(data);
587
- }
588
- guardModalSource() {
589
- if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
590
- }
591
- rejectModalSubmit() {
592
- throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
593
- "showModal",
594
- "this interaction is a modal submit",
595
- "A modal cannot open another modal.",
596
- this.routeId
597
- ]);
598
- }
599
- sourceInteraction(method) {
600
- const { interaction } = this;
601
- if (interaction.isMessageComponent()) return interaction;
602
- if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
603
- throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
604
- }
605
- replyOptions(response, flags) {
606
- const reply = this.serialize(response);
607
- return {
608
- components: reply.components,
609
- flags,
610
- ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
611
- ...reply.files && { files: reply.files.map(toDjsFile) }
612
- };
613
- }
614
- editOptions(response) {
615
- const reply = this.serialize(response);
616
- return {
617
- components: reply.components,
618
- flags: MessageFlags.IsComponentsV2,
619
- ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
620
- ...reply.files && { files: reply.files.map(toDjsFile) }
621
- };
622
- }
623
- updateOptions(response) {
624
- const reply = this.serialize(response);
625
- return {
626
- components: reply.components,
627
- flags: MessageFlags.IsComponentsV2,
628
- ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
629
- ...reply.files && { files: reply.files.map(toDjsFile) }
630
- };
631
- }
632
- createdMessage(result) {
633
- return result.resource?.message ?? void 0;
634
- }
635
- };
636
-
637
551
  //#endregion
638
552
  //#region src/handlers/RepliableHandler.ts
639
553
  /**
@@ -650,8 +564,9 @@ var RepliableHandler = class extends RepliableHandler$1 {
650
564
  getEvent() {
651
565
  return this.event;
652
566
  }
653
- buildSender(event, core, dispatch) {
654
- return new ReplySender(event, dispatch, core.bus);
567
+ /** The user who sent this interaction. */
568
+ get user() {
569
+ return this.event.user;
655
570
  }
656
571
  };
657
572
 
@@ -694,8 +609,8 @@ var InteractionMiddleware = class extends RepliableHandler {
694
609
  * @typeParam Repliable - The interaction type this handler processes
695
610
  */
696
611
  var InteractionHandler = class extends RepliableHandler {
697
- constructor(event, core, dispatch) {
698
- super(event, core, dispatch);
612
+ constructor(event, core, dispatch, sender) {
613
+ super(event, core, dispatch, sender);
699
614
  }
700
615
  /**
701
616
  * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
@@ -706,14 +621,6 @@ var InteractionHandler = class extends RepliableHandler {
706
621
  }
707
622
  };
708
623
 
709
- //#endregion
710
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
711
- function slashRouteOf(interaction) {
712
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
713
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
714
- return buildSlashRoute(interaction.commandName, subcommand, group);
715
- }
716
-
717
624
  //#endregion
718
625
  //#region src/handlers/interaction/SlashHandler.ts
719
626
  /**
@@ -721,7 +628,7 @@ function slashRouteOf(interaction) {
721
628
  *
722
629
  * Pass the route from the generated registry as the generic, the same string as `@SlashRoute`, then read
723
630
  * `this.options` for a single command or `this.match` for several. Command authoring stays plain
724
- * discord.js, `seedcord codegen` reads its `toJSON()` to type these options.
631
+ * discord.js. `seedcord codegen` reads its `toJSON()` to type these options.
725
632
  *
726
633
  * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'ban'` or `'ban' | 'kick'`.
727
634
  * @typeParam Cache - The interaction cache state. The command's contexts set it.
@@ -742,28 +649,28 @@ var SlashHandler = class extends InteractionHandler {
742
649
  * The typed options for this command's route. Only the getters for kinds this command declares appear.
743
650
  * Each getter's name argument is restricted to that command's options of its kind.
744
651
  *
745
- * - A required option drops the null.
652
+ * - A required option's getter never returns `null`.
746
653
  *
747
654
  * - Choices narrow to their literal union.
748
655
  *
749
656
  * - A channel option declared with `addChannelTypes` narrows to those types.
750
657
  *
751
658
  * Use `this.event.options` directly for anything outside this view, such as `getSubcommand()`.
752
- * However, for the common case, the methods in this would suffice.
753
659
  */
754
660
  get options() {
755
661
  return this.event.options;
756
662
  }
757
663
  /**
758
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
664
+ * Run the case for the selected command. Use this only when the handler is registered for several
759
665
  * routes.
760
666
  *
761
- * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
762
- * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
667
+ * Provide one case per registered route, keyed by its route string, and each case receives that route's
668
+ * own narrowed options. The cases must cover every route in the generic. A missing route or an unknown
763
669
  * key is a compile error.
764
670
  *
765
- * @param arms - One handler per registered route, keyed by route string.
766
- * @returns The result of the arm that ran.
671
+ * @param cases - One handler per registered route, keyed by route string.
672
+ * @typeParam Ret - The type that every case must return. Leave it out to get the union of what the cases return.
673
+ * @returns The result of the case that ran.
767
674
  *
768
675
  * @example
769
676
  * ```ts
@@ -778,29 +685,13 @@ var SlashHandler = class extends InteractionHandler {
778
685
  * }
779
686
  * ```
780
687
  */
781
- async match(arms) {
782
- const route = slashRouteOf(this.event);
783
- const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
784
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
785
- this.constructor.name,
786
- "command route",
787
- route
788
- ]);
789
- return await arm(this.options);
688
+ async match(cases) {
689
+ return await runCase(this, cases, "command route", this.route, this.options);
790
690
  }
791
691
  };
792
692
 
793
693
  //#endregion
794
694
  //#region src/handlers/interaction/ContextMenuHandler.ts
795
- function armFor(handler, arms, name) {
796
- const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
797
- if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
798
- handler.constructor.name,
799
- "context menu command",
800
- name
801
- ]);
802
- return arm;
803
- }
804
695
  /**
805
696
  * Base class for a user context-menu command handler (right-click a user).
806
697
  *
@@ -828,21 +719,18 @@ var UserContextMenuHandler = class extends InteractionHandler {
828
719
  get targetMember() {
829
720
  return this.event.targetMember;
830
721
  }
831
- /** Which of the registered commands fired. */
832
- get commandName() {
833
- return this.event.commandName;
834
- }
835
722
  /**
836
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
723
+ * Run the case for the selected command. Use this only when the handler is registered for several
837
724
  * names.
838
725
  *
839
- * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
840
- * arm receives the clicked user and that user's guild member, both narrowed to the cache state of that
841
- * one command. The arms must cover every name in the generic. A missing name or an unknown key is a
726
+ * Provide one case per registered name, keyed by the name Discord shows in the right-click menu. Each
727
+ * case receives the clicked user and that user's guild member, both narrowed to the cache state of that
728
+ * one command. The cases must cover every name in the generic. A missing name or an unknown key is a
842
729
  * compile error.
843
730
  *
844
- * @param arms - One handler per registered command name.
845
- * @returns The result of the arm that ran.
731
+ * @param cases - One handler per registered command name.
732
+ * @typeParam Ret - The type that every case must return. Leave it out to get the union of what the cases return.
733
+ * @returns The result of the case that ran.
846
734
  *
847
735
  * @example
848
736
  * ```ts
@@ -857,8 +745,8 @@ var UserContextMenuHandler = class extends InteractionHandler {
857
745
  * }
858
746
  * ```
859
747
  */
860
- async match(arms) {
861
- return await armFor(this, arms, this.event.commandName)(this.target, this.targetMember);
748
+ async match(cases) {
749
+ return await runCase(this, cases, "context menu command", this.route, this.target, this.targetMember);
862
750
  }
863
751
  };
864
752
  /**
@@ -884,20 +772,17 @@ var MessageContextMenuHandler = class extends InteractionHandler {
884
772
  get target() {
885
773
  return this.event.targetMessage;
886
774
  }
887
- /** Which of the registered commands fired. */
888
- get commandName() {
889
- return this.event.commandName;
890
- }
891
775
  /**
892
- * Run the arm for whichever command fired. Use this only when the handler is registered for several
776
+ * Run the case for the selected command. Use this only when the handler is registered for several
893
777
  * names.
894
778
  *
895
- * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
896
- * arm receives the clicked message, narrowed to the cache state of that one command. The arms must
779
+ * Provide one case per registered name, keyed by the name Discord shows in the right-click menu. Each
780
+ * case receives the clicked message, narrowed to the cache state of that one command. The cases must
897
781
  * cover every name in the generic. A missing name or an unknown key is a compile error.
898
782
  *
899
- * @param arms - One handler per registered command name.
900
- * @returns The result of the arm that ran.
783
+ * @param cases - One handler per registered command name.
784
+ * @typeParam Ret - The type that every case must return. Leave it out to get the union of what the cases return.
785
+ * @returns The result of the case that ran.
901
786
  *
902
787
  * @example
903
788
  * ```ts
@@ -912,8 +797,8 @@ var MessageContextMenuHandler = class extends InteractionHandler {
912
797
  * }
913
798
  * ```
914
799
  */
915
- async match(arms) {
916
- return await armFor(this, arms, this.event.commandName)(this.target);
800
+ async match(cases) {
801
+ return await runCase(this, cases, "context menu command", this.route, this.target);
917
802
  }
918
803
  };
919
804
 
@@ -936,7 +821,7 @@ var BaseHandler = class extends BaseHandler$1 {
936
821
  *
937
822
  * Pass the command route(s) from the generated registry as the generic, the same string(s) as
938
823
  * `@AutocompleteRoute`. Branch on the focused field with `this.match`, read already-entered sibling options
939
- * with `this.options`, and find which command fired with `this.route`.
824
+ * with `this.options`, and read the selected command's path from `this.route`.
940
825
  *
941
826
  * @typeParam Route - One or more route keys from {@link SlashRegistry}, e.g. `'search'` or `'search' | 'find'`.
942
827
  * @typeParam Cache - The interaction cache state. The command's contexts set it.
@@ -954,8 +839,14 @@ var BaseHandler = class extends BaseHandler$1 {
954
839
  * ```
955
840
  */
956
841
  var AutocompleteHandler = class extends BaseHandler {
957
- constructor(event, core, dispatch) {
842
+ constructor(event, core, dispatch, responder) {
958
843
  super(event, core, dispatch, "interactions");
844
+ this.#responder = responder;
845
+ }
846
+ #responder;
847
+ /** The user who sent this interaction. */
848
+ get user() {
849
+ return this.event.user;
959
850
  }
960
851
  decodedFocused;
961
852
  /**
@@ -973,16 +864,17 @@ var AutocompleteHandler = class extends BaseHandler {
973
864
  return focused;
974
865
  }
975
866
  /**
976
- * Run the arm for the focused field. Because an autocomplete always dispatches by which field is
867
+ * Run the case for the focused field. Because an autocomplete always dispatches by which field is
977
868
  * focused, match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
978
869
  *
979
- * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
980
- * arm receives the focused partial value and a `respond` typed to that field's choice value. The
981
- * arms must cover every autocompletable field. A missing field or an unknown key is a compile error.
870
+ * Provide one case per autocompletable field across the registered commands, keyed by field name. Each
871
+ * case receives the focused partial value and a `respond` typed to that field's choice value. The
872
+ * cases must cover every autocompletable field. A missing field or an unknown key is a compile error.
982
873
  *
983
- * @param arms - One handler per autocompletable field, keyed by field name.
984
- * @returns The result of the arm that ran.
985
- * @throws A **SeedcordTypeError** with `MatchArmMissing` when the focused field has no arm. Only a stale-deployed command can send one.
874
+ * @param cases - One handler per autocompletable field, keyed by field name.
875
+ * @typeParam Ret - The type that every case must return. Leave it out to get the union of what the cases return.
876
+ * @returns The result of the case that ran.
877
+ * @throws A **SeedcordTypeError** with `MatchCaseMissing` when the focused field doesn't have a case. Only a stale-deployed command can send one.
986
878
  *
987
879
  * @example
988
880
  * ```ts
@@ -997,28 +889,14 @@ var AutocompleteHandler = class extends BaseHandler {
997
889
  * }
998
890
  * ```
999
891
  */
1000
- async match(arms) {
892
+ async match(cases) {
1001
893
  const { name, value } = this.focused;
1002
894
  const respond = (choices) => this.respond(choices);
1003
- const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1004
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1005
- this.constructor.name,
1006
- "focused field",
1007
- name
1008
- ]);
1009
- return await arm(value, respond);
895
+ return await runCase(this, cases, "focused field", name, value, respond);
1010
896
  }
1011
897
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1012
- async respond(choices) {
1013
- await reportedWrite({
1014
- bus: this.core.bus,
1015
- dispatch: this.dispatch,
1016
- interactionId: this.event.id
1017
- }, "respond", () => this.event.respond(choices));
1018
- }
1019
- /** The firing command route, for a field whose completion differs per registered command. */
1020
- get route() {
1021
- return slashRouteOf(this.event);
898
+ respond(choices) {
899
+ return this.#responder.respond(choices);
1022
900
  }
1023
901
  /**
1024
902
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
@@ -1051,7 +929,7 @@ var ComponentHandler = class extends InteractionHandler {
1051
929
  return this.sender.deferUpdate();
1052
930
  }
1053
931
  decoded;
1054
- get route() {
932
+ get decodedRoute() {
1055
933
  this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
1056
934
  return this.decoded;
1057
935
  }
@@ -1065,20 +943,21 @@ var ComponentHandler = class extends InteractionHandler {
1065
943
  * @throws An **InvalidCustomId** when the wire is corrupt. The controller boundary turns it into a reply.
1066
944
  */
1067
945
  get params() {
1068
- return this.route.params;
946
+ return this.decodedRoute.params;
1069
947
  }
1070
948
  /**
1071
- * Run the arm for whichever route the component was minted from. Use this only when the handler is
1072
- * registered for several routes. A single-route handler reads `this.params` directly. On a multi-route
949
+ * Run the case for whichever route the component was minted from. Use this only when the handler is
950
+ * registered for several routes. On a single-route handler, read `this.params` directly. On a multi-route
1073
951
  * handler `this.params` is `never`, leaving match as the only way to read the decoded params.
1074
952
  *
1075
- * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1076
- * decoded params. The compiler checks that the arms cover every registered route prefix. `match`
1077
- * decodes the wire before it runs an arm.
953
+ * Provide one case per registered route, keyed by its prefix, and each case receives that route's own
954
+ * decoded params. The compiler checks that the cases cover every registered route prefix. `match`
955
+ * decodes the wire before it runs a case.
1078
956
  *
1079
- * @param arms - One callback per registered route, keyed by prefix.
1080
- * @returns The result of the arm that ran.
1081
- * @throws A **SeedcordTypeError** with `MatchArmMissing` when the wire's prefix has no arm.
957
+ * @param cases - One callback per registered route, keyed by prefix.
958
+ * @typeParam Ret - The type that every case must return. Leave it out to get the union of what the cases return.
959
+ * @returns The result of the case that ran.
960
+ * @throws A **SeedcordTypeError** with `MatchCaseMissing` when the wire's prefix doesn't have a case.
1082
961
  * @throws A **StaleCustomId** when the wire was encoded under an older shape.
1083
962
  * @throws An **InvalidCustomId** when the wire is corrupt.
1084
963
  *
@@ -1095,15 +974,9 @@ var ComponentHandler = class extends InteractionHandler {
1095
974
  * }
1096
975
  * ```
1097
976
  */
1098
- async match(arms) {
1099
- const { prefix, params } = this.route;
1100
- const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1101
- if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1102
- this.constructor.name,
1103
- "component route",
1104
- prefix
1105
- ]);
1106
- return await arm(params);
977
+ async match(cases) {
978
+ const { prefix, params } = this.decodedRoute;
979
+ return await runCase(this, cases, "component route", prefix, params);
1107
980
  }
1108
981
  };
1109
982
 
@@ -1175,7 +1048,7 @@ var ModalHandler = class extends ComponentHandler {
1175
1048
  }
1176
1049
  ensureSourceMessage(method) {
1177
1050
  if (this.event.isFromMessage()) return;
1178
- throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1051
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.dispatch.routeId]);
1179
1052
  }
1180
1053
  };
1181
1054
 
@@ -1338,8 +1211,8 @@ var MentionableMenuHandler = class extends SelectMenuHandler {
1338
1211
  /**
1339
1212
  * Base class for a Discord client event handler.
1340
1213
  *
1341
- * Pass the event name(s) as the generic, the same one(s) as `@RegisterEvent`. A single-event handler reads
1342
- * `this.event` (the payload tuple) directly. A handler registered for several events branches with `this.match`,
1214
+ * Pass the event name(s) as the generic, the same one(s) as `@RegisterEvent`. On a single-event handler, read
1215
+ * `this.event` (the payload tuple) directly. On a handler registered for several events, branch with `this.match`,
1343
1216
  * keyed by event name, since the union of payload tuples is not directly readable.
1344
1217
  *
1345
1218
  * @typeParam Names - One or more `ClientEvents` keys, e.g. `Events.MessageCreate` or `Events.MessageCreate | Events.MessageUpdate`.
@@ -1358,22 +1231,21 @@ var MentionableMenuHandler = class extends SelectMenuHandler {
1358
1231
  * ```
1359
1232
  */
1360
1233
  var EventHandler = class extends BaseHandler {
1361
- firedEvent;
1362
- constructor(event, core, dispatch, eventName) {
1234
+ constructor(event, core, dispatch) {
1363
1235
  super(event, core, dispatch, "events");
1364
- this.firedEvent = eventName;
1365
1236
  }
1366
1237
  /**
1367
- * Run the arm for whichever event fired. Use this only when the handler is registered for several
1368
- * events. A single-event handler reads `this.event` directly. On a multi-event handler `this.event` is
1238
+ * Run the case for the current event. Use this only when the handler is registered for several
1239
+ * events. On a single-event handler, read `this.event` directly. On a multi-event handler `this.event` is
1369
1240
  * `never`, so match is the only way to read the payload.
1370
1241
  *
1371
- * Provide one arm per registered event, keyed by its name, and each arm receives that event's own
1372
- * payload tuple. The arms must cover every event in the generic, a missing event or an unknown key is a
1242
+ * Provide one case per registered event, keyed by its name, and each case receives that event's own
1243
+ * payload tuple. The cases must cover every event in the generic. A missing event or an unknown key is a
1373
1244
  * compile error.
1374
1245
  *
1375
- * @param arms - One handler per registered event, keyed by event name.
1376
- * @returns The result of the arm that ran.
1246
+ * @param cases - One handler per registered event, keyed by event name.
1247
+ * @typeParam Ret - The type that every case must return. Leave it out to get the union of what the cases return.
1248
+ * @returns The result of the case that ran.
1377
1249
  *
1378
1250
  * @example
1379
1251
  * ```ts
@@ -1388,16 +1260,8 @@ var EventHandler = class extends BaseHandler {
1388
1260
  * }
1389
1261
  * ```
1390
1262
  */
1391
- async match(arms) {
1392
- const name = this.firedEvent;
1393
- if (name === void 0) throw new SeedcordError(SeedcordErrorCode.EventNameUnavailable, [this.constructor.name, "match()"]);
1394
- const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1395
- if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.MatchArmMissing, [
1396
- this.constructor.name,
1397
- "event",
1398
- name
1399
- ]);
1400
- return await arm(...this.getEvent());
1263
+ async match(cases) {
1264
+ return await runCase(this, cases, "event", this.route, ...this.getEvent());
1401
1265
  }
1402
1266
  };
1403
1267
 
@@ -1408,26 +1272,135 @@ var EventHandler = class extends BaseHandler {
1408
1272
  *
1409
1273
  * Middleware runs before event handlers and can stop the event by throwing a `Silence`. Unlike `EventHandler`, it
1410
1274
  * runs the SAME for every event it is registered for, so it has no `match`. Specify a single event in the
1411
- * generic and `{ events }` to read `this.event` fully typed. Span several events (or omit `{ events }` for a
1412
- * catchall) and `this.event` narrows to `never`, read `this.eventName` and do work that does not depend on the
1413
- * payload shape. A middleware that needs each event's payload is written one event per class.
1275
+ * generic and `{ events }` to read `this.event` fully typed. If you span several events (or omit `{ events }` for
1276
+ * a catchall), `this.event` narrows to `never`. Read `this.route` there, for work that doesn't depend on the
1277
+ * payload shape. Write one class per event for a middleware that needs each event's payload.
1414
1278
  *
1415
1279
  * @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
1416
1280
  */
1417
1281
  var EventMiddleware = class extends BaseHandler {
1418
- firedEvent;
1419
- constructor(event, core, dispatch, eventName) {
1282
+ constructor(event, core, dispatch) {
1420
1283
  super(event, core, dispatch, "events");
1421
- this.firedEvent = eventName;
1422
1284
  }
1423
- /**
1424
- * The event that fired this middleware. Read it on a catchall or multi-event middleware, where `this.event`
1425
- * is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
1426
- * `this.event` directly and does not need this.
1427
- */
1428
- get eventName() {
1429
- if (this.firedEvent === void 0) throw new SeedcordError(SeedcordErrorCode.EventNameUnavailable, [this.constructor.name, "this.eventName"]);
1430
- return this.firedEvent;
1285
+ };
1286
+
1287
+ //#endregion
1288
+ //#region src/bot/ReplySender.ts
1289
+ function isReplyFile(file) {
1290
+ return typeof file === "object" && "data" in file && ArrayBuffer.isView(file.data);
1291
+ }
1292
+ function toDjsFile(file) {
1293
+ if (!isReplyFile(file)) return file;
1294
+ return {
1295
+ attachment: Buffer.from(file.data),
1296
+ name: file.name,
1297
+ ...file.description !== void 0 && { description: file.description },
1298
+ ...file.title !== void 0 && { title: file.title }
1299
+ };
1300
+ }
1301
+ function seedState(interaction) {
1302
+ if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
1303
+ if (interaction.replied) return "replied";
1304
+ if (interaction.deferred) return "deferred-reply";
1305
+ return "unacked";
1306
+ }
1307
+ /** Writes interaction responses through discord.js. The repliable handler bases and the dispatcher construct it. */
1308
+ var ReplySender = class extends BaseReplySender {
1309
+ interaction;
1310
+ constructor(interaction, dispatch, bus) {
1311
+ super({
1312
+ bus,
1313
+ dispatch,
1314
+ interactionId: interaction.id
1315
+ }, seedState(interaction));
1316
+ this.interaction = interaction;
1317
+ }
1318
+ async writeReply(response, opts) {
1319
+ const result = await this.interaction.reply({
1320
+ ...this.replyOptions(response, sendFlags(opts)),
1321
+ withResponse: true
1322
+ });
1323
+ return this.createdMessage(result);
1324
+ }
1325
+ async writeDefer(opts) {
1326
+ await this.interaction.deferReply({ flags: deferFlags(opts) });
1327
+ }
1328
+ async writeDeferUpdate() {
1329
+ await this.sourceInteraction("deferUpdate").deferUpdate();
1330
+ }
1331
+ async writeUpdate(response) {
1332
+ const result = await this.sourceInteraction("update").update({
1333
+ ...this.updateOptions(response),
1334
+ withResponse: true
1335
+ });
1336
+ return this.createdMessage(result);
1337
+ }
1338
+ async writeFollowUp(response, opts) {
1339
+ return await this.interaction.followUp(this.replyOptions(response, sendFlags(opts)));
1340
+ }
1341
+ async writeEditOriginal(response) {
1342
+ return await this.interaction.editReply(this.editOptions(response));
1343
+ }
1344
+ async writeEditTarget(targetId, response) {
1345
+ return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
1346
+ }
1347
+ async writeDeleteOriginal() {
1348
+ await this.interaction.deleteReply();
1349
+ }
1350
+ async writeDeleteTarget(targetId) {
1351
+ await this.interaction.deleteReply(targetId);
1352
+ }
1353
+ async writeModal(data) {
1354
+ const { interaction } = this;
1355
+ if (interaction.isModalSubmit()) this.rejectModalSubmit();
1356
+ await interaction.showModal(data);
1357
+ }
1358
+ guardModalSource() {
1359
+ if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
1360
+ }
1361
+ rejectModalSubmit() {
1362
+ throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
1363
+ "showModal",
1364
+ "this interaction is a modal submit",
1365
+ "A modal cannot open another modal.",
1366
+ this.routeId
1367
+ ]);
1368
+ }
1369
+ sourceInteraction(method) {
1370
+ const { interaction } = this;
1371
+ if (interaction.isMessageComponent()) return interaction;
1372
+ if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
1373
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1374
+ }
1375
+ replyOptions(response, flags) {
1376
+ const reply = this.serialize(response);
1377
+ return {
1378
+ components: reply.components,
1379
+ flags,
1380
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
1381
+ ...reply.files && { files: reply.files.map(toDjsFile) }
1382
+ };
1383
+ }
1384
+ editOptions(response) {
1385
+ const reply = this.serialize(response);
1386
+ return {
1387
+ components: reply.components,
1388
+ flags: MessageFlags.IsComponentsV2,
1389
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
1390
+ ...reply.files && { files: reply.files.map(toDjsFile) }
1391
+ };
1392
+ }
1393
+ updateOptions(response) {
1394
+ const reply = this.serialize(response);
1395
+ return {
1396
+ components: reply.components,
1397
+ flags: MessageFlags.IsComponentsV2,
1398
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
1399
+ ...reply.files && { files: reply.files.map(toDjsFile) }
1400
+ };
1401
+ }
1402
+ createdMessage(result) {
1403
+ return result.resource?.message ?? void 0;
1431
1404
  }
1432
1405
  };
1433
1406
 
@@ -1520,14 +1493,14 @@ var CursorSource = class extends CursorSourceBase {};
1520
1493
  /**
1521
1494
  * Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
1522
1495
  *
1523
- * @typeParam KeyOfSubscribers - The subscription key this handler receives
1496
+ * @typeParam KeyOfSubscribers - The subscription key or keys that this handler receives, the same ones as `@Subscribe`
1524
1497
  */
1525
1498
  var Subscriber = class extends Subscriber$1 {};
1526
1499
  /**
1527
1500
  * Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
1528
1501
  * `this.core` to the gateway {@link Core}.
1529
1502
  *
1530
- * @typeParam KeyOfSubscribers - The subscription key this reporter receives
1503
+ * @typeParam KeyOfSubscribers - The subscription key or keys that this reporter receives, the same ones as `@Subscribe`
1531
1504
  */
1532
1505
  var WebhookLog = class extends WebhookLog$1 {};
1533
1506
 
@@ -1540,8 +1513,8 @@ const GATEWAY_SCOPE = {
1540
1513
  /**
1541
1514
  * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
1542
1515
  *
1543
- * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1544
- * `this.core` is typed to the shared members only.
1516
+ * To run on either transport, extend the base from `@seedcord/core/plugin`. Its `this.core` carries
1517
+ * only the shared members.
1545
1518
  *
1546
1519
  * @typeParam Opts - The transport and runtime the plugin declares.
1547
1520
  */
@@ -1552,15 +1525,23 @@ var Plugin = class extends Plugin$1 {
1552
1525
 
1553
1526
  //#endregion
1554
1527
  //#region src/miscellaneous/deriveEventActor.ts
1528
+ function actorOf(message) {
1529
+ return {
1530
+ guild: message.guild,
1531
+ member: message.member,
1532
+ user: message.author,
1533
+ channelId: message.channelId
1534
+ };
1535
+ }
1555
1536
  function deriveEventActor(args) {
1556
1537
  const tuple = Array.isArray(args) ? args : [args];
1538
+ let partial;
1557
1539
  for (const arg of tuple) {
1558
- if (arg instanceof Message) return {
1559
- guild: arg.guild,
1560
- member: arg.member,
1561
- user: arg.author,
1562
- channelId: arg.channelId
1563
- };
1540
+ if (arg instanceof Message) {
1541
+ const message = arg;
1542
+ if (!message.partial) return actorOf(message);
1543
+ partial ??= message;
1544
+ }
1564
1545
  if (arg instanceof GuildMember) return {
1565
1546
  guild: arg.guild,
1566
1547
  member: arg,
@@ -1574,7 +1555,7 @@ function deriveEventActor(args) {
1574
1555
  channelId: null
1575
1556
  };
1576
1557
  }
1577
- return {
1558
+ return partial ? actorOf(partial) : {
1578
1559
  guild: null,
1579
1560
  member: null,
1580
1561
  user: null,
@@ -1583,7 +1564,8 @@ function deriveEventActor(args) {
1583
1564
  }
1584
1565
 
1585
1566
  //#endregion
1586
- //#region src/bot/gates/runGates.ts
1567
+ //#region src/bot/gates/context.ts
1568
+ const NO_PERMISSIONS = 0n;
1587
1569
  function interactionGateContext(interaction, core, dispatch) {
1588
1570
  const rawMember = interaction.member;
1589
1571
  const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
@@ -1602,8 +1584,7 @@ function interactionGateContext(interaction, core, dispatch) {
1602
1584
  memberPermissions: interaction.memberPermissions?.bitfield ?? null,
1603
1585
  appPermissions: interaction.appPermissions.bitfield,
1604
1586
  memberGuildPermissions: rawMember instanceof GuildMember ? rawMember.permissions.bitfield : null,
1605
- appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
1606
- declaredRoute: null
1587
+ appGuildPermissions: interaction.guild ? interaction.guild.members.me?.permissions.bitfield ?? null : NO_PERMISSIONS
1607
1588
  };
1608
1589
  }
1609
1590
  function eventGateContext(eventName, args, core, dispatch) {
@@ -1622,161 +1603,76 @@ function eventGateContext(eventName, args, core, dispatch) {
1622
1603
  channelId: actor.channelId,
1623
1604
  memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
1624
1605
  memberPermissions: actor.member?.permissions.bitfield ?? null,
1625
- appPermissions: null,
1626
1606
  memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
1627
- appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
1628
- declaredRoute: null
1607
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null
1629
1608
  };
1630
1609
  }
1631
1610
 
1632
1611
  //#endregion
1633
- //#region src/miscellaneous/extractErrorResponse.ts
1634
- const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
1635
- function extractErrorResponse(error, core, fault) {
1636
- const uuid = crypto.randomUUID();
1637
- const { dispatch } = fault;
1638
- const developerUsername = core.config.notifications?.developerUsername;
1639
- const ctx = developerUsername === void 0 ? {
1640
- uuid,
1641
- dispatch
1642
- } : {
1643
- uuid,
1644
- developerUsername,
1645
- dispatch
1646
- };
1647
- if (error instanceof Notice) {
1648
- if (error.report) reportFault(error, core, fault, uuid);
1649
- return {
1650
- uuid,
1651
- response: error.render(ctx)
1652
- };
1653
- }
1654
- reportRawFault(error, core, fault, uuid);
1655
- const Override = core.config.errors?.defaultError;
1656
- return {
1657
- uuid,
1658
- response: Override ? new Override(uuid).render(ctx) : new Fault().render(ctx)
1659
- };
1660
- }
1661
- function reportFault(denial, core, fault, uuid) {
1662
- logger$2.error(`${denial.name}: ${uuid}`, denial);
1663
- if (fault.interaction) core.bus[PublishDefault]("handledException", {
1664
- denial,
1665
- uuid,
1666
- dispatchId: fault.dispatch.id,
1667
- origin: fault.origin,
1668
- source: buildInteractionSource(fault.interaction)
1669
- });
1670
- else if (fault.event) core.bus[PublishDefault]("handledException", {
1671
- denial,
1672
- uuid,
1673
- dispatchId: fault.dispatch.id,
1674
- origin: fault.origin,
1675
- source: buildEventSource(fault.event, fault)
1676
- });
1677
- else core.bus[PublishDefault]("unknownException", {
1678
- uuid,
1679
- dispatchId: fault.dispatch.id,
1680
- error: denial,
1681
- origin: fault.origin,
1682
- ...scalarActors(fault),
1683
- metadata: metadataFor(fault)
1684
- });
1685
- }
1686
- function reportRawFault(error, core, fault, uuid) {
1687
- logUnknownFault(logger$2, uuid, error, core.config.errors);
1688
- core.bus[PublishDefault]("unknownException", {
1689
- uuid,
1690
- dispatchId: fault.dispatch.id,
1691
- error,
1692
- origin: fault.origin,
1693
- ...scalarActors(fault),
1694
- metadata: metadataFor(fault)
1695
- });
1696
- }
1697
- function scalarActors(fault) {
1612
+ //#region src/miscellaneous/faultReports.ts
1613
+ function interactionFaultReport(interaction, dispatch) {
1698
1614
  return {
1699
- guild: fault.guild ? {
1700
- id: fault.guild.id,
1701
- name: fault.guild.name
1702
- } : void 0,
1703
- user: fault.user ? {
1704
- id: fault.user.id,
1705
- username: fault.user.username
1706
- } : void 0
1707
- };
1708
- }
1709
- function metadataFor(fault) {
1710
- if (fault.event) return {
1711
- eventName: fault.event.name,
1712
- handler: fault.event.handler,
1713
- args: fault.event.args
1615
+ origin: dispatch.routeId,
1616
+ source: interactionFaultSource(dispatch, {
1617
+ customId: "customId" in interaction ? interaction.customId : null,
1618
+ userId: interaction.user.id,
1619
+ guildId: interaction.guildId,
1620
+ channelId: interaction.channelId,
1621
+ interactionId: interaction.id,
1622
+ raw: interaction
1623
+ }),
1624
+ ...scalarActors(interaction.guild, interaction.user),
1625
+ metadata: interaction
1714
1626
  };
1715
- return fault.metadata;
1716
1627
  }
1717
- function buildEventSource(event, fault) {
1628
+ function eventFaultReport(event) {
1718
1629
  return {
1719
- kind: "event",
1720
- eventName: event.name,
1721
- handler: event.handler,
1722
- userId: fault.user?.id ?? null,
1723
- guildId: fault.guild?.id ?? null,
1724
- channelId: event.channelId,
1725
- raw: event.args
1630
+ origin: `event:${event.name}:${event.handler}`,
1631
+ source: {
1632
+ kind: "event",
1633
+ eventName: event.name,
1634
+ handler: event.handler,
1635
+ userId: event.user?.id ?? null,
1636
+ guildId: event.guild?.id ?? null,
1637
+ channelId: event.channelId,
1638
+ raw: event.args
1639
+ },
1640
+ ...scalarActors(event.guild, event.user),
1641
+ metadata: {
1642
+ eventName: event.name,
1643
+ handler: event.handler,
1644
+ args: event.args
1645
+ }
1726
1646
  };
1727
1647
  }
1728
- function buildInteractionSource(interaction) {
1729
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1730
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1648
+ function scalarActors(guild, user) {
1731
1649
  return {
1732
- kind: "interaction",
1733
- interactionKind: interactionKind(interaction),
1734
- command,
1735
- customId,
1736
- userId: interaction.user.id,
1737
- guildId: interaction.guildId,
1738
- channelId: interaction.channelId,
1739
- interactionId: interaction.id,
1740
- raw: interaction
1650
+ guild: guild ? {
1651
+ id: guild.id,
1652
+ name: guild.name
1653
+ } : void 0,
1654
+ user: user ? {
1655
+ id: user.id,
1656
+ username: user.username
1657
+ } : void 0
1741
1658
  };
1742
1659
  }
1743
- function interactionKind(interaction) {
1744
- if (interaction.isChatInputCommand()) return "slash";
1745
- if (interaction.isContextMenuCommand()) return "context-menu";
1746
- if (interaction.isButton()) return "button";
1747
- if (interaction.isAnySelectMenu()) return "select";
1748
- return "modal";
1749
- }
1750
1660
 
1751
1661
  //#endregion
1752
1662
  //#region src/bot/handleEventFault.ts
1753
- const logger$1 = new Logger("Faults", { channel: "errors" });
1754
1663
  function handleEventFault(caught, fault, core) {
1755
- const { eventName, handlerName, args, dispatch } = fault;
1756
- if (caught instanceof Silence) {
1757
- if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
1758
- return;
1759
- }
1760
- const error = asError(caught);
1761
- const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
1762
- if (error instanceof DiscordAPIError && ignore.has(error.code)) {
1763
- logger$1.debug(`swallowed api code ${error.code}`);
1764
- return;
1765
- }
1766
- if (error instanceof Notice && !error.report) return;
1767
- const actor = deriveEventActor(args);
1768
- extractErrorResponse(error, core, {
1769
- event: {
1770
- name: eventName,
1664
+ const { handlerName, args, dispatch } = fault;
1665
+ new FaultBoundary(core, core.config.errors?.ignoreEventApiCodes, DiscordAPIError, dispatch, () => {
1666
+ const { channelId, guild, user } = deriveEventActor(args);
1667
+ return eventFaultReport({
1668
+ name: dispatch.route,
1771
1669
  handler: handlerName,
1772
1670
  args,
1773
- channelId: actor.channelId
1774
- },
1775
- origin: `event:${eventName}:${handlerName}`,
1776
- dispatch,
1777
- guild: actor.guild,
1778
- user: actor.user
1779
- });
1671
+ channelId,
1672
+ guild,
1673
+ user
1674
+ });
1675
+ }).report(caught);
1780
1676
  }
1781
1677
 
1782
1678
  //#endregion
@@ -1818,33 +1714,29 @@ const eventMiddleware = (ctor) => {
1818
1714
  */
1819
1715
  var EventDispatcher = class {
1820
1716
  core;
1717
+ inFlight;
1821
1718
  logger = new Logger("Events", { channel: "events" });
1822
1719
  isInitialized = false;
1823
1720
  eventMap = /* @__PURE__ */ new Map();
1824
1721
  middlewares = new MiddlewareRegistry(eventMiddleware);
1825
- executedOnceHandlers = /* @__PURE__ */ new Set();
1722
+ onceRuns = new OnceTracker();
1826
1723
  attachedEvents = /* @__PURE__ */ new Set();
1827
- inFlight = new InFlight(this.logger, "Events");
1828
- loading = false;
1829
- loadedHandlers = [];
1830
- loadedMiddlewares = [];
1831
- hmrHandler;
1832
- constructor(core) {
1724
+ loader;
1725
+ constructor(core, inFlight) {
1833
1726
  this.core = core;
1727
+ this.inFlight = inFlight;
1834
1728
  const eventsDir = this.core.config.bot.events.path;
1835
1729
  if (!eventsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["EventDispatcher", "events"]);
1836
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
1837
- this.hmrHandler = new HmrModuleHandler$1({
1730
+ this.loader = new HandlerLoader({
1838
1731
  handlersDir: eventsDir,
1839
- ...hasKeys(this.core.config.bot.events, ["middlewares"]) && { middlewaresDir: this.core.config.bot.events.middlewares },
1840
- isHandler: this.isEventHandlerClass.bind(this),
1841
- isMiddleware: this.isMiddlewareClass.bind(this),
1842
- registerHandler: this.registerHandler.bind(this),
1843
- registerMiddleware: this.registerMiddleware.bind(this),
1844
- unregisterHandler: this.unregisterHandler.bind(this),
1845
- unregisterMiddleware: this.unregisterMiddleware.bind(this),
1846
- getArtifacts: this.getArtifacts.bind(this),
1847
- logger: this.logger
1732
+ middlewaresDir: hasKeys(this.core.config.bot.events, ["middlewares"]) ? this.core.config.bot.events.middlewares : void 0,
1733
+ middlewares: this.middlewares,
1734
+ logger: this.logger,
1735
+ isHandler: (value) => this.isEventHandlerClass(value),
1736
+ isMiddleware: (value) => this.isMiddlewareClass(value),
1737
+ storeHandler: (ctor) => this.storeHandler(ctor),
1738
+ removeHandler: (ctor, events) => this.unregisterHandler(ctor, events),
1739
+ artifactsOf: (ctor) => this.getArtifacts(ctor)
1848
1740
  });
1849
1741
  }
1850
1742
  getArtifacts(ctor) {
@@ -1855,49 +1747,25 @@ var EventDispatcher = class {
1855
1747
  async init() {
1856
1748
  if (this.isInitialized) return;
1857
1749
  this.isInitialized = true;
1858
- const handlersDir = this.core.config.bot.events.path;
1859
- if (!handlersDir) return;
1860
- const middlewareDir = hasKeys(this.core.config.bot.events, ["middlewares"]) ? this.core.config.bot.events.middlewares : void 0;
1861
- this.loading = true;
1862
- this.loadedHandlers.length = 0;
1863
- this.loadedMiddlewares.length = 0;
1864
- try {
1865
- if (middlewareDir) await this.loadMiddlewares(middlewareDir);
1866
- await this.loadHandlers(handlersDir);
1867
- } finally {
1868
- this.loading = false;
1869
- }
1750
+ await this.loader.load();
1870
1751
  this.attachToClient();
1871
1752
  this.reportLoad();
1872
1753
  }
1873
1754
  reportLoad() {
1874
1755
  const { utils } = this.logger;
1756
+ const { loadedHandlers, loadedMiddlewares } = this.loader;
1875
1757
  utils.summary("Loaded", {
1876
- "event handlers": this.loadedHandlers.length,
1877
- middlewares: this.loadedMiddlewares.length
1758
+ "event handlers": loadedHandlers.length,
1759
+ middlewares: loadedMiddlewares.length
1878
1760
  }, "debug");
1879
- if (this.loadedMiddlewares.length > 0) utils.block("Loaded event middlewares", utils.entries(this.loadedMiddlewares), "debug");
1761
+ if (loadedMiddlewares.length > 0) utils.block("Loaded event middlewares", utils.entries(loadedMiddlewares), "debug");
1880
1762
  const perEvent = {};
1881
1763
  for (const [event, handlers] of this.eventMap) perEvent[event] = handlers.length;
1882
- utils.block("Loaded event handlers", [...utils.entries(this.loadedHandlers), ...utils.counts(perEvent)], "debug");
1883
- }
1884
- async loadHandlers(dir) {
1885
- for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
1886
- if (!this.isEventHandlerClass(val)) continue;
1887
- this.registerHandler(val, relativePath);
1888
- this.hmrHandler?.trackHandler(fullPath, val);
1889
- }
1890
- }
1891
- async loadMiddlewares(dir) {
1892
- for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
1893
- if (!this.isMiddlewareClass(val)) continue;
1894
- this.registerMiddleware(val, relativePath);
1895
- this.hmrHandler?.trackMiddleware(fullPath, val);
1896
- }
1764
+ utils.block("Loaded event handlers", [...utils.entries(loadedHandlers), ...utils.counts(perEvent)], "debug");
1897
1765
  }
1898
1766
  /** @internal */
1899
1767
  async onHmr(event) {
1900
- await this.hmrHandler?.handle(event);
1768
+ await this.loader.onHmr(event);
1901
1769
  }
1902
1770
  unregisterHandler(handlerClass, artifacts) {
1903
1771
  const events = artifacts ?? [...this.eventMap.keys()];
@@ -1910,26 +1778,13 @@ var EventDispatcher = class {
1910
1778
  if (handlers.length === 0) this.eventMap.delete(event);
1911
1779
  }
1912
1780
  }
1913
- unregisterMiddleware(middlewareCtor) {
1914
- this.middlewares.unregister(middlewareCtor);
1915
- }
1916
- registerMiddleware(middlewareCtor, relativePath) {
1917
- const registration = this.middlewares.register(middlewareCtor);
1918
- if (!registration || !this.loading) return;
1919
- const scope = registration.keys ? `${registration.priority}, ${registration.keys.join(", ")}` : registration.priority;
1920
- this.loadedMiddlewares.push({
1921
- name: `${middlewareCtor.name} (${String(scope)})`,
1922
- from: formatFilePath(relativePath)
1923
- });
1924
- }
1925
1781
  async runMiddlewares(eventName, args, dispatch, ran) {
1926
1782
  for (const Middleware of this.middlewares.chainFor(eventName)) try {
1927
- const middleware = new Middleware(args, this.core, dispatch, eventName);
1783
+ const middleware = new Middleware(args, this.core, dispatch);
1928
1784
  ran.push(middleware);
1929
1785
  await middleware.execute();
1930
1786
  } catch (caught) {
1931
1787
  handleEventFault(caught, {
1932
- eventName: String(eventName),
1933
1788
  handlerName: Middleware.name,
1934
1789
  args,
1935
1790
  dispatch
@@ -1946,8 +1801,8 @@ var EventDispatcher = class {
1946
1801
  if (typeof obj !== "function") return false;
1947
1802
  return obj.prototype instanceof EventMiddleware && Reflect.hasMetadata(EventMiddlewareMetadataKey, obj);
1948
1803
  }
1949
- registerHandler(handlerClass, relativePath) {
1950
- const raw = Reflect.getMetadata(EventMetadataKey, handlerClass);
1804
+ storeHandler(handlerClass) {
1805
+ const entries = Reflect.getMetadata(EventMetadataKey, handlerClass);
1951
1806
  const register = (key, frequency) => {
1952
1807
  let handlers = this.eventMap.get(key);
1953
1808
  if (!handlers) {
@@ -1960,16 +1815,8 @@ var EventDispatcher = class {
1960
1815
  });
1961
1816
  if (this.isInitialized && !this.attachedEvents.has(key)) this.attachListener(key);
1962
1817
  };
1963
- if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
1964
- else {
1965
- const names = isStringArray(raw) ? raw : typeof raw === "string" ? [raw] : [];
1966
- for (const name of names) register(name, "on");
1967
- }
1968
- const from = formatFilePath(relativePath);
1969
- if (this.loading) this.loadedHandlers.push({
1970
- name: handlerClass.name,
1971
- from
1972
- });
1818
+ for (const entry of entries) register(entry.event, entry.frequency);
1819
+ return true;
1973
1820
  }
1974
1821
  attachToClient() {
1975
1822
  for (const [eventName] of this.eventMap) this.attachListener(eventName);
@@ -1980,28 +1827,21 @@ var EventDispatcher = class {
1980
1827
  const handlerEntries = this.eventMap.get(eventName);
1981
1828
  this.logger.debug(`Attaching ${paint.sky.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
1982
1829
  this.core.bot.client.on(eventName, (...args) => {
1983
- if (this.inFlight.closed) return;
1984
- const run = this.processEvent(eventName, args).catch((caught) => {
1985
- const error = asError(caught);
1986
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
1987
- this.core.bus[PublishDefault]("unhandledEventError", { error });
1988
- });
1989
- this.inFlight.track(run);
1830
+ this.inFlight.admit(() => this.processEvent(eventName, args).catch((caught) => this.reportUnhandled(caught)));
1990
1831
  });
1991
1832
  }
1992
- stopAccepting() {
1993
- this.inFlight.close();
1994
- }
1995
- drain(timeoutMs) {
1996
- return this.inFlight.drain(timeoutMs);
1833
+ reportUnhandled(caught) {
1834
+ const error = asError(caught);
1835
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
1836
+ this.core.bus[PublishDefault]("unhandledEventError", { error });
1997
1837
  }
1998
1838
  async processEvent(eventName, args) {
1999
1839
  const startedAt = performance.now();
2000
1840
  const handlerEntries = this.eventMap.get(eventName);
2001
1841
  if (!handlerEntries || handlerEntries.length === 0) return;
2002
- const handlersToExecute = handlerEntries.filter((entry) => entry.frequency !== "once" || !this.executedOnceHandlers.has(entry.ctor));
1842
+ const handlersToExecute = handlerEntries.filter((entry) => !this.onceRuns.isSpent(entry, eventName));
2003
1843
  if (handlersToExecute.length === 0) return;
2004
- const dispatch = new DispatchContext(`event:${String(eventName)}`);
1844
+ const dispatch = new DispatchContext("event", eventName);
2005
1845
  reportEventDispatching(this.core, dispatch.id, eventName, args);
2006
1846
  const ran = [];
2007
1847
  const handlers = [];
@@ -2009,13 +1849,8 @@ var EventDispatcher = class {
2009
1849
  try {
2010
1850
  stopped = await this.runMiddlewares(eventName, args, dispatch, ran);
2011
1851
  if (stopped) return;
2012
- for (const entry of handlersToExecute) {
2013
- if (entry.frequency === "once") {
2014
- if (this.executedOnceHandlers.has(entry.ctor)) continue;
2015
- this.executedOnceHandlers.add(entry.ctor);
2016
- }
2017
- handlers.push(await this.processHandler(eventName, entry.ctor, args, dispatch));
2018
- }
1852
+ const results = handlersToExecute.filter((entry) => this.onceRuns.claim(entry, eventName)).map((entry) => this.processHandler(eventName, entry.ctor, args, dispatch));
1853
+ handlers.push(...await Promise.all(results));
2019
1854
  } finally {
2020
1855
  const result = eventResultFor(stopped, handlers);
2021
1856
  reportEventDispatched(this.core, dispatch.id, eventName, result, startedAt);
@@ -2024,8 +1859,8 @@ var EventDispatcher = class {
2024
1859
  }
2025
1860
  async processHandler(eventName, Ctor, args, dispatch) {
2026
1861
  try {
2027
- this.logger.debug(`Processing ${paint.sky.bold(eventName)} with ${paint.mute(Ctor.name)}`);
2028
- const handler = new Ctor(args, this.core, dispatch, eventName);
1862
+ this.logger.trace(`Processing ${paint.sky.bold(eventName)} with ${paint.mute(Ctor.name)}`);
1863
+ const handler = new Ctor(args, this.core, dispatch);
2029
1864
  const eventCtx = eventGateContext(eventName, args, this.core, dispatch);
2030
1865
  await runHandlerGates(Ctor, eventCtx);
2031
1866
  await handler.execute();
@@ -2035,7 +1870,6 @@ var EventDispatcher = class {
2035
1870
  };
2036
1871
  } catch (caught) {
2037
1872
  handleEventFault(caught, {
2038
- eventName: String(eventName),
2039
1873
  handlerName: Ctor.name,
2040
1874
  args,
2041
1875
  dispatch
@@ -2048,6 +1882,40 @@ var EventDispatcher = class {
2048
1882
  }
2049
1883
  };
2050
1884
 
1885
+ //#endregion
1886
+ //#region src/bot/autocompleteResponder.ts
1887
+ function autocompleteResponder(interaction, core, dispatch) {
1888
+ return new AutocompleteResponder({
1889
+ bus: core.bus,
1890
+ dispatch,
1891
+ interactionId: interaction.id
1892
+ }, (choices) => interaction.respond(choices));
1893
+ }
1894
+
1895
+ //#endregion
1896
+ //#region src/bot/controllers/gatewayRunConfig.ts
1897
+ function gatewayRunConfig(run) {
1898
+ const { interaction, core, dispatch, HandlerCtor, middlewares, logger } = run;
1899
+ return {
1900
+ dispatch,
1901
+ core,
1902
+ event: interaction,
1903
+ HandlerCtor,
1904
+ middlewares,
1905
+ apiError: DiscordAPIError,
1906
+ logger,
1907
+ interactionId: interaction.id,
1908
+ userId: interaction.user.id,
1909
+ guildId: interaction.guildId,
1910
+ customId: "customId" in interaction ? interaction.customId : null,
1911
+ faultDetails: () => interactionFaultReport(interaction, dispatch),
1912
+ target: interaction.isAutocomplete() ? { responder: autocompleteResponder(interaction, core, dispatch) } : {
1913
+ gateContext: () => interactionGateContext(interaction, core, dispatch),
1914
+ sender: new ReplySender(interaction, dispatch, core.bus)
1915
+ }
1916
+ };
1917
+ }
1918
+
2051
1919
  //#endregion
2052
1920
  //#region src/bot/defaults/UnhandledRepliable.ts
2053
1921
  var UnhandledRepliable = class extends InteractionHandler {
@@ -2065,166 +1933,73 @@ var UnhandledAutocomplete = class extends AutocompleteHandler {
2065
1933
  };
2066
1934
 
2067
1935
  //#endregion
2068
- //#region src/bot/harmlessApiCodes.ts
2069
- /**
2070
- * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2071
- * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2072
- *
2073
- * @internal
2074
- */
2075
- const HARMLESS_API_CODES = /* @__PURE__ */ new Set([
2076
- RESTJSONErrorCodes.UnknownInteraction,
2077
- RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2078
- RESTJSONErrorCodes.UnknownMessage
2079
- ]);
2080
-
2081
- //#endregion
2082
- //#region src/bot/handleInteractionFault.ts
2083
- const logger = new Logger("Faults", { channel: "errors" });
2084
- async function handleInteractionFault(caught, interaction, core, dispatch, sender) {
2085
- const { routeId } = dispatch;
2086
- if (caught instanceof Silence) {
2087
- if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2088
- return;
2089
- }
2090
- const error = asError(caught);
2091
- const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
2092
- if (error instanceof DiscordAPIError && ignore.has(error.code)) {
2093
- logger.debug(`swallowed api code ${error.code}`);
2094
- return;
2095
- }
2096
- if (interaction.isAutocomplete()) {
2097
- extractErrorResponse(error, core, {
2098
- origin: routeId,
2099
- dispatch,
2100
- guild: interaction.guild,
2101
- user: interaction.user,
2102
- metadata: interaction
2103
- });
2104
- await sendEmptyChoices(interaction, core, dispatch);
2105
- return;
2106
- }
2107
- const { response } = extractErrorResponse(error, core, {
2108
- interaction,
2109
- origin: routeId,
2110
- dispatch,
2111
- guild: interaction.guild,
2112
- user: interaction.user,
2113
- metadata: interaction
2114
- });
2115
- await sendGuarded(sender ?? new ReplySender(interaction, dispatch, core.bus), response, error instanceof Notice ? error.ephemeral : true);
2116
- }
2117
- async function sendEmptyChoices(interaction, core, dispatch) {
2118
- const telemetry = {
2119
- bus: core.bus,
2120
- dispatch,
2121
- interactionId: interaction.id
2122
- };
2123
- try {
2124
- await reportedWrite(telemetry, "respond", () => interaction.respond([]));
2125
- } catch (error) {
2126
- logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
2127
- }
2128
- }
2129
- async function sendGuarded(sender, response, ephemeral) {
2130
- try {
2131
- await sender.send(response, { ephemeral });
2132
- } catch (error) {
2133
- if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2134
- logger.debug(`reply send hit harmless code ${error.code}`);
2135
- return;
2136
- }
2137
- logger.error("reply send failed", error);
2138
- }
1936
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1937
+ function slashRouteOf(interaction) {
1938
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
1939
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
1940
+ return buildSlashRoute(interaction.commandName, subcommand, group);
2139
1941
  }
2140
1942
 
2141
1943
  //#endregion
2142
1944
  //#region src/bot/controllers/InteractionDispatcher.ts
1945
+ const UNHANDLED = {
1946
+ repliable: UnhandledRepliable,
1947
+ autocomplete: UnhandledAutocomplete
1948
+ };
2143
1949
  var InteractionDispatcher = class {
2144
1950
  core;
1951
+ inFlight;
2145
1952
  logger = new Logger("Interactions", { channel: "interactions" });
2146
1953
  isInitialized = false;
2147
- maps = {
2148
- [InteractionKind.Slash]: /* @__PURE__ */ new Map(),
2149
- [InteractionKind.Button]: /* @__PURE__ */ new Map(),
2150
- [InteractionKind.Modal]: /* @__PURE__ */ new Map(),
2151
- [InteractionKind.StringMenu]: /* @__PURE__ */ new Map(),
2152
- [InteractionKind.UserMenu]: /* @__PURE__ */ new Map(),
2153
- [InteractionKind.RoleMenu]: /* @__PURE__ */ new Map(),
2154
- [InteractionKind.ChannelMenu]: /* @__PURE__ */ new Map(),
2155
- [InteractionKind.MentionableMenu]: /* @__PURE__ */ new Map(),
2156
- [InteractionKind.MessageContextMenu]: /* @__PURE__ */ new Map(),
2157
- [InteractionKind.UserContextMenu]: /* @__PURE__ */ new Map(),
2158
- [InteractionKind.Autocomplete]: /* @__PURE__ */ new Map()
2159
- };
2160
- handlerFiles = /* @__PURE__ */ new Map();
1954
+ routes = new RouteRegistry();
1955
+ get maps() {
1956
+ return this.routes.maps;
1957
+ }
2161
1958
  keysToIgnore = /* @__PURE__ */ new Set();
2162
1959
  middlewares = new MiddlewareRegistry(interactionMiddleware);
2163
- inFlight = new InFlight(this.logger, "Interactions");
2164
- loading = false;
2165
- loadedHandlers = [];
2166
- loadedMiddlewares = [];
2167
- hmrHandler;
2168
- constructor(core) {
1960
+ loader;
1961
+ constructor(core, inFlight) {
2169
1962
  this.core = core;
1963
+ this.inFlight = inFlight;
2170
1964
  const ignoredKeysFromConfig = hasKeys(this.core.config.bot.interactions, ["ignoreCustomIds"]) ? this.core.config.bot.interactions.ignoreCustomIds : void 0;
2171
1965
  if (ignoredKeysFromConfig) for (const ignoredKey of ignoredKeysFromConfig) this.keysToIgnore.add(ignoredKey);
2172
1966
  this.keysToIgnore.add(CONFIRM_DEF);
2173
1967
  const interactionsDir = this.core.config.bot.interactions.path;
2174
1968
  if (!interactionsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["InteractionDispatcher", "interactions"]);
2175
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
2176
- this.hmrHandler = new HmrModuleHandler$1({
1969
+ this.loader = new HandlerLoader({
2177
1970
  handlersDir: interactionsDir,
2178
- ...hasKeys(this.core.config.bot.interactions, ["middlewares"]) && this.core.config.bot.interactions.middlewares && { middlewaresDir: this.core.config.bot.interactions.middlewares },
2179
- isHandler: this.isHandlerClass.bind(this),
2180
- isMiddleware: this.isMiddlewareClass.bind(this),
2181
- registerHandler: this.registerHandler.bind(this),
2182
- registerMiddleware: this.registerMiddleware.bind(this),
2183
- unregisterHandler: this.unregisterHandler.bind(this),
2184
- unregisterMiddleware: this.unregisterMiddleware.bind(this),
2185
- getArtifacts: this.getArtifacts.bind(this),
2186
- logger: this.logger
1971
+ middlewaresDir: hasKeys(this.core.config.bot.interactions, ["middlewares"]) ? this.core.config.bot.interactions.middlewares : void 0,
1972
+ middlewares: this.middlewares,
1973
+ logger: this.logger,
1974
+ isHandler: (value) => this.isHandlerClass(value),
1975
+ isMiddleware: (value) => this.isMiddlewareClass(value),
1976
+ storeHandler: (ctor, from) => this.routes.register(ctor, from),
1977
+ removeHandler: (ctor, routes) => this.routes.unregister(ctor, routes),
1978
+ artifactsOf: (ctor) => this.routes.routesOf(ctor)
2187
1979
  });
2188
1980
  }
2189
1981
  warnUnhandledRoutes(commandLeaves) {
2190
- for (const route of commandLeaves) if (!this.maps[InteractionKind.Slash].has(route)) this.logger.warn(`Slash route ${paint.sky.bold(route)} has no registered ${paint.bold("@SlashRoute")} handler.`);
1982
+ this.routes.warnUnhandledRoutes(this.logger, commandLeaves);
2191
1983
  }
2192
1984
  warnUnhandledContextMenuRoutes(leaves) {
2193
- for (const name of leaves.user) if (!this.maps[InteractionKind.UserContextMenu].has(name)) this.logger.warn(`User context menu ${paint.sky.bold(name)} has no registered ${paint.bold("@ContextMenuRoute")} handler.`);
2194
- for (const name of leaves.message) if (!this.maps[InteractionKind.MessageContextMenu].has(name)) this.logger.warn(`Message context menu ${paint.sky.bold(name)} has no registered ${paint.bold("@ContextMenuRoute")} handler.`);
2195
- }
2196
- getArtifacts(handlerClass) {
2197
- return interactionRoutesOf(handlerClass).map(([routeType, routes]) => ({
2198
- routeType,
2199
- routes
2200
- }));
1985
+ this.routes.warnUnhandledContextMenuRoutes(this.logger, leaves);
2201
1986
  }
2202
1987
  async init() {
2203
1988
  if (this.isInitialized) return;
2204
1989
  this.isInitialized = true;
2205
- const handlersDir = this.core.config.bot.interactions.path;
2206
- if (!handlersDir) return;
2207
- const middlewareDir = hasKeys(this.core.config.bot.interactions, ["middlewares"]) ? this.core.config.bot.interactions.middlewares : void 0;
2208
- this.loading = true;
2209
- this.loadedHandlers.length = 0;
2210
- this.loadedMiddlewares.length = 0;
2211
- try {
2212
- if (middlewareDir) await this.loadMiddlewares(middlewareDir);
2213
- await this.loadHandlers(handlersDir);
2214
- } finally {
2215
- this.loading = false;
2216
- }
1990
+ await this.loader.load();
2217
1991
  this.attachToClient();
2218
1992
  this.reportLoad();
2219
1993
  }
2220
1994
  reportLoad() {
2221
1995
  const { utils } = this.logger;
1996
+ const { loadedHandlers, loadedMiddlewares } = this.loader;
2222
1997
  utils.summary("Loaded", {
2223
- "interaction handlers": this.loadedHandlers.length,
2224
- middlewares: this.loadedMiddlewares.length
1998
+ "interaction handlers": loadedHandlers.length,
1999
+ middlewares: loadedMiddlewares.length
2225
2000
  }, "debug");
2226
- if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares), "debug");
2227
- utils.block("Loaded interaction handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({
2001
+ if (loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(loadedMiddlewares), "debug");
2002
+ utils.block("Loaded interaction handlers", [...utils.entries(loadedHandlers), ...utils.counts({
2228
2003
  slash: this.maps[InteractionKind.Slash].size,
2229
2004
  buttons: this.maps[InteractionKind.Button].size,
2230
2005
  modals: this.maps[InteractionKind.Modal].size,
@@ -2238,29 +2013,6 @@ var InteractionDispatcher = class {
2238
2013
  autocomplete: this.maps[InteractionKind.Autocomplete].size
2239
2014
  })], "debug");
2240
2015
  }
2241
- async loadHandlers(dir) {
2242
- for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
2243
- if (!this.isHandlerClass(val)) continue;
2244
- this.registerHandler(val, relativePath);
2245
- this.hmrHandler?.trackHandler(fullPath, val);
2246
- }
2247
- }
2248
- async loadMiddlewares(dir) {
2249
- for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const val of Object.values(imported)) {
2250
- if (!this.isMiddlewareClass(val)) continue;
2251
- this.registerMiddleware(val, relativePath);
2252
- this.hmrHandler?.trackMiddleware(fullPath, val);
2253
- }
2254
- }
2255
- registerMiddleware(middlewareCtor, relativePath) {
2256
- const metadata = this.middlewares.register(middlewareCtor);
2257
- if (!metadata || !this.loading) return;
2258
- const scope = metadata.keys ? `${metadata.priority}, ${metadata.keys.join(", ")}` : metadata.priority;
2259
- this.loadedMiddlewares.push({
2260
- name: `${middlewareCtor.name} (${String(scope)})`,
2261
- from: formatFilePath(relativePath)
2262
- });
2263
- }
2264
2016
  isHandlerClass(obj) {
2265
2017
  if (typeof obj !== "function") return false;
2266
2018
  return obj.prototype instanceof InteractionHandler && Reflect.hasMetadata(InteractionMetadataKey, obj) || obj.prototype instanceof AutocompleteHandler && Reflect.hasMetadata(InteractionMetadataKey, obj);
@@ -2269,239 +2021,47 @@ var InteractionDispatcher = class {
2269
2021
  if (typeof obj !== "function") return false;
2270
2022
  return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(InteractionMiddlewareMetadataKey, obj);
2271
2023
  }
2272
- registerHandler(handlerClass, relativePath) {
2273
- const from = formatFilePath(relativePath);
2274
- const writes = [];
2275
- for (const [kind, routes] of interactionRoutesOf(handlerClass)) for (const route of routes) {
2276
- const existing = this.maps[kind].get(route);
2277
- if (existing && existing !== handlerClass) throw new SeedcordTypeError(SeedcordErrorCode.CoreDuplicateName, [
2278
- "route",
2279
- `${kind}:${route}`,
2280
- [`${paint.bold(existing.name)} (${paint.path(this.handlerFiles.get(existing) ?? "unknown file")})`, `${paint.bold(handlerClass.name)} (${paint.path(from)})`]
2281
- ]);
2282
- writes.push({
2283
- kind,
2284
- route
2285
- });
2286
- }
2287
- if (writes.length === 0) return;
2288
- for (const { kind, route } of writes) this.maps[kind].set(route, handlerClass);
2289
- this.handlerFiles.set(handlerClass, from);
2290
- if (this.loading) this.loadedHandlers.push({
2291
- name: handlerClass.name,
2292
- from
2293
- });
2294
- }
2295
2024
  async onHmr(event) {
2296
- await this.hmrHandler?.handle(event);
2297
- }
2298
- unregisterHandler(handlerClass, artifacts) {
2299
- this.handlerFiles.delete(handlerClass);
2300
- for (const { routeType, routes } of artifacts ?? this.getArtifacts(handlerClass)) for (const route of routes) this.maps[routeType].delete(route);
2301
- }
2302
- unregisterMiddleware(middlewareCtor) {
2303
- this.middlewares.unregister(middlewareCtor);
2025
+ await this.loader.onHmr(event);
2304
2026
  }
2305
2027
  attachToClient() {
2306
2028
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
2307
- if (this.inFlight.closed) return;
2308
- this.core.bus[PublishDefault]("anyInteraction", { interaction });
2309
- const run = this.handleInteraction(interaction).catch((caught) => {
2310
- const error = asError(caught);
2311
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2312
- this.core.bus[PublishDefault]("unhandledInteractionError", { error });
2029
+ this.inFlight.admit(() => {
2030
+ this.core.bus[PublishDefault]("anyInteraction", { interaction });
2031
+ return this.handleInteraction(interaction).catch((caught) => {
2032
+ const error = asError(caught);
2033
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2034
+ this.core.bus[PublishDefault]("unhandledInteractionError", { error });
2035
+ });
2313
2036
  });
2314
- this.inFlight.track(run);
2315
2037
  });
2316
2038
  }
2317
- stopAccepting() {
2318
- this.inFlight.close();
2319
- }
2320
- drain(timeoutMs) {
2321
- return this.inFlight.drain(timeoutMs);
2322
- }
2323
2039
  async handleCustomIdInteraction(interaction, kind) {
2324
2040
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
2325
- const prefix = prefixOf(interaction.customId);
2326
- if (!prefix) this.logger.warn(`${paint.sky.bold(kind)} has invalid customId: ${paint.mute(interaction.customId)}`);
2327
- await this.processInteraction(interaction, kind, () => prefix);
2041
+ await this.processInteraction(interaction, kind, prefixOf(interaction.customId));
2328
2042
  }
2329
- async processInteraction(interaction, kind, extractKey, fallback = UnhandledRepliable) {
2330
- const key = extractKey(interaction);
2331
- const startedAt = performance.now();
2332
- const queuedMs = queuedMsFor(interaction.id);
2333
- const matched = this.maps[kind].get(key);
2334
- const HandlerCtor = matched ?? fallback;
2335
- const dispatch = new DispatchContext(`${kind}:${key || "unrouted"}`);
2336
- const report = this.reporterFor({
2043
+ async processInteraction(interaction, kind, key) {
2044
+ const { fallback, ctor: HandlerCtor } = matchRoute(this.maps, kind, key, UNHANDLED);
2045
+ const config = gatewayRunConfig({
2337
2046
  interaction,
2338
- kind,
2339
- fallback: !matched,
2340
- startedAt,
2341
- queuedMs,
2342
- dispatchId: dispatch.id,
2343
- routeId: dispatch.routeId
2047
+ core: this.core,
2048
+ dispatch: new DispatchContext(kind, key, fallback),
2049
+ HandlerCtor,
2050
+ middlewares: this.middlewares,
2051
+ logger: this.logger
2344
2052
  });
2345
- let sender;
2346
- const ran = [];
2347
- let result = { outcome: "handled" };
2348
- try {
2349
- const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
2350
- if (handler instanceof RepliableHandler) sender = handler.sender;
2351
- const refusal = await this.refusalBeforeHandler({
2352
- HandlerCtor,
2353
- kind,
2354
- interaction,
2355
- dispatch,
2356
- sender,
2357
- ran,
2358
- declaredRoute: matched ? dispatch.routeId : void 0
2359
- });
2360
- if (refusal) {
2361
- result = resultFor(refusal.caught);
2362
- await this.answer(refusal.caught, interaction, dispatch, sender, report);
2363
- return;
2364
- }
2365
- await handler.execute();
2366
- report("handled");
2367
- } catch (caught) {
2368
- if (result.outcome === "handled") result = resultFor(caught);
2369
- await this.answer(caught, interaction, dispatch, sender, report);
2370
- } finally {
2371
- await runAfter(ran, result, this.logger);
2372
- }
2373
- }
2374
- async answer(caught, interaction, dispatch, sender, report) {
2375
- try {
2376
- await handleInteractionFault(caught, interaction, this.core, dispatch, sender);
2377
- } finally {
2378
- report(outcomeFor(caught));
2379
- }
2380
- }
2381
- buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2382
- if (isFallback) this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2383
- this.logger.debug(`Processing ${paint.sky.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
2384
- return new HandlerCtor(interaction, this.core, dispatch);
2385
- }
2386
- reporterFor(row) {
2387
- let reported = false;
2388
- return (outcome) => {
2389
- if (reported) return;
2390
- reported = true;
2391
- reportDispatch(this.core.bus, {
2392
- dispatchId: row.dispatchId,
2393
- routeId: row.routeId,
2394
- interactionId: row.interaction.id,
2395
- kind: row.kind,
2396
- outcome,
2397
- startedAt: row.startedAt,
2398
- fallback: row.fallback,
2399
- userId: row.interaction.user.id,
2400
- guildId: row.interaction.guildId,
2401
- queuedMs: row.queuedMs
2402
- });
2403
- };
2404
- }
2405
- async refusalBeforeHandler(step) {
2406
- const { HandlerCtor, kind, interaction, dispatch, sender, ran, declaredRoute } = step;
2407
- if (kind !== InteractionKind.Autocomplete && sender) try {
2408
- await this.runMiddlewares(kind, interaction, dispatch, sender, ran);
2409
- } catch (caught) {
2410
- return { caught };
2411
- }
2412
- if (interaction.isAutocomplete()) return null;
2413
- return this.gateRefusal(HandlerCtor, interaction, dispatch, declaredRoute);
2414
- }
2415
- async runMiddlewares(kind, interaction, dispatch, sender, ran) {
2416
- for (const Middleware of this.middlewares.chainFor(kind)) {
2417
- const middleware = new Middleware(interaction, this.core, dispatch, sender);
2418
- ran.push(middleware);
2419
- await middleware.execute();
2420
- }
2421
- }
2422
- async gateRefusal(HandlerCtor, interaction, dispatch, declaredRoute) {
2423
- const monitor = slowGateMonitor();
2424
- try {
2425
- await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core, dispatch), declaredRoute, monitor?.observe);
2426
- return null;
2427
- } catch (caught) {
2428
- return { caught };
2429
- } finally {
2430
- monitor?.report(declaredRoute ?? null);
2431
- }
2053
+ await (await new InteractionRun(config).prepare())?.();
2432
2054
  }
2433
2055
  async handleInteraction(interaction) {
2434
- switch (true) {
2435
- case interaction.isChatInputCommand():
2436
- await this.handleSlashCommand(interaction);
2437
- break;
2438
- case interaction.isButton():
2439
- await this.handleButton(interaction);
2440
- break;
2441
- case interaction.isModalSubmit():
2442
- await this.handleModal(interaction);
2443
- break;
2444
- case interaction.isStringSelectMenu():
2445
- await this.handleStringSelectMenu(interaction);
2446
- break;
2447
- case interaction.isUserSelectMenu():
2448
- await this.handleUserSelectMenu(interaction);
2449
- break;
2450
- case interaction.isRoleSelectMenu():
2451
- await this.handleRoleSelectMenu(interaction);
2452
- break;
2453
- case interaction.isChannelSelectMenu():
2454
- await this.handleChannelSelectMenu(interaction);
2455
- break;
2456
- case interaction.isMentionableSelectMenu():
2457
- await this.handleMentionableSelectMenu(interaction);
2458
- break;
2459
- case interaction.isMessageContextMenuCommand():
2460
- await this.handleMessageContextMenu(interaction);
2461
- break;
2462
- case interaction.isUserContextMenuCommand():
2463
- await this.handleUserContextMenu(interaction);
2464
- break;
2465
- case interaction.isAutocomplete():
2466
- await this.handleAutocomplete(interaction);
2467
- break;
2468
- default: this.logger.warn(`Unhandled interaction type: ${interaction.type}`);
2469
- }
2470
- }
2471
- async handleSlashCommand(interaction) {
2472
- const route = slashRouteOf(interaction);
2473
- await this.processInteraction(interaction, InteractionKind.Slash, () => route);
2474
- }
2475
- async handleButton(interaction) {
2476
- await this.handleCustomIdInteraction(interaction, InteractionKind.Button);
2477
- }
2478
- async handleModal(interaction) {
2479
- await this.handleCustomIdInteraction(interaction, InteractionKind.Modal);
2480
- }
2481
- async handleStringSelectMenu(interaction) {
2482
- await this.handleCustomIdInteraction(interaction, InteractionKind.StringMenu);
2483
- }
2484
- async handleUserSelectMenu(interaction) {
2485
- await this.handleCustomIdInteraction(interaction, InteractionKind.UserMenu);
2486
- }
2487
- async handleRoleSelectMenu(interaction) {
2488
- await this.handleCustomIdInteraction(interaction, InteractionKind.RoleMenu);
2489
- }
2490
- async handleChannelSelectMenu(interaction) {
2491
- await this.handleCustomIdInteraction(interaction, InteractionKind.ChannelMenu);
2492
- }
2493
- async handleMentionableSelectMenu(interaction) {
2494
- await this.handleCustomIdInteraction(interaction, InteractionKind.MentionableMenu);
2495
- }
2496
- async handleMessageContextMenu(interaction) {
2497
- await this.processInteraction(interaction, InteractionKind.MessageContextMenu, () => interaction.commandName);
2498
- }
2499
- async handleUserContextMenu(interaction) {
2500
- await this.processInteraction(interaction, InteractionKind.UserContextMenu, () => interaction.commandName);
2501
- }
2502
- async handleAutocomplete(interaction) {
2503
- const route = slashRouteOf(interaction);
2504
- await this.processInteraction(interaction, InteractionKind.Autocomplete, () => route, UnhandledAutocomplete);
2056
+ if (interaction.isChatInputCommand()) await this.processInteraction(interaction, InteractionKind.Slash, slashRouteOf(interaction));
2057
+ else if (interaction.isAutocomplete()) await this.processInteraction(interaction, InteractionKind.Autocomplete, slashRouteOf(interaction));
2058
+ else if (interaction.isModalSubmit()) await this.handleCustomIdInteraction(interaction, InteractionKind.Modal);
2059
+ else if (interaction.isButton() || interaction.isAnySelectMenu()) {
2060
+ const kind = componentKindOf(interaction.componentType);
2061
+ if (kind) await this.handleCustomIdInteraction(interaction, kind);
2062
+ } else if (interaction.isMessageContextMenuCommand()) await this.processInteraction(interaction, InteractionKind.MessageContextMenu, interaction.commandName);
2063
+ else if (interaction.isUserContextMenuCommand()) await this.processInteraction(interaction, InteractionKind.UserContextMenu, interaction.commandName);
2064
+ else this.logger.warn(`Unhandled interaction type: ${interaction.type}`);
2505
2065
  }
2506
2066
  };
2507
2067
 
@@ -2535,6 +2095,7 @@ var Bot = class {
2535
2095
  #logger = new Logger("Bot", { channel: "bot" });
2536
2096
  /** @internal */
2537
2097
  [loggerSlot] = this.#logger;
2098
+ #inFlight = new InFlight(this.#logger, "Dispatch");
2538
2099
  #isInitialized = false;
2539
2100
  #readyToDeploy = Promise.withResolvers();
2540
2101
  #intents;
@@ -2553,8 +2114,8 @@ var Bot = class {
2553
2114
  constructor(core) {
2554
2115
  this.#intents = core.config.bot.clientOptions.intents;
2555
2116
  this.#client = new Client(core.config.bot.clientOptions);
2556
- if (core.config.bot.interactions.path) this.#interactions = new InteractionDispatcher(core);
2557
- if (core.config.bot.events.path) this.#events = new EventDispatcher(core);
2117
+ if (core.config.bot.interactions.path) this.#interactions = new InteractionDispatcher(core, this.#inFlight);
2118
+ if (core.config.bot.events.path) this.#events = new EventDispatcher(core, this.#inFlight);
2558
2119
  if (core.config.bot.commands.path) this.#commandRegistry = new CommandRegistry(core);
2559
2120
  this.#emojiInjector = new EmojiInjector(core);
2560
2121
  this.#registerStartupTasks(core);
@@ -2575,39 +2136,36 @@ var Bot = class {
2575
2136
  return application.id;
2576
2137
  }
2577
2138
  #registerStartupTasks(core) {
2139
+ const interactions = this.#interactions;
2140
+ if (interactions) core.startup.addTask(StartupPhase.Configuration, "interactions-initialization", () => interactions.init());
2141
+ const events = this.#events;
2142
+ if (events) core.startup.addTask(StartupPhase.Configuration, "events-initialization", () => events.init());
2578
2143
  core.startup.addTask(StartupPhase.Login, "bot-initialization", async (signal) => {
2579
2144
  this.#logger.utils.initialization("Bot", "start");
2580
2145
  await this.#init(signal);
2581
2146
  this.#logger.utils.initialization("Bot", "end");
2582
2147
  });
2583
2148
  const registry = this.#commandRegistry;
2584
- if (!registry) return;
2585
- core.startup.addTask(StartupPhase.Login, "command-deploy", (signal) => this.#deployCommands(registry, signal), COMMAND_DEPLOY_TIMEOUT_MS);
2149
+ if (registry) core.startup.addTask(StartupPhase.Login, "command-deploy", (signal) => this.#deployCommands(registry, signal), COMMAND_DEPLOY_TIMEOUT_MS);
2150
+ core.startup.addTask(StartupPhase.Ready, "start-dispatch", () => {
2151
+ this.#inFlight.open();
2152
+ return Promise.resolve();
2153
+ });
2586
2154
  }
2587
2155
  #registerShutdownTasks(core) {
2588
2156
  core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
2589
- this.#stopAccepting();
2157
+ this.#inFlight.close();
2590
2158
  return Promise.resolve();
2591
2159
  }, UNBIND_TIMEOUT_MS);
2592
- core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.#drain(), DRAIN_TASK_TIMEOUT_MS);
2160
+ core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.#inFlight.drain(DRAIN_WINDOW_MS), DRAIN_TASK_TIMEOUT_MS);
2593
2161
  core.shutdown.addTask(ShutdownPhase.Logout, "stop-bot", async () => await this.#stop(), LOGOUT_TIMEOUT_MS);
2594
2162
  }
2595
- #stopAccepting() {
2596
- this.#interactions?.stopAccepting();
2597
- this.#events?.stopAccepting();
2598
- }
2599
- async #drain() {
2600
- await Promise.allSettled([this.#interactions?.drain(DRAIN_WINDOW_MS), this.#events?.drain(DRAIN_WINDOW_MS)]);
2601
- }
2602
2163
  async #init(signal) {
2603
2164
  if (this.#isInitialized) return;
2604
2165
  this.#isInitialized = true;
2605
2166
  signal.addEventListener("abort", () => this.#readyToDeploy.resolve(false), { once: true });
2606
2167
  try {
2607
- const token = this.botToken;
2608
- if (this.#interactions) await this.#interactions.init();
2609
- if (this.#events) await this.#events.init();
2610
- await this.#login(token);
2168
+ await this.#login(this.botToken);
2611
2169
  await this.#emojiInjector.init();
2612
2170
  } catch (error) {
2613
2171
  this.#readyToDeploy.resolve(false);
@@ -2629,8 +2187,7 @@ var Bot = class {
2629
2187
  async #login(token) {
2630
2188
  const ready = Promise.withResolvers();
2631
2189
  this.#client.once(Events.ClientReady, () => ready.resolve());
2632
- this.#client.login(token);
2633
- await ready.promise;
2190
+ await Promise.all([this.#client.login(token), ready.promise]);
2634
2191
  this.#logger.info(`Logged in as ${paint.sky.bold(this.#client.user?.username)}!`);
2635
2192
  return this;
2636
2193
  }
@@ -2646,7 +2203,7 @@ var Bot = class {
2646
2203
  //#endregion
2647
2204
  //#region src/version.ts
2648
2205
  /** Package version */
2649
- const version = "0.8.0";
2206
+ const version = "0.9.0-next.2";
2650
2207
 
2651
2208
  //#endregion
2652
2209
  //#region src/Seedcord.ts
@@ -2664,8 +2221,6 @@ var Seedcord = class extends ServerHost {
2664
2221
  [HostVersion] = version;
2665
2222
  #healthCheck;
2666
2223
  #hmrManager;
2667
- /** @see {@link Bus} */
2668
- bus;
2669
2224
  #subscribers;
2670
2225
  /** @see {@link Bot} */
2671
2226
  bot;
@@ -2675,24 +2230,20 @@ var Seedcord = class extends ServerHost {
2675
2230
  get applicationId() {
2676
2231
  return this.bot.applicationId;
2677
2232
  }
2678
- /** @see {@link IRateLimiter} */
2679
- rateLimiter;
2680
2233
  /**
2681
2234
  * @param config - Bot configuration including paths and Discord client options
2682
2235
  * @throws A **SeedcordError** when another `Seedcord` exists in this process and has not shut down.
2683
2236
  * @throws A **SeedcordError** when the running Node or Bun is older than the range in `engines`.
2684
2237
  */
2685
2238
  constructor(config) {
2686
- super("gateway", new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup());
2239
+ super("gateway", new CoordinatedShutdown(config.lifecycle?.shutdownDeadline), new CoordinatedStartup(), config.kv);
2687
2240
  this.config = config;
2688
2241
  bindBotColor(() => this.config.botColor);
2689
2242
  this.#hmrManager = new HmrManager();
2690
2243
  this.#hmrManager.init(connectDevChannel());
2691
- this.bus = new Bus(this);
2692
2244
  this.#subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
2693
2245
  this.bot = new Bot(this);
2694
2246
  this.rest = this.bot.client.rest;
2695
- this.rateLimiter = config.store ?? new MemoryRateLimiter();
2696
2247
  this.#healthCheck = HealthCheck.fromOption(shutdownOf(this), config.healthCheck);
2697
2248
  this.#registerStartupTasks();
2698
2249
  }
@@ -2735,5 +2286,5 @@ var Seedcord = class extends ServerHost {
2735
2286
  };
2736
2287
 
2737
2288
  //#endregion
2738
- export { ArraySource, AutocompleteHandler, BaseHandler, ButtonHandler, ChannelMenuHandler, ComponentHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, GatewayEmoji, HmrModuleHandler, IgnoreBots, InteractionHandler, InteractionMiddleware, MentionableMenuHandler, MessageContextMenuHandler, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RegisterEventMiddleware, RepliableHandler, ReplySender, RoleMenuHandler, Seedcord, SelectMenuHandler, SlashHandler, StringMenuHandler, Subscriber, UserContextMenuHandler, UserMenuHandler, WebhookLog, WinstonConsoleSink, WinstonFileSink, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
2289
+ export { ArraySource, AutocompleteHandler, BaseHandler, ButtonHandler, ChannelMenuHandler, ComponentHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, GatewayEmoji, HmrModuleHandler, IgnoreBots, InteractionHandler, InteractionMiddleware, MentionPrefix, MentionableMenuHandler, MessageContextMenuHandler, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RegisterEventMiddleware, RepliableHandler, ReplySender, RoleMenuHandler, Seedcord, SelectMenuHandler, SlashHandler, StringMenuHandler, Subscriber, UserContextMenuHandler, UserMenuHandler, WebhookLog, WinstonConsoleSink, WinstonFileSink, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
2739
2290
  //# sourceMappingURL=index.mjs.map