@seedcord/gateway 0.1.0-next.1 → 0.1.0-next.3

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
@@ -27,27 +27,36 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
 
28
28
  //#endregion
29
29
  require("reflect-metadata");
30
- let _seedcord_core = require("@seedcord/core");
30
+ let _seedcord_logger_node = require("@seedcord/logger/node");
31
31
  let _seedcord_core_internal = require("@seedcord/core/internal");
32
- let discord_js = require("discord.js");
33
32
  let _seedcord_errors = require("@seedcord/errors");
34
33
  let _seedcord_errors_internal = require("@seedcord/errors/internal");
35
- let _seedcord_utils = require("@seedcord/utils");
36
- let _seedcord_services = require("@seedcord/services");
37
- let chalk = require("chalk");
38
- chalk = __toESM(chalk, 1);
34
+ let _seedcord_core = require("@seedcord/core");
35
+ let _seedcord_logger = require("@seedcord/logger");
39
36
  let _discordjs_builders = require("@discordjs/builders");
40
37
  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
41
  let _seedcord_event_emitter = require("@seedcord/event-emitter");
42
- let node_fs = require("node:fs");
43
- let node_path = require("node:path");
44
- let node_url = require("node:url");
42
+ let chalk = require("chalk");
43
+ chalk = __toESM(chalk, 1);
44
+ let envapt = require("envapt");
45
+ let discord_api_types_v10 = require("discord-api-types/v10");
46
+ let _seedcord_utils = require("@seedcord/utils");
47
+ let _discordjs_rest = require("@discordjs/rest");
48
+ let _seedcord_core_node = require("@seedcord/core/node");
45
49
  let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
46
50
  let _seedcord_types_internal = require("@seedcord/types/internal");
47
- let envapt = require("envapt");
48
- let node_crypto = require("node:crypto");
49
- node_crypto = __toESM(node_crypto, 1);
51
+ let envapt_legacy = require("envapt/legacy");
52
+ let node_path = require("node:path");
53
+ let _seedcord_core_hmr = require("@seedcord/core/hmr");
54
+ let _seedcord_utils_node = require("@seedcord/utils/node");
50
55
 
56
+ //#region src/logger-bootstrap.ts
57
+ (0, _seedcord_logger_node.installNodeDefaults)();
58
+
59
+ //#endregion
51
60
  //#region src/bot/decorators/Events.ts
52
61
  /**
53
62
  * Registers an event handler class with one or more Discord.js events.
@@ -140,190 +149,6 @@ function Gated(...gates) {
140
149
  };
141
150
  }
142
151
 
143
- //#endregion
144
- //#region src/miscellaneous/areRoutes.ts
145
- /**
146
- * Checks if the provided value is an array of strings.
147
- *
148
- * @internal
149
- */
150
- function areRoutes(routes) {
151
- return Array.isArray(routes) && routes.every((r) => typeof r === "string");
152
- }
153
-
154
- //#endregion
155
- //#region src/bot/decorators/Interactions.ts
156
- /**
157
- * Types of select menus supported for routing
158
- */
159
- let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
160
- SelectMenuKind["String"] = "string";
161
- SelectMenuKind["User"] = "user";
162
- SelectMenuKind["Role"] = "role";
163
- SelectMenuKind["Channel"] = "channel";
164
- SelectMenuKind["Mentionable"] = "mentionable";
165
- return SelectMenuKind;
166
- }({});
167
- /**
168
- * Routes one or more slash commands to a {@link SlashHandler}.
169
- *
170
- * Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
171
- * commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
172
- * fewer or more than the handler declares is a compile error. Subcommands use a slash path.
173
- *
174
- * @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
175
- * @decorator
176
- * @example
177
- * ```ts
178
- * \@SlashRoute('ban')
179
- * class BanHandler extends SlashHandler<'ban'> {
180
- * async execute() {
181
- * const target = this.options.getUser('target');
182
- * }
183
- * }
184
- * ```
185
- *
186
- * @example
187
- * ```ts
188
- * \@SlashRoute('ban', 'kick')
189
- * class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
190
- * async execute() {
191
- * await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
192
- * }
193
- * }
194
- * ```
195
- */
196
- function SlashRoute(...routes) {
197
- return function(constructor) {
198
- storeMetadata(_seedcord_core_internal.InteractionRoutes.Slash, routes, constructor);
199
- };
200
- }
201
- /**
202
- * Routes button interactions to handler classes.
203
- *
204
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
205
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
206
- * matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
207
- * reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
208
- *
209
- * @param defs - The customId definition(s) this handler decodes, one per route.
210
- * @decorator
211
- */
212
- function ButtonRoute(...defs) {
213
- return function(constructor) {
214
- storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Button, defs, constructor);
215
- };
216
- }
217
- /**
218
- * Routes modal submissions to handler classes.
219
- *
220
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
221
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
222
- * matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
223
- * on read, the same as a button route.
224
- *
225
- * @param defs - The customId definition(s) this handler decodes, one per route.
226
- * @decorator
227
- */
228
- function ModalRoute(...defs) {
229
- return function(constructor) {
230
- storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Modal, defs, constructor);
231
- };
232
- }
233
- /**
234
- * Routes one or more context-menu commands to a {@link ContextMenuHandler}.
235
- *
236
- * Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
237
- * each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
238
- * handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
239
- * reads `this.target` uniformly with no branch.
240
- *
241
- * @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
242
- * @param names - The command name(s) this handler serves, keyed off the matching registry.
243
- * @decorator
244
- * @example
245
- * ```ts
246
- * \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
247
- * class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
248
- * async execute() {
249
- * const user = this.target;
250
- * }
251
- * }
252
- * ```
253
- */
254
- function ContextMenuRoute(kind, ...names) {
255
- return function(constructor) {
256
- storeMetadata(kind === discord_js.ApplicationCommandType.User ? _seedcord_core_internal.InteractionRoutes.UserContextMenu : _seedcord_core_internal.InteractionRoutes.MessageContextMenu, names, constructor);
257
- };
258
- }
259
- /**
260
- * Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
261
- *
262
- * Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
263
- * over its autocompletable fields, several commands share one handler whose arms span every command's
264
- * fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
265
- * handler declares is a compile error.
266
- *
267
- * @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
268
- * @decorator
269
- * @example
270
- * ```ts
271
- * \@AutocompleteRoute('search')
272
- * class SearchAutocomplete extends AutocompleteHandler<'search'> {
273
- * async execute() {
274
- * await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
275
- * }
276
- * }
277
- * ```
278
- */
279
- function AutocompleteRoute(...routes) {
280
- return function(constructor) {
281
- storeMetadata(_seedcord_core_internal.InteractionRoutes.Autocomplete, routes, constructor);
282
- };
283
- }
284
- /**
285
- * Routes select menu interactions to handler classes.
286
- *
287
- * Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
288
- * generic must list the same definitions, and its second generic argument must be the matching select
289
- * interaction type, or it is a compile error.
290
- *
291
- * @param type - Select menu kind from {@link SelectMenuKind}.
292
- * @param defs - The customId definition(s) this handler decodes, one per route.
293
- * @decorator
294
- *
295
- * @example
296
- * ```typescript
297
- * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
298
- * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
299
- * // handles user select menus minted from AssignId
300
- * }
301
- * ```
302
- */
303
- function SelectMenuRoute(type, ...defs) {
304
- return function(constructor) {
305
- storeComponentRoute({
306
- ["string"]: _seedcord_core_internal.InteractionRoutes.StringMenu,
307
- ["user"]: _seedcord_core_internal.InteractionRoutes.UserMenu,
308
- ["role"]: _seedcord_core_internal.InteractionRoutes.RoleMenu,
309
- ["channel"]: _seedcord_core_internal.InteractionRoutes.ChannelMenu,
310
- ["mentionable"]: _seedcord_core_internal.InteractionRoutes.MentionableMenu
311
- }[type], defs, constructor);
312
- };
313
- }
314
- function storeComponentRoute(route, defs, constructor) {
315
- storeMetadata(route, defs.map((def) => def.prefix), constructor);
316
- Reflect.defineMetadata(_seedcord_core_internal.ComponentDefsKey, defs, constructor);
317
- }
318
- function storeMetadata(route, routes, constructor) {
319
- const key = _seedcord_core_internal.InteractionRouteKeys[route];
320
- const savedRoutes = Reflect.getMetadata(key, constructor);
321
- const existing = areRoutes(savedRoutes) ? savedRoutes : [];
322
- const toStore = Array.isArray(routes) ? routes : [routes];
323
- Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
324
- Reflect.defineMetadata(_seedcord_core_internal.InteractionMetadataKey, true, constructor);
325
- }
326
-
327
152
  //#endregion
328
153
  //#region src/bot/decorators/Middlewares.ts
329
154
  /**
@@ -392,21 +217,6 @@ const IgnoreBots = (0, _seedcord_core.defineGate)("IgnoreBots", (ctx) => {
392
217
  if (ctx.user?.bot) throw new _seedcord_core.Silence("actor is a bot");
393
218
  });
394
219
 
395
- //#endregion
396
- //#region src/bot/notices/utils.ts
397
- function mentionFor(subject) {
398
- if (subject instanceof discord_js.Role) return `<@&${subject.id}>`;
399
- if (subject instanceof discord_js.TextChannel) return `<#${subject.id}>`;
400
- if (subject instanceof discord_js.GuildMember) return `<@${subject.id}>`;
401
- return `\`${subject.name}\``;
402
- }
403
- function labelFor(subject) {
404
- if (subject instanceof discord_js.Role) return "role";
405
- if (subject instanceof discord_js.TextChannel) return "channel";
406
- if (subject instanceof discord_js.GuildMember) return "member";
407
- return "guild";
408
- }
409
-
410
220
  //#endregion
411
221
  //#region src/bot/notices/index.ts
412
222
  var NotNsfw = class extends _seedcord_core_internal.GateNotice {
@@ -414,13 +224,6 @@ var NotNsfw = class extends _seedcord_core_internal.GateNotice {
414
224
  super(message);
415
225
  }
416
226
  };
417
- var MissingRole = class extends _seedcord_core_internal.GateNotice {
418
- role;
419
- constructor(message, role) {
420
- super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
421
- this.role = role;
422
- }
423
- };
424
227
  var RoleHigherThanMe = class extends _seedcord_core.Notice {
425
228
  role;
426
229
  botRole;
@@ -441,30 +244,19 @@ var CannotAssignBotRole = class extends _seedcord_core.Notice {
441
244
  return { components: [new _seedcord_core_internal.NoticeCard("I cannot assign a managed role.").component] };
442
245
  }
443
246
  };
444
- var MissingPermissions = class extends _seedcord_core.Notice {
445
- where;
446
- missingPerms;
447
- constructor(message, where, missingPerms) {
448
- super(message);
449
- this.where = where;
450
- this.missingPerms = missingPerms;
451
- }
452
- render() {
453
- const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
454
- return { components: [new _seedcord_core_internal.NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
455
- }
456
- };
457
247
  var HasDangerousPermissions = class extends _seedcord_core.Notice {
458
- target;
248
+ subject;
459
249
  dangerousPerms;
460
- constructor(message, target, dangerousPerms) {
461
- super(message);
462
- this.target = target;
250
+ customLead;
251
+ constructor(message, subject, dangerousPerms) {
252
+ super(message ?? "A dangerous permission is enabled.");
253
+ this.subject = subject;
463
254
  this.dangerousPerms = dangerousPerms;
255
+ this.customLead = message;
464
256
  }
465
257
  render() {
466
258
  const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
467
- return { components: [new _seedcord_core_internal.NoticeCard(`The ${labelFor(this.target)} ${mentionFor(this.target)} has the following permission entries that must not be enabled:\n\n${bullets}`).component] };
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] };
468
260
  }
469
261
  };
470
262
  var UserNotFound = class extends _seedcord_core.Notice {
@@ -540,129 +332,6 @@ function Nsfw(options) {
540
332
  });
541
333
  }
542
334
 
543
- //#endregion
544
- //#region src/bot/utilities/permissions/checkPermissions.ts
545
- /**
546
- * Map of permission bits to their prettified human readable names.
547
- */
548
- const PermissionNames = new Map(Object.entries(discord_js.PermissionFlagsBits).map(([key, bit]) => [bit, (0, _seedcord_utils.prettify)(key)]));
549
- function checkPermissions(a, b, c, d, e) {
550
- const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
551
- for: a,
552
- in: b,
553
- scope: c,
554
- inverse: d ?? false,
555
- ...e
556
- } : a;
557
- const Missing = missingCtor ?? MissingPermissions;
558
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
559
- const perms = pIn instanceof discord_js.Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
560
- const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
561
- const present = scope.filter((bit) => perms.has(bit, true));
562
- const presentNames = names(present);
563
- if (inverse) {
564
- if (present.length > 0) throw new Dangerous(`${pFor instanceof discord_js.Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof discord_js.Guild ? "guild" : "channel"}`, pFor, presentNames);
565
- return;
566
- }
567
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
568
- if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
569
- }
570
-
571
- //#endregion
572
- //#region src/bot/gates/catalog/permissions.ts
573
- const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
574
- /**
575
- * Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
576
- *
577
- * Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
578
- *
579
- * @param scope - The permission flag bits the caller must all hold.
580
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
581
- *
582
- * @see {@link Gated}
583
- *
584
- * @example
585
- * ```ts
586
- * import { PermissionFlagsBits } from 'discord.js';
587
- *
588
- * \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
589
- * \@SlashRoute('ban')
590
- * class BanHandler extends SlashHandler<'ban'> {
591
- * async execute() {
592
- * const target = this.options.getUser('target');
593
- * }
594
- * }
595
- * ```
596
- */
597
- function RequirePermissions(scope, options) {
598
- return (0, _seedcord_core.defineGate)("RequirePermissions", (ctx) => {
599
- if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
600
- if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
601
- checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
602
- });
603
- }
604
- /**
605
- * Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
606
- *
607
- * It reads the bot's own member, so it attaches to modal handlers as well.
608
- *
609
- * @param scope - The permission flag bits the bot must all hold.
610
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
611
- *
612
- * @see {@link Gated}
613
- *
614
- * @example
615
- * ```ts
616
- * import { PermissionFlagsBits } from 'discord.js';
617
- *
618
- * \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
619
- * \@ModalRoute(MyModalId)
620
- * class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
621
- * async execute() {
622
- * // ...
623
- * }
624
- * }
625
- * ```
626
- */
627
- function RequireBotPermissions(scope, options) {
628
- return (0, _seedcord_core.defineGate)("RequireBotPermissions", (ctx) => {
629
- const botMember = ctx.guild?.members.me;
630
- if (!ctx.guild || !botMember) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
631
- checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
632
- });
633
- }
634
- /**
635
- * Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
636
- * when the role is missing.
637
- *
638
- * Interaction-only and excludes ModalSubmit.
639
- *
640
- * @param roleId - The role snowflake the caller must hold, read from the member's role cache.
641
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
642
- *
643
- * @see {@link Gated}
644
- *
645
- * @example
646
- * ```ts
647
- * \@Gated(RequireRole('123456789012345678'))
648
- * \@SlashRoute('vip')
649
- * class VipHandler extends SlashHandler<'vip'> {
650
- * async execute() {
651
- * // ...
652
- * }
653
- * }
654
- * ```
655
- */
656
- function RequireRole(roleId, options) {
657
- return (0, _seedcord_core.defineGate)("RequireRole", (ctx) => {
658
- if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
659
- if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
660
- if (ctx.member.roles.cache.has(roleId)) return;
661
- const role = ctx.guild.roles.cache.get(roleId) ?? null;
662
- throw (0, _seedcord_core_internal.pickNotice)(options?.missingRole, (message) => new MissingRole(message, role));
663
- });
664
- }
665
-
666
335
  //#endregion
667
336
  //#region src/bot/injectors/EmojiInjector.ts
668
337
  const emojiStorage = {};
@@ -684,14 +353,14 @@ const Emojis = emojiStorage;
684
353
  */
685
354
  var EmojiInjector = class {
686
355
  core;
687
- logger = new _seedcord_services.Logger("Emojis");
356
+ logger = new _seedcord_logger.Logger("Emojis");
688
357
  constructor(core) {
689
358
  this.core = core;
690
359
  }
691
360
  async init() {
692
361
  this.clearEmojis();
693
362
  if (!this.core.config.bot.emojis || Object.keys(this.core.config.bot.emojis).length === 0) {
694
- this.logger.debug(chalk.default.bold.yellow("No emojis configured, skipping emoji injection."));
363
+ this.logger.debug("No emojis configured, skipping emoji injection.");
695
364
  return;
696
365
  }
697
366
  const configEmojis = this.core.config.bot.emojis;
@@ -759,7 +428,7 @@ const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
759
428
  */
760
429
  var CommandMentionInjector = class {
761
430
  core;
762
- logger = new _seedcord_services.Logger("CommandMentions");
431
+ logger = new _seedcord_logger.Logger("CommandMentions");
763
432
  constructor(core) {
764
433
  this.core = core;
765
434
  }
@@ -851,6 +520,37 @@ async function fetchText(client, channelId) {
851
520
  throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
852
521
  }
853
522
 
523
+ //#endregion
524
+ //#region src/bot/notices/utils.ts
525
+ function mentionFor(subject) {
526
+ if (subject instanceof discord_js.Role) return `<@&${subject.id}>`;
527
+ return `<@${subject.id}>`;
528
+ }
529
+
530
+ //#endregion
531
+ //#region src/bot/utilities/permissions/checkPermissions.ts
532
+ 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 ? {
534
+ for: a,
535
+ in: b,
536
+ scope: c,
537
+ inverse: d ?? false,
538
+ ...e
539
+ } : a;
540
+ const Missing = missingCtor ?? _seedcord_core_internal.MissingPermissions;
541
+ const Dangerous = dangerousCtor ?? HasDangerousPermissions;
542
+ const subject = mentionFor(pFor);
543
+ 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));
552
+ }
553
+
854
554
  //#endregion
855
555
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
856
556
  /**
@@ -878,17 +578,17 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
878
578
  if (target instanceof discord_js.Guild) {
879
579
  const me = target.members.me;
880
580
  if (!me) {
881
- const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
882
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
581
+ const names = scope.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
582
+ throw new ((errors?.missingNotice) ?? _seedcord_core_internal.MissingPermissions)(void 0, "The bot", names);
883
583
  }
884
584
  checkPermissions(me, target, scope, inverse, errors);
885
585
  return;
886
586
  }
887
587
  const me = target.guild.members.me;
888
588
  if (!me) {
889
- new _seedcord_services.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
890
- const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
891
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
589
+ new _seedcord_logger.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
590
+ const names = scope.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
591
+ throw new ((errors?.missingNotice) ?? _seedcord_core_internal.MissingPermissions)(void 0, "The bot", names);
892
592
  }
893
593
  checkPermissions(me, target, scope, inverse, errors);
894
594
  }
@@ -925,26 +625,26 @@ function getBotRole(guild) {
925
625
  * // Check with custom error handling
926
626
  * hasPermsToAssign({
927
627
  * targetRole: role,
928
- * errors: {
929
- * higher: CustomHigherError,
930
- * managed: CustomManagedError,
931
- * missing: CustomMissingPermissionsError
628
+ * noticeOverrides: {
629
+ * higherNotice: CustomHigherNotice,
630
+ * managedNotice: CustomManagedNotice,
631
+ * missingNotice: CustomMissingPermissionsNotice
932
632
  * }
933
633
  * });
934
634
  * ```
935
635
  */
936
636
  function hasPermsToAssign(roleOrOptions) {
937
- const { targetRole, errors } = roleOrOptions instanceof discord_js.Role ? {
637
+ const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof discord_js.Role ? {
938
638
  targetRole: roleOrOptions,
939
- errors: void 0
639
+ noticeOverrides: void 0
940
640
  } : roleOrOptions;
941
- const HigherErr = errors?.higher ?? RoleHigherThanMe;
942
- const ManagedErr = errors?.managed ?? CannotAssignBotRole;
943
- const Missing = errors?.missing ?? MissingPermissions;
641
+ const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
642
+ const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
643
+ const MissingNotice = errors?.missingNotice ?? _seedcord_core_internal.MissingPermissions;
944
644
  const botRole = getBotRole(targetRole.guild);
945
- if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherErr("Role is higher than me", targetRole, botRole);
946
- if (targetRole.managed) throw new ManagedErr(`Cannot assign managed role ${targetRole.name}`);
947
- checkBotPermissions(targetRole.guild, [discord_js.PermissionFlagsBits.ManageRoles], false, { missing: Missing });
645
+ if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
646
+ if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
647
+ checkBotPermissions(targetRole.guild, [discord_js.PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
948
648
  }
949
649
 
950
650
  //#endregion
@@ -1072,114 +772,309 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
1072
772
  }
1073
773
 
1074
774
  //#endregion
1075
- //#region src/bot/harmlessApiCodes.ts
1076
- /**
1077
- * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
1078
- * message. There is nothing to show the user and no bug to report. The controller boundary drops them on
1079
- * the original throw, and {@link ReplySender} drops them on its own send.
1080
- *
1081
- * @internal
1082
- */
1083
- const HARMLESS_API_CODES = new Set([
1084
- discord_js.RESTJSONErrorCodes.UnknownInteraction,
1085
- discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1086
- discord_js.RESTJSONErrorCodes.UnknownMessage
1087
- ]);
1088
-
1089
- //#endregion
1090
- //#region src/miscellaneous/flagsFor.ts
775
+ //#region src/bot/ReplySender.ts
776
+ function seedState(interaction) {
777
+ if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
778
+ if (interaction.replied) return "replied";
779
+ if (interaction.deferred) return "deferred-reply";
780
+ return "unacked";
781
+ }
1091
782
  /**
1092
- * Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
1093
- * IsComponentsV2 flag is always set.
1094
- *
1095
- * @param ephemeral - Whether the reply is ephemeral.
1096
- * @returns The OR'd `MessageFlags` value as a plain number.
1097
- *
1098
- * @internal
783
+ * Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
784
+ * handler bases and the dispatcher.
1099
785
  */
1100
- function flagsFor(ephemeral) {
1101
- let flags = discord_js.MessageFlags.IsComponentsV2;
1102
- if (ephemeral) flags |= discord_js.MessageFlags.Ephemeral;
1103
- return flags;
1104
- }
1105
-
1106
- //#endregion
1107
- //#region src/bot/ReplySender.ts
1108
- /**
1109
- * Sends a {@link ReplyResponse} to an interaction, picking reply, editReply, or followUp from the live
1110
- * acknowledgement state. Logs and drops its own send failures so a dead token never escapes into the
1111
- * controller.
1112
- */
1113
- var ReplySender = class {
786
+ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
1114
787
  interaction;
1115
- logger = new _seedcord_services.Logger("ReplySender");
1116
- constructor(interaction) {
788
+ constructor(interaction, routeId) {
789
+ super(routeId, seedState(interaction));
1117
790
  this.interaction = interaction;
1118
791
  }
1119
- /**
1120
- * Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
1121
- * a failed send is logged and dropped and resolves to `undefined`.
1122
- *
1123
- * @param response - The ComponentsV2 reply to show.
1124
- * @param ephemeral - Whether the reply is ephemeral. {@default `true`}
1125
- * @returns The sent {@link Message}, or `undefined` when the send was swallowed.
1126
- */
1127
- async send(response, ephemeral = true) {
1128
- try {
1129
- return await this.dispatch(response, ephemeral);
1130
- } catch (error) {
1131
- this.logSwallowed("send", error);
1132
- return;
1133
- }
792
+ async writeReply(response, opts) {
793
+ const result = await this.interaction.reply({
794
+ ...this.replyOptions(response, (0, _seedcord_core_internal.sendFlags)(opts)),
795
+ withResponse: true
796
+ });
797
+ return this.createdMessage(result);
1134
798
  }
1135
- /**
1136
- * Edits an already-sent message to a new {@link ReplyResponse}. Used to swap a confirmation prompt for
1137
- * its outcome. Never throws, a failed edit is logged and dropped.
1138
- *
1139
- * @param message - The message to edit (the handle a prior {@link send} returned).
1140
- * @param response - The reply to replace it with.
1141
- */
1142
- async edit(message, response) {
1143
- try {
1144
- await this.interaction.webhook.editMessage(message, this.editBody(response));
1145
- } catch (error) {
1146
- this.logSwallowed("edit", error);
1147
- }
799
+ async writeDefer(opts) {
800
+ await this.interaction.deferReply({ flags: (0, _seedcord_core_internal.deferFlags)(opts) });
1148
801
  }
1149
- async dispatch(response, ephemeral) {
1150
- if (this.interaction.replied) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1151
- if (this.interaction.deferred) {
1152
- if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1153
- return await this.interaction.editReply(this.editBody(response));
1154
- }
1155
- await this.interaction.reply(this.replyOptions(response, ephemeral));
1156
- return await this.interaction.fetchReply();
802
+ async writeDeferUpdate() {
803
+ await this.sourceInteraction("deferUpdate").deferUpdate();
804
+ }
805
+ async writeUpdate(response) {
806
+ const result = await this.sourceInteraction("update").update({
807
+ ...this.updateOptions(response),
808
+ withResponse: true
809
+ });
810
+ return this.createdMessage(result);
811
+ }
812
+ async writeFollowUp(response, opts) {
813
+ return await this.interaction.followUp(this.replyOptions(response, (0, _seedcord_core_internal.sendFlags)(opts)));
814
+ }
815
+ async writeEditOriginal(response) {
816
+ return await this.interaction.editReply(this.editOptions(response));
817
+ }
818
+ async writeEditTarget(targetId, response) {
819
+ return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
820
+ }
821
+ async writeDeleteOriginal() {
822
+ await this.interaction.deleteReply();
1157
823
  }
1158
- replyOptions(response, ephemeral) {
824
+ async writeDeleteTarget(targetId) {
825
+ await this.interaction.deleteReply(targetId);
826
+ }
827
+ async writeModal(data) {
828
+ const { interaction } = this;
829
+ if (interaction.isModalSubmit()) this.rejectModalSubmit();
830
+ await interaction.showModal(data);
831
+ }
832
+ guardModalSource() {
833
+ if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
834
+ }
835
+ rejectModalSubmit() {
836
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ReplyIllegalAckState, [
837
+ "showModal",
838
+ "this interaction is a modal submit",
839
+ "A modal cannot open another modal.",
840
+ this.routeId
841
+ ]);
842
+ }
843
+ sourceInteraction(method) {
844
+ const { interaction } = this;
845
+ if (interaction.isMessageComponent()) return interaction;
846
+ if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
847
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
848
+ }
849
+ replyOptions(response, flags) {
850
+ const reply = this.serialize(response);
1159
851
  return {
1160
- components: response.components,
1161
- flags: flagsFor(ephemeral),
1162
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1163
- ...response.files && { files: response.files }
852
+ components: reply.components,
853
+ flags,
854
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
855
+ ...reply.files && { files: [...reply.files] }
1164
856
  };
1165
857
  }
1166
- editBody(response) {
858
+ editOptions(response) {
859
+ const reply = this.serialize(response);
1167
860
  return {
1168
- components: response.components,
861
+ components: reply.components,
1169
862
  flags: discord_js.MessageFlags.IsComponentsV2,
1170
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1171
- ...response.files && { files: response.files }
863
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
864
+ ...reply.files && { files: [...reply.files] }
1172
865
  };
1173
866
  }
1174
- logSwallowed(action, error) {
1175
- if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
1176
- this.logger.debug(`reply ${action} hit harmless code ${error.code}`);
1177
- return;
1178
- }
1179
- this.logger.error(`reply ${action} failed`, error);
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();
1180
926
  }
1181
927
  };
1182
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
+
1183
1078
  //#endregion
1184
1079
  //#region src/bot/confirm/reserved.ts
1185
1080
  const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
@@ -1224,9 +1119,9 @@ async function collectChoice(message, userId, timeoutMs) {
1224
1119
  return null;
1225
1120
  }
1226
1121
  }
1227
- async function settle(sender, interaction, message, outcome) {
1122
+ async function settle(sender, message, outcome) {
1228
1123
  if (outcome === void 0) {
1229
- await interaction.deleteReply(message).catch(() => void 0);
1124
+ await sender.delete(message).catch(() => void 0);
1230
1125
  return;
1231
1126
  }
1232
1127
  await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
@@ -1234,30 +1129,19 @@ async function settle(sender, interaction, message, outcome) {
1234
1129
  async function getConfirmation(interaction, prompt, options) {
1235
1130
  const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1236
1131
  const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1237
- const sender = new ReplySender(interaction);
1238
- const message = await sender.send(response, ephemeral);
1239
- if (!message) return false;
1132
+ const sender = new ReplySender(interaction, interactionRoute(interaction));
1133
+ const message = await sender.send(response, { ephemeral });
1240
1134
  const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1241
1135
  if (!winner) {
1242
- await settle(sender, interaction, message, options?.onTimeout);
1136
+ await settle(sender, message, options?.onTimeout);
1243
1137
  return false;
1244
1138
  }
1245
1139
  await winner.deferUpdate().catch(() => void 0);
1246
1140
  const confirmed = winner.customId === CONFIRM_IDS.confirm;
1247
- await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
1141
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1248
1142
  return confirmed;
1249
1143
  }
1250
1144
 
1251
- //#endregion
1252
- //#region src/hmr/devChannel.ts
1253
- let channel;
1254
- function setDevChannel(next) {
1255
- channel = next;
1256
- }
1257
- function getDevChannel() {
1258
- return channel;
1259
- }
1260
-
1261
1145
  //#endregion
1262
1146
  //#region src/interfaces/Plugin.ts
1263
1147
  /**
@@ -1285,7 +1169,7 @@ var Plugin = class extends _seedcord_event_emitter.TypedEventEmitter {
1285
1169
  * @param patterns - Glob patterns relative to the project root
1286
1170
  */
1287
1171
  registerCriticalFiles(patterns) {
1288
- getDevChannel()?.send("seedcord:register-critical-files", { patterns });
1172
+ (0, _seedcord_core_internal.getDevChannel)()?.send("seedcord:register-critical-files", { patterns });
1289
1173
  }
1290
1174
  /** @internal */
1291
1175
  removeListener(event, listener) {
@@ -1367,19 +1251,10 @@ var Pluggable = class Pluggable extends _seedcord_event_emitter.TypedEventEmitte
1367
1251
  //#endregion
1368
1252
  //#region src/handlers/BaseHandler.ts
1369
1253
  /**
1370
- * Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
1254
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1255
+ * gate hook.
1371
1256
  */
1372
- var BaseHandler = class {
1373
- core;
1374
- event;
1375
- logger;
1376
- dispatch;
1377
- constructor(event, core, dispatch) {
1378
- this.core = core;
1379
- this.event = event;
1380
- this.logger = new _seedcord_services.Logger(this.constructor.name);
1381
- this.dispatch = dispatch;
1382
- }
1257
+ var BaseHandler = class extends _seedcord_core.BaseHandler {
1383
1258
  /** @internal */
1384
1259
  getEvent() {
1385
1260
  return this.event;
@@ -1403,36 +1278,49 @@ var InteractionMiddleware = class extends BaseHandler {
1403
1278
  };
1404
1279
 
1405
1280
  //#endregion
1406
- //#region src/handlers/interaction/InteractionHandler.ts
1281
+ //#region src/handlers/RepliableHandler.ts
1407
1282
  /**
1408
- * Shared base the typed interaction handlers extend.
1283
+ * Shared base the repliable interaction handlers extend.
1409
1284
  *
1410
- * Not a public entry point. You should be using {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1411
- * or {@link SelectMenuHandler} instead. This class only carries the repliable-event plumbing those bases share,
1412
- * so DO NOT use it directly.
1285
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1286
+ * {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
1287
+ * to the gateway sender.
1413
1288
  *
1414
- * @typeParam Repliable - The interaction type this handler processes
1289
+ * @typeParam Event - The repliable interaction type this handler processes
1415
1290
  */
1416
- var InteractionHandler = class extends BaseHandler {
1417
- constructor(event, core, dispatch) {
1418
- super(event, core, dispatch);
1291
+ var RepliableHandler = class extends _seedcord_core.RepliableHandler {
1292
+ /** @internal */
1293
+ getEvent() {
1294
+ return this.event;
1295
+ }
1296
+ buildSender(event, _core, routeId) {
1297
+ return new ReplySender(event, routeId);
1419
1298
  }
1420
1299
  };
1421
1300
 
1422
1301
  //#endregion
1423
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1302
+ //#region src/handlers/interaction/InteractionHandler.ts
1424
1303
  /**
1425
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
1426
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
1427
- * interaction and a generated registry key always match.
1304
+ * Shared base the typed interaction handlers extend.
1428
1305
  *
1429
- * @internal
1306
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1307
+ * or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
1308
+ * share.
1309
+ *
1310
+ * @typeParam Repliable - The interaction type this handler processes
1430
1311
  */
1431
- function slashRouteOf(interaction) {
1432
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
1433
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
1434
- return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
1435
- }
1312
+ var InteractionHandler = class extends RepliableHandler {
1313
+ constructor(event, core, dispatch) {
1314
+ super(event, core, dispatch);
1315
+ }
1316
+ /**
1317
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
1318
+ * compile time (Discord forbids a modal in response to a modal).
1319
+ */
1320
+ showModal(modal) {
1321
+ return this.sender.showModal(modal);
1322
+ }
1323
+ };
1436
1324
 
1437
1325
  //#endregion
1438
1326
  //#region src/handlers/interaction/SlashHandler.ts
@@ -1468,7 +1356,7 @@ var SlashHandler = class extends InteractionHandler {
1468
1356
  }
1469
1357
  /**
1470
1358
  * Run the arm for whichever command fired. Use this only when the handler is registered for several
1471
- * routes. A single-route handler reads `this.options` directly instead, match buys nothing there.
1359
+ * routes.
1472
1360
  *
1473
1361
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1474
1362
  * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
@@ -1492,7 +1380,7 @@ var SlashHandler = class extends InteractionHandler {
1492
1380
  */
1493
1381
  async match(arms) {
1494
1382
  const route = slashRouteOf(this.event);
1495
- const arm = arms[route];
1383
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1496
1384
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SlashMatchArmMissing, [route]);
1497
1385
  return await arm(this.options);
1498
1386
  }
@@ -1563,11 +1451,11 @@ var AutocompleteHandler = class extends BaseHandler {
1563
1451
  constructor(event, core, dispatch) {
1564
1452
  super(event, core, dispatch);
1565
1453
  }
1454
+ decodedFocused;
1566
1455
  /**
1567
1456
  * The focused option for this interaction. `value` is always the raw partial string the user is typing,
1568
1457
  * even for an integer or number option, so coerce it yourself when you need a number.
1569
1458
  */
1570
- decodedFocused;
1571
1459
  get focused() {
1572
1460
  if (this.decodedFocused) return this.decodedFocused;
1573
1461
  const raw = this.event.options.getFocused(true);
@@ -1583,7 +1471,7 @@ var AutocompleteHandler = class extends BaseHandler {
1583
1471
  * match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
1584
1472
  *
1585
1473
  * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
1586
- * arm receives the focused partial value and a `respond` pinned to that field's choice value type. The
1474
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
1587
1475
  * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
1588
1476
  * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
1589
1477
  *
@@ -1605,11 +1493,15 @@ var AutocompleteHandler = class extends BaseHandler {
1605
1493
  */
1606
1494
  async match(arms) {
1607
1495
  const { name, value } = this.focused;
1608
- const respond = (choices) => this.event.respond(choices);
1609
- const arm = arms[name];
1496
+ const respond = (choices) => this.respond(choices);
1497
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1610
1498
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1611
1499
  return await arm(value, respond);
1612
1500
  }
1501
+ /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1502
+ async respond(choices) {
1503
+ await this.event.respond(choices);
1504
+ }
1613
1505
  /** The firing command route, for a field whose completion differs per registered command. */
1614
1506
  get route() {
1615
1507
  return slashRouteOf(this.event);
@@ -1617,7 +1509,7 @@ var AutocompleteHandler = class extends BaseHandler {
1617
1509
  /**
1618
1510
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
1619
1511
  * (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
1620
- * types the focused field. Read the focused field from `this.focused`, not here.
1512
+ * types the focused field. The focused field is on `this.focused`.
1621
1513
  */
1622
1514
  get options() {
1623
1515
  return this.event.options;
@@ -1629,25 +1521,25 @@ var AutocompleteHandler = class extends BaseHandler {
1629
1521
  /**
1630
1522
  * Shared base the customId-routed component handlers extend.
1631
1523
  *
1632
- * Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectMenuHandler}, or
1633
- * {@link ModalHandler} instead. This class only carries the customId decode and route-matching plumbing
1634
- * those bases share, so DO NOT use it directly.
1524
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
1525
+ * instead. This class defines the customId decode and the route matching those bases share.
1635
1526
  *
1636
1527
  * @typeParam Event - The component interaction type this handler processes
1637
1528
  * @typeParam Defs - The customId route definitions registered on the concrete handler
1638
1529
  */
1639
1530
  var ComponentHandler = class extends InteractionHandler {
1640
- get registeredDefs() {
1641
- const defs = Reflect.getMetadata(_seedcord_core_internal.ComponentDefsKey, this.constructor);
1642
- if (!defs) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdHandlerRouteMissing, [this.constructor.name]);
1643
- return defs;
1531
+ /** Rewrite the source message this component interaction came from. */
1532
+ update(response) {
1533
+ return this.sender.update(response);
1534
+ }
1535
+ /** Acknowledge the component without changing the source message. */
1536
+ deferUpdate() {
1537
+ return this.sender.deferUpdate();
1644
1538
  }
1645
1539
  decoded;
1646
1540
  get route() {
1647
- if (this.decoded) return this.decoded;
1648
- const decoded = (0, _seedcord_core_internal.decodeFor)(this.registeredDefs, this.event.customId);
1649
- this.decoded = decoded;
1650
- return decoded;
1541
+ this.decoded ??= (0, _seedcord_core_internal.decodeComponentRoute)(this.constructor, this.event.customId);
1542
+ return this.decoded;
1651
1543
  }
1652
1544
  /**
1653
1545
  * The decoded params of the single route this handler is registered for.
@@ -1666,10 +1558,11 @@ var ComponentHandler = class extends InteractionHandler {
1666
1558
  * handler `this.params` is `never`, so match is the only way to read the decoded params.
1667
1559
  *
1668
1560
  * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1669
- * decoded params. The arms must cover every registered def, a missing prefix or an unknown key is a
1670
- * compile error. Decoding runs before any arm, so a stale or corrupt wire throws before an arm body executes.
1561
+ * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1562
+ * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1563
+ * corrupt wire throws before an arm body executes.
1671
1564
  *
1672
- * @param arms - One handler per registered route, keyed by prefix.
1565
+ * @param arms - One callback per registered route, keyed by prefix.
1673
1566
  * @returns The result of the arm that ran.
1674
1567
  *
1675
1568
  * @example
@@ -1678,8 +1571,8 @@ var ComponentHandler = class extends InteractionHandler {
1678
1571
  * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1679
1572
  * async execute() {
1680
1573
  * await this.match({
1681
- * approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
1682
- * reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
1574
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1575
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1683
1576
  * });
1684
1577
  * }
1685
1578
  * }
@@ -1687,7 +1580,7 @@ var ComponentHandler = class extends InteractionHandler {
1687
1580
  */
1688
1581
  async match(arms) {
1689
1582
  const { prefix, params } = this.route;
1690
- const arm = arms[prefix];
1583
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1691
1584
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1692
1585
  return await arm(params);
1693
1586
  }
@@ -1699,8 +1592,9 @@ var ComponentHandler = class extends InteractionHandler {
1699
1592
  * Base class for a button interaction handler.
1700
1593
  *
1701
1594
  * Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
1702
- * generic, then read `this.params` for a single route or `this.match` for several. Passing different
1703
- * definitions to the decorator and the generic is a compile error.
1595
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
1596
+ * members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
1597
+ * and the generic is a compile error.
1704
1598
  *
1705
1599
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1706
1600
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1711,7 +1605,7 @@ var ComponentHandler = class extends InteractionHandler {
1711
1605
  * class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
1712
1606
  * async execute() {
1713
1607
  * const { userId } = this.params;
1714
- * await this.event.reply(`approved <@${userId}>`);
1608
+ * await this.reply(`approved <@${userId}>`);
1715
1609
  * }
1716
1610
  * }
1717
1611
  * ```
@@ -1725,7 +1619,8 @@ var ButtonHandler = class extends ComponentHandler {};
1725
1619
  *
1726
1620
  * Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
1727
1621
  * generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
1728
- * inputs from `this.event.fields`.
1622
+ * inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
1623
+ * response to a modal, so `showModal` fails compilation on this kind.
1729
1624
  *
1730
1625
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1731
1626
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1737,7 +1632,7 @@ var ButtonHandler = class extends ComponentHandler {};
1737
1632
  * async execute() {
1738
1633
  * const { guildId } = this.params;
1739
1634
  * const name = this.event.fields.getTextInputValue('name');
1740
- * await this.event.reply(`saved ${name} for ${guildId}`);
1635
+ * await this.reply(`saved ${name} for ${guildId}`);
1741
1636
  * }
1742
1637
  * }
1743
1638
  * ```
@@ -1751,7 +1646,8 @@ var ModalHandler = class extends ComponentHandler {};
1751
1646
  *
1752
1647
  * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1753
1648
  * so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
1754
- * route or `this.match` for several.
1649
+ * route or `this.match` for several, and reply through the handler members or rewrite the source message
1650
+ * with `this.update`.
1755
1651
  *
1756
1652
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1757
1653
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
@@ -1763,7 +1659,7 @@ var ModalHandler = class extends ComponentHandler {};
1763
1659
  * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1764
1660
  * async execute() {
1765
1661
  * const { roleId } = this.params;
1766
- * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1662
+ * await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1767
1663
  * }
1768
1664
  * }
1769
1665
  * ```
@@ -2029,16 +1925,16 @@ var Paginator = class {
2029
1925
  const loadPage = (ctx, n) => this.page(ctx, n);
2030
1926
  this.Handler = class Nav extends ButtonHandler {
2031
1927
  async execute() {
2032
- await this.event.deferUpdate();
1928
+ await this.deferUpdate();
2033
1929
  const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2034
- await new ReplySender(this.event).edit(this.event.message, response);
1930
+ await this.update(response);
2035
1931
  }
2036
1932
  };
2037
1933
  }
2038
1934
  /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2039
1935
  async start(interaction, core) {
2040
1936
  const response = await this.page(contextOf(interaction, core), 0);
2041
- return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
1937
+ return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
2042
1938
  }
2043
1939
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2044
1940
  async page(ctx, n) {
@@ -2120,7 +2016,7 @@ var Subscriber = class {
2120
2016
  constructor(data, core) {
2121
2017
  this.data = data;
2122
2018
  this.core = core;
2123
- this.logger = new _seedcord_services.Logger(this.constructor.name);
2019
+ this.logger = new _seedcord_logger.Logger(this.constructor.name);
2124
2020
  }
2125
2021
  };
2126
2022
 
@@ -2163,183 +2059,183 @@ function Subscribe(subscriber, options) {
2163
2059
  }
2164
2060
 
2165
2061
  //#endregion
2166
- //#region src/subscribers/bases/WebhookLog.ts
2062
+ //#region src/subscribers/decorators/WebhookUrl.ts
2167
2063
  /**
2168
- * Abstract webhook logging handler for subscriber events.
2064
+ * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
2169
2065
  *
2170
- * Extends Subscriber to provide webhook-based logging capabilities.
2171
- * Implementations must define the webhook client to send messages to by implementing the webhook property.
2066
+ * An unset variable disables the reporter with a boot warning. A set but malformed value throws
2067
+ * at boot.
2172
2068
  *
2173
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
2069
+ * @param envKey - The environment variable name holding the webhook url
2070
+ * @decorator
2071
+ * @example
2072
+ * ```ts
2073
+ * \@Subscribe('memberBanned')
2074
+ * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
2075
+ * class BanLog extends WebhookLog<'memberBanned'> {
2076
+ * report() {
2077
+ * return { components: [new BanCard(this.data).component] };
2078
+ * }
2079
+ * }
2080
+ * ```
2174
2081
  */
2175
- var WebhookLog = class extends Subscriber {
2176
- constructor(data, core) {
2177
- super(data, core);
2082
+ function WebhookUrl(envKey) {
2083
+ return function(constructor) {
2084
+ Reflect.defineMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, envKey, constructor);
2085
+ };
2086
+ }
2087
+
2088
+ //#endregion
2089
+ //#region src/miscellaneous/flagsFor.ts
2090
+ /** @internal */
2091
+ function flagsFor(ephemeral) {
2092
+ let flags = discord_api_types_v10.MessageFlags.IsComponentsV2;
2093
+ if (ephemeral) flags |= discord_api_types_v10.MessageFlags.Ephemeral;
2094
+ return flags;
2095
+ }
2096
+
2097
+ //#endregion
2098
+ //#region src/subscribers/bases/webhookHelpers.ts
2099
+ const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
2100
+ function isDiscordWebhookUrl(value) {
2101
+ return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
2102
+ }
2103
+ function jsonAttachment(name, description, data) {
2104
+ const content = (0, _seedcord_utils.filterCirculars)(data);
2105
+ return {
2106
+ name,
2107
+ description,
2108
+ data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2109
+ };
2110
+ }
2111
+ function neutralizeFences(text) {
2112
+ return text.replaceAll("```", "`​`​`");
2113
+ }
2114
+ /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2115
+ function errorReport(error) {
2116
+ let report = error.stack ?? `${error.name}: ${error.message}`;
2117
+ if (Error.isError(error.cause)) {
2118
+ const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2119
+ report += `\n\nCaused by:\n${cause}`;
2120
+ }
2121
+ const stripped = neutralizeFences((0, _seedcord_utils.stripAnsi)(report));
2122
+ return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2123
+ }
2124
+ var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
2125
+ constructor() {
2126
+ super("separator");
2127
+ this.instance.setSpacing(discord_api_types_v10.SeparatorSpacingSize.Small).setDivider(true);
2178
2128
  }
2179
2129
  };
2180
2130
 
2181
2131
  //#endregion
2182
- //#region src/hmr/HmrModuleHandler.ts
2132
+ //#region src/subscribers/bases/WebhookSender.ts
2133
+ const HTTP_NOT_FOUND = 404;
2134
+ const HTTP_UNAUTHORIZED = 401;
2183
2135
  /**
2184
- * Reloads THandler and optional TMiddleware modules on HMR updates.
2185
- *
2186
- * You can either implement a custom `onHmr()` method in your class by extending `HmrAware`, or if it's a plugin, override the existing `onHmr()` with fully custom logic. Or, use this class's `handle()` method inside your `onHmr()` to get standard HMR handling for your modules based on the provided options you give to the constructor.
2136
+ * Sends component messages to a Discord webhook through `@discordjs/rest`.
2187
2137
  *
2188
- * This is only useful during development.
2138
+ * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2189
2139
  */
2190
- var HmrModuleHandler = class {
2191
- store;
2192
- options;
2193
- constructor(options) {
2194
- this.options = {
2195
- ...options,
2196
- logger: options.logger.inChannel("hmr")
2197
- };
2198
- this.store = {
2199
- fileToHandlers: /* @__PURE__ */ new Map(),
2200
- fileToMiddlewares: /* @__PURE__ */ new Map(),
2201
- handlerArtifacts: /* @__PURE__ */ new Map()
2202
- };
2140
+ var WebhookSender = class {
2141
+ rest = new _discordjs_rest.REST();
2142
+ route;
2143
+ constructor(url) {
2144
+ const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2145
+ if (!match?.[1] || !match[2]) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2146
+ this.route = discord_api_types_v10.Routes.webhook(match[1], match[2]);
2203
2147
  }
2204
2148
  /**
2205
- * Handles an HMR update event by reloading affected modules based on the event details and config. It receives an {@link HmrUpdateEvent}, then checks the type of update.
2206
- *
2207
- * - If it's a deletion, it unloads the module.
2208
- * - If it's an update/creation, it checks if the file is tracked, unloads it if so, then reloads the file if it is in the handlers or middlewares directory and is a valid handler/middleware.
2149
+ * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2150
+ * Discord and `unreachable` on any other failure.
2209
2151
  */
2210
- async handle(event) {
2211
- const { file, affectedModules, type } = event;
2212
- const { logger, handlersDir, middlewaresDir } = this.options;
2213
- if (type === "delete" || type === "deleteDir") {
2214
- this.unload(file);
2215
- return;
2216
- }
2217
- if (type === "update" && !(0, node_fs.existsSync)(file)) {
2218
- if (this.isTracked(file)) {
2219
- logger.info(`Received update for non-existent file: ${(0, _seedcord_utils.formatFilePath)(file)}, treating as delete`);
2220
- this.unload(file);
2221
- }
2222
- return;
2223
- }
2224
- const rollbackEnabled = event.rollback ?? true;
2225
- const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
2226
- const absHandlersDir = (0, node_path.resolve)(process.cwd(), handlersDir);
2227
- const absMiddlewaresDir = middlewaresDir ? (0, node_path.resolve)(process.cwd(), middlewaresDir) : null;
2228
- for (const fileToReload of filesToReload) {
2229
- const isHandler = fileToReload.startsWith(absHandlersDir);
2230
- const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir) : false;
2231
- if (!isHandler && !isMiddleware) continue;
2232
- await this.reloadWithRollback(fileToReload, rollbackEnabled);
2233
- }
2234
- }
2235
- async reloadWithRollback(file, rollbackEnabled) {
2236
- const { logger } = this.options;
2237
- const snapshot = this.snapshotUnits(file);
2238
- this.unload(file);
2239
- if (await this.reloadFile(file)) return;
2240
- this.unload(file);
2241
- if (!rollbackEnabled) return;
2242
- this.restoreUnits(file, snapshot);
2243
- logger.warn(`${chalk.default.yellow.bold("Rolled back")} ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))} to the last-good version`);
2244
- }
2245
- /**
2246
- * Tracks a handler module as being loaded from a specific file.
2247
- * @param file - The file path the handler was loaded from.
2248
- * @param handler - The handler module instance.
2249
- */
2250
- trackHandler(file, handler) {
2251
- let set = this.store.fileToHandlers.get(file);
2252
- if (!set) {
2253
- set = /* @__PURE__ */ new Set();
2254
- this.store.fileToHandlers.set(file, set);
2255
- }
2256
- set.add(handler);
2257
- if (this.options.getArtifacts) {
2258
- const artifacts = this.options.getArtifacts(handler);
2259
- this.store.handlerArtifacts.set(handler, artifacts);
2152
+ async verify() {
2153
+ try {
2154
+ await this.rest.get(this.route, { auth: false });
2155
+ return "ok";
2156
+ } catch (error) {
2157
+ return error instanceof _discordjs_rest.DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2260
2158
  }
2261
2159
  }
2262
- /**
2263
- * Tracks a middleware module as being loaded from a specific file.
2264
- * @param file - The file path the middleware was loaded from.
2265
- * @param middleware - The middleware module instance.
2266
- */
2267
- trackMiddleware(file, middleware) {
2268
- let set = this.store.fileToMiddlewares.get(file);
2269
- if (!set) {
2270
- set = /* @__PURE__ */ new Set();
2271
- this.store.fileToMiddlewares.set(file, set);
2272
- }
2273
- set.add(middleware);
2274
- }
2275
- isTracked(file) {
2276
- return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
2277
- }
2278
- unload(file) {
2279
- const { logger } = this.options;
2280
- const handlers = this.store.fileToHandlers.get(file);
2281
- if (handlers) {
2282
- for (const handler of handlers) {
2283
- const artifacts = this.store.handlerArtifacts.get(handler);
2284
- this.options.unregisterHandler(handler, artifacts);
2285
- this.store.handlerArtifacts.delete(handler);
2286
- const name = handler.name ?? "Handler";
2287
- logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
2288
- }
2289
- this.store.fileToHandlers.delete(file);
2290
- }
2291
- const middlewares = this.store.fileToMiddlewares.get(file);
2292
- if (middlewares) {
2293
- for (const middleware of middlewares) {
2294
- this.options.unregisterMiddleware?.(middleware);
2295
- const name = middleware.name ?? "Middleware";
2296
- logger.info(`${chalk.default.red.bold("Unloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
2297
- }
2298
- this.store.fileToMiddlewares.delete(file);
2299
- }
2160
+ async send(options) {
2161
+ const { flags, username, components, files } = options;
2162
+ await this.rest.post(this.route, {
2163
+ auth: false,
2164
+ query: new URLSearchParams({
2165
+ with_components: "true",
2166
+ wait: "true"
2167
+ }),
2168
+ body: {
2169
+ flags,
2170
+ username,
2171
+ components: components.map((component) => component.toJSON()),
2172
+ ...files?.length && { attachments: files.map((file, id) => ({
2173
+ id,
2174
+ filename: file.name,
2175
+ description: file.description
2176
+ })) }
2177
+ },
2178
+ ...files?.length && { files: files.map((file) => ({
2179
+ name: file.name,
2180
+ data: file.data
2181
+ })) }
2182
+ });
2300
2183
  }
2301
- async reloadFile(file) {
2302
- const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
2303
- if (!(0, node_fs.existsSync)(file)) {
2304
- logger.info(`File does not exist, skipping reload: ${(0, _seedcord_utils.formatFilePath)(file)}`);
2305
- return false;
2184
+ };
2185
+
2186
+ //#endregion
2187
+ //#region src/subscribers/bases/WebhookLog.ts
2188
+ /**
2189
+ * Base class for subscribers that deliver their event to a Discord webhook.
2190
+ *
2191
+ * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2192
+ * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2193
+ * the reporter is skipped at registration with a boot warning.
2194
+ *
2195
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2196
+ */
2197
+ var WebhookLog = class WebhookLog extends Subscriber {
2198
+ static senders = /* @__PURE__ */ new Map();
2199
+ async execute() {
2200
+ const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2201
+ if (url === null) {
2202
+ this.logger.warn(`${chalk.default.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2203
+ return;
2306
2204
  }
2205
+ const { username, components, files } = await this.report();
2307
2206
  try {
2308
- let fileUrl = (0, node_url.pathToFileURL)(file).href;
2309
- if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
2310
- const imported = await import(fileUrl);
2311
- for (const val of Object.values(imported)) if (isHandler(val)) {
2312
- registerHandler(val, file);
2313
- this.trackHandler(file, val);
2314
- const name = val.name ?? "Handler";
2315
- logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
2316
- } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
2317
- registerMiddleware(val, file);
2318
- this.trackMiddleware(file, val);
2319
- const name = val.name ?? "Middleware";
2320
- logger.info(`${chalk.default.blue.bold("Reloaded")} ${chalk.default.cyan.bold(name)} from ${chalk.default.gray((0, _seedcord_utils.formatFilePath)(file))}`);
2321
- }
2322
- return true;
2207
+ await WebhookLog.senderFor(url).send({
2208
+ flags: flagsFor(false),
2209
+ username: username ?? this.constructor.name,
2210
+ components,
2211
+ files
2212
+ });
2323
2213
  } catch (error) {
2324
- logger.error(`Failed to reload file: ${file}`, error);
2325
- return false;
2214
+ this.logger.error("Failed to send webhook report", error);
2326
2215
  }
2327
2216
  }
2328
- snapshotUnits(file) {
2329
- return {
2330
- handlers: [...this.store.fileToHandlers.get(file) ?? []],
2331
- middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
2332
- };
2333
- }
2334
- restoreUnits(file, snapshot) {
2335
- for (const handler of snapshot.handlers) {
2336
- this.options.registerHandler(handler, file);
2337
- this.trackHandler(file, handler);
2338
- }
2339
- if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
2340
- this.options.registerMiddleware(middleware, file);
2341
- this.trackMiddleware(file, middleware);
2217
+ /** @internal */
2218
+ static senderFor(url) {
2219
+ let sender = WebhookLog.senders.get(url);
2220
+ if (!sender) {
2221
+ sender = new WebhookSender(url);
2222
+ WebhookLog.senders.set(url, sender);
2342
2223
  }
2224
+ return sender;
2225
+ }
2226
+ /** @internal */
2227
+ static envKeyOf(ctor) {
2228
+ const envKey = Reflect.getMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, ctor);
2229
+ if (!envKey) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2230
+ return envKey;
2231
+ }
2232
+ /** @internal Returns null when the env var is unset, throws when set but malformed. */
2233
+ static urlOf(envKey) {
2234
+ if (!envapt.Envapter.has(envKey)) return null;
2235
+ const raw = envapt.Envapter.getRequired(envKey, envapt.Converters.String).trim();
2236
+ if (raw === "") return null;
2237
+ if (!isDiscordWebhookUrl(raw)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2238
+ return raw;
2343
2239
  }
2344
2240
  };
2345
2241
 
@@ -2398,11 +2294,13 @@ var CommandRegistry = class {
2398
2294
  core;
2399
2295
  onDeployed;
2400
2296
  name = "Commands";
2401
- logger = new _seedcord_services.Logger("Commands");
2297
+ logger = new _seedcord_logger.Logger("Commands");
2402
2298
  isInitialised = false;
2403
2299
  globalCommands = [];
2404
2300
  guildCommands = new discord_js.Collection();
2405
2301
  ctorToCommand = /* @__PURE__ */ new Map();
2302
+ loading = false;
2303
+ loadedCommands = [];
2406
2304
  hmrHandler;
2407
2305
  pendingEvents = /* @__PURE__ */ new Map();
2408
2306
  constructor(core, onDeployed) {
@@ -2411,7 +2309,7 @@ var CommandRegistry = class {
2411
2309
  const commandsDir = this.core.config.bot.commands.path;
2412
2310
  if (!commandsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
2413
2311
  if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
2414
- this.hmrHandler = new HmrModuleHandler({
2312
+ this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
2415
2313
  handlersDir: commandsDir,
2416
2314
  isHandler: this.isCommandClass.bind(this),
2417
2315
  registerHandler: this.registerCommand.bind(this),
@@ -2429,15 +2327,32 @@ var CommandRegistry = class {
2429
2327
  this.isInitialised = true;
2430
2328
  const commandsDir = this.core.config.bot.commands.path;
2431
2329
  if (!commandsDir) return;
2432
- this.logger.info(chalk.default.bold(commandsDir));
2433
- await this.loadCommands(commandsDir);
2434
- this.logger.utils.summary("Loaded commands", {
2330
+ this.loading = true;
2331
+ this.loadedCommands.length = 0;
2332
+ try {
2333
+ await this.loadCommands(commandsDir);
2334
+ } finally {
2335
+ this.loading = false;
2336
+ }
2337
+ this.reportLoad();
2338
+ (0, _seedcord_core_internal.getDevChannel)()?.on("seedcord:refresh-commands", (data) => {
2339
+ this.refresh(data.shouldRefresh);
2340
+ });
2341
+ }
2342
+ reportLoad() {
2343
+ const { utils } = this.logger;
2344
+ utils.summary("Loaded commands", {
2435
2345
  global: this.globalCommands.length,
2436
2346
  "guild groups": this.guildCommands.size
2437
2347
  });
2438
- getDevChannel()?.on("seedcord:refresh-commands", (data) => {
2439
- this.refresh(data.shouldRefresh);
2440
- });
2348
+ let slash = 0;
2349
+ let menu = 0;
2350
+ for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
2351
+ else menu++;
2352
+ utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
2353
+ "slash commands": slash,
2354
+ "context menus": menu
2355
+ })]);
2441
2356
  }
2442
2357
  /** @internal */
2443
2358
  async refresh(shouldRefresh = true) {
@@ -2457,11 +2372,11 @@ var CommandRegistry = class {
2457
2372
  if (commandsDir && event.file.startsWith((0, node_path.resolve)(process.cwd(), commandsDir))) {
2458
2373
  this.pendingEvents.delete(event.file);
2459
2374
  this.pendingEvents.set(event.file, event);
2460
- getDevChannel()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => (0, _seedcord_utils.formatFilePath)(f)) });
2375
+ (0, _seedcord_core_internal.getDevChannel)()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => (0, _seedcord_utils.formatFilePath)(f)) });
2461
2376
  } else await this.hmrHandler?.handle(event);
2462
2377
  }
2463
2378
  async loadCommands(dir) {
2464
- await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, rel, mod) => {
2379
+ await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, rel, mod) => {
2465
2380
  for (const exported of Object.values(mod)) if (this.isCommandClass(exported)) {
2466
2381
  this.registerCommand(exported, rel);
2467
2382
  this.hmrHandler?.trackHandler(fullPath, exported);
@@ -2476,25 +2391,27 @@ var CommandRegistry = class {
2476
2391
  const meta = Reflect.getMetadata(_seedcord_core_internal.CommandMetadataKey, ctor);
2477
2392
  if (!meta) return;
2478
2393
  const comp = new ctor().component;
2479
- const commandType = comp instanceof _discordjs_builders.SlashCommandBuilder ? "slash command" : "context menu command";
2480
- if (meta.scope === "global") {
2481
- this.globalCommands.push(comp);
2482
- this.logger.utils.registration(ctor.name, rel);
2483
- this.logger.utils.item(`Global ${commandType}: ${chalk.default.bold.cyan(comp.name)}`);
2484
- } else {
2485
- for (const g of meta.guilds) {
2486
- const arr = this.guildCommands.get(g) ?? [];
2487
- arr.push(comp);
2488
- this.guildCommands.set(g, arr);
2489
- }
2490
- this.logger.utils.registration(ctor.name, rel);
2491
- this.logger.utils.item(`Guild ${commandType}: ${chalk.default.bold.cyan(comp.name)} for ${chalk.default.magenta.bold(meta.guilds.length)} guild(s)`);
2394
+ const kind = comp instanceof _discordjs_builders.SlashCommandBuilder ? "slash command" : "context menu";
2395
+ if (meta.scope === "global") this.globalCommands.push(comp);
2396
+ else for (const g of meta.guilds) {
2397
+ const arr = this.guildCommands.get(g) ?? [];
2398
+ arr.push(comp);
2399
+ this.guildCommands.set(g, arr);
2492
2400
  }
2493
2401
  this.ctorToCommand.set(ctor, {
2494
2402
  name: comp.name,
2495
2403
  scope: meta.scope,
2496
2404
  ...meta.scope === "guild" && { guilds: meta.guilds }
2497
2405
  });
2406
+ if (this.loading) {
2407
+ this.loadedCommands.push({
2408
+ name: comp.name,
2409
+ from: (0, _seedcord_utils.formatFilePath)(rel),
2410
+ kind
2411
+ });
2412
+ return;
2413
+ }
2414
+ this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
2498
2415
  }
2499
2416
  unregisterCommand(ctor, artifacts) {
2500
2417
  const info = artifacts ?? this.ctorToCommand.get(ctor);
@@ -2521,8 +2438,7 @@ var CommandRegistry = class {
2521
2438
  const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
2522
2439
  if (deployed) result.global = deployed;
2523
2440
  const tag = this.globalCommands.length === 1 ? "command" : "commands";
2524
- this.logger.utils.summary("Configured global", { [tag]: this.globalCommands.length });
2525
- this.logger.utils.item(`${this.globalCommands.map((command) => chalk.default.bold.cyan(command.name)).join(", ")}`);
2441
+ this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
2526
2442
  }
2527
2443
  for (const [guildId, commands] of this.guildCommands.entries()) {
2528
2444
  const guild = this.core.bot.client.guilds.cache.get(guildId);
@@ -2533,269 +2449,101 @@ var CommandRegistry = class {
2533
2449
  const deployed = await guild.commands.set(commands);
2534
2450
  result.guilds.set(guildId, deployed);
2535
2451
  const tag = commands.length === 1 ? "command" : "commands";
2536
- this.logger.utils.summary(`Configured commands for ${chalk.default.bold.yellow(guild.name)}`, { [tag]: commands.length });
2537
- this.logger.utils.item(`${commands.map((command) => chalk.default.bold.cyan(command.name)).join(", ")}`);
2538
- }
2539
- this.onDeployed?.(result);
2540
- return result;
2541
- }
2542
- /** The deduplicated slash route keys across every global and guild command. @internal */
2543
- routeLeaves() {
2544
- return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2545
- }
2546
- /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2547
- contextMenuLeaves() {
2548
- return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2549
- }
2550
- };
2551
-
2552
- //#endregion
2553
- //#region src/miscellaneous/deriveEventActor.ts
2554
- function deriveEventActor(args) {
2555
- const tuple = Array.isArray(args) ? args : [args];
2556
- for (const arg of tuple) {
2557
- if (arg instanceof discord_js.Message) return {
2558
- guild: arg.guild,
2559
- member: arg.member,
2560
- user: arg.author,
2561
- channelId: arg.channelId
2562
- };
2563
- if (arg instanceof discord_js.GuildMember) return {
2564
- guild: arg.guild,
2565
- member: arg,
2566
- user: arg.user,
2567
- channelId: null
2568
- };
2569
- if (arg instanceof discord_js.User) return {
2570
- guild: null,
2571
- member: null,
2572
- user: arg,
2573
- channelId: null
2574
- };
2575
- }
2576
- return {
2577
- guild: null,
2578
- member: null,
2579
- user: null,
2580
- channelId: null
2581
- };
2582
- }
2583
-
2584
- //#endregion
2585
- //#region src/bot/gates/runGates.ts
2586
- function interactionGateContext(interaction, core) {
2587
- const rawMember = interaction.member;
2588
- const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2589
- return {
2590
- kind: "interaction",
2591
- interaction,
2592
- core,
2593
- user: interaction.user,
2594
- guild: interaction.guild,
2595
- member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
2596
- userId: interaction.user.id,
2597
- guildId: interaction.guildId,
2598
- channelId: interaction.channelId,
2599
- memberRoleIds,
2600
- memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2601
- routeId: null
2602
- };
2603
- }
2604
- function eventGateContext(eventName, args, core) {
2605
- const actor = deriveEventActor(args);
2606
- return {
2607
- kind: "event",
2608
- core,
2609
- eventName,
2610
- payload: args,
2611
- user: actor.user,
2612
- guild: actor.guild,
2613
- member: actor.member,
2614
- userId: actor.user?.id ?? null,
2615
- guildId: actor.guild?.id ?? null,
2616
- channelId: actor.channelId,
2617
- memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2618
- memberPermissions: actor.member?.permissions.bitfield ?? null,
2619
- routeId: null
2620
- };
2621
- }
2622
-
2623
- //#endregion
2624
- //#region src/miscellaneous/FaultThrottle.ts
2625
- /**
2626
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
2627
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
2628
- * the map stays small, and a stale entry is evicted on the next read of its key.
2629
- *
2630
- * @internal
2631
- */
2632
- var FaultThrottle = class {
2633
- windowMs;
2634
- now;
2635
- lastReportedAt = /* @__PURE__ */ new Map();
2636
- constructor(windowMs = 6e4, now = () => Date.now()) {
2637
- this.windowMs = windowMs;
2638
- this.now = now;
2639
- }
2640
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
2641
- shouldReport(key) {
2642
- const last = this.lastReportedAt.get(key);
2643
- if (last === void 0) return true;
2644
- if (this.now() - last < this.windowMs) return false;
2645
- this.lastReportedAt.delete(key);
2646
- return true;
2647
- }
2648
- markReported(key) {
2649
- this.lastReportedAt.set(key, this.now());
2650
- }
2651
- clear() {
2652
- this.lastReportedAt.clear();
2653
- }
2654
- };
2655
-
2656
- //#endregion
2657
- //#region src/miscellaneous/extractErrorResponse.ts
2658
- const logger$2 = new _seedcord_services.Logger("ErrorsHandling");
2659
- /**
2660
- * The single throttle for every fault publish, so a recurring fault reports once per window across both
2661
- * the interaction and event paths. Exported so tests reset it between cases.
2662
- *
2663
- * @internal
2664
- */
2665
- const faultThrottle = new FaultThrottle();
2666
- /**
2667
- * Processes an error into a user-facing reply and routes its report.
2668
- *
2669
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
2670
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
2671
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
2672
- * threaded into both the reply and the bus payload.
2673
- *
2674
- * @internal
2675
- */
2676
- function extractErrorResponse(error, core, origin) {
2677
- const uuid = node_crypto.randomUUID();
2678
- const developerUsername = core.config.notifications?.developerUsername;
2679
- const ctx = developerUsername === void 0 ? { uuid } : {
2680
- uuid,
2681
- developerUsername
2682
- };
2683
- if (error instanceof _seedcord_core.Notice) {
2684
- if (error.report) reportFault(error, core, origin, uuid);
2685
- return {
2686
- uuid,
2687
- response: error.render(ctx)
2688
- };
2689
- }
2690
- reportRawFault(error, core, origin, uuid);
2691
- const override = core.config.errors?.defaultError;
2692
- return {
2693
- uuid,
2694
- response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
2695
- };
2696
- }
2697
- function withThrottle(origin, error, publish) {
2698
- const key = faultKey(origin, error);
2699
- if (!faultThrottle.shouldReport(key)) {
2700
- logger$2.info(`throttled duplicate fault ${key}`);
2701
- return;
2702
- }
2703
- publish();
2704
- faultThrottle.markReported(key);
2705
- }
2706
- function reportFault(denial, core, origin, uuid) {
2707
- withThrottle(origin, denial, () => {
2708
- logger$2.error(`${denial.name}: ${uuid}`, denial);
2709
- if (origin.interaction) core.bus.publish("handledException", {
2710
- denial,
2711
- uuid,
2712
- source: buildInteractionSource(origin.interaction)
2713
- });
2714
- else if (origin.event) core.bus.publish("handledException", {
2715
- denial,
2716
- uuid,
2717
- source: buildEventSource(origin.event, origin)
2718
- });
2719
- else core.bus.publish("unknownException", {
2720
- uuid,
2721
- error: denial,
2722
- guild: origin.guild,
2723
- user: origin.user,
2724
- metadata: metadataFor(origin)
2725
- });
2726
- });
2727
- }
2728
- function reportRawFault(error, core, origin, uuid) {
2729
- withThrottle(origin, error, () => {
2730
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
2731
- else logger$2.error(`${uuid} | ${error.message}`);
2732
- core.bus.publish("unknownException", {
2733
- uuid,
2734
- error,
2735
- guild: origin.guild,
2736
- user: origin.user,
2737
- metadata: metadataFor(origin)
2738
- });
2739
- });
2740
- }
2741
- function metadataFor(origin) {
2742
- if (origin.event) return {
2743
- eventName: origin.event.name,
2744
- handler: origin.event.handler,
2745
- args: origin.event.args
2746
- };
2747
- return origin.metadata;
2748
- }
2749
- function buildEventSource(event, origin) {
2452
+ 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");
2453
+ }
2454
+ this.onDeployed?.(result);
2455
+ return result;
2456
+ }
2457
+ /** The deduplicated slash route keys across every global and guild command. @internal */
2458
+ routeLeaves() {
2459
+ return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2460
+ }
2461
+ /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2462
+ contextMenuLeaves() {
2463
+ return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2464
+ }
2465
+ };
2466
+
2467
+ //#endregion
2468
+ //#region src/miscellaneous/deriveEventActor.ts
2469
+ function deriveEventActor(args) {
2470
+ const tuple = Array.isArray(args) ? args : [args];
2471
+ for (const arg of tuple) {
2472
+ if (arg instanceof discord_js.Message) return {
2473
+ guild: arg.guild,
2474
+ member: arg.member,
2475
+ user: arg.author,
2476
+ channelId: arg.channelId
2477
+ };
2478
+ if (arg instanceof discord_js.GuildMember) return {
2479
+ guild: arg.guild,
2480
+ member: arg,
2481
+ user: arg.user,
2482
+ channelId: null
2483
+ };
2484
+ if (arg instanceof discord_js.User) return {
2485
+ guild: null,
2486
+ member: null,
2487
+ user: arg,
2488
+ channelId: null
2489
+ };
2490
+ }
2750
2491
  return {
2751
- kind: "event",
2752
- eventName: event.name,
2753
- handler: event.handler,
2754
- userId: origin.user?.id ?? null,
2755
- guildId: origin.guild?.id ?? null,
2756
- channelId: event.channelId,
2757
- raw: event.args
2492
+ guild: null,
2493
+ member: null,
2494
+ user: null,
2495
+ channelId: null
2758
2496
  };
2759
2497
  }
2760
- function faultKey(origin, error) {
2761
- const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
2762
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
2763
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
2764
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
2765
- return `autocomplete:${name}`;
2766
- }
2767
- function interactionRoute(interaction) {
2768
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
2769
- if (interaction.isContextMenuCommand()) return interaction.commandName;
2770
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
2771
- return "interaction";
2772
- }
2773
- function buildInteractionSource(interaction) {
2774
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
2775
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
2498
+
2499
+ //#endregion
2500
+ //#region src/bot/gates/runGates.ts
2501
+ function interactionGateContext(interaction, core) {
2502
+ const rawMember = interaction.member;
2503
+ const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2776
2504
  return {
2777
2505
  kind: "interaction",
2778
- interactionKind: interactionKind(interaction),
2779
- command,
2780
- customId,
2506
+ interaction,
2507
+ core,
2508
+ user: interaction.user,
2509
+ guild: interaction.guild,
2510
+ member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
2781
2511
  userId: interaction.user.id,
2782
2512
  guildId: interaction.guildId,
2783
2513
  channelId: interaction.channelId,
2784
- interactionId: interaction.id,
2785
- raw: interaction
2514
+ memberRoleIds,
2515
+ memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2516
+ appPermissions: interaction.appPermissions.bitfield,
2517
+ memberGuildPermissions: rawMember instanceof discord_js.GuildMember ? rawMember.permissions.bitfield : null,
2518
+ appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
2519
+ routeId: null
2786
2520
  };
2787
2521
  }
2788
- function interactionKind(interaction) {
2789
- if (interaction.isChatInputCommand()) return "slash";
2790
- if (interaction.isContextMenuCommand()) return "context-menu";
2791
- if (interaction.isButton()) return "button";
2792
- if (interaction.isAnySelectMenu()) return "select";
2793
- return "modal";
2522
+ function eventGateContext(eventName, args, core) {
2523
+ const actor = deriveEventActor(args);
2524
+ return {
2525
+ kind: "event",
2526
+ core,
2527
+ eventName,
2528
+ payload: args,
2529
+ user: actor.user,
2530
+ guild: actor.guild,
2531
+ member: actor.member,
2532
+ userId: actor.user?.id ?? null,
2533
+ guildId: actor.guild?.id ?? null,
2534
+ channelId: actor.channelId,
2535
+ memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2536
+ memberPermissions: actor.member?.permissions.bitfield ?? null,
2537
+ appPermissions: null,
2538
+ memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
2539
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
2540
+ routeId: null
2541
+ };
2794
2542
  }
2795
2543
 
2796
2544
  //#endregion
2797
2545
  //#region src/bot/handleEventFault.ts
2798
- const logger$1 = new _seedcord_services.Logger("EventBoundary");
2546
+ const logger$1 = new _seedcord_logger.Logger("EventBoundary");
2799
2547
  /**
2800
2548
  * The event controller boundary. Reports a throw from an event handler and never replies, since a
2801
2549
  * generic event has no reply target. A {@link Silence} or a non-reporting {@link Notice} stops quietly, a
@@ -2805,7 +2553,7 @@ const logger$1 = new _seedcord_services.Logger("EventBoundary");
2805
2553
  */
2806
2554
  function handleEventFault(caught, eventName, handlerName, args, core) {
2807
2555
  if (caught instanceof _seedcord_core.Silence) {
2808
- if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
2556
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2809
2557
  return;
2810
2558
  }
2811
2559
  if (!Error.isError(caught)) throw caught;
@@ -2839,20 +2587,23 @@ function handleEventFault(caught, eventName, handlerName, args, core) {
2839
2587
  */
2840
2588
  var EventDispatcher = class {
2841
2589
  core;
2842
- logger = new _seedcord_services.Logger("Events");
2590
+ logger = new _seedcord_logger.Logger("Events");
2843
2591
  isInitialized = false;
2844
2592
  name = "Events";
2845
2593
  eventMap = /* @__PURE__ */ new Map();
2846
2594
  middlewares = [];
2847
2595
  executedOnceHandlers = /* @__PURE__ */ new Set();
2848
2596
  attachedEvents = /* @__PURE__ */ new Set();
2597
+ loading = false;
2598
+ loadedHandlers = [];
2599
+ loadedMiddlewares = [];
2849
2600
  hmrHandler;
2850
2601
  constructor(core) {
2851
2602
  this.core = core;
2852
2603
  const eventsDir = this.core.config.bot.events.path;
2853
2604
  if (!eventsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["EventDispatcher", "events"]);
2854
2605
  if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
2855
- this.hmrHandler = new HmrModuleHandler({
2606
+ this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
2856
2607
  handlersDir: eventsDir,
2857
2608
  ...(0, _seedcord_utils.hasKeys)(this.core.config.bot.events, ["middlewares"]) && { middlewaresDir: this.core.config.bot.events.middlewares },
2858
2609
  isHandler: this.isEventHandlerClass.bind(this),
@@ -2875,23 +2626,32 @@ var EventDispatcher = class {
2875
2626
  this.isInitialized = true;
2876
2627
  const handlersDir = this.core.config.bot.events.path;
2877
2628
  if (!handlersDir) return;
2878
- this.logger.info(chalk.default.bold(handlersDir));
2879
2629
  const middlewareDir = (0, _seedcord_utils.hasKeys)(this.core.config.bot.events, ["middlewares"]) ? this.core.config.bot.events.middlewares : void 0;
2880
- if (middlewareDir) {
2881
- this.logger.info(`${chalk.default.bold(middlewareDir)} ${chalk.default.gray("(middlewares)")}`);
2882
- await this.loadMiddlewares(middlewareDir);
2630
+ this.loading = true;
2631
+ this.loadedHandlers.length = 0;
2632
+ this.loadedMiddlewares.length = 0;
2633
+ try {
2634
+ if (middlewareDir) await this.loadMiddlewares(middlewareDir);
2635
+ await this.loadHandlers(handlersDir);
2636
+ } finally {
2637
+ this.loading = false;
2883
2638
  }
2884
- await this.loadHandlers(handlersDir);
2885
2639
  this.attachToClient();
2886
- this.logger.info(`${chalk.default.bold.green("Loaded event handlers:")}`);
2887
- const eventList = [`${chalk.default.magenta.bold(this.middlewares.length)} middlewares`];
2888
- this.eventMap.forEach((handlers, eventName) => {
2889
- eventList.push(`${chalk.default.magenta.bold(handlers.length)} ${eventName}`);
2640
+ this.reportLoad();
2641
+ }
2642
+ reportLoad() {
2643
+ const { utils } = this.logger;
2644
+ utils.summary("Loaded", {
2645
+ "event handlers": this.loadedHandlers.length,
2646
+ middlewares: this.loadedMiddlewares.length
2890
2647
  });
2891
- this.logger.utils.list(eventList);
2648
+ if (this.loadedMiddlewares.length > 0) utils.block("Loaded event middlewares", utils.entries(this.loadedMiddlewares));
2649
+ const perEvent = {};
2650
+ for (const [event, handlers] of this.eventMap) perEvent[event] = handlers.length;
2651
+ utils.block("Loaded event handlers", [...utils.entries(this.loadedHandlers), ...utils.counts(perEvent)]);
2892
2652
  }
2893
2653
  async loadHandlers(dir) {
2894
- await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
2654
+ await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
2895
2655
  for (const val of Object.values(imported)) {
2896
2656
  if (!this.isEventHandlerClass(val)) continue;
2897
2657
  this.registerHandler(val, relativePath);
@@ -2900,7 +2660,7 @@ var EventDispatcher = class {
2900
2660
  }, this.logger);
2901
2661
  }
2902
2662
  async loadMiddlewares(dir) {
2903
- await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
2663
+ await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
2904
2664
  for (const val of Object.values(imported)) {
2905
2665
  if (!this.isMiddlewareClass(val)) continue;
2906
2666
  this.registerMiddleware(val, relativePath);
@@ -2938,7 +2698,14 @@ var EventDispatcher = class {
2938
2698
  ...metadata.events && { events: metadata.events }
2939
2699
  });
2940
2700
  this.middlewares.sort((a, b) => a.priority - b.priority);
2941
- this.logger.utils.registration(`${middlewareCtor.name} ${chalk.default.gray(`(${metadata.priority})`)}`, relativePath, "event middleware");
2701
+ if (this.loading) {
2702
+ this.loadedMiddlewares.push({
2703
+ name: `${middlewareCtor.name} (${metadata.priority})`,
2704
+ from: (0, _seedcord_utils.formatFilePath)(relativePath)
2705
+ });
2706
+ return;
2707
+ }
2708
+ this.logger.utils.registration(`${middlewareCtor.name} ${_seedcord_logger.paint.mute(`(${metadata.priority})`)}`, relativePath, "event middleware");
2942
2709
  }
2943
2710
  async runMiddlewares(eventName, args) {
2944
2711
  for (const { ctor, events } of this.middlewares) {
@@ -2976,10 +2743,15 @@ var EventDispatcher = class {
2976
2743
  };
2977
2744
  if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
2978
2745
  else {
2979
- const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
2746
+ const names = (0, _seedcord_core_internal.areRoutes)(raw) ? raw : typeof raw === "string" ? [raw] : [];
2980
2747
  for (const name of names) register(name, "on");
2981
2748
  }
2982
- this.logger.utils.registration(handlerClass.name, relativePath);
2749
+ const from = (0, _seedcord_utils.formatFilePath)(relativePath);
2750
+ if (this.loading) this.loadedHandlers.push({
2751
+ name: handlerClass.name,
2752
+ from
2753
+ });
2754
+ else this.logger.utils.registration(handlerClass.name, from);
2983
2755
  }
2984
2756
  attachToClient() {
2985
2757
  for (const [eventName] of this.eventMap) this.attachListener(eventName);
@@ -2988,12 +2760,12 @@ var EventDispatcher = class {
2988
2760
  if (this.attachedEvents.has(eventName)) return;
2989
2761
  this.attachedEvents.add(eventName);
2990
2762
  const handlerEntries = this.eventMap.get(eventName);
2991
- this.logger.debug(`Attaching ${chalk.default.bold.green(eventName)} to the client with ${chalk.default.gray(handlerEntries?.length ?? 0)} handler(s)`);
2763
+ this.logger.debug(`Attaching ${_seedcord_logger.paint.mint.bold(eventName)} to the client with ${_seedcord_logger.paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2992
2764
  this.core.bot.client.on(eventName, (...args) => {
2993
2765
  this.core.bot.emitSafe("any:event", eventName, ...args);
2994
2766
  (async () => {
2995
2767
  await this.processEvent(eventName, args).catch((err) => {
2996
- this.logger.error(`[${chalk.default.bold.red("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2768
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2997
2769
  this.core.bot.emit("error:unhandled:event", err);
2998
2770
  });
2999
2771
  })();
@@ -3015,7 +2787,7 @@ var EventDispatcher = class {
3015
2787
  }
3016
2788
  async processHandler(eventName, ctor, args) {
3017
2789
  try {
3018
- this.logger.debug(`Processing ${chalk.default.bold.green(eventName)} with ${chalk.default.gray(ctor.name)}`);
2790
+ this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(eventName)} with ${_seedcord_logger.paint.mute(ctor.name)}`);
3019
2791
  const handler = new ctor(args, this.core, eventName);
3020
2792
  await (0, _seedcord_core_internal.runHandlerGates)(ctor, eventGateContext(eventName, args, this.core));
3021
2793
  await handler.execute();
@@ -3026,35 +2798,55 @@ var EventDispatcher = class {
3026
2798
  };
3027
2799
 
3028
2800
  //#endregion
3029
- //#region src/bot/defaults/UnhandledEvent.ts
2801
+ //#region src/bot/defaults/UnhandledRepliable.ts
3030
2802
  /**
3031
- * Default handler for unhandled interactions. A reply failure here rides the controller boundary as a
3032
- * raw fault, so it needs no decorator of its own.
3033
- *
3034
- * @internal
2803
+ * @internal Default handler for unhandled repliable interactions.
3035
2804
  */
3036
- var UnhandledEvent = class extends InteractionHandler {
2805
+ var UnhandledRepliable = class extends InteractionHandler {
3037
2806
  async execute() {
3038
- await this.event.reply({
3039
- content: `Feature not implemented yet.`,
3040
- flags: discord_js.MessageFlags.Ephemeral
3041
- });
2807
+ await this.reply("Feature not implemented yet.");
2808
+ }
2809
+ };
2810
+
2811
+ //#endregion
2812
+ //#region src/bot/defaults/UnhandledAutocomplete.ts
2813
+ /**
2814
+ * @internal Default handler for an autocomplete with no registered handler.
2815
+ */
2816
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
2817
+ async execute() {
2818
+ await this.respond([]);
3042
2819
  }
3043
2820
  };
3044
2821
 
2822
+ //#endregion
2823
+ //#region src/bot/harmlessApiCodes.ts
2824
+ /**
2825
+ * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2826
+ * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2827
+ *
2828
+ * @internal
2829
+ */
2830
+ const HARMLESS_API_CODES = new Set([
2831
+ discord_js.RESTJSONErrorCodes.UnknownInteraction,
2832
+ discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2833
+ discord_js.RESTJSONErrorCodes.UnknownMessage
2834
+ ]);
2835
+
3045
2836
  //#endregion
3046
2837
  //#region src/bot/handleInteractionFault.ts
3047
- const logger = new _seedcord_services.Logger("InteractionBoundary");
2838
+ const logger = new _seedcord_logger.Logger("InteractionBoundary");
3048
2839
  /**
3049
2840
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
3050
- * user reply plus the right bus event, building a {@link ReplySender} lazily so it reads the live
3051
- * acknowledgement state at catch time.
2841
+ * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
2842
+ * sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
2843
+ * boundary builds one from the interaction flags.
3052
2844
  *
3053
2845
  * @internal
3054
2846
  */
3055
- async function handleInteractionFault(caught, interaction, core) {
2847
+ async function handleInteractionFault(caught, interaction, core, sender) {
3056
2848
  if (caught instanceof _seedcord_core.Silence) {
3057
- if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
2849
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
3058
2850
  return;
3059
2851
  }
3060
2852
  if (!Error.isError(caught)) throw caught;
@@ -3078,7 +2870,18 @@ async function handleInteractionFault(caught, interaction, core) {
3078
2870
  user: interaction.user,
3079
2871
  metadata: interaction
3080
2872
  });
3081
- await new ReplySender(interaction).send(response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2873
+ await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2874
+ }
2875
+ async function sendGuarded(sender, response, ephemeral) {
2876
+ try {
2877
+ await sender.send(response, { ephemeral });
2878
+ } catch (error) {
2879
+ if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2880
+ logger.debug(`reply send hit harmless code ${error.code}`);
2881
+ return;
2882
+ }
2883
+ logger.error("reply send failed", error);
2884
+ }
3082
2885
  }
3083
2886
 
3084
2887
  //#endregion
@@ -3091,7 +2894,7 @@ async function handleInteractionFault(caught, interaction, core) {
3091
2894
  */
3092
2895
  var InteractionDispatcher = class {
3093
2896
  core;
3094
- logger = new _seedcord_services.Logger("Interactions");
2897
+ logger = new _seedcord_logger.Logger("Interactions");
3095
2898
  isInitialized = false;
3096
2899
  name = "Interactions";
3097
2900
  slashMap = /* @__PURE__ */ new Map();
@@ -3107,6 +2910,9 @@ var InteractionDispatcher = class {
3107
2910
  autocompleteMap = /* @__PURE__ */ new Map();
3108
2911
  keysToIgnore = /* @__PURE__ */ new Set();
3109
2912
  middlewares = [];
2913
+ loading = false;
2914
+ loadedHandlers = [];
2915
+ loadedMiddlewares = [];
3110
2916
  hmrHandler;
3111
2917
  routeTypes = [
3112
2918
  [_seedcord_core_internal.InteractionRoutes.Slash, this.slashMap],
@@ -3129,7 +2935,7 @@ var InteractionDispatcher = class {
3129
2935
  const interactionsDir = this.core.config.bot.interactions.path;
3130
2936
  if (!interactionsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["InteractionDispatcher", "interactions"]);
3131
2937
  if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
3132
- this.hmrHandler = new HmrModuleHandler({
2938
+ this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
3133
2939
  handlersDir: interactionsDir,
3134
2940
  ...(0, _seedcord_utils.hasKeys)(this.core.config.bot.interactions, ["middlewares"]) && this.core.config.bot.interactions.middlewares && { middlewaresDir: this.core.config.bot.interactions.middlewares },
3135
2941
  isHandler: this.isHandlerClass.bind(this),
@@ -3144,28 +2950,28 @@ var InteractionDispatcher = class {
3144
2950
  }
3145
2951
  /**
3146
2952
  * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
3147
- * UnhandledEvent at runtime. A bot may route commands outside the registry, so this does not throw.
2953
+ * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
3148
2954
  *
3149
2955
  * @internal
3150
2956
  */
3151
2957
  warnUnhandledRoutes(commandLeaves) {
3152
- for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${chalk.default.bold.cyan(route)} has no registered ${chalk.default.bold("@SlashRoute")} handler.`);
2958
+ 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.`);
3153
2959
  }
3154
2960
  /**
3155
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
2961
+ * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
3156
2962
  * at runtime. Checked per kind since a user and a message command can share a name.
3157
2963
  *
3158
2964
  * @internal
3159
2965
  */
3160
2966
  warnUnhandledContextMenuRoutes(leaves) {
3161
- for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${chalk.default.bold.cyan(name)} has no registered ${chalk.default.bold("@ContextMenuRoute")} handler.`);
3162
- for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${chalk.default.bold.cyan(name)} has no registered ${chalk.default.bold("@ContextMenuRoute")} handler.`);
2967
+ 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.`);
2968
+ 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.`);
3163
2969
  }
3164
2970
  getArtifacts(handlerClass) {
3165
2971
  const artifacts = [];
3166
2972
  for (const [routeType] of this.routeTypes) {
3167
2973
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3168
- if (areRoutes(meta)) artifacts.push({
2974
+ if ((0, _seedcord_core_internal.areRoutes)(meta)) artifacts.push({
3169
2975
  routeType,
3170
2976
  routes: meta
3171
2977
  });
@@ -3177,32 +2983,42 @@ var InteractionDispatcher = class {
3177
2983
  this.isInitialized = true;
3178
2984
  const handlersDir = this.core.config.bot.interactions.path;
3179
2985
  if (!handlersDir) return;
3180
- this.logger.info(chalk.default.bold(handlersDir));
3181
2986
  const middlewareDir = (0, _seedcord_utils.hasKeys)(this.core.config.bot.interactions, ["middlewares"]) ? this.core.config.bot.interactions.middlewares : void 0;
3182
- if (middlewareDir) {
3183
- this.logger.info(`${chalk.default.bold(middlewareDir)} ${chalk.default.gray("(middlewares)")}`);
3184
- await this.loadMiddlewares(middlewareDir);
2987
+ this.loading = true;
2988
+ this.loadedHandlers.length = 0;
2989
+ this.loadedMiddlewares.length = 0;
2990
+ try {
2991
+ if (middlewareDir) await this.loadMiddlewares(middlewareDir);
2992
+ await this.loadHandlers(handlersDir);
2993
+ } finally {
2994
+ this.loading = false;
3185
2995
  }
3186
- await this.loadHandlers(handlersDir);
3187
2996
  this.attachToClient();
3188
- this.logger.info(`${chalk.default.bold.green("Loaded interaction handlers:")}`);
3189
- this.logger.utils.list([
3190
- `${chalk.default.magenta.bold(this.middlewares.length)} middlewares`,
3191
- `${chalk.default.magenta.bold(this.slashMap.size)} slash commands`,
3192
- `${chalk.default.magenta.bold(this.buttonMap.size)} buttons`,
3193
- `${chalk.default.magenta.bold(this.modalMap.size)} modals`,
3194
- `${chalk.default.magenta.bold(this.stringSelectMap.size)} string selects`,
3195
- `${chalk.default.magenta.bold(this.userSelectMap.size)} user selects`,
3196
- `${chalk.default.magenta.bold(this.roleSelectMap.size)} role selects`,
3197
- `${chalk.default.magenta.bold(this.channelSelectMap.size)} channel selects`,
3198
- `${chalk.default.magenta.bold(this.mentionableSelectMap.size)} mentionable selects`,
3199
- `${chalk.default.magenta.bold(this.messageContextMenuMap.size)} message context menus`,
3200
- `${chalk.default.magenta.bold(this.userContextMenuMap.size)} user context menus`,
3201
- `${chalk.default.magenta.bold(this.autocompleteMap.size)} autocomplete`
3202
- ]);
2997
+ this.reportLoad();
2998
+ }
2999
+ reportLoad() {
3000
+ const { utils } = this.logger;
3001
+ utils.summary("Loaded", {
3002
+ "interaction handlers": this.loadedHandlers.length,
3003
+ middlewares: this.loadedMiddlewares.length
3004
+ });
3005
+ if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares));
3006
+ utils.block("Loaded interaction handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({
3007
+ slash: this.slashMap.size,
3008
+ buttons: this.buttonMap.size,
3009
+ modals: this.modalMap.size,
3010
+ "string selects": this.stringSelectMap.size,
3011
+ "user selects": this.userSelectMap.size,
3012
+ "role selects": this.roleSelectMap.size,
3013
+ "channel selects": this.channelSelectMap.size,
3014
+ "mentionable selects": this.mentionableSelectMap.size,
3015
+ "message menus": this.messageContextMenuMap.size,
3016
+ "user menus": this.userContextMenuMap.size,
3017
+ autocomplete: this.autocompleteMap.size
3018
+ })]);
3203
3019
  }
3204
3020
  async loadHandlers(dir) {
3205
- await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3021
+ await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3206
3022
  for (const val of Object.values(imported)) {
3207
3023
  if (!this.isHandlerClass(val)) continue;
3208
3024
  this.registerHandler(val, relativePath);
@@ -3211,7 +3027,7 @@ var InteractionDispatcher = class {
3211
3027
  }, this.logger);
3212
3028
  }
3213
3029
  async loadMiddlewares(dir) {
3214
- await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3030
+ await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3215
3031
  for (const val of Object.values(imported)) {
3216
3032
  if (!this.isMiddlewareClass(val)) continue;
3217
3033
  this.registerMiddleware(val, relativePath);
@@ -3229,7 +3045,14 @@ var InteractionDispatcher = class {
3229
3045
  priority: metadata.priority
3230
3046
  });
3231
3047
  this.middlewares.sort((a, b) => a.priority - b.priority);
3232
- this.logger.utils.registration(`${middlewareCtor.name} ${chalk.default.gray(`(${metadata.priority})`)}`, relativePath, "middleware");
3048
+ if (this.loading) {
3049
+ this.loadedMiddlewares.push({
3050
+ name: `${middlewareCtor.name} (${metadata.priority})`,
3051
+ from: (0, _seedcord_utils.formatFilePath)(relativePath)
3052
+ });
3053
+ return;
3054
+ }
3055
+ this.logger.utils.registration(`${middlewareCtor.name} ${_seedcord_logger.paint.mute(`(${metadata.priority})`)}`, relativePath, "middleware");
3233
3056
  }
3234
3057
  isHandlerClass(obj) {
3235
3058
  if (typeof obj !== "function") return false;
@@ -3243,7 +3066,7 @@ var InteractionDispatcher = class {
3243
3066
  const writes = [];
3244
3067
  for (const [routeType, map] of this.routeTypes) {
3245
3068
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3246
- if (!areRoutes(meta)) continue;
3069
+ if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
3247
3070
  for (const route of meta) {
3248
3071
  const existing = map.get(route);
3249
3072
  if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
@@ -3253,9 +3076,15 @@ var InteractionDispatcher = class {
3253
3076
  ]);
3254
3077
  writes.push([map, route]);
3255
3078
  }
3256
- this.logger.utils.registration(handlerClass.name, (0, _seedcord_utils.formatFilePath)(relativePath));
3257
3079
  }
3080
+ if (writes.length === 0) return;
3258
3081
  for (const [map, route] of writes) map.set(route, handlerClass);
3082
+ const from = (0, _seedcord_utils.formatFilePath)(relativePath);
3083
+ if (this.loading) this.loadedHandlers.push({
3084
+ name: handlerClass.name,
3085
+ from
3086
+ });
3087
+ else this.logger.utils.registration(handlerClass.name, from);
3259
3088
  }
3260
3089
  /** @internal For use in dev mode */
3261
3090
  async onHmr(event) {
@@ -3271,7 +3100,7 @@ var InteractionDispatcher = class {
3271
3100
  }
3272
3101
  for (const [routeType, map] of this.routeTypes) {
3273
3102
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3274
- if (!areRoutes(meta)) continue;
3103
+ if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
3275
3104
  meta.forEach((route) => map.delete(route));
3276
3105
  }
3277
3106
  }
@@ -3283,7 +3112,7 @@ var InteractionDispatcher = class {
3283
3112
  this.core.bot.client.on(discord_js.Events.InteractionCreate, (interaction) => {
3284
3113
  this.core.bot.emitSafe("any:interaction", interaction);
3285
3114
  this.handleInteraction(interaction).catch((err) => {
3286
- this.logger.error(`[${chalk.default.bold.red("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3115
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3287
3116
  this.core.bot.emit("error:unhandled:interaction", err);
3288
3117
  });
3289
3118
  });
@@ -3294,22 +3123,31 @@ var InteractionDispatcher = class {
3294
3123
  if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3295
3124
  await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
3296
3125
  }
3297
- async processInteraction(interaction, extractKey, getHandler) {
3126
+ async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
3298
3127
  const key = extractKey(interaction);
3128
+ let sender;
3299
3129
  try {
3300
3130
  if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3301
3131
  let HandlerCtor = getHandler(key);
3302
3132
  if (!HandlerCtor) {
3303
- this.logger.warn(`No handler found for key ${chalk.default.bold.cyan(key)}. Falling back to UnhandledEvent.`);
3304
- HandlerCtor = UnhandledEvent;
3133
+ this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3134
+ HandlerCtor = fallback;
3305
3135
  }
3306
- this.logger.debug(`Processing ${chalk.default.bold.green(key)} with ${chalk.default.gray(HandlerCtor.name)}`);
3136
+ this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
3307
3137
  const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor));
3308
3138
  const handler = new HandlerCtor(interaction, this.core, dispatch);
3309
- if (!interaction.isAutocomplete()) await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core));
3139
+ if (handler instanceof RepliableHandler) sender = handler.getSender();
3140
+ if (!interaction.isAutocomplete()) {
3141
+ const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
3142
+ try {
3143
+ await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3144
+ } finally {
3145
+ monitor?.report(dispatch.routeId);
3146
+ }
3147
+ }
3310
3148
  await handler.execute();
3311
3149
  } catch (caught) {
3312
- await handleInteractionFault(caught, interaction, this.core);
3150
+ await handleInteractionFault(caught, interaction, this.core, sender);
3313
3151
  }
3314
3152
  }
3315
3153
  async handleInteraction(interaction) {
@@ -3385,12 +3223,7 @@ var InteractionDispatcher = class {
3385
3223
  }
3386
3224
  async handleAutocomplete(interaction) {
3387
3225
  const route = slashRouteOf(interaction);
3388
- if (!this.autocompleteMap.has(route)) {
3389
- this.logger.warn(`No autocomplete handler for ${chalk.default.bold.cyan(route)}.`);
3390
- await interaction.respond([]);
3391
- return;
3392
- }
3393
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
3226
+ await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3394
3227
  }
3395
3228
  };
3396
3229
 
@@ -3418,7 +3251,7 @@ function __decorate(decorators, target, key, desc) {
3418
3251
  */
3419
3252
  var Bot = class extends Plugin {
3420
3253
  /** @internal */
3421
- logger = new _seedcord_services.Logger("Bot");
3254
+ logger = new _seedcord_logger.Logger("Bot");
3422
3255
  isInitialized = false;
3423
3256
  _client;
3424
3257
  interactions;
@@ -3446,7 +3279,7 @@ var Bot = class extends Plugin {
3446
3279
  });
3447
3280
  }
3448
3281
  this.emojiInjector = new EmojiInjector(core);
3449
- core.shutdown.addTask(_seedcord_services.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3282
+ core.shutdown.addTask(_seedcord_core_node.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3450
3283
  }
3451
3284
  /**
3452
3285
  * Initializes Discord client and all controllers
@@ -3479,7 +3312,9 @@ var Bot = class extends Plugin {
3479
3312
  * Logs the bot into Discord using the configured token
3480
3313
  */
3481
3314
  async login(token) {
3482
- await this._client.login(token);
3315
+ this._client.once(discord_js.Events.ClientReady, () => this.emit("ready"));
3316
+ this._client.login(token);
3317
+ await this.waitFor("ready");
3483
3318
  this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
3484
3319
  return this;
3485
3320
  }
@@ -3503,199 +3338,72 @@ var Bot = class extends Plugin {
3503
3338
  this.logger.error(`listener for ${String(event)} threw`, error);
3504
3339
  }
3505
3340
  };
3506
- __decorate([(0, envapt.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3507
-
3508
- //#endregion
3509
- //#region src/hmr/HmrManager.ts
3510
- var HmrManager = class {
3511
- logger = new _seedcord_services.Logger("HMR", { channel: "hmr" });
3512
- listeners = /* @__PURE__ */ new Set();
3513
- constructor() {}
3514
- /** @internal */
3515
- init() {
3516
- if (!{}.hot) return;
3517
- setDevChannel((0, _seedcord_types_internal.wrapHot)({}.hot));
3518
- if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) {
3519
- this.logger.info("Enabled");
3520
- getDevChannel()?.on("seedcord:hmr", (payload) => {
3521
- const affected = payload.affectedModules?.length ?? 0;
3522
- this.logger.info(`${chalk.default.bold("1")} module changed, ${chalk.default.bold(affected)} affected modules`);
3523
- if (payload.affectedModules) this.logger.utils.list(payload.affectedModules.map((mod) => (0, _seedcord_utils.formatFilePath)(mod)), "Affected modules: ");
3524
- this.handleUpdate(payload);
3525
- });
3526
- }
3527
- }
3528
- /** @internal */
3529
- register(listener) {
3530
- this.listeners.add(listener);
3531
- }
3532
- /** @internal */
3533
- unregister(listener) {
3534
- this.listeners.delete(listener);
3535
- }
3536
- async handleUpdate(event) {
3537
- const promises = [...this.listeners].map(async (listener) => {
3538
- try {
3539
- await listener.onHmr(event);
3540
- } catch (error) {
3541
- this.logger.error(`Error handling HMR update in ${chalk.default.bold(listener.name)}: `, error);
3542
- }
3543
- });
3544
- await Promise.all(promises);
3545
- }
3546
- };
3547
-
3548
- //#endregion
3549
- //#region src/subscribers/bases/webhookHelpers.ts
3550
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
3551
- /**
3552
- * Whether a string is a well-formed Discord webhook URL.
3553
- *
3554
- * @internal
3555
- */
3556
- function isDiscordWebhookUrl(value) {
3557
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
3558
- }
3559
- /**
3560
- * Serializes a value to a JSON attachment, made circular-safe first.
3561
- *
3562
- * @internal
3563
- */
3564
- function jsonAttachment(name, description, data) {
3565
- const content = (0, _seedcord_utils.filterCirculars)(data);
3566
- return new discord_js.AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
3567
- name,
3568
- description
3569
- });
3570
- }
3571
- /**
3572
- * The small divider the default webhook reporters place between sections.
3573
- *
3574
- * @internal
3575
- */
3576
- var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
3577
- constructor() {
3578
- super("separator");
3579
- this.instance.setSpacing(discord_js.SeparatorSpacingSize.Small).setDivider(true);
3580
- }
3581
- };
3341
+ __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);
3582
3342
 
3583
3343
  //#endregion
3584
3344
  //#region src/subscribers/default/HandledException.ts
3585
- var _HandledException;
3586
- function webhookUrlValidator$1(raw) {
3587
- if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3588
- const value = raw?.trim() ?? "";
3589
- if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
3590
- if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3591
- return value;
3592
- }
3593
3345
  let HandledException = class HandledException extends WebhookLog {
3594
- static {
3595
- _HandledException = this;
3596
- }
3597
- webhook = new discord_js.WebhookClient({ url: _HandledException.handledExceptionWebhookUrl });
3598
- async execute() {
3599
- const { source } = this.data;
3600
- try {
3601
- await this.webhook.send({
3602
- flags: flagsFor(false),
3603
- withComponents: true,
3604
- username: "Handled Exception",
3605
- components: [new HandledExceptionContainer(this.data).component],
3606
- files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
3607
- });
3608
- } catch (error) {
3609
- this.logger.error("Failed to send handled exception webhook", error);
3610
- }
3346
+ report() {
3347
+ return {
3348
+ username: "Handled Exception",
3349
+ components: [new HandledExceptionContainer(this.data).component],
3350
+ files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3351
+ };
3611
3352
  }
3612
3353
  };
3613
- __decorate([(0, envapt.Envapt)("HANDLED_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator$1(raw) }), __decorateMetadata("design:type", String)], HandledException, "handledExceptionWebhookUrl", void 0);
3614
- HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
3354
+ HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3615
3355
  var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
3616
3356
  constructor(data) {
3617
3357
  super("container");
3618
3358
  const { denial, uuid, source } = data;
3619
- this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${causeStack(denial)}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3359
+ 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"));
3620
3360
  }
3621
3361
  };
3622
- /**
3623
- * The summary block of a fault report, narrowed per source kind so an interaction fault shows its
3624
- * command and customId while an event fault shows its event name and handler.
3625
- *
3626
- * @internal
3627
- */
3628
3362
  function faultSummary(denial, source) {
3629
- const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
3363
+ const head = (0, _seedcord_utils.stripAnsi)(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3630
3364
  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"}\``;
3631
3365
  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}\``;
3632
3366
  }
3633
- /**
3634
- * The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
3635
- * circular object) so building the report never throws.
3636
- *
3637
- * @internal
3638
- */
3639
3367
  function causeStack(denial) {
3640
3368
  const { cause } = denial;
3641
- if (Error.isError(cause)) return cause.stack ?? cause.message;
3642
- if (typeof cause === "string") return cause;
3369
+ if (Error.isError(cause)) return (0, _seedcord_utils.stripAnsi)(cause.stack ?? cause.message);
3370
+ if (typeof cause === "string") return (0, _seedcord_utils.stripAnsi)(cause);
3643
3371
  if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3644
3372
  if (cause !== void 0) try {
3645
3373
  return JSON.stringify(cause);
3646
3374
  } catch {
3647
3375
  return "[unserializable cause]";
3648
3376
  }
3649
- return denial.stack ?? "No cause recorded.";
3377
+ return (0, _seedcord_utils.stripAnsi)(denial.stack ?? "No cause recorded.");
3650
3378
  }
3651
3379
 
3652
3380
  //#endregion
3653
3381
  //#region src/subscribers/default/UnknownException.ts
3654
- var _UnknownException;
3655
- function webhookUrlValidator(raw) {
3656
- if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3657
- const value = raw?.trim() ?? "";
3658
- if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
3659
- if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3660
- return value;
3661
- }
3662
3382
  let UnknownException = class UnknownException extends WebhookLog {
3663
- static {
3664
- _UnknownException = this;
3665
- }
3666
- webhook = new discord_js.WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
3667
- async execute() {
3383
+ report() {
3668
3384
  const { metadata } = this.data;
3669
- const metadataFile = metadata ? jsonAttachment("metadata.json", "Metadata associated with the error", metadata) : null;
3670
- try {
3671
- await this.webhook.send({
3672
- flags: flagsFor(false),
3673
- withComponents: true,
3674
- username: "Unknown Exception",
3675
- components: [new UnhandledErrorContainer(this.data).component],
3676
- files: metadataFile ? [metadataFile] : []
3677
- });
3678
- } catch (error) {
3679
- this.logger.error("Failed to send unknown exception webhook", error);
3680
- }
3385
+ return {
3386
+ username: "Unknown Exception",
3387
+ components: [new UnhandledErrorContainer(this.data).component],
3388
+ files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3389
+ };
3681
3390
  }
3682
3391
  };
3683
- __decorate([(0, envapt.Envapt)("UNKNOWN_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator(raw) }), __decorateMetadata("design:type", String)], UnknownException, "unknownExceptionWebhookUrl", void 0);
3684
- UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
3392
+ UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3685
3393
  var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3686
3394
  constructor(data) {
3687
3395
  super("container");
3688
3396
  const { uuid, error, guild, user, metadata } = data;
3689
- 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\`\`\`${error.stack}\`\`\``));
3397
+ 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)}\`\`\``));
3690
3398
  this.addTimestampsIfAvailable(error);
3691
3399
  this.addMetadataIfAvailable(metadata);
3692
3400
  }
3693
3401
  addTimestampsIfAvailable(error) {
3694
- if (!(error instanceof discord_js.DiscordAPIError)) return;
3402
+ if (!(error instanceof _discordjs_rest.DiscordAPIError)) return;
3695
3403
  const now = Date.now();
3696
3404
  const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3697
3405
  if (!snowflake) return void 0;
3698
- const interactionTs = Number(discord_js.SnowflakeUtil.deconstruct(snowflake).timestamp);
3406
+ const interactionTs = (0, _seedcord_utils.timestampFromSnowflake)(snowflake);
3699
3407
  const diff = now - interactionTs;
3700
3408
  const seconds = Math.floor(diff / 1e3);
3701
3409
  const millis = diff % 1e3;
@@ -3710,28 +3418,25 @@ var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3710
3418
  //#endregion
3711
3419
  //#region src/subscribers/Bus.ts
3712
3420
  /**
3713
- * Manages application subscribers and event handling
3714
- *
3715
- * Provides a centralized system for registering and executing custom subscribers
3716
- * throughout the application lifecycle. Bus subscribers are loaded from configured directories
3717
- * and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
3718
- * construct it directly.
3421
+ * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3422
+ * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3719
3423
  */
3720
3424
  var Bus = class extends Plugin {
3721
3425
  core;
3722
- logger = new _seedcord_services.Logger("Subscribers");
3426
+ logger = new _seedcord_logger.Logger("Subscribers");
3723
3427
  /** @internal */
3724
3428
  name = "Subscribers";
3725
3429
  isInitialized = false;
3726
3430
  subscribersMap = /* @__PURE__ */ new Map();
3727
3431
  executedOnceHandlers = /* @__PURE__ */ new Set();
3432
+ webhookProbes = /* @__PURE__ */ new Map();
3728
3433
  hmrHandler;
3729
3434
  constructor(core) {
3730
3435
  super(core);
3731
3436
  this.core = core;
3732
3437
  if (this.core.config.subscribers.path) {
3733
3438
  if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
3734
- this.hmrHandler = new HmrModuleHandler({
3439
+ this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
3735
3440
  handlersDir: this.core.config.subscribers.path,
3736
3441
  isHandler: this.isSubscriber.bind(this),
3737
3442
  registerHandler: this.registerSubscriber.bind(this),
@@ -3750,7 +3455,6 @@ var Bus = class extends Plugin {
3750
3455
  this.isInitialized = true;
3751
3456
  this.registerSubscriber(UnknownException);
3752
3457
  this.registerSubscriber(HandledException);
3753
- envapt.Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
3754
3458
  const subscribersDir = this.core.config.subscribers.path;
3755
3459
  if (subscribersDir) {
3756
3460
  this.logger.info(chalk.default.bold(subscribersDir));
@@ -3758,13 +3462,24 @@ var Bus = class extends Plugin {
3758
3462
  const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3759
3463
  this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
3760
3464
  }
3465
+ if (!envapt.Envapter.isTest) await this.verifyWebhooks();
3466
+ }
3467
+ /** @internal Exposed for tests. */
3468
+ async verifyWebhooks() {
3469
+ const missing = [];
3470
+ await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3471
+ const result = await WebhookLog.senderFor(url).verify();
3472
+ if (result === "missing") missing.push(...envKeys);
3473
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${_seedcord_logger.paint.sky.bold(envKeys.join(", "))}`);
3474
+ }));
3475
+ if (missing.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3761
3476
  }
3762
- /** @internal For use in dev mode */
3477
+ /** @internal */
3763
3478
  async onHmr(event) {
3764
3479
  if (this.hmrHandler) await this.hmrHandler.handle(event);
3765
3480
  }
3766
3481
  async loadSubscribers(dir) {
3767
- await (0, _seedcord_utils.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3482
+ await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, relativePath, imported) => {
3768
3483
  for (const exportName of Object.keys(imported)) {
3769
3484
  const val = imported[exportName];
3770
3485
  if (this.isSubscriber(val)) {
@@ -3776,6 +3491,17 @@ var Bus = class extends Plugin {
3776
3491
  }, this.logger);
3777
3492
  }
3778
3493
  registerSubscriber(handler) {
3494
+ if (handler.prototype instanceof WebhookLog) {
3495
+ const envKey = WebhookLog.envKeyOf(handler);
3496
+ const url = WebhookLog.urlOf(envKey);
3497
+ if (url === null) {
3498
+ this.logger.warn(`${chalk.default.bold(handler.name)} disabled, ${_seedcord_logger.paint.sky.bold(envKey)} is not set`);
3499
+ return;
3500
+ }
3501
+ const envKeys = this.webhookProbes.get(url) ?? [];
3502
+ if (!envKeys.includes(envKey)) envKeys.push(envKey);
3503
+ this.webhookProbes.set(url, envKeys);
3504
+ }
3779
3505
  const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
3780
3506
  let handlers = this.subscribersMap.get(options.subscriber);
3781
3507
  if (!handlers) {
@@ -3835,7 +3561,7 @@ var Bus = class extends Plugin {
3835
3561
  //#endregion
3836
3562
  //#region src/version.ts
3837
3563
  /** Package version */
3838
- const version = "0.1.0-next.1";
3564
+ const version = "0.1.0-next.3";
3839
3565
 
3840
3566
  //#endregion
3841
3567
  //#region src/Seedcord.ts
@@ -3854,6 +3580,8 @@ var Seedcord = class Seedcord extends Pluggable {
3854
3580
  /** @internal */
3855
3581
  version = version;
3856
3582
  static isInstantiated = false;
3583
+ healthCheck;
3584
+ hmrManager;
3857
3585
  /** @see {@link CoordinatedShutdown} */
3858
3586
  shutdown;
3859
3587
  /** @see {@link CoordinatedStartup} */
@@ -3864,10 +3592,6 @@ var Seedcord = class Seedcord extends Pluggable {
3864
3592
  bot;
3865
3593
  /** @see {@link IRateLimiter} */
3866
3594
  rateLimiter;
3867
- /** @see {@link HealthCheck} */
3868
- healthCheck;
3869
- /** @see {@link HmrManager} */
3870
- hmrManager;
3871
3595
  /**
3872
3596
  * Creates a new Seedcord instance
3873
3597
  *
@@ -3877,42 +3601,43 @@ var Seedcord = class Seedcord extends Pluggable {
3877
3601
  constructor(config) {
3878
3602
  if (Seedcord.isInstantiated) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreSingletonViolation);
3879
3603
  Seedcord.isInstantiated = true;
3880
- const shutdown = new _seedcord_services.CoordinatedShutdown(config.shutdownEnabled);
3881
- const startup = new _seedcord_services.CoordinatedStartup();
3604
+ const shutdown = new _seedcord_core_node.CoordinatedShutdown(config.shutdownEnabled);
3605
+ const startup = new _seedcord_core_node.CoordinatedStartup();
3882
3606
  super(shutdown, startup);
3883
3607
  this.config = config;
3608
+ (0, _seedcord_logger_node.installNodeDefaults)(config.logger);
3884
3609
  (0, _seedcord_core_internal.setBotColor)(config.botColor);
3885
3610
  this.shutdown = shutdown;
3886
3611
  this.startup = startup;
3887
- this.hmrManager = new HmrManager();
3612
+ this.hmrManager = new _seedcord_core_internal.HmrManager();
3888
3613
  this.hmrManager.init();
3889
3614
  this.bus = new Bus(this);
3890
3615
  this.bot = new Bot(this);
3891
3616
  this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
3892
- this.healthCheck = new _seedcord_services.HealthCheck(this.shutdown, config.healthCheck);
3617
+ this.healthCheck = new _seedcord_core_node.HealthCheck(this.shutdown, config.healthCheck);
3893
3618
  this.registerStartupTasks();
3894
3619
  }
3895
- /** @internal */
3620
+ /** The bot's discord username, populated after login. */
3621
+ get username() {
3622
+ return this.bot.client.user?.username;
3623
+ }
3896
3624
  static reset() {
3897
3625
  Seedcord.isInstantiated = false;
3626
+ _seedcord_logger.LoggerChannelRegistry.instance.reset();
3898
3627
  }
3899
- /**
3900
- * Registers default startup tasks
3901
- * @internal
3902
- */
3903
3628
  registerStartupTasks() {
3904
3629
  if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
3905
- this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "Bus Initialization", async () => {
3630
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Configuration, "Bus Initialization", async () => {
3906
3631
  this.bus.logger.utils.initialization("Subscribers", "start");
3907
3632
  await this.bus.init();
3908
3633
  this.bus.logger.utils.initialization("Subscribers", "end");
3909
3634
  });
3910
- this.startup.addTask(_seedcord_services.StartupPhase.Instantiation, "Bot Initialization", async () => {
3635
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Instantiation, "Bot Initialization", async () => {
3911
3636
  this.bot.logger.utils.initialization("Bot", "start");
3912
3637
  await this.bot.init();
3913
3638
  this.bot.logger.utils.initialization("Bot", "end");
3914
3639
  });
3915
- this.startup.addTask(_seedcord_services.StartupPhase.Ready, "Health Check", async () => {
3640
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Ready, "Health Check", async () => {
3916
3641
  this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3917
3642
  await this.healthCheck.init();
3918
3643
  this.healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3928,7 +3653,7 @@ var Seedcord = class Seedcord extends Pluggable {
3928
3653
  return this;
3929
3654
  }
3930
3655
  registerHmrAwareModules() {
3931
- this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "HMR Registration", async () => {
3656
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Configuration, "HMR Registration", async () => {
3932
3657
  this.hmrManager.register(this.bot);
3933
3658
  this.hmrManager.register(this.bus);
3934
3659
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
@@ -3940,49 +3665,45 @@ var Seedcord = class Seedcord extends Pluggable {
3940
3665
  //#endregion
3941
3666
  exports.ArraySource = ArraySource;
3942
3667
  exports.AutocompleteHandler = AutocompleteHandler;
3943
- exports.AutocompleteRoute = AutocompleteRoute;
3944
3668
  exports.ButtonHandler = ButtonHandler;
3945
- exports.ButtonRoute = ButtonRoute;
3946
3669
  exports.CommandMentions = CommandMentions;
3947
3670
  exports.ContextMenuHandler = ContextMenuHandler;
3948
- exports.ContextMenuRoute = ContextMenuRoute;
3949
3671
  exports.CursorSource = CursorSource;
3950
3672
  exports.Emojis = Emojis;
3951
3673
  exports.EventHandler = EventHandler;
3952
3674
  exports.EventMiddleware = EventMiddleware;
3953
3675
  exports.Gated = Gated;
3954
- exports.HmrModuleHandler = HmrModuleHandler;
3955
3676
  exports.IgnoreBots = IgnoreBots;
3956
3677
  exports.InteractionHandler = InteractionHandler;
3957
3678
  exports.InteractionMiddleware = InteractionMiddleware;
3958
3679
  exports.Middleware = Middleware;
3959
3680
  exports.MiddlewareType = MiddlewareType;
3960
3681
  exports.ModalHandler = ModalHandler;
3961
- exports.ModalRoute = ModalRoute;
3962
3682
  exports.Nsfw = Nsfw;
3963
3683
  exports.Paginator = Paginator;
3964
3684
  exports.Pluggable = Pluggable;
3965
3685
  exports.Plugin = Plugin;
3966
- Object.defineProperty(exports, 'RegisterCommand', {
3686
+ exports.RegisterEvent = RegisterEvent;
3687
+ exports.RepliableHandler = RepliableHandler;
3688
+ exports.Seedcord = Seedcord;
3689
+ exports.SelectMenuHandler = SelectMenuHandler;
3690
+ Object.defineProperty(exports, 'ShutdownPhase', {
3967
3691
  enumerable: true,
3968
3692
  get: function () {
3969
- return _seedcord_core.RegisterCommand;
3693
+ return _seedcord_core_node.ShutdownPhase;
3970
3694
  }
3971
3695
  });
3972
- exports.RegisterEvent = RegisterEvent;
3973
- exports.ReplySender = ReplySender;
3974
- exports.RequireBotPermissions = RequireBotPermissions;
3975
- exports.RequirePermissions = RequirePermissions;
3976
- exports.RequireRole = RequireRole;
3977
- exports.Seedcord = Seedcord;
3978
- exports.SelectMenuHandler = SelectMenuHandler;
3979
- exports.SelectMenuKind = SelectMenuKind;
3980
- exports.SelectMenuRoute = SelectMenuRoute;
3981
3696
  exports.SlashHandler = SlashHandler;
3982
- exports.SlashRoute = SlashRoute;
3697
+ Object.defineProperty(exports, 'StartupPhase', {
3698
+ enumerable: true,
3699
+ get: function () {
3700
+ return _seedcord_core_node.StartupPhase;
3701
+ }
3702
+ });
3983
3703
  exports.Subscribe = Subscribe;
3984
3704
  exports.Subscriber = Subscriber;
3985
3705
  exports.WebhookLog = WebhookLog;
3706
+ exports.WebhookUrl = WebhookUrl;
3986
3707
  exports.checkBotPermissions = checkBotPermissions;
3987
3708
  exports.checkPermissions = checkPermissions;
3988
3709
  exports.fetchGuildMember = fetchGuildMember;
@@ -4017,17 +3738,17 @@ Object.keys(_seedcord_event_emitter).forEach(function (k) {
4017
3738
  });
4018
3739
  });
4019
3740
 
4020
- Object.keys(_seedcord_rate_limiter).forEach(function (k) {
3741
+ Object.keys(_seedcord_logger).forEach(function (k) {
4021
3742
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
4022
3743
  enumerable: true,
4023
- get: function () { return _seedcord_rate_limiter[k]; }
3744
+ get: function () { return _seedcord_logger[k]; }
4024
3745
  });
4025
3746
  });
4026
3747
 
4027
- Object.keys(_seedcord_services).forEach(function (k) {
3748
+ Object.keys(_seedcord_rate_limiter).forEach(function (k) {
4028
3749
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
4029
3750
  enumerable: true,
4030
- get: function () { return _seedcord_services[k]; }
3751
+ get: function () { return _seedcord_rate_limiter[k]; }
4031
3752
  });
4032
3753
  });
4033
3754