@seedcord/gateway 0.1.0-next.4 → 0.1.0-next.6

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,27 +1,24 @@
1
1
  import "reflect-metadata";
2
2
  import { installNodeDefaults } from "@seedcord/logger/node";
3
- import { BaseReplySender, CommandMetadataKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, SubscribeMetadataKey, WebhookUrlMetadataKey, areRoutes, decodeComponentRoute, deferFlags, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
3
+ import { BaseReplySender, CommandInjector, EventMetadataKey, FaultThrottle, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, accessorStore, areRoutes, asError, clearStore, decodeComponentRoute, deferFlags, dispatchedPayload, guardedAccessor, isEmojiTuple, outcomeFor, pickNotice, prefixOf, queuedMsFor, reportedWrite, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
4
4
  import { SeedcordErrorCode } from "@seedcord/errors";
5
- import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
- import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
5
+ import { SeedcordError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
+ import { ArraySource as ArraySource$1, BaseHandler, BuilderComponent, Bus, CursorSource as CursorSource$1, CustomId, DispatchContext, Fault, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
7
7
  import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
8
- import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
9
- import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
10
- import { ApplicationCommandType, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, TextChannel, User, chatInputApplicationCommandMention } from "discord.js";
11
- import * as crypto from "node:crypto";
12
- import chalk from "chalk";
13
- import { Converters, Envapter } from "envapt";
14
- import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
15
- import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
16
- import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
17
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, settleWithin } from "@seedcord/core/node/internal";
8
+ import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, User } from "discord.js";
9
+ import { ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
10
+ import { buildSlashRoute } from "@seedcord/utils/internal";
11
+ import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
12
+ import { CommandRegistry, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, SubscriberLoader, settleWithin } from "@seedcord/core/node/internal";
18
13
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
19
14
  import { SeedcordBrand } from "@seedcord/types/internal";
20
- import { TypedEventEmitter } from "@seedcord/event-emitter";
15
+ import { Envapter } from "envapt";
16
+ import chalk from "chalk";
21
17
  import { Envapt } from "envapt/legacy";
22
- import { resolve } from "node:path";
23
18
  import { HmrModuleHandler } from "@seedcord/core/hmr";
19
+ import { formatFilePath, hasKeys } from "@seedcord/utils";
24
20
  import { traverseDirectory } from "@seedcord/utils/node";
21
+ import * as crypto from "node:crypto";
25
22
 
26
23
  export * from "@seedcord/errors"
27
24
 
@@ -29,10 +26,10 @@ export * from "@seedcord/core"
29
26
 
30
27
  export * from "@seedcord/logger"
31
28
 
32
- export * from "@seedcord/utils"
33
-
34
29
  export * from "@seedcord/rate-limiter"
35
30
 
31
+ export * from "@seedcord/utils"
32
+
36
33
  export * from "@seedcord/event-emitter"
37
34
 
38
35
  //#region src/logger-bootstrap.ts
@@ -147,7 +144,7 @@ let MiddlewareType = /* @__PURE__ */ function(MiddlewareType) {
147
144
  * Interaction middleware cannot specify event filters.
148
145
  *
149
146
  * @param type - Middleware kind from {@link MiddlewareType}
150
- * @param priority - Ordering value where lower runs earlier. {@default `0`}
147
+ * @param priority - Ordering value where lower runs earlier. {@default `0` }
151
148
  * @param options - Additional registration options
152
149
  *
153
150
  * @decorator
@@ -226,59 +223,6 @@ var CannotAssignBotRole = class extends Notice {
226
223
  return { components: [new NoticeCard("I cannot assign a managed role.").component] };
227
224
  }
228
225
  };
229
- var HasDangerousPermissions = class extends Notice {
230
- subject;
231
- dangerousPerms;
232
- customLead;
233
- constructor(message, subject, dangerousPerms) {
234
- super(message ?? "A dangerous permission is enabled.");
235
- this.subject = subject;
236
- this.dangerousPerms = dangerousPerms;
237
- this.customLead = message;
238
- }
239
- render() {
240
- const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
241
- return { components: [new NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
242
- }
243
- };
244
- var UserNotFound = class extends Notice {
245
- userArg;
246
- constructor(userArg) {
247
- super(`User not found: ${userArg}`);
248
- this.userArg = userArg;
249
- }
250
- render() {
251
- return { components: [new NoticeCard(`User probably doesn't exist or was deleted.\n**User Argument:** \`${this.userArg}\`\nPlease check the user ID and try again. Only pass valid user IDs as the argument.`, "User Not Found").component] };
252
- }
253
- };
254
- var UserNotInGuild = class extends Notice {
255
- constructor(message = "User is not in the guild.") {
256
- super(message);
257
- }
258
- render() {
259
- return { components: [new NoticeCard(this.message).component] };
260
- }
261
- };
262
- var RoleDoesNotExist = class extends Notice {
263
- roleId;
264
- constructor(message, roleId) {
265
- super(message);
266
- this.roleId = roleId;
267
- }
268
- render() {
269
- return { components: [new NoticeCard(`The role with ID \`${this.roleId}\` does not exist.`).component] };
270
- }
271
- };
272
- var CouldNotFindChannel = class extends Notice {
273
- channelId;
274
- constructor(message, channelId) {
275
- super(message);
276
- this.channelId = channelId;
277
- }
278
- render() {
279
- return { components: [new NoticeCard(`Could not find channel with ID \`${this.channelId}\`. It could also be that the channel is not a text channel.`).component] };
280
- }
281
- };
282
226
 
283
227
  //#endregion
284
228
  //#region src/bot/gates/catalog/Nsfw.ts
@@ -316,26 +260,21 @@ function Nsfw(options) {
316
260
 
317
261
  //#endregion
318
262
  //#region src/bot/injectors/EmojiInjector.ts
319
- const emojiStorage = {};
320
- function isEmojiTuple(v) {
321
- return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string";
322
- }
263
+ const emojiStorage = accessorStore();
323
264
  /**
324
- * Global emoji mappings object
325
- *
326
- * @see {@link EmojiMap}
265
+ * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
266
+ * A read before that throws.
327
267
  */
328
- const Emojis = emojiStorage;
268
+ const Emojis = guardedAccessor("Emojis", emojiStorage);
329
269
  /**
330
- * Loads and injects emojis based on bot configuration.
331
- *
332
- * For emojis injected from specific guilds, ensure that the Guilds intent is provided in client options.
270
+ * Loads and injects emojis based on bot configuration. A guild emoji requires the Guilds intent in the client
271
+ * options.
333
272
  *
334
273
  * @internal
335
274
  */
336
275
  var EmojiInjector = class {
337
276
  core;
338
- logger = new Logger("Emojis");
277
+ logger = new Logger("Emojis", { channel: "bot" });
339
278
  constructor(core) {
340
279
  this.core = core;
341
280
  }
@@ -382,126 +321,10 @@ var EmojiInjector = class {
382
321
  emojiStorage[key] = appEmoji;
383
322
  }
384
323
  clearEmojis() {
385
- for (const key of Object.keys(emojiStorage)) Reflect.deleteProperty(emojiStorage, key);
324
+ clearStore(emojiStorage);
386
325
  }
387
326
  };
388
327
 
389
- //#endregion
390
- //#region src/bot/injectors/CommandMentionInjector.ts
391
- const mentionStorage = {};
392
- /**
393
- * Ready-to-send command mention strings, keyed by {@link SlashOptionRegistry}.
394
- *
395
- * A value is a clickable `</name:id>` when the command's id resolved from the deploy, otherwise the plain
396
- * `/name` text. Populated by {@link CommandMentionInjector} after each deploy.
397
- */
398
- const CommandMentions = mentionStorage;
399
- const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
400
- /**
401
- * Turns each deployed slash route into a clickable command mention exposed through {@link CommandMentions}.
402
- *
403
- * Consumes the ids {@link DeployResult} carries, so it runs after a deploy rather than resolving from a name
404
- * cache (discord.js offers none for command ids). A global command is clickable everywhere by its app-wide id.
405
- * A guild command deployed to one guild is clickable by that guild's id. A command deployed to two or more
406
- * guilds mints a different id per guild, so one accessor key cannot resolve it, and it falls back to plain
407
- * `/name` text with one warn. On a non-owning shard a guild command also falls back to plain text.
408
- *
409
- * @internal
410
- */
411
- var CommandMentionInjector = class {
412
- core;
413
- logger = new Logger("CommandMentions");
414
- constructor(core) {
415
- this.core = core;
416
- }
417
- inject(deploy) {
418
- this.clear();
419
- const globalIds = this.indexByName(deploy.global);
420
- const guildIds = this.indexGuilds(deploy.guilds);
421
- const warned = /* @__PURE__ */ new Set();
422
- let clickable = 0;
423
- for (const command of this.allSlashBuilders()) {
424
- const id = this.resolveId(command.name, globalIds, guildIds, warned);
425
- for (const leaf of routeLeavesOf(command.toJSON())) {
426
- mentionStorage[leaf.route] = id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route);
427
- if (id) clickable++;
428
- }
429
- }
430
- this.logger.utils.summary("Linked mentions", {
431
- clickable,
432
- plain: Object.keys(mentionStorage).length - clickable
433
- });
434
- }
435
- allSlashBuilders() {
436
- const registry = this.core.bot.commands;
437
- if (!registry) return [];
438
- const slash = [...registry.globalCommands, ...registry.guildCommands.values()].flat().filter((command) => command instanceof SlashCommandBuilder);
439
- return [...new Set(slash)];
440
- }
441
- indexByName(collection) {
442
- const map = /* @__PURE__ */ new Map();
443
- for (const command of collection.values()) map.set(command.name, command.id);
444
- return map;
445
- }
446
- indexGuilds(guilds) {
447
- const map = /* @__PURE__ */ new Map();
448
- for (const collection of guilds.values()) for (const command of collection.values()) {
449
- const existing = map.get(command.name);
450
- if (existing === void 0) map.set(command.name, command.id);
451
- else if (existing !== command.id) map.set(command.name, MULTI_GUILD);
452
- }
453
- return map;
454
- }
455
- resolveId(name, globalIds, guildIds, warned) {
456
- const global = globalIds.get(name);
457
- if (global) return global;
458
- const guild = guildIds.get(name);
459
- if (guild === void 0) return void 0;
460
- if (guild === MULTI_GUILD) {
461
- if (!warned.has(name)) {
462
- this.logger.warn(`${name} is deployed to multiple guilds, falling back to plain text (not clickable).`);
463
- warned.add(name);
464
- }
465
- return;
466
- }
467
- return guild;
468
- }
469
- toMention(route, id) {
470
- const [name, middle, sub] = route.split("/");
471
- if (name && middle && sub) return chatInputApplicationCommandMention(name, middle, sub, id);
472
- if (name && middle) return chatInputApplicationCommandMention(name, middle, id);
473
- return chatInputApplicationCommandMention(name ?? route, id);
474
- }
475
- toPlain(route) {
476
- return `/${route.replaceAll("/", " ")}`;
477
- }
478
- clear() {
479
- for (const key of Object.keys(mentionStorage)) Reflect.deleteProperty(mentionStorage, key);
480
- }
481
- };
482
-
483
- //#endregion
484
- //#region src/bot/utilities/channels/fetchText.ts
485
- /**
486
- * Fetches and validates a text channel. Refuses when the channel doesn't exist or isn't a text channel.
487
- *
488
- * @param client - The Discord client instance
489
- * @param channelId - Channel ID or TextChannel instance
490
- * @returns Promise resolving to the text channel
491
- */
492
- async function fetchText(client, channelId) {
493
- if (channelId instanceof TextChannel) return channelId;
494
- let channel = client.channels.cache.get(channelId);
495
- if (!channel) try {
496
- channel = await client.channels.fetch(channelId);
497
- } catch (err) {
498
- if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownChannel) throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
499
- throw err;
500
- }
501
- if (channel?.isTextBased()) return channel;
502
- throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
503
- }
504
-
505
328
  //#endregion
506
329
  //#region src/bot/notices/utils.ts
507
330
  function mentionFor(subject) {
@@ -512,48 +335,37 @@ function mentionFor(subject) {
512
335
  //#endregion
513
336
  //#region src/bot/utilities/permissions/checkPermissions.ts
514
337
  function checkPermissions(a, b, c, d, e) {
515
- const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
338
+ const { for: pFor, in: pIn, scope, inverse = false, missingNotice, dangerousNotice } = a instanceof Role || a instanceof GuildMember ? {
516
339
  for: a,
517
340
  in: b,
518
341
  scope: c,
519
342
  inverse: d ?? false,
520
343
  ...e
521
344
  } : a;
522
- const Missing = missingCtor ?? MissingPermissions;
523
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
524
- const subject = mentionFor(pFor);
525
345
  const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
526
- const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
527
- if (inverse) {
528
- const present = scope.filter((bit) => perms.has(bit, true));
529
- if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
530
- return;
531
- }
532
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
533
- if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
346
+ assertPermissions({
347
+ subject: mentionFor(pFor),
348
+ permissions: perms.bitfield,
349
+ scope,
350
+ inverse,
351
+ ...missingNotice && { missingNotice },
352
+ ...dangerousNotice && { dangerousNotice }
353
+ });
534
354
  }
535
355
 
536
356
  //#endregion
537
357
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
538
358
  /**
539
- * Checks if the bot has required permissions in a {@link Guild} or {@link TextChannel}. Refuses when a
540
- * required permission is missing.
541
- *
542
- * @param target - Guild or text channel to check in
543
- * @param scope - Permission bits to validate
544
- * @param inverse - Whether to check for absence of the given permissions
545
- * @param errors - Optional custom error constructors
359
+ * Checks the bot's own permissions in a {@link Guild} or {@link TextChannel}. Refuses when a permission in
360
+ * `scope` is missing, or with `inverse` when one is present. When the bot member is uncached, the refusal
361
+ * reports every permission in `scope` as missing.
546
362
  *
547
363
  * @example
548
364
  * ```ts
549
365
  * import { PermissionFlagsBits } from 'discord.js';
550
366
  * import { checkBotPermissions } from '@seedcord/gateway';
551
367
  *
552
- * // Check if the bot has SendMessages and ViewChannel permissions in a text channel
553
- * checkBotPermissions(textChannel, [
554
- * PermissionFlagsBits.SendMessages,
555
- * PermissionFlagsBits.ViewChannel
556
- * ]);
368
+ * checkBotPermissions(textChannel, [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel]);
557
369
  * ```
558
370
  */
559
371
  function checkBotPermissions(target, scope, inverse = false, errors) {
@@ -568,7 +380,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
568
380
  }
569
381
  const me = target.guild.members.me;
570
382
  if (!me) {
571
- new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
383
+ new Logger("checkBotPermissions", { channel: "errors" }).warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
572
384
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
573
385
  throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
574
386
  }
@@ -576,26 +388,17 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
576
388
  }
577
389
 
578
390
  //#endregion
579
- //#region src/bot/utilities/roles/getBotRole.ts
580
- /**
581
- * Gets the bot's managed role in a guild.
582
- *
583
- * @param guild - The guild to get the bot role from
584
- * @returns The bot's managed role in the guild
585
- * @throws A **SeedcordError** if the client user is unavailable or if the bot role is missing
586
- */
587
- function getBotRole(guild) {
391
+ //#region src/bot/utilities/permissions/hasPermsToAssign.ts
392
+ function botRoleOf(guild) {
588
393
  const botRole = guild.roles.botRoleFor(guild.client.user);
589
394
  if (!botRole) throw new SeedcordError(SeedcordErrorCode.CoreBotRoleMissing, [guild.id]);
590
395
  return botRole;
591
396
  }
592
-
593
- //#endregion
594
- //#region src/bot/utilities/permissions/hasPermsToAssign.ts
595
397
  /**
596
- * Validates if 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.
398
+ * 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.
597
399
  *
598
400
  * @param roleOrOptions - Target role or complete options for the check
401
+ * @throws A `Notice` on refusal, or a `SeedcordError` when the bot has no role in the guild's role cache.
599
402
  *
600
403
  * @example
601
404
  * ```ts
@@ -623,135 +426,108 @@ function hasPermsToAssign(roleOrOptions) {
623
426
  const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
624
427
  const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
625
428
  const MissingNotice = errors?.missingNotice ?? MissingPermissions;
626
- const botRole = getBotRole(targetRole.guild);
429
+ const botRole = botRoleOf(targetRole.guild);
627
430
  if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
628
431
  if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
629
432
  checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
630
433
  }
631
434
 
632
435
  //#endregion
633
- //#region src/bot/utilities/roles/fetchRole.ts
634
- function isUnknownRole(err) {
635
- return err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownRole;
636
- }
637
- async function fetchRoleInGuild(guild, roleId) {
638
- const cached = guild.roles.cache.get(roleId);
639
- if (cached) return cached;
640
- try {
641
- return await guild.roles.fetch(roleId);
642
- } catch (err) {
643
- if (isUnknownRole(err)) throw new RoleDoesNotExist("Role not found in specified guild", roleId);
644
- throw err;
645
- }
646
- }
647
- async function scanGuildsForRole(client, roleId) {
648
- const cached = client.guilds.cache.map((guild) => guild.roles.cache.get(roleId)).find(Boolean);
649
- if (cached) return cached;
650
- for (const guild of client.guilds.cache.values()) try {
651
- const role = await guild.roles.fetch(roleId);
652
- if (role) return role;
653
- } catch (err) {
654
- if (isUnknownRole(err)) continue;
655
- throw err;
656
- }
657
- return null;
658
- }
436
+ //#region src/bot/confirm/reserved.ts
437
+ const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
659
438
  /**
660
- * Fetches a role by ID from a client or guild. Refuses when the role doesn't exist.
439
+ * The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
440
+ * in-process collector reads them. Do not mint a real component from it.
661
441
  *
662
- * @param clientOrGuild - Discord client or guild instance
663
- * @param roleId - The role ID to fetch
664
- * @returns Promise resolving to the role
442
+ * @internal
665
443
  */
666
- async function fetchRole(clientOrGuild, roleId) {
667
- if (!roleId) throw new RoleDoesNotExist("Role ID is null or undefined", roleId);
668
- const role = clientOrGuild instanceof Guild ? await fetchRoleInGuild(clientOrGuild, roleId) : await scanGuildsForRole(clientOrGuild, roleId);
669
- if (!role) throw new RoleDoesNotExist("Role not found", roleId);
670
- return role;
671
- }
444
+ const CONFIRM_DEF = new CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
672
445
 
673
446
  //#endregion
674
- //#region src/bot/utilities/users/fetchGuildMember.ts
675
- /**
676
- * Fetches a guild member by user ID. Refuses when the user is not in the guild.
677
- *
678
- * @param guild - The guild to fetch the member from
679
- * @param userId - The Discord user ID
680
- * @returns Promise resolving to the guild member
681
- */
682
- async function fetchGuildMember(guild, userId) {
683
- let user = guild.members.cache.get(userId);
684
- user ??= await guild.members.fetch(userId).catch((err) => {
685
- if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownMember) throw new UserNotInGuild(`User with ID ${userId} not found in guild`);
686
- throw err;
687
- });
688
- return user;
447
+ //#region src/bot/confirm/getConfirmation.ts
448
+ const DEFAULT_TIMEOUT_MS = 3e4;
449
+ const CONFIRM_IDS = {
450
+ confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
451
+ cancel: CONFIRM_DEF.encode({ choice: "cancel" })
452
+ };
453
+ var ConfirmButtons = class extends RowComponent {
454
+ constructor(options) {
455
+ super("button");
456
+ this.instance.addComponents(new ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? ButtonStyle.Danger), new ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(ButtonStyle.Secondary));
457
+ }
458
+ };
459
+ var ConfirmPromptCard = class extends BuilderComponent {
460
+ constructor(text, options) {
461
+ super("container");
462
+ this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
463
+ }
464
+ };
465
+ function defaultPrompt(text, options) {
466
+ return { components: [new ConfirmPromptCard(text, options).component] };
689
467
  }
690
-
691
- //#endregion
692
- //#region src/bot/utilities/users/fetchManyGuildMembers.ts
693
- /**
694
- * Fetches multiple guild members by user IDs.
695
- *
696
- * @param guild - The guild to fetch members from
697
- * @param userIds - Array of Discord user IDs
698
- * @returns Promise resolving to array of successfully fetched members
699
- */
700
- async function fetchManyGuildMembers(guild, userIds) {
701
- return (await Promise.allSettled(userIds.map((userId) => fetchGuildMember(guild, userId)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
468
+ async function collectChoice(message, userId, timeoutMs) {
469
+ try {
470
+ return await message.awaitMessageComponent({
471
+ componentType: ComponentType.Button,
472
+ time: timeoutMs,
473
+ filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
474
+ });
475
+ } catch {
476
+ return null;
477
+ }
702
478
  }
703
-
704
- //#endregion
705
- //#region src/bot/utilities/users/fetchUser.ts
706
- /**
707
- * Fetches a Discord user by ID. Refuses when the user doesn't exist, with `options.throwAs` or the default.
708
- *
709
- * @param client - The Discord client instance
710
- * @param userId - The Discord user ID
711
- * @param options - Optional overrides, including the {@link FetchUserOptions.throwAs} notice
712
- * @returns Promise resolving to the user
713
- */
714
- async function fetchUser(client, userId, options) {
715
- const Throw = options?.throwAs ?? UserNotFound;
716
- let user = client.users.cache.get(userId);
717
- user ??= await client.users.fetch(userId).catch((err) => {
718
- if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownUser) throw new Throw(userId);
719
- throw err;
720
- });
721
- return user;
479
+ async function settle(sender, message, outcome) {
480
+ if (outcome === void 0) {
481
+ await sender.delete(message).catch(() => void 0);
482
+ return;
483
+ }
484
+ await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
485
+ }
486
+ async function getConfirmation(handler, prompt, options) {
487
+ const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
488
+ const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
489
+ const interaction = handler.getEvent();
490
+ const sender = handler.getSender();
491
+ const message = await sender.send(response, { ephemeral });
492
+ const winner = await collectChoice(message, interaction.user.id, timeoutMs);
493
+ if (!winner) {
494
+ await settle(sender, message, options?.onTimeout);
495
+ return false;
496
+ }
497
+ await winner.deferUpdate().catch(() => void 0);
498
+ const confirmed = winner.customId === CONFIRM_IDS.confirm;
499
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
500
+ return confirmed;
722
501
  }
723
502
 
724
503
  //#endregion
725
- //#region src/bot/utilities/users/fetchManyUsers.ts
504
+ //#region src/handlers/BaseHandler.ts
726
505
  /**
727
- * Fetches multiple Discord users by IDs.
728
- *
729
- * @param client - The Discord client instance
730
- * @param userIds - Array of Discord user IDs
731
- * @param options - Optional overrides threaded to each {@link fetchUser} call
732
- * @returns Promise resolving to array of successfully fetched users
506
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
507
+ * gate hook.
733
508
  */
734
- async function fetchManyUsers(client, userIds, options) {
735
- return (await Promise.allSettled(userIds.map((userId) => fetchUser(client, userId, options)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
736
- }
509
+ var BaseHandler$1 = class extends BaseHandler {
510
+ /** @internal */
511
+ getEvent() {
512
+ return this.event;
513
+ }
514
+ };
737
515
 
738
516
  //#endregion
739
- //#region src/bot/utilities/users/updateMemberRoles.ts
517
+ //#region src/handlers/interaction/InteractionMiddleware.ts
740
518
  /**
741
- * Updates a guild member's roles by adding and removing specified roles.
519
+ * Base class for interaction middleware
742
520
  *
743
- * @param rolesToAdd - Array of role IDs to add to the member
744
- * @param rolesToRemove - Array of role IDs to remove from the member
745
- * @param member - The guild member to update
746
- * @returns Promise that resolves when roles are updated
521
+ * Middleware runs before interaction handlers and can modify behavior or block execution.
522
+ * Unlike handlers, middleware should not send responses directly.
523
+ *
524
+ * @typeParam Repliable - The interaction type this middleware processes
747
525
  */
748
- async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
749
- const current = new Set(member.roles.cache.map((r) => r.id));
750
- const toAdd = new Set(rolesToAdd);
751
- const toRemove = new Set(rolesToRemove);
752
- const updated = current.union(toAdd).difference(toRemove);
753
- await member.roles.set([...updated]);
754
- }
526
+ var InteractionMiddleware = class extends BaseHandler$1 {
527
+ constructor(event, core) {
528
+ super(event, core, void 0, "interactions");
529
+ }
530
+ };
755
531
 
756
532
  //#endregion
757
533
  //#region src/bot/ReplySender.ts
@@ -767,8 +543,11 @@ function seedState(interaction) {
767
543
  */
768
544
  var ReplySender = class extends BaseReplySender {
769
545
  interaction;
770
- constructor(interaction, routeId) {
771
- super(routeId, seedState(interaction));
546
+ constructor(interaction, routeId, bus) {
547
+ super(routeId, seedState(interaction), bus && {
548
+ bus,
549
+ interactionId: interaction.id
550
+ });
772
551
  this.interaction = interaction;
773
552
  }
774
553
  async writeReply(response, opts) {
@@ -837,319 +616,26 @@ var ReplySender = class extends BaseReplySender {
837
616
  ...reply.files && { files: [...reply.files] }
838
617
  };
839
618
  }
840
- editOptions(response) {
841
- const reply = this.serialize(response);
842
- return {
843
- components: reply.components,
844
- flags: MessageFlags.IsComponentsV2,
845
- ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
846
- ...reply.files && { files: [...reply.files] }
847
- };
848
- }
849
- updateOptions(response) {
850
- const reply = this.serialize(response);
851
- return {
852
- components: reply.components,
853
- flags: MessageFlags.IsComponentsV2,
854
- ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
855
- ...reply.files && { files: [...reply.files] }
856
- };
857
- }
858
- createdMessage(result) {
859
- return result.resource?.message ?? void 0;
860
- }
861
- };
862
-
863
- //#endregion
864
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
865
- /**
866
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
867
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
868
- * interaction and a generated registry key always match.
869
- *
870
- * @internal
871
- */
872
- function slashRouteOf(interaction) {
873
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
874
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
875
- return buildSlashRoute(interaction.commandName, subcommand, group);
876
- }
877
-
878
- //#endregion
879
- //#region src/miscellaneous/FaultThrottle.ts
880
- /**
881
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
882
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
883
- * the map stays small, and a stale entry is evicted on the next read of its key.
884
- *
885
- * @internal
886
- */
887
- var FaultThrottle = class {
888
- windowMs;
889
- now;
890
- lastReportedAt = /* @__PURE__ */ new Map();
891
- constructor(windowMs = 6e4, now = () => Date.now()) {
892
- this.windowMs = windowMs;
893
- this.now = now;
894
- }
895
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
896
- shouldReport(key) {
897
- const last = this.lastReportedAt.get(key);
898
- if (last === void 0) return true;
899
- if (this.now() - last < this.windowMs) return false;
900
- this.lastReportedAt.delete(key);
901
- return true;
902
- }
903
- markReported(key) {
904
- this.lastReportedAt.set(key, this.now());
905
- }
906
- clear() {
907
- this.lastReportedAt.clear();
908
- }
909
- };
910
-
911
- //#endregion
912
- //#region src/miscellaneous/extractErrorResponse.ts
913
- const logger$2 = new Logger("ErrorsHandling");
914
- const faultThrottle = new FaultThrottle();
915
- /**
916
- * Processes an error into a user-facing reply and routes its report.
917
- *
918
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
919
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
920
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
921
- * threaded into both the reply and the bus payload.
922
- */
923
- function extractErrorResponse(error, core, origin) {
924
- const uuid = crypto.randomUUID();
925
- const developerUsername = core.config.notifications?.developerUsername;
926
- const ctx = developerUsername === void 0 ? { uuid } : {
927
- uuid,
928
- developerUsername
929
- };
930
- if (error instanceof Notice) {
931
- if (error.report) reportFault(error, core, origin, uuid);
932
- return {
933
- uuid,
934
- response: error.render(ctx)
935
- };
936
- }
937
- reportRawFault(error, core, origin, uuid);
938
- const override = core.config.errors?.defaultError;
939
- return {
940
- uuid,
941
- response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
942
- };
943
- }
944
- function withThrottle(origin, error, publish) {
945
- const key = faultKey(origin, error);
946
- if (!faultThrottle.shouldReport(key)) {
947
- logger$2.info(`throttled duplicate fault ${key}`);
948
- return;
949
- }
950
- publish();
951
- faultThrottle.markReported(key);
952
- }
953
- function reportFault(denial, core, origin, uuid) {
954
- withThrottle(origin, denial, () => {
955
- logger$2.error(`${denial.name}: ${uuid}`, denial);
956
- if (origin.interaction) core.bus.publish("handledException", {
957
- denial,
958
- uuid,
959
- source: buildInteractionSource(origin.interaction)
960
- });
961
- else if (origin.event) core.bus.publish("handledException", {
962
- denial,
963
- uuid,
964
- source: buildEventSource(origin.event, origin)
965
- });
966
- else core.bus.publish("unknownException", {
967
- uuid,
968
- error: denial,
969
- ...scalarActors(origin),
970
- metadata: metadataFor(origin)
971
- });
972
- });
973
- }
974
- function causeLine(error) {
975
- const { cause } = error;
976
- if (!Error.isError(cause)) return "";
977
- const [first] = cause.message.split("\n");
978
- return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
979
- }
980
- function reportRawFault(error, core, origin, uuid) {
981
- withThrottle(origin, error, () => {
982
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
983
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
984
- core.bus.publish("unknownException", {
985
- uuid,
986
- error,
987
- ...scalarActors(origin),
988
- metadata: metadataFor(origin)
989
- });
990
- });
991
- }
992
- function scalarActors(origin) {
993
- return {
994
- guild: origin.guild ? {
995
- id: origin.guild.id,
996
- name: origin.guild.name
997
- } : void 0,
998
- user: origin.user ? {
999
- id: origin.user.id,
1000
- username: origin.user.username
1001
- } : void 0
1002
- };
1003
- }
1004
- function metadataFor(origin) {
1005
- if (origin.event) return {
1006
- eventName: origin.event.name,
1007
- handler: origin.event.handler,
1008
- args: origin.event.args
1009
- };
1010
- return origin.metadata;
1011
- }
1012
- function buildEventSource(event, origin) {
1013
- return {
1014
- kind: "event",
1015
- eventName: event.name,
1016
- handler: event.handler,
1017
- userId: origin.user?.id ?? null,
1018
- guildId: origin.guild?.id ?? null,
1019
- channelId: event.channelId,
1020
- raw: event.args
1021
- };
1022
- }
1023
- function faultKey(origin, error) {
1024
- const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
1025
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1026
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1027
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
1028
- return `autocomplete:${name}`;
1029
- }
1030
- /** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
1031
- function interactionRoute(interaction) {
1032
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1033
- if (interaction.isContextMenuCommand()) return interaction.commandName;
1034
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
1035
- return "interaction";
1036
- }
1037
- function buildInteractionSource(interaction) {
1038
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1039
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1040
- return {
1041
- kind: "interaction",
1042
- interactionKind: interactionKind(interaction),
1043
- command,
1044
- customId,
1045
- userId: interaction.user.id,
1046
- guildId: interaction.guildId,
1047
- channelId: interaction.channelId,
1048
- interactionId: interaction.id,
1049
- raw: interaction
1050
- };
1051
- }
1052
- function interactionKind(interaction) {
1053
- if (interaction.isChatInputCommand()) return "slash";
1054
- if (interaction.isContextMenuCommand()) return "context-menu";
1055
- if (interaction.isButton()) return "button";
1056
- if (interaction.isAnySelectMenu()) return "select";
1057
- return "modal";
1058
- }
1059
-
1060
- //#endregion
1061
- //#region src/bot/confirm/reserved.ts
1062
- const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
1063
- /**
1064
- * The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
1065
- * in-process collector reads them. Do not mint a real component from it.
1066
- *
1067
- * @internal
1068
- */
1069
- const CONFIRM_DEF = new CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
1070
-
1071
- //#endregion
1072
- //#region src/bot/confirm/getConfirmation.ts
1073
- const DEFAULT_TIMEOUT_MS = 3e4;
1074
- const CONFIRM_IDS = {
1075
- confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
1076
- cancel: CONFIRM_DEF.encode({ choice: "cancel" })
1077
- };
1078
- var ConfirmButtons = class extends RowComponent {
1079
- constructor(options) {
1080
- super("button");
1081
- this.instance.addComponents(new ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? ButtonStyle.Danger), new ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(ButtonStyle.Secondary));
1082
- }
1083
- };
1084
- var ConfirmPromptCard = class extends BuilderComponent {
1085
- constructor(text, options) {
1086
- super("container");
1087
- this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
1088
- }
1089
- };
1090
- function defaultPrompt(text, options) {
1091
- return { components: [new ConfirmPromptCard(text, options).component] };
1092
- }
1093
- async function collectChoice(message, userId, timeoutMs) {
1094
- try {
1095
- return await message.awaitMessageComponent({
1096
- componentType: ComponentType.Button,
1097
- time: timeoutMs,
1098
- filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
1099
- });
1100
- } catch {
1101
- return null;
1102
- }
1103
- }
1104
- async function settle(sender, message, outcome) {
1105
- if (outcome === void 0) {
1106
- await sender.delete(message).catch(() => void 0);
1107
- return;
1108
- }
1109
- await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
1110
- }
1111
- async function getConfirmation(interaction, prompt, options) {
1112
- const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1113
- const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1114
- const sender = new ReplySender(interaction, interactionRoute(interaction));
1115
- const message = await sender.send(response, { ephemeral });
1116
- const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1117
- if (!winner) {
1118
- await settle(sender, message, options?.onTimeout);
1119
- return false;
1120
- }
1121
- await winner.deferUpdate().catch(() => void 0);
1122
- const confirmed = winner.customId === CONFIRM_IDS.confirm;
1123
- await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1124
- return confirmed;
1125
- }
1126
-
1127
- //#endregion
1128
- //#region src/handlers/BaseHandler.ts
1129
- /**
1130
- * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1131
- * gate hook.
1132
- */
1133
- var BaseHandler$1 = class extends BaseHandler {
1134
- /** @internal */
1135
- getEvent() {
1136
- return this.event;
1137
- }
1138
- };
1139
-
1140
- //#endregion
1141
- //#region src/handlers/interaction/InteractionMiddleware.ts
1142
- /**
1143
- * Base class for interaction middleware
1144
- *
1145
- * Middleware runs before interaction handlers and can modify behavior or block execution.
1146
- * Unlike handlers, middleware should not send responses directly.
1147
- *
1148
- * @typeParam Repliable - The interaction type this middleware processes
1149
- */
1150
- var InteractionMiddleware = class extends BaseHandler$1 {
1151
- constructor(event, core) {
1152
- super(event, core);
619
+ editOptions(response) {
620
+ const reply = this.serialize(response);
621
+ return {
622
+ components: reply.components,
623
+ flags: MessageFlags.IsComponentsV2,
624
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
625
+ ...reply.files && { files: [...reply.files] }
626
+ };
627
+ }
628
+ updateOptions(response) {
629
+ const reply = this.serialize(response);
630
+ return {
631
+ components: reply.components,
632
+ flags: MessageFlags.IsComponentsV2,
633
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
634
+ ...reply.files && { files: [...reply.files] }
635
+ };
636
+ }
637
+ createdMessage(result) {
638
+ return result.resource?.message ?? void 0;
1153
639
  }
1154
640
  };
1155
641
 
@@ -1169,8 +655,8 @@ var RepliableHandler = class extends RepliableHandler$1 {
1169
655
  getEvent() {
1170
656
  return this.event;
1171
657
  }
1172
- buildSender(event, _core, routeId) {
1173
- return new ReplySender(event, routeId);
658
+ buildSender(event, core, routeId) {
659
+ return new ReplySender(event, routeId, core.bus);
1174
660
  }
1175
661
  };
1176
662
 
@@ -1198,6 +684,21 @@ var InteractionHandler = class extends RepliableHandler {
1198
684
  }
1199
685
  };
1200
686
 
687
+ //#endregion
688
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
689
+ /**
690
+ * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
691
+ * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
692
+ * interaction and a generated registry key always match.
693
+ *
694
+ * @internal
695
+ */
696
+ function slashRouteOf(interaction) {
697
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
698
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
699
+ return buildSlashRoute(interaction.commandName, subcommand, group);
700
+ }
701
+
1201
702
  //#endregion
1202
703
  //#region src/handlers/interaction/SlashHandler.ts
1203
704
  /**
@@ -1325,7 +826,7 @@ var ContextMenuHandler = class extends InteractionHandler {
1325
826
  */
1326
827
  var AutocompleteHandler = class extends BaseHandler$1 {
1327
828
  constructor(event, core, dispatch) {
1328
- super(event, core, dispatch);
829
+ super(event, core, dispatch, "interactions");
1329
830
  }
1330
831
  decodedFocused;
1331
832
  /**
@@ -1376,7 +877,11 @@ var AutocompleteHandler = class extends BaseHandler$1 {
1376
877
  }
1377
878
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1378
879
  async respond(choices) {
1379
- await this.event.respond(choices);
880
+ const routeId = this.dispatch?.routeId ?? "autocomplete";
881
+ await reportedWrite({
882
+ bus: this.core.bus,
883
+ interactionId: this.event.id
884
+ }, routeId, "respond", () => this.event.respond(choices));
1380
885
  }
1381
886
  /** The firing command route, for a field whose completion differs per registered command. */
1382
887
  get route() {
@@ -1513,7 +1018,22 @@ var ButtonHandler = class extends ComponentHandler {};
1513
1018
  * }
1514
1019
  * ```
1515
1020
  */
1516
- var ModalHandler = class extends ComponentHandler {};
1021
+ var ModalHandler = class extends ComponentHandler {
1022
+ /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1023
+ update(response) {
1024
+ this.ensureSourceMessage("update");
1025
+ return super.update(response);
1026
+ }
1027
+ /** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
1028
+ deferUpdate() {
1029
+ this.ensureSourceMessage("deferUpdate");
1030
+ return super.deferUpdate();
1031
+ }
1032
+ ensureSourceMessage(method) {
1033
+ if (this.event.isFromMessage()) return;
1034
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1035
+ }
1036
+ };
1517
1037
 
1518
1038
  //#endregion
1519
1039
  //#region src/handlers/interaction/components/SelectMenuHandler.ts
@@ -1569,7 +1089,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1569
1089
  var EventHandler = class extends BaseHandler$1 {
1570
1090
  firedEvent;
1571
1091
  constructor(event, core, eventName) {
1572
- super(event, core);
1092
+ super(event, core, void 0, "events");
1573
1093
  this.firedEvent = eventName;
1574
1094
  }
1575
1095
  /**
@@ -1622,7 +1142,7 @@ var EventHandler = class extends BaseHandler$1 {
1622
1142
  var EventMiddleware = class extends BaseHandler$1 {
1623
1143
  firedEvent;
1624
1144
  constructor(event, core, eventName) {
1625
- super(event, core);
1145
+ super(event, core, void 0, "events");
1626
1146
  this.firedEvent = eventName;
1627
1147
  }
1628
1148
  /**
@@ -1636,130 +1156,6 @@ var EventMiddleware = class extends BaseHandler$1 {
1636
1156
  }
1637
1157
  };
1638
1158
 
1639
- //#endregion
1640
- //#region src/pagination/controls.ts
1641
- const DEFAULT_LABEL = {
1642
- first: "First",
1643
- prev: "Prev",
1644
- indicator: "",
1645
- next: "Next",
1646
- last: "Last"
1647
- };
1648
- const CONTROL_SLOT = {
1649
- first: 0,
1650
- prev: 1,
1651
- indicator: 2,
1652
- next: 3,
1653
- last: 4
1654
- };
1655
- const MAX_BUTTONS_PER_ROW = 5;
1656
- var Controls = class {
1657
- cursor;
1658
- view;
1659
- constructor(cursor, view) {
1660
- this.cursor = cursor;
1661
- this.view = view;
1662
- }
1663
- button(key, cosmetics) {
1664
- const { target, disabled } = this.navState(key);
1665
- const defaultLabel = key === "indicator" ? this.indicatorText() : DEFAULT_LABEL[key];
1666
- const label = cosmetics?.label ?? (key === "indicator" || !cosmetics?.emoji ? defaultLabel : void 0);
1667
- const button = new ButtonBuilder().setCustomId(this.cursor.encode({
1668
- page: Math.min(Math.max(0, target), PAGE_MAX),
1669
- slot: CONTROL_SLOT[key]
1670
- })).setStyle(cosmetics?.style ?? ButtonStyle.Secondary).setDisabled(disabled);
1671
- if (label !== void 0) button.setLabel(label);
1672
- if (cosmetics?.emoji) button.setEmoji(cosmetics.emoji);
1673
- return button;
1674
- }
1675
- navState(key) {
1676
- const { page, totalPages, hasPrev, hasNext } = this.view;
1677
- const knownTotal = totalPages !== void 0;
1678
- return {
1679
- target: {
1680
- first: 0,
1681
- prev: page - 1,
1682
- indicator: page,
1683
- next: page + 1,
1684
- last: knownTotal ? totalPages - 1 : page
1685
- }[key],
1686
- disabled: {
1687
- first: !hasPrev,
1688
- prev: !hasPrev,
1689
- indicator: true,
1690
- next: !hasNext || page + 1 > PAGE_MAX,
1691
- last: !hasNext || !knownTotal || totalPages - 1 > PAGE_MAX
1692
- }[key]
1693
- };
1694
- }
1695
- row(...keys) {
1696
- if (keys.length === 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationEmptyControls);
1697
- if (keys.length > MAX_BUTTONS_PER_ROW) throw new SeedcordRangeError(SeedcordErrorCode.PaginationTooManyControls, [keys.length]);
1698
- const seen = /* @__PURE__ */ new Set();
1699
- for (const key of keys) {
1700
- if (seen.has(key)) throw new SeedcordTypeError(SeedcordErrorCode.PaginationDuplicateControls, [key]);
1701
- seen.add(key);
1702
- }
1703
- return new ActionRowBuilder().addComponents(keys.map((key) => this.button(key)));
1704
- }
1705
- indicatorText() {
1706
- const { page, totalPages } = this.view;
1707
- return totalPages === void 0 ? `Page ${page + 1}` : `Page ${page + 1} of ${totalPages}`;
1708
- }
1709
- };
1710
-
1711
- //#endregion
1712
- //#region src/pagination/render.ts
1713
- var PageContainer = class extends BuilderComponent {
1714
- constructor(view, renderItem, controlRow) {
1715
- super("container");
1716
- const base = view.page * view.perPage;
1717
- let lines = [];
1718
- const flush = () => {
1719
- if (lines.length === 0) return;
1720
- this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(lines.join("\n")));
1721
- lines = [];
1722
- };
1723
- view.items.forEach((item, offset) => {
1724
- const rendered = renderItem(item, base + offset);
1725
- if (typeof rendered === "string") {
1726
- lines.push(rendered);
1727
- return;
1728
- }
1729
- flush();
1730
- this.instance.addSectionComponents(rendered.component);
1731
- });
1732
- flush();
1733
- this.instance.addActionRowComponents(controlRow);
1734
- }
1735
- };
1736
- function toReplyResponse(renderable) {
1737
- if (typeof renderable === "string") return { components: [new TextDisplayBuilder().setContent(renderable)] };
1738
- if (Array.isArray(renderable)) return { components: renderable };
1739
- return renderable;
1740
- }
1741
- const ARRAY_KEYS = [
1742
- "first",
1743
- "prev",
1744
- "indicator",
1745
- "next",
1746
- "last"
1747
- ];
1748
- const CURSOR_KEYS = [
1749
- "prev",
1750
- "indicator",
1751
- "next"
1752
- ];
1753
- /**
1754
- * Build a page's V2 reply. A `render` override builds the whole tree, otherwise the default container lists
1755
- * the items and appends the controls (all five for a known total, prev/indicator/next for a cursor source).
1756
- */
1757
- function renderPage(view, cursor, config) {
1758
- const controls = new Controls(cursor, view);
1759
- if (config.render) return toReplyResponse(config.render(view, controls));
1760
- return { components: [new PageContainer(view, config.renderItem ?? String, controls.row(...view.totalPages === void 0 ? CURSOR_KEYS : ARRAY_KEYS)).component] };
1761
- }
1762
-
1763
1159
  //#endregion
1764
1160
  //#region src/pagination/Paginator.ts
1765
1161
  function contextOf(interaction, core) {
@@ -1767,7 +1163,7 @@ function contextOf(interaction, core) {
1767
1163
  interaction,
1768
1164
  user: interaction.user,
1769
1165
  guild: interaction.guild,
1770
- ...core && { core }
1166
+ core
1771
1167
  };
1772
1168
  }
1773
1169
  /**
@@ -1789,15 +1185,11 @@ function contextOf(interaction, core) {
1789
1185
  * export class BansNav extends Bans.Handler {}
1790
1186
  * ```
1791
1187
  */
1792
- var Paginator = class {
1793
- /** The page cursor, pass it to your `@ButtonRoute`. */
1794
- cursor;
1188
+ var Paginator = class extends PaginatorBase {
1795
1189
  /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
1796
1190
  Handler;
1797
- config;
1798
1191
  constructor(config) {
1799
- this.cursor = pageCursor(config.prefix);
1800
- this.config = config;
1192
+ super(config);
1801
1193
  const loadPage = (ctx, n) => this.page(ctx, n);
1802
1194
  this.Handler = class Nav extends ButtonHandler {
1803
1195
  async execute() {
@@ -1807,39 +1199,29 @@ var Paginator = class {
1807
1199
  }
1808
1200
  };
1809
1201
  }
1810
- /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
1811
- async start(interaction, core) {
1812
- const response = await this.page(contextOf(interaction, core), 0);
1813
- return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
1814
- }
1815
- /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1816
- async page(ctx, n) {
1817
- return renderPage(await this.config.source.page(ctx, n), this.cursor, this.config);
1202
+ /**
1203
+ * Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
1204
+ * ack state.
1205
+ *
1206
+ * @param handler - The handler starting the paginator, normally `this`.
1207
+ */
1208
+ async start(handler) {
1209
+ const interaction = handler.getEvent();
1210
+ const response = await this.page(contextOf(interaction, handler.core), 0);
1211
+ return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1818
1212
  }
1819
1213
  };
1820
1214
 
1821
1215
  //#endregion
1822
1216
  //#region src/pagination/sources.ts
1823
- const DEFAULT_PER_PAGE = 10;
1824
1217
  /**
1825
1218
  * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
1826
- * page via {@link paginate}, so the real total is known (a real last button, "Page X of Y"). Cache inside
1827
- * your loader if the load is expensive.
1219
+ * page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
1220
+ * load is expensive.
1828
1221
  *
1829
1222
  * @typeParam Item - The item type, inferred from the loader's return.
1830
1223
  */
1831
- var ArraySource = class {
1832
- load;
1833
- perPage;
1834
- constructor(load, opts) {
1835
- this.load = load;
1836
- this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
1837
- if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
1838
- }
1839
- async page(ctx, n) {
1840
- return paginate(await this.load(ctx), n, this.perPage);
1841
- }
1842
- };
1224
+ var ArraySource = class extends ArraySource$1 {};
1843
1225
  /**
1844
1226
  * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1845
1227
  * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
@@ -1848,496 +1230,35 @@ var ArraySource = class {
1848
1230
  *
1849
1231
  * @typeParam Item - The item type, inferred from the fetcher's slice.
1850
1232
  */
1851
- var CursorSource = class {
1852
- fetch;
1853
- perPage;
1854
- constructor(fetch, opts) {
1855
- this.fetch = fetch;
1856
- this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
1857
- if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
1858
- }
1859
- async page(ctx, n) {
1860
- const page = Math.max(0, Math.trunc(n));
1861
- const { items, hasNext } = await this.fetch(ctx, page, this.perPage);
1862
- return {
1863
- items: [...items],
1864
- page,
1865
- perPage: this.perPage,
1866
- hasPrev: page > 0,
1867
- hasNext
1868
- };
1869
- }
1870
- };
1871
-
1872
- //#endregion
1873
- //#region src/subscribers/Subscriber.ts
1874
- /**
1875
- * Abstract base class for handling application subscribers.
1876
- *
1877
- * Provides type-safe access to subscribers data and the core framework instance.
1878
- * Extend this class to create custom subscriber handlers.
1879
- *
1880
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
1881
- */
1882
- var Subscriber = class {
1883
- data;
1884
- core;
1885
- logger;
1886
- /**
1887
- * Instantiates the subscriber with event data and core.
1888
- *
1889
- * @param data - The subscriber event data
1890
- * @param core - The core framework instance
1891
- */
1892
- constructor(data, core) {
1893
- this.data = data;
1894
- this.core = core;
1895
- this.logger = new Logger(this.constructor.name);
1896
- }
1897
- };
1898
-
1899
- //#endregion
1900
- //#region src/subscribers/decorators/Subscribe.ts
1901
- /**
1902
- * Registers a subscriber handler class with a specific subscriber event.
1903
- *
1904
- * Associates the decorated class with a subscriber event type for automatic
1905
- * registration and execution when the subscriber is emitted.
1906
- *
1907
- * @typeParam TSubscriber - The subscriber event name to register for
1908
- * @param subscriber - The subscriber event name to register for
1909
- * @param options - Options to configure the subscriber handler registration.
1910
- * @decorator
1911
- * @example
1912
- * ```typescript
1913
- * \@Subscribe('userJoin')
1914
- * class WelcomeHandler extends Subscriber<'userJoin'> {
1915
- * async execute() {
1916
- * // Handle user join event
1917
- * }
1918
- * }
1919
- * ```
1920
- * @example
1921
- * ```ts
1922
- * \@Subscribe('userJoin', { frequency: 'once' })
1923
- * // or
1924
- * \@Subscribe('userJoin')
1925
- * ```
1926
- */
1927
- function Subscribe(subscriber, options) {
1928
- return function(constructor) {
1929
- const meta = {
1930
- subscriber,
1931
- frequency: options?.frequency
1932
- };
1933
- Reflect.defineMetadata(SubscribeMetadataKey, meta, constructor);
1934
- };
1935
- }
1936
-
1937
- //#endregion
1938
- //#region src/subscribers/decorators/WebhookUrl.ts
1939
- /**
1940
- * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
1941
- *
1942
- * An unset variable disables the reporter with a boot warning. A set but malformed value throws
1943
- * at boot.
1944
- *
1945
- * @param envKey - The environment variable name holding the webhook url
1946
- * @decorator
1947
- * @example
1948
- * ```ts
1949
- * \@Subscribe('memberBanned')
1950
- * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
1951
- * class BanLog extends WebhookLog<'memberBanned'> {
1952
- * report() {
1953
- * return { components: [new BanCard(this.data).component] };
1954
- * }
1955
- * }
1956
- * ```
1957
- */
1958
- function WebhookUrl(envKey) {
1959
- return function(constructor) {
1960
- Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
1961
- };
1962
- }
1963
-
1964
- //#endregion
1965
- //#region src/miscellaneous/flagsFor.ts
1966
- /** @internal */
1967
- function flagsFor(ephemeral) {
1968
- let flags = MessageFlags$1.IsComponentsV2;
1969
- if (ephemeral) flags |= MessageFlags$1.Ephemeral;
1970
- return flags;
1971
- }
1233
+ var CursorSource = class extends CursorSource$1 {};
1972
1234
 
1973
1235
  //#endregion
1974
- //#region src/subscribers/bases/webhookHelpers.ts
1975
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
1976
- function isDiscordWebhookUrl(value) {
1977
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
1978
- }
1979
- function jsonAttachment(name, description, data) {
1980
- const content = filterCirculars(data);
1981
- return {
1982
- name,
1983
- description,
1984
- data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
1985
- };
1986
- }
1987
- function neutralizeFences(text) {
1988
- return text.replaceAll("```", "`​`​`");
1989
- }
1990
- /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
1991
- function errorReport(error) {
1992
- let report = error.stack ?? `${error.name}: ${error.message}`;
1993
- if (Error.isError(error.cause)) {
1994
- const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
1995
- report += `\n\nCaused by:\n${cause}`;
1996
- }
1997
- const stripped = neutralizeFences(stripAnsi(report));
1998
- return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
1999
- }
2000
- var WebhookSeparator = class extends BuilderComponent {
2001
- constructor() {
2002
- super("separator");
2003
- this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
2004
- }
2005
- };
2006
-
2007
- //#endregion
2008
- //#region src/subscribers/bases/WebhookSender.ts
2009
- const HTTP_NOT_FOUND = 404;
2010
- const HTTP_UNAUTHORIZED = 401;
1236
+ //#region src/subscribers/index.ts
2011
1237
  /**
2012
- * Sends component messages to a Discord webhook through `@discordjs/rest`.
1238
+ * Base class for a gateway subscriber, binding `this.core` to the gateway {@link Core}.
2013
1239
  *
2014
- * @throws A `SeedcordError` when the url does not carry a webhook id and token.
1240
+ * @typeParam KeyOfSubscribers - The subscription key this handler receives
2015
1241
  */
2016
- var WebhookSender = class {
2017
- rest = new REST();
2018
- route;
2019
- constructor(url) {
2020
- const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2021
- if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2022
- this.route = Routes.webhook(match[1], match[2]);
2023
- }
2024
- /**
2025
- * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2026
- * Discord and `unreachable` on any other failure.
2027
- */
2028
- async verify() {
2029
- try {
2030
- await this.rest.get(this.route, { auth: false });
2031
- return "ok";
2032
- } catch (error) {
2033
- return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2034
- }
2035
- }
2036
- async send(options) {
2037
- const { flags, username, components, files } = options;
2038
- await this.rest.post(this.route, {
2039
- auth: false,
2040
- query: new URLSearchParams({
2041
- with_components: "true",
2042
- wait: "true"
2043
- }),
2044
- body: {
2045
- flags,
2046
- username,
2047
- components: components.map((component) => component.toJSON()),
2048
- ...files?.length && { attachments: files.map((file, id) => ({
2049
- id,
2050
- filename: file.name,
2051
- description: file.description
2052
- })) }
2053
- },
2054
- ...files?.length && { files: files.map((file) => ({
2055
- name: file.name,
2056
- data: file.data
2057
- })) }
2058
- });
2059
- }
2060
- };
2061
-
2062
- //#endregion
2063
- //#region src/subscribers/bases/WebhookLog.ts
1242
+ var Subscriber = class extends Subscriber$1 {};
2064
1243
  /**
2065
- * Base class for subscribers that deliver their event to a Discord webhook.
2066
- *
2067
- * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2068
- * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2069
- * the reporter is skipped at registration with a boot warning.
1244
+ * Base class for a gateway subscriber that delivers its event to a Discord webhook, binding
1245
+ * `this.core` to the gateway {@link Core}.
2070
1246
  *
2071
1247
  * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2072
1248
  */
2073
- var WebhookLog = class WebhookLog extends Subscriber {
2074
- static senders = /* @__PURE__ */ new Map();
2075
- async execute() {
2076
- const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2077
- if (url === null) {
2078
- this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2079
- return;
2080
- }
2081
- const { username, components, files } = await this.report();
2082
- try {
2083
- await WebhookLog.senderFor(url).send({
2084
- flags: flagsFor(false),
2085
- username: username ?? this.constructor.name,
2086
- components,
2087
- files
2088
- });
2089
- } catch (error) {
2090
- this.logger.error("Failed to send webhook report", error);
2091
- }
2092
- }
2093
- /** @internal */
2094
- static senderFor(url) {
2095
- let sender = WebhookLog.senders.get(url);
2096
- if (!sender) {
2097
- sender = new WebhookSender(url);
2098
- WebhookLog.senders.set(url, sender);
2099
- }
2100
- return sender;
2101
- }
2102
- /** @internal */
2103
- static envKeyOf(ctor) {
2104
- const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
2105
- if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2106
- return envKey;
2107
- }
2108
- /** @internal Returns null when the env var is unset, throws when set but malformed. */
2109
- static urlOf(envKey) {
2110
- if (!Envapter.has(envKey)) return null;
2111
- const raw = Envapter.getRequired(envKey, Converters.String).trim();
2112
- if (raw === "") return null;
2113
- if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2114
- return raw;
2115
- }
2116
- };
2117
-
2118
- //#endregion
2119
- //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
2120
- /**
2121
- * Collects the names of every registered context-menu command across a set of command builders, split into a
2122
- * user set and a message set. The same builder may appear more than once (a guild command is pushed once per
2123
- * guild), and slash commands carry no context-menu name, so both collapse here. The split mirrors the
2124
- * controller's two name-keyed maps, so the boot guard checks each name against its own kind.
2125
- *
2126
- * @internal
2127
- */
2128
- function contextMenuLeaves(commands) {
2129
- const user = /* @__PURE__ */ new Set();
2130
- const message = /* @__PURE__ */ new Set();
2131
- for (const command of commands) {
2132
- if (!(command instanceof ContextMenuCommandBuilder)) continue;
2133
- if (command.type === ApplicationCommandType.User) user.add(command.name);
2134
- else message.add(command.name);
2135
- }
2136
- return {
2137
- user,
2138
- message
2139
- };
2140
- }
2141
-
2142
- //#endregion
2143
- //#region src/bot/utilities/miscellaneous/slashRouteLeaves.ts
2144
- /**
2145
- * Collects the route keys of every executable slash leaf across a set of command builders, deduplicated. The
2146
- * same builder may appear more than once (a guild command is pushed once per guild), and context-menu
2147
- * commands carry no slash route, so both collapse here. Route keys come from the same {@link routeLeavesOf}
2148
- * that `seedcord codegen` reads, so the boot-time check compares against the generated registry's keys.
2149
- *
2150
- * @internal
2151
- */
2152
- function slashRouteLeaves(commands) {
2153
- const leaves = /* @__PURE__ */ new Set();
2154
- for (const command of commands) {
2155
- if (!(command instanceof SlashCommandBuilder)) continue;
2156
- for (const leaf of routeLeavesOf(command.toJSON())) leaves.add(leaf.route);
2157
- }
2158
- return leaves;
2159
- }
2160
-
2161
- //#endregion
2162
- //#region src/bot/controllers/CommandRegistry.ts
2163
- /**
2164
- * Manages Discord application command registration and deployment.
2165
- *
2166
- * Scans command directories, builds command structures, and registers both global and guild-scoped commands
2167
- * to Discord's API. Accessed via `core.bot.commands`. Do not construct it directly.
2168
- */
2169
- var CommandRegistry = class {
2170
- core;
2171
- onDeployed;
2172
- logger = new Logger("Commands");
2173
- isInitialised = false;
2174
- globalCommands = [];
2175
- guildCommands = new Collection();
2176
- ctorToCommand = /* @__PURE__ */ new Map();
2177
- loading = false;
2178
- loadedCommands = [];
2179
- hmrHandler;
2180
- pendingEvents = /* @__PURE__ */ new Map();
2181
- constructor(core, onDeployed) {
2182
- this.core = core;
2183
- this.onDeployed = onDeployed;
2184
- const commandsDir = this.core.config.bot.commands.path;
2185
- if (!commandsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
2186
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
2187
- this.hmrHandler = new HmrModuleHandler({
2188
- handlersDir: commandsDir,
2189
- isHandler: this.isCommandClass.bind(this),
2190
- registerHandler: this.registerCommand.bind(this),
2191
- unregisterHandler: this.unregisterCommand.bind(this),
2192
- getArtifacts: this.getArtifacts.bind(this),
2193
- logger: this.logger
2194
- });
2195
- }
2196
- getArtifacts(ctor) {
2197
- return this.ctorToCommand.get(ctor);
2198
- }
2199
- /** @internal */
2200
- async init() {
2201
- if (this.isInitialised) return;
2202
- this.isInitialised = true;
2203
- const commandsDir = this.core.config.bot.commands.path;
2204
- if (!commandsDir) return;
2205
- this.loading = true;
2206
- this.loadedCommands.length = 0;
2207
- try {
2208
- await this.loadCommands(commandsDir);
2209
- } finally {
2210
- this.loading = false;
2211
- }
2212
- this.reportLoad();
2213
- getDevChannel()?.on("seedcord:refresh-commands", (data) => {
2214
- this.refresh(data.shouldRefresh);
2215
- });
2216
- }
2217
- reportLoad() {
2218
- const { utils } = this.logger;
2219
- utils.summary("Loaded commands", {
2220
- global: this.globalCommands.length,
2221
- "guild groups": this.guildCommands.size
2222
- });
2223
- let slash = 0;
2224
- let menu = 0;
2225
- for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
2226
- else menu++;
2227
- utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
2228
- "slash commands": slash,
2229
- "context menus": menu
2230
- })]);
2231
- }
2232
- /** @internal */
2233
- async refresh(shouldRefresh = true) {
2234
- if (!shouldRefresh) {
2235
- this.logger.info(chalk.italic("Command refresh cancelled."));
2236
- this.pendingEvents.clear();
2237
- return;
2238
- }
2239
- this.logger.info(chalk.italic("Refreshing commands..."));
2240
- for (const event of this.pendingEvents.values()) await this.hmrHandler?.handle(event);
2241
- this.pendingEvents.clear();
2242
- await this.setCommands();
2243
- }
2244
- /** @internal */
2245
- async onHmr(event) {
2246
- const commandsDir = this.core.config.bot.commands.path;
2247
- if (commandsDir && event.file.startsWith(resolve(process.cwd(), commandsDir))) {
2248
- this.pendingEvents.delete(event.file);
2249
- this.pendingEvents.set(event.file, event);
2250
- getDevChannel()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => formatFilePath(f)) });
2251
- } else await this.hmrHandler?.handle(event);
2252
- }
2253
- async loadCommands(dir) {
2254
- await traverseDirectory(dir, (fullPath, rel, mod) => {
2255
- for (const exported of Object.values(mod)) if (this.isCommandClass(exported)) {
2256
- this.registerCommand(exported, rel);
2257
- this.hmrHandler?.trackHandler(fullPath, exported);
2258
- }
2259
- }, this.logger);
2260
- }
2261
- isCommandClass(obj) {
2262
- if (typeof obj !== "function") return false;
2263
- return obj.prototype instanceof BuilderComponent && Reflect.hasMetadata(CommandMetadataKey, obj);
2264
- }
2265
- registerCommand(ctor, rel) {
2266
- const meta = Reflect.getMetadata(CommandMetadataKey, ctor);
2267
- if (!meta) return;
2268
- const comp = new ctor().component;
2269
- const kind = comp instanceof SlashCommandBuilder ? "slash command" : "context menu";
2270
- if (meta.scope === "global") this.globalCommands.push(comp);
2271
- else for (const g of meta.guilds) {
2272
- const arr = this.guildCommands.get(g) ?? [];
2273
- arr.push(comp);
2274
- this.guildCommands.set(g, arr);
2275
- }
2276
- this.ctorToCommand.set(ctor, {
2277
- name: comp.name,
2278
- scope: meta.scope,
2279
- ...meta.scope === "guild" && { guilds: meta.guilds }
2280
- });
2281
- if (this.loading) {
2282
- this.loadedCommands.push({
2283
- name: comp.name,
2284
- from: formatFilePath(rel),
2285
- kind
2286
- });
2287
- return;
2288
- }
2289
- this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
2290
- }
2291
- unregisterCommand(ctor, artifacts) {
2292
- const info = artifacts ?? this.ctorToCommand.get(ctor);
2293
- if (!info) return;
2294
- if (info.scope === "global") {
2295
- const idx = this.globalCommands.findIndex((c) => c.name === info.name);
2296
- if (idx !== -1) this.globalCommands.splice(idx, 1);
2297
- } else for (const g of info.guilds ?? []) {
2298
- const arr = this.guildCommands.get(g);
2299
- if (arr) {
2300
- const idx = arr.findIndex((c) => c.name === info.name);
2301
- if (idx !== -1) arr.splice(idx, 1);
2302
- }
2303
- }
2304
- this.ctorToCommand.delete(ctor);
2305
- }
2306
- /** @internal */
2307
- async setCommands() {
2308
- const result = {
2309
- global: new Collection(),
2310
- guilds: new Collection()
2311
- };
2312
- if (this.globalCommands.length > 0) {
2313
- const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
2314
- if (deployed) result.global = deployed;
2315
- const tag = this.globalCommands.length === 1 ? "command" : "commands";
2316
- this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
2317
- }
2318
- for (const [guildId, commands] of this.guildCommands.entries()) {
2319
- const guild = this.core.bot.client.guilds.cache.get(guildId);
2320
- if (!guild) {
2321
- this.logger.warn(`Guild with ID ${guildId} not found, skipping command registration.`);
2322
- continue;
2323
- }
2324
- const deployed = await guild.commands.set(commands);
2325
- result.guilds.set(guildId, deployed);
2326
- const tag = commands.length === 1 ? "command" : "commands";
2327
- this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
2328
- }
2329
- this.onDeployed?.(result);
2330
- return result;
2331
- }
2332
- /** The deduplicated slash route keys across every global and guild command. @internal */
2333
- routeLeaves() {
2334
- return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2335
- }
2336
- /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2337
- contextMenuLeaves() {
2338
- return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2339
- }
2340
- };
1249
+ var WebhookLog = class extends WebhookLog$1 {};
1250
+
1251
+ //#endregion
1252
+ //#region src/plugin.ts
1253
+ /**
1254
+ * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
1255
+ *
1256
+ * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1257
+ * `this.core` is typed to the shared members only.
1258
+ *
1259
+ * @typeParam Opts - The transport and runtime the plugin declares.
1260
+ */
1261
+ var Plugin = class extends Plugin$1 {};
2341
1262
 
2342
1263
  //#endregion
2343
1264
  //#region src/miscellaneous/deriveEventActor.ts
@@ -2416,9 +1337,157 @@ function eventGateContext(eventName, args, core) {
2416
1337
  };
2417
1338
  }
2418
1339
 
1340
+ //#endregion
1341
+ //#region src/miscellaneous/extractErrorResponse.ts
1342
+ const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
1343
+ /**
1344
+ * Processes an error into a user-facing reply and routes its report.
1345
+ *
1346
+ * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
1347
+ * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
1348
+ * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
1349
+ * threaded into both the reply and the bus payload.
1350
+ */
1351
+ function extractErrorResponse(error, core, origin) {
1352
+ const uuid = crypto.randomUUID();
1353
+ const developerUsername = core.config.notifications?.developerUsername;
1354
+ const ctx = developerUsername === void 0 ? { uuid } : {
1355
+ uuid,
1356
+ developerUsername
1357
+ };
1358
+ if (error instanceof Notice) {
1359
+ if (error.report) reportFault(error, core, origin, uuid);
1360
+ return {
1361
+ uuid,
1362
+ response: error.render(ctx)
1363
+ };
1364
+ }
1365
+ reportRawFault(error, core, origin, uuid);
1366
+ const override = core.config.errors?.defaultError;
1367
+ return {
1368
+ uuid,
1369
+ response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
1370
+ };
1371
+ }
1372
+ function withThrottle(core, origin, error, publish) {
1373
+ const throttle = FaultThrottle.for(core);
1374
+ const key = faultKey(origin, error);
1375
+ if (!throttle.shouldReport(key)) {
1376
+ logger$2.debug(`throttled duplicate fault ${key}`);
1377
+ return;
1378
+ }
1379
+ publish();
1380
+ throttle.markReported(key);
1381
+ }
1382
+ function reportFault(denial, core, origin, uuid) {
1383
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
1384
+ withThrottle(core, origin, denial, () => {
1385
+ if (origin.interaction) core.bus[PublishDefault]("handledException", {
1386
+ denial,
1387
+ uuid,
1388
+ source: buildInteractionSource(origin.interaction)
1389
+ });
1390
+ else if (origin.event) core.bus[PublishDefault]("handledException", {
1391
+ denial,
1392
+ uuid,
1393
+ source: buildEventSource(origin.event, origin)
1394
+ });
1395
+ else core.bus[PublishDefault]("unknownException", {
1396
+ uuid,
1397
+ error: denial,
1398
+ ...scalarActors(origin),
1399
+ metadata: metadataFor(origin)
1400
+ });
1401
+ });
1402
+ }
1403
+ function causeLine(error) {
1404
+ const { cause } = error;
1405
+ if (!Error.isError(cause)) return "";
1406
+ const [first] = cause.message.split("\n");
1407
+ return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
1408
+ }
1409
+ function reportRawFault(error, core, origin, uuid) {
1410
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1411
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1412
+ withThrottle(core, origin, error, () => {
1413
+ core.bus[PublishDefault]("unknownException", {
1414
+ uuid,
1415
+ error,
1416
+ ...scalarActors(origin),
1417
+ metadata: metadataFor(origin)
1418
+ });
1419
+ });
1420
+ }
1421
+ function scalarActors(origin) {
1422
+ return {
1423
+ guild: origin.guild ? {
1424
+ id: origin.guild.id,
1425
+ name: origin.guild.name
1426
+ } : void 0,
1427
+ user: origin.user ? {
1428
+ id: origin.user.id,
1429
+ username: origin.user.username
1430
+ } : void 0
1431
+ };
1432
+ }
1433
+ function metadataFor(origin) {
1434
+ if (origin.event) return {
1435
+ eventName: origin.event.name,
1436
+ handler: origin.event.handler,
1437
+ args: origin.event.args
1438
+ };
1439
+ return origin.metadata;
1440
+ }
1441
+ function buildEventSource(event, origin) {
1442
+ return {
1443
+ kind: "event",
1444
+ eventName: event.name,
1445
+ handler: event.handler,
1446
+ userId: origin.user?.id ?? null,
1447
+ guildId: origin.guild?.id ?? null,
1448
+ channelId: event.channelId,
1449
+ raw: event.args
1450
+ };
1451
+ }
1452
+ function faultKey(origin, error) {
1453
+ const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
1454
+ if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1455
+ if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1456
+ if (origin.route) return `autocomplete:${origin.route}:${name}`;
1457
+ return `autocomplete:${name}`;
1458
+ }
1459
+ function interactionRoute(interaction) {
1460
+ if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1461
+ if (interaction.isContextMenuCommand()) return interaction.commandName;
1462
+ if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
1463
+ return "interaction";
1464
+ }
1465
+ function buildInteractionSource(interaction) {
1466
+ const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1467
+ const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1468
+ return {
1469
+ kind: "interaction",
1470
+ interactionKind: interactionKind(interaction),
1471
+ command,
1472
+ customId,
1473
+ userId: interaction.user.id,
1474
+ guildId: interaction.guildId,
1475
+ channelId: interaction.channelId,
1476
+ interactionId: interaction.id,
1477
+ raw: interaction
1478
+ };
1479
+ }
1480
+ function interactionKind(interaction) {
1481
+ if (interaction.isChatInputCommand()) return "slash";
1482
+ if (interaction.isContextMenuCommand()) return "context-menu";
1483
+ if (interaction.isButton()) return "button";
1484
+ if (interaction.isAnySelectMenu()) return "select";
1485
+ return "modal";
1486
+ }
1487
+
2419
1488
  //#endregion
2420
1489
  //#region src/bot/handleEventFault.ts
2421
- const logger$1 = new Logger("EventBoundary");
1490
+ const logger$1 = new Logger("EventBoundary", { channel: "errors" });
2422
1491
  /**
2423
1492
  * The event controller boundary. Reports a throw from an event handler and never replies, since a
2424
1493
  * generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
@@ -2431,15 +1500,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2431
1500
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2432
1501
  return;
2433
1502
  }
2434
- if (!Error.isError(caught)) throw caught;
1503
+ const error = asError(caught);
2435
1504
  const ignore = new Set(core.config.errors?.ignoreEventApiCodes ?? []);
2436
- if (caught instanceof DiscordAPIError && ignore.has(caught.code)) {
2437
- logger$1.debug(`swallowed api code ${caught.code}`);
1505
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
1506
+ logger$1.debug(`swallowed api code ${error.code}`);
2438
1507
  return;
2439
1508
  }
2440
- if (caught instanceof Notice && !caught.report) return;
1509
+ if (error instanceof Notice && !error.report) return;
2441
1510
  const actor = deriveEventActor(args);
2442
- extractErrorResponse(caught, core, {
1511
+ extractErrorResponse(error, core, {
2443
1512
  event: {
2444
1513
  name: eventName,
2445
1514
  handler: handlerName,
@@ -2454,15 +1523,15 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2454
1523
  //#endregion
2455
1524
  //#region src/bot/controllers/EventDispatcher.ts
2456
1525
  /**
2457
- * Scans event handler directories, registers handlers with Discord client events,
2458
- * and coordinates event execution through the handler system. Does not handle interactions.
2459
- * Multiple handlers can point to one event.
1526
+ * Scans event handler directories, attaches one client listener per registered event, and runs the
1527
+ * handlers for each fire. Interactions route through InteractionDispatcher. Multiple handlers can
1528
+ * point to one event.
2460
1529
  *
2461
1530
  * @internal
2462
1531
  */
2463
1532
  var EventDispatcher = class {
2464
1533
  core;
2465
- logger = new Logger("Events");
1534
+ logger = new Logger("Events", { channel: "events" });
2466
1535
  isInitialized = false;
2467
1536
  eventMap = /* @__PURE__ */ new Map();
2468
1537
  middlewares = [];
@@ -2533,7 +1602,7 @@ var EventDispatcher = class {
2533
1602
  this.registerHandler(val, relativePath);
2534
1603
  this.hmrHandler?.trackHandler(fullPath, val);
2535
1604
  }
2536
- }, this.logger);
1605
+ });
2537
1606
  }
2538
1607
  async loadMiddlewares(dir) {
2539
1608
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -2542,7 +1611,7 @@ var EventDispatcher = class {
2542
1611
  this.registerMiddleware(val, relativePath);
2543
1612
  this.hmrHandler?.trackMiddleware(fullPath, val);
2544
1613
  }
2545
- }, this.logger);
1614
+ });
2546
1615
  }
2547
1616
  /** @internal For use in dev mode */
2548
1617
  async onHmr(event) {
@@ -2639,10 +1708,14 @@ var EventDispatcher = class {
2639
1708
  this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2640
1709
  this.core.bot.client.on(eventName, (...args) => {
2641
1710
  if (this.draining) return;
2642
- this.core.bot.emitSafe("any:event", eventName, ...args);
2643
- const run = this.processEvent(eventName, args).catch((err) => {
2644
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2645
- this.core.bot.emit("error:unhandled:event", err);
1711
+ this.core.bus[PublishDefault]("anyEvent", {
1712
+ name: eventName,
1713
+ args
1714
+ });
1715
+ const run = this.processEvent(eventName, args).catch((caught) => {
1716
+ const error = asError(caught);
1717
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
1718
+ this.core.bus[PublishDefault]("unhandledEventError", { error });
2646
1719
  });
2647
1720
  this.inFlight.add(run);
2648
1721
  run.finally(() => this.inFlight.delete(run));
@@ -2718,7 +1791,7 @@ const HARMLESS_API_CODES = new Set([
2718
1791
 
2719
1792
  //#endregion
2720
1793
  //#region src/bot/handleInteractionFault.ts
2721
- const logger = new Logger("InteractionBoundary");
1794
+ const logger = new Logger("InteractionBoundary", { channel: "errors" });
2722
1795
  /**
2723
1796
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2724
1797
  * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
@@ -2727,33 +1800,45 @@ const logger = new Logger("InteractionBoundary");
2727
1800
  *
2728
1801
  * @internal
2729
1802
  */
2730
- async function handleInteractionFault(caught, interaction, core, sender) {
1803
+ async function handleInteractionFault(caught, interaction, core, routeId, sender) {
2731
1804
  if (caught instanceof Silence) {
2732
1805
  if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2733
1806
  return;
2734
1807
  }
2735
- if (!Error.isError(caught)) throw caught;
1808
+ const error = asError(caught);
2736
1809
  const ignore = new Set(core.config.errors?.ignoreApiCodes ?? []);
2737
- if (caught instanceof DiscordAPIError && ignore.has(caught.code)) {
2738
- logger.debug(`swallowed api code ${caught.code}`);
1810
+ if (error instanceof DiscordAPIError && ignore.has(error.code)) {
1811
+ logger.debug(`swallowed api code ${error.code}`);
2739
1812
  return;
2740
1813
  }
2741
1814
  if (interaction.isAutocomplete()) {
2742
- extractErrorResponse(caught, core, {
1815
+ extractErrorResponse(error, core, {
2743
1816
  guild: interaction.guild,
2744
1817
  user: interaction.user,
2745
1818
  metadata: interaction,
2746
1819
  route: slashRouteOf(interaction)
2747
1820
  });
1821
+ await sendEmptyChoices(interaction, core, routeId);
2748
1822
  return;
2749
1823
  }
2750
- const { response } = extractErrorResponse(caught, core, {
1824
+ const { response } = extractErrorResponse(error, core, {
2751
1825
  interaction,
2752
1826
  guild: interaction.guild,
2753
1827
  user: interaction.user,
2754
1828
  metadata: interaction
2755
1829
  });
2756
- await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
1830
+ await sendGuarded(sender ?? new ReplySender(interaction, routeId, core.bus), response, error instanceof Notice ? error.ephemeral : true);
1831
+ }
1832
+ async function sendEmptyChoices(interaction, core, routeId) {
1833
+ const telemetry = {
1834
+ bus: core.bus,
1835
+ interactionId: interaction.id
1836
+ };
1837
+ try {
1838
+ await reportedWrite(telemetry, routeId, "respond", () => interaction.respond([]));
1839
+ } catch (error) {
1840
+ logger.debug(`autocomplete empty-choices send failed: ${String(error)}`);
1841
+ }
2757
1842
  }
2758
1843
  async function sendGuarded(sender, response, ephemeral) {
2759
1844
  try {
@@ -2777,7 +1862,7 @@ async function sendGuarded(sender, response, ephemeral) {
2777
1862
  */
2778
1863
  var InteractionDispatcher = class {
2779
1864
  core;
2780
- logger = new Logger("Interactions");
1865
+ logger = new Logger("Interactions", { channel: "interactions" });
2781
1866
  isInitialized = false;
2782
1867
  slashMap = /* @__PURE__ */ new Map();
2783
1868
  buttonMap = /* @__PURE__ */ new Map();
@@ -2790,6 +1875,7 @@ var InteractionDispatcher = class {
2790
1875
  messageContextMenuMap = /* @__PURE__ */ new Map();
2791
1876
  userContextMenuMap = /* @__PURE__ */ new Map();
2792
1877
  autocompleteMap = /* @__PURE__ */ new Map();
1878
+ handlerFiles = /* @__PURE__ */ new Map();
2793
1879
  keysToIgnore = /* @__PURE__ */ new Set();
2794
1880
  middlewares = [];
2795
1881
  inFlight = /* @__PURE__ */ new Set();
@@ -2832,21 +1918,9 @@ var InteractionDispatcher = class {
2832
1918
  logger: this.logger
2833
1919
  });
2834
1920
  }
2835
- /**
2836
- * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
2837
- * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
2838
- *
2839
- * @internal
2840
- */
2841
1921
  warnUnhandledRoutes(commandLeaves) {
2842
1922
  for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${paint.sky.bold(route)} has no registered ${chalk.bold("@SlashRoute")} handler.`);
2843
1923
  }
2844
- /**
2845
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
2846
- * at runtime. Checked per kind since a user and a message command can share a name.
2847
- *
2848
- * @internal
2849
- */
2850
1924
  warnUnhandledContextMenuRoutes(leaves) {
2851
1925
  for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
2852
1926
  for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
@@ -2908,7 +1982,7 @@ var InteractionDispatcher = class {
2908
1982
  this.registerHandler(val, relativePath);
2909
1983
  this.hmrHandler?.trackHandler(fullPath, val);
2910
1984
  }
2911
- }, this.logger);
1985
+ });
2912
1986
  }
2913
1987
  async loadMiddlewares(dir) {
2914
1988
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -2917,7 +1991,7 @@ var InteractionDispatcher = class {
2917
1991
  this.registerMiddleware(val, relativePath);
2918
1992
  this.hmrHandler?.trackMiddleware(fullPath, val);
2919
1993
  }
2920
- }, this.logger);
1994
+ });
2921
1995
  }
2922
1996
  registerMiddleware(middlewareCtor, relativePath) {
2923
1997
  const metadata = Reflect.getMetadata(MiddlewareMetadataKey, middlewareCtor);
@@ -2947,6 +2021,7 @@ var InteractionDispatcher = class {
2947
2021
  return obj.prototype instanceof InteractionMiddleware && Reflect.hasMetadata(MiddlewareMetadataKey, obj);
2948
2022
  }
2949
2023
  registerHandler(handlerClass, relativePath) {
2024
+ const from = formatFilePath(relativePath);
2950
2025
  const writes = [];
2951
2026
  for (const [routeType, map] of this.routeTypes) {
2952
2027
  const meta = Reflect.getMetadata(InteractionRouteKeys[routeType], handlerClass);
@@ -2954,16 +2029,16 @@ var InteractionDispatcher = class {
2954
2029
  for (const route of meta) {
2955
2030
  const existing = map.get(route);
2956
2031
  if (existing && existing !== handlerClass) throw new SeedcordError(SeedcordErrorCode.InteractionDuplicateRoute, [
2957
- route,
2958
- existing.name,
2959
- handlerClass.name
2032
+ `${routeType}:${route}`,
2033
+ `${existing.name} (${this.handlerFiles.get(existing) ?? "unknown file"})`,
2034
+ `${handlerClass.name} (${from})`
2960
2035
  ]);
2961
2036
  writes.push([map, route]);
2962
2037
  }
2963
2038
  }
2964
2039
  if (writes.length === 0) return;
2965
2040
  for (const [map, route] of writes) map.set(route, handlerClass);
2966
- const from = formatFilePath(relativePath);
2041
+ this.handlerFiles.set(handlerClass, from);
2967
2042
  if (this.loading) this.loadedHandlers.push({
2968
2043
  name: handlerClass.name,
2969
2044
  from
@@ -2975,6 +2050,7 @@ var InteractionDispatcher = class {
2975
2050
  await this.hmrHandler?.handle(event);
2976
2051
  }
2977
2052
  unregisterHandler(handlerClass, artifacts) {
2053
+ this.handlerFiles.delete(handlerClass);
2978
2054
  if (artifacts) {
2979
2055
  for (const { routeType, routes } of artifacts) {
2980
2056
  const map = this.routeTypes.find(([type]) => type === routeType)?.[1];
@@ -2995,10 +2071,11 @@ var InteractionDispatcher = class {
2995
2071
  attachToClient() {
2996
2072
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
2997
2073
  if (this.draining) return;
2998
- this.core.bot.emitSafe("any:interaction", interaction);
2999
- const run = this.handleInteraction(interaction).catch((err) => {
3000
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3001
- this.core.bot.emit("error:unhandled:interaction", err);
2074
+ this.core.bus[PublishDefault]("anyInteraction", { interaction });
2075
+ const run = this.handleInteraction(interaction).catch((caught) => {
2076
+ const error = asError(caught);
2077
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${error.name}`, error.stack);
2078
+ this.core.bus[PublishDefault]("unhandledInteractionError", { error });
3002
2079
  });
3003
2080
  this.inFlight.add(run);
3004
2081
  run.finally(() => this.inFlight.delete(run));
@@ -3010,37 +2087,75 @@ var InteractionDispatcher = class {
3010
2087
  drain(timeoutMs) {
3011
2088
  return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
3012
2089
  }
3013
- async handleCustomIdInteraction(interaction, getMap, interactionType) {
2090
+ async handleCustomIdInteraction(interaction, kind, getMap) {
3014
2091
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3015
2092
  const prefix = prefixOf(interaction.customId);
3016
- if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3017
- await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
2093
+ if (!prefix) this.logger.warn(`${paint.sky.bold(kind)} has invalid customId: ${paint.mute(interaction.customId)}`);
2094
+ await this.processInteraction(interaction, kind, () => prefix, (key) => getMap().get(key));
3018
2095
  }
3019
- async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
2096
+ async processInteraction(interaction, kind, extractKey, getHandler, fallback = UnhandledRepliable) {
3020
2097
  const key = extractKey(interaction);
2098
+ const startedAt = performance.now();
2099
+ const queuedMs = queuedMsFor(interaction.id);
2100
+ const matched = getHandler(key);
2101
+ let routeId = `${kind}:${key || "unrouted"}`;
2102
+ let reported = false;
2103
+ const report = (outcome) => {
2104
+ if (reported) return;
2105
+ reported = true;
2106
+ this.core.bus[PublishDefault]("interactionDispatched", dispatchedPayload({
2107
+ routeId,
2108
+ interactionId: interaction.id,
2109
+ kind,
2110
+ outcome,
2111
+ startedAt,
2112
+ fallback: !matched,
2113
+ queuedMs
2114
+ }));
2115
+ };
3021
2116
  let sender;
3022
2117
  try {
3023
- if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3024
- let HandlerCtor = getHandler(key);
3025
- if (!HandlerCtor) {
3026
- this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3027
- HandlerCtor = fallback;
3028
- }
3029
- this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
3030
- const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
3031
- const handler = new HandlerCtor(interaction, this.core, dispatch);
2118
+ if (!interaction.isAutocomplete()) await this.runMiddlewares(interaction);
2119
+ const HandlerCtor = matched ?? fallback;
2120
+ const dispatch = new DispatchContext(routeIdOf(HandlerCtor) ?? routeId);
2121
+ routeId = dispatch.routeId ?? routeId;
2122
+ const handler = this.buildHandler(HandlerCtor, interaction, dispatch, key, !matched);
3032
2123
  if (handler instanceof RepliableHandler) sender = handler.getSender();
3033
- if (!interaction.isAutocomplete()) {
3034
- const monitor = slowGateMonitor();
3035
- try {
3036
- await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3037
- } finally {
3038
- monitor?.report(dispatch.routeId);
3039
- }
2124
+ const refusal = interaction.isAutocomplete() ? null : await this.gateRefusal(HandlerCtor, interaction, dispatch.routeId);
2125
+ if (refusal) {
2126
+ await this.answer(refusal.caught, interaction, routeId, sender, report);
2127
+ return;
3040
2128
  }
3041
2129
  await handler.execute();
2130
+ report("handled");
2131
+ } catch (caught) {
2132
+ await this.answer(caught, interaction, routeId, sender, report);
2133
+ }
2134
+ }
2135
+ async answer(caught, interaction, routeId, sender, report) {
2136
+ try {
2137
+ await handleInteractionFault(caught, interaction, this.core, routeId, sender);
2138
+ } finally {
2139
+ report(outcomeFor(caught));
2140
+ }
2141
+ }
2142
+ buildHandler(HandlerCtor, interaction, dispatch, key, isFallback) {
2143
+ if (isFallback) this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${HandlerCtor.name}.`);
2144
+ this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
2145
+ return new HandlerCtor(interaction, this.core, dispatch);
2146
+ }
2147
+ async runMiddlewares(interaction) {
2148
+ for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
2149
+ }
2150
+ async gateRefusal(HandlerCtor, interaction, routeId) {
2151
+ const monitor = slowGateMonitor();
2152
+ try {
2153
+ await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), routeId ?? void 0, monitor?.observe);
2154
+ return null;
3042
2155
  } catch (caught) {
3043
- await handleInteractionFault(caught, interaction, this.core, sender);
2156
+ return { caught };
2157
+ } finally {
2158
+ monitor?.report(routeId);
3044
2159
  }
3045
2160
  }
3046
2161
  async handleInteraction(interaction) {
@@ -3085,47 +2200,41 @@ var InteractionDispatcher = class {
3085
2200
  }
3086
2201
  async handleSlashCommand(interaction) {
3087
2202
  const route = slashRouteOf(interaction);
3088
- await this.processInteraction(interaction, () => route, (key) => this.slashMap.get(key));
2203
+ await this.processInteraction(interaction, InteractionRoutes.Slash, () => route, (key) => this.slashMap.get(key));
3089
2204
  }
3090
2205
  async handleButton(interaction) {
3091
- await this.handleCustomIdInteraction(interaction, () => this.buttonMap, "Button");
2206
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.Button, () => this.buttonMap);
3092
2207
  }
3093
2208
  async handleModal(interaction) {
3094
- await this.handleCustomIdInteraction(interaction, () => this.modalMap, "Modal");
2209
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.Modal, () => this.modalMap);
3095
2210
  }
3096
2211
  async handleStringSelectMenu(interaction) {
3097
- await this.handleCustomIdInteraction(interaction, () => this.stringSelectMap, "String select menu");
2212
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.StringMenu, () => this.stringSelectMap);
3098
2213
  }
3099
2214
  async handleUserSelectMenu(interaction) {
3100
- await this.handleCustomIdInteraction(interaction, () => this.userSelectMap, "User select menu");
2215
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.UserMenu, () => this.userSelectMap);
3101
2216
  }
3102
2217
  async handleRoleSelectMenu(interaction) {
3103
- await this.handleCustomIdInteraction(interaction, () => this.roleSelectMap, "Role select menu");
2218
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.RoleMenu, () => this.roleSelectMap);
3104
2219
  }
3105
2220
  async handleChannelSelectMenu(interaction) {
3106
- await this.handleCustomIdInteraction(interaction, () => this.channelSelectMap, "Channel select menu");
2221
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.ChannelMenu, () => this.channelSelectMap);
3107
2222
  }
3108
2223
  async handleMentionableSelectMenu(interaction) {
3109
- await this.handleCustomIdInteraction(interaction, () => this.mentionableSelectMap, "Mentionable select menu");
2224
+ await this.handleCustomIdInteraction(interaction, InteractionRoutes.MentionableMenu, () => this.mentionableSelectMap);
3110
2225
  }
3111
2226
  async handleMessageContextMenu(interaction) {
3112
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
2227
+ await this.processInteraction(interaction, InteractionRoutes.MessageContextMenu, () => interaction.commandName, (key) => this.messageContextMenuMap.get(key));
3113
2228
  }
3114
2229
  async handleUserContextMenu(interaction) {
3115
- await this.processInteraction(interaction, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
2230
+ await this.processInteraction(interaction, InteractionRoutes.UserContextMenu, () => interaction.commandName, (key) => this.userContextMenuMap.get(key));
3116
2231
  }
3117
2232
  async handleAutocomplete(interaction) {
3118
2233
  const route = slashRouteOf(interaction);
3119
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
2234
+ await this.processInteraction(interaction, InteractionRoutes.Autocomplete, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3120
2235
  }
3121
2236
  };
3122
2237
 
3123
- //#endregion
3124
- //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
3125
- function __decorateMetadata(k, v) {
3126
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3127
- }
3128
-
3129
2238
  //#endregion
3130
2239
  //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorate.js
3131
2240
  function __decorate(decorators, target, key, desc) {
@@ -3144,38 +2253,47 @@ const LOGOUT_TIMEOUT_MS = 2e3;
3144
2253
  /**
3145
2254
  * The Discord client and its controllers. Access it through `core.bot`.
3146
2255
  */
3147
- var Bot = class extends TypedEventEmitter {
2256
+ var Bot = class {
3148
2257
  /** @internal */
3149
- logger = new Logger("Bot");
2258
+ logger = new Logger("Bot", { channel: "bot" });
3150
2259
  isInitialized = false;
3151
2260
  _client;
3152
2261
  interactions;
3153
2262
  events;
3154
2263
  emojiInjector;
3155
- commands;
3156
- emojis = Emojis;
3157
- mentions = CommandMentions;
2264
+ commandRegistry;
3158
2265
  /** @internal */
3159
2266
  async onHmr(event) {
3160
2267
  if (this.interactions) await this.interactions.onHmr(event);
3161
2268
  if (this.events) await this.events.onHmr(event);
3162
- if (this.commands) await this.commands.onHmr(event);
2269
+ if (this.commandRegistry) await this.commandRegistry.onHmr(event);
3163
2270
  }
3164
2271
  /** @internal */
3165
2272
  constructor(core) {
3166
- super();
3167
2273
  this._client = new Client(core.config.bot.clientOptions);
3168
2274
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3169
2275
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
3170
- if (core.config.bot.commands.path) {
3171
- const mentionInjector = new CommandMentionInjector(core);
3172
- this.commands = new CommandRegistry(core, (result) => {
3173
- mentionInjector.inject(result);
2276
+ const commandsDir = core.config.bot.commands.path;
2277
+ if (commandsDir) {
2278
+ const injector = new CommandInjector();
2279
+ const registry = new CommandRegistry({
2280
+ dir: commandsDir,
2281
+ rest: this._client.rest,
2282
+ applicationId: () => this.applicationId(),
2283
+ onDeployed: (result) => {
2284
+ injector.inject(result, registry.allCommands());
2285
+ }
3174
2286
  });
2287
+ this.commandRegistry = registry;
3175
2288
  }
3176
2289
  this.emojiInjector = new EmojiInjector(core);
3177
2290
  this.registerShutdownTasks(core);
3178
2291
  }
2292
+ applicationId() {
2293
+ const { application } = this._client;
2294
+ if (!application) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
2295
+ return application.id;
2296
+ }
3179
2297
  registerShutdownTasks(core) {
3180
2298
  core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
3181
2299
  this.stopAccepting();
@@ -3200,11 +2318,11 @@ var Bot = class extends TypedEventEmitter {
3200
2318
  if (this.events) await this.events.init();
3201
2319
  await this.login(token);
3202
2320
  await this.emojiInjector.init();
3203
- if (this.commands) {
3204
- await this.commands.init();
3205
- await this.commands.setCommands();
3206
- this.interactions?.warnUnhandledRoutes(this.commands.routeLeaves());
3207
- this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
2321
+ if (this.commandRegistry) {
2322
+ await this.commandRegistry.init();
2323
+ await this.commandRegistry.setCommands();
2324
+ this.interactions?.warnUnhandledRoutes(this.commandRegistry.routeLeaves());
2325
+ this.interactions?.warnUnhandledContextMenuRoutes(this.commandRegistry.contextMenuLeaves());
3208
2326
  }
3209
2327
  }
3210
2328
  /** @internal */
@@ -3213,9 +2331,10 @@ var Bot = class extends TypedEventEmitter {
3213
2331
  await this.logout();
3214
2332
  }
3215
2333
  async login(token) {
3216
- this._client.once(Events.ClientReady, () => this.emit("ready"));
2334
+ const ready = Promise.withResolvers();
2335
+ this._client.once(Events.ClientReady, () => ready.resolve());
3217
2336
  this._client.login(token);
3218
- await this.waitFor("ready");
2337
+ await ready.promise;
3219
2338
  this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
3220
2339
  return this;
3221
2340
  }
@@ -3226,244 +2345,13 @@ var Bot = class extends TypedEventEmitter {
3226
2345
  get client() {
3227
2346
  return this._client;
3228
2347
  }
3229
- /**
3230
- * The overloads enforce the key/args correlation at compile time only. At runtime this forwards
3231
- * straight to the underlying TypedEventEmitter.
3232
- *
3233
- * @internal
3234
- */
3235
- emit(event, ...args) {
3236
- return super.emit(event, ...args);
3237
- }
3238
- emitSafe(event, ...args) {
3239
- return super.emitSafe(event, ...args);
3240
- }
3241
- onListenerError(error, event) {
3242
- this.logger.error(`listener for ${String(event)} threw`, error);
3243
- }
3244
- };
3245
- __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3246
-
3247
- //#endregion
3248
- //#region src/subscribers/default/HandledException.ts
3249
- let HandledException = class HandledException extends WebhookLog {
3250
- report() {
3251
- return {
3252
- username: "Handled Exception",
3253
- components: [new HandledExceptionContainer(this.data).component],
3254
- files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3255
- };
3256
- }
3257
- };
3258
- HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3259
- var HandledExceptionContainer = class extends BuilderComponent {
3260
- constructor(data) {
3261
- super("container");
3262
- const { denial, uuid, source } = data;
3263
- this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3264
- }
3265
- };
3266
- function faultSummary(denial, source) {
3267
- const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3268
- if (source.kind === "event") return `${head}**Event:** ${source.eventName}\n**Handler:** ${source.handler}\n**User ID:** \`${source.userId ?? "n/a"}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\``;
3269
- return `${head}**Kind:** ${source.interactionKind}\n**Command:** ${source.command ?? "n/a"}\n**CustomId:** ${source.customId ?? "n/a"}\n**User ID:** \`${source.userId}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\`\n**Interaction ID:** \`${source.interactionId}\``;
3270
- }
3271
- function causeStack(denial) {
3272
- const { cause } = denial;
3273
- if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
3274
- if (typeof cause === "string") return stripAnsi(cause);
3275
- if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3276
- if (cause !== void 0) try {
3277
- return JSON.stringify(cause);
3278
- } catch {
3279
- return "[unserializable cause]";
3280
- }
3281
- return stripAnsi(denial.stack ?? "No cause recorded.");
3282
- }
3283
-
3284
- //#endregion
3285
- //#region src/subscribers/default/UnknownException.ts
3286
- let UnknownException = class UnknownException extends WebhookLog {
3287
- report() {
3288
- const { metadata } = this.data;
3289
- return {
3290
- username: "Unknown Exception",
3291
- components: [new UnhandledErrorContainer(this.data).component],
3292
- files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3293
- };
3294
- }
3295
- };
3296
- UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3297
- var UnhandledErrorContainer = class extends BuilderComponent {
3298
- constructor(data) {
3299
- super("container");
3300
- const { uuid, error, guild, user, metadata } = data;
3301
- this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
3302
- this.addTimestampsIfAvailable(error);
3303
- this.addMetadataIfAvailable(metadata);
3304
- }
3305
- addTimestampsIfAvailable(error) {
3306
- if (!(error instanceof DiscordAPIError$1)) return;
3307
- const now = Date.now();
3308
- const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3309
- if (!snowflake) return void 0;
3310
- const interactionTs = timestampFromSnowflake(snowflake);
3311
- const diff = now - interactionTs;
3312
- const seconds = Math.floor(diff / 1e3);
3313
- const millis = diff % 1e3;
3314
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### Timestamps\n- **\`Interaction sent\` :** ${new Date(interactionTs).toISOString()} (${interactionTs})\n- **\`Error logged \` :** ${new Date(now).toISOString()} (${now})\n- **\`Offset \` :** ${seconds}s ${millis}ms`));
3315
- }
3316
- addMetadataIfAvailable(metadata) {
3317
- if (!metadata) return;
3318
- this.instance.addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Metadata")).addFileComponents((file) => file.setURL("attachment://metadata.json"));
3319
- }
3320
- };
3321
-
3322
- //#endregion
3323
- //#region src/subscribers/Bus.ts
3324
- /**
3325
- * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3326
- * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3327
- */
3328
- var Bus = class extends TypedEventEmitter {
3329
- core;
3330
- logger = new Logger("Subscribers");
3331
- isInitialized = false;
3332
- subscribersMap = /* @__PURE__ */ new Map();
3333
- executedOnceHandlers = /* @__PURE__ */ new Set();
3334
- webhookProbes = /* @__PURE__ */ new Map();
3335
- hmrHandler;
3336
- constructor(core) {
3337
- super();
3338
- this.core = core;
3339
- if (this.core.config.subscribers.path) {
3340
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
3341
- this.hmrHandler = new HmrModuleHandler({
3342
- handlersDir: this.core.config.subscribers.path,
3343
- isHandler: this.isSubscriber.bind(this),
3344
- registerHandler: this.registerSubscriber.bind(this),
3345
- unregisterHandler: this.unregisterSubscriber.bind(this),
3346
- getArtifacts: this.getArtifacts.bind(this),
3347
- logger: this.logger
3348
- });
3349
- }
3350
- }
3351
- getArtifacts(ctor) {
3352
- return [Reflect.getMetadata(SubscribeMetadataKey, ctor).subscriber];
3353
- }
3354
- /** @internal */
3355
- async init() {
3356
- if (this.isInitialized) return;
3357
- this.isInitialized = true;
3358
- this.registerSubscriber(UnknownException);
3359
- this.registerSubscriber(HandledException);
3360
- const subscribersDir = this.core.config.subscribers.path;
3361
- if (subscribersDir) {
3362
- this.logger.info(chalk.bold(subscribersDir));
3363
- await this.loadSubscribers(subscribersDir);
3364
- const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3365
- this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
3366
- }
3367
- if (!Envapter.isTest) await this.verifyWebhooks();
3368
- }
3369
- /** @internal Exposed for tests. */
3370
- async verifyWebhooks() {
3371
- const missing = [];
3372
- await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3373
- const result = await WebhookLog.senderFor(url).verify();
3374
- if (result === "missing") missing.push(...envKeys);
3375
- else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
3376
- }));
3377
- if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3378
- }
3379
- /** @internal */
3380
- async onHmr(event) {
3381
- if (this.hmrHandler) await this.hmrHandler.handle(event);
3382
- }
3383
- async loadSubscribers(dir) {
3384
- await traverseDirectory(dir, (fullPath, relativePath, imported) => {
3385
- for (const exportName of Object.keys(imported)) {
3386
- const val = imported[exportName];
3387
- if (this.isSubscriber(val)) {
3388
- this.registerSubscriber(val);
3389
- this.hmrHandler?.trackHandler(fullPath, val);
3390
- this.logger.utils.registration(val.name, relativePath);
3391
- }
3392
- }
3393
- }, this.logger);
3394
- }
3395
- registerSubscriber(handler) {
3396
- if (handler.prototype instanceof WebhookLog) {
3397
- const envKey = WebhookLog.envKeyOf(handler);
3398
- const url = WebhookLog.urlOf(envKey);
3399
- if (url === null) {
3400
- this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
3401
- return;
3402
- }
3403
- const envKeys = this.webhookProbes.get(url) ?? [];
3404
- if (!envKeys.includes(envKey)) envKeys.push(envKey);
3405
- this.webhookProbes.set(url, envKeys);
3406
- }
3407
- const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
3408
- let handlers = this.subscribersMap.get(options.subscriber);
3409
- if (!handlers) {
3410
- handlers = [];
3411
- this.subscribersMap.set(options.subscriber, handlers);
3412
- }
3413
- handlers.push({
3414
- ctor: handler,
3415
- frequency: options.frequency ?? "on"
3416
- });
3417
- }
3418
- unregisterSubscriber(handler, artifacts) {
3419
- const subscribers = artifacts ?? [...this.subscribersMap.keys()];
3420
- for (const subscriber of subscribers) {
3421
- const handlers = this.subscribersMap.get(subscriber);
3422
- if (!handlers) continue;
3423
- const index = handlers.findIndex((entry) => entry.ctor === handler);
3424
- if (index !== -1) handlers.splice(index, 1);
3425
- }
3426
- }
3427
- isSubscriber(obj) {
3428
- if (typeof obj !== "function") return false;
3429
- return obj.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, obj);
3430
- }
3431
- /**
3432
- * Publishes an event to its subscribers and native listeners.
3433
- *
3434
- * Fire-and-forget, so subscriber handlers run asynchronously and this returns before they
3435
- * complete, and callers must not assume side effects have landed. Errors thrown by a subscriber
3436
- * are caught and logged, never surfaced here. A `'once'` subscriber is marked as executed when
3437
- * it starts (even if it throws), so it never runs twice.
3438
- *
3439
- * @param event - The subscription key to publish
3440
- * @param data - Payload passed to each subscriber
3441
- * @returns Whether the underlying emitter had native listeners for the event
3442
- */
3443
- publish(event, data) {
3444
- this.processSubscriber(event, data);
3445
- return super.emit(event, ...[data]);
3446
- }
3447
- async processSubscriber(subscriberName, data) {
3448
- const handlers = this.subscribersMap.get(subscriberName);
3449
- if (!handlers) return;
3450
- for (const entry of handlers) {
3451
- if (entry.frequency === "once" && this.executedOnceHandlers.has(entry.ctor)) continue;
3452
- try {
3453
- if (entry.frequency === "once") this.executedOnceHandlers.add(entry.ctor);
3454
- const Ctor = entry.ctor;
3455
- await new Ctor(data, this.core).execute();
3456
- } catch (err) {
3457
- this.logger.error(`Error in subscriber ${String(subscriberName)} handler ${entry.ctor.name}:`, err);
3458
- }
3459
- }
3460
- }
3461
2348
  };
2349
+ __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) })], Bot.prototype, "botToken", void 0);
3462
2350
 
3463
2351
  //#endregion
3464
2352
  //#region src/version.ts
3465
2353
  /** Package version */
3466
- const version = "0.1.0-next.4";
2354
+ const version = "0.1.0-next.6";
3467
2355
 
3468
2356
  //#endregion
3469
2357
  //#region src/Seedcord.ts
@@ -3481,14 +2369,13 @@ var Seedcord = class Seedcord extends Pluggable {
3481
2369
  version = version;
3482
2370
  healthCheck;
3483
2371
  hmrManager;
3484
- /** @see {@link CoordinatedShutdown} */
3485
- shutdown;
3486
- /** @see {@link CoordinatedStartup} */
3487
- startup;
3488
2372
  /** @see {@link Bus} */
3489
2373
  bus;
2374
+ subscribers;
3490
2375
  /** @see {@link Bot} */
3491
2376
  bot;
2377
+ /** The discord.js client's REST client. Its token is set during the Login phase. */
2378
+ rest;
3492
2379
  /** @see {@link IRateLimiter} */
3493
2380
  rateLimiter;
3494
2381
  /**
@@ -3496,18 +2383,16 @@ var Seedcord = class Seedcord extends Pluggable {
3496
2383
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3497
2384
  */
3498
2385
  constructor(config) {
3499
- const shutdown = new CoordinatedShutdown();
3500
- const startup = new CoordinatedStartup();
3501
- super(shutdown, startup);
2386
+ super(new CoordinatedShutdown(), new CoordinatedStartup());
3502
2387
  this.config = config;
3503
2388
  installNodeDefaults(config.logger);
3504
2389
  setBotColor(config.botColor);
3505
- this.shutdown = shutdown;
3506
- this.startup = startup;
3507
2390
  this.hmrManager = new HmrManager();
3508
2391
  this.hmrManager.init();
3509
2392
  this.bus = new Bus(this);
2393
+ this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
3510
2394
  this.bot = new Bot(this);
2395
+ this.rest = this.bot.client.rest;
3511
2396
  this.rateLimiter = config.store ?? new MemoryRateLimiter();
3512
2397
  this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
3513
2398
  this.registerStartupTasks();
@@ -3516,31 +2401,24 @@ var Seedcord = class Seedcord extends Pluggable {
3516
2401
  get username() {
3517
2402
  return this.bot.client.user?.username;
3518
2403
  }
3519
- pluginCapabilities() {
3520
- return {
3521
- client: this.bot.client,
3522
- token: this.bot.botToken,
3523
- rest: this.bot.client.rest
3524
- };
3525
- }
3526
2404
  static reset() {
3527
2405
  super.reset();
3528
2406
  LoggerChannelRegistry.instance.reset();
3529
2407
  }
3530
2408
  registerStartupTasks() {
3531
2409
  if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
3532
- this.startup.addTask(StartupPhase.Configuration, "Bus Initialization", async () => {
2410
+ this.startup.addTask(StartupPhase.Configuration, "bus-initialization", async () => {
3533
2411
  this.bus.logger.utils.initialization("Subscribers", "start");
3534
- await this.bus.init();
2412
+ await this.subscribers.init();
3535
2413
  this.bus.logger.utils.initialization("Subscribers", "end");
3536
2414
  });
3537
- this.startup.addTask(StartupPhase.Login, "Bot Initialization", async () => {
2415
+ this.startup.addTask(StartupPhase.Login, "bot-initialization", async () => {
3538
2416
  this.bot.logger.utils.initialization("Bot", "start");
3539
2417
  await this.bot.init();
3540
2418
  this.bot.logger.utils.initialization("Bot", "end");
3541
2419
  });
3542
2420
  const { healthCheck } = this;
3543
- if (healthCheck) this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
2421
+ if (healthCheck) this.startup.addTask(StartupPhase.Ready, "health-check", async () => {
3544
2422
  healthCheck.logger.utils.initialization("HealthCheck", "start");
3545
2423
  await healthCheck.init();
3546
2424
  healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3562,9 +2440,9 @@ var Seedcord = class Seedcord extends Pluggable {
3562
2440
  return this;
3563
2441
  }
3564
2442
  registerHmrAwareModules() {
3565
- this.startup.addTask(StartupPhase.Configuration, "HMR Registration", async () => {
2443
+ this.startup.addTask(StartupPhase.Configuration, "hmr-registration", async () => {
3566
2444
  this.hmrManager.register(this.bot);
3567
- this.hmrManager.register(this.bus);
2445
+ this.hmrManager.register(this.subscribers);
3568
2446
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
3569
2447
  await Promise.resolve();
3570
2448
  });
@@ -3572,5 +2450,5 @@ var Seedcord = class Seedcord extends Pluggable {
3572
2450
  };
3573
2451
 
3574
2452
  //#endregion
3575
- export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
2453
+ export { ArraySource, AutocompleteHandler, ButtonHandler, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
3576
2454
  //# sourceMappingURL=index.mjs.map