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