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

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
@@ -28,28 +28,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  //#endregion
29
29
  require("reflect-metadata");
30
30
  let _seedcord_logger_node = require("@seedcord/logger/node");
31
- let _seedcord_core = require("@seedcord/core");
32
31
  let _seedcord_core_internal = require("@seedcord/core/internal");
33
- let discord_js = require("discord.js");
34
32
  let _seedcord_errors = require("@seedcord/errors");
35
33
  let _seedcord_errors_internal = require("@seedcord/errors/internal");
36
- let _seedcord_utils = require("@seedcord/utils");
34
+ let _seedcord_core = require("@seedcord/core");
37
35
  let _seedcord_logger = require("@seedcord/logger");
38
36
  let _discordjs_builders = require("@discordjs/builders");
39
37
  let _seedcord_utils_internal = require("@seedcord/utils/internal");
40
- let _seedcord_event_emitter = require("@seedcord/event-emitter");
41
- let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
42
- let _seedcord_services = require("@seedcord/services");
43
- let _seedcord_types_internal = require("@seedcord/types/internal");
44
- let envapt = require("envapt");
38
+ let discord_js = require("discord.js");
39
+ let node_crypto = require("node:crypto");
40
+ node_crypto = __toESM(node_crypto, 1);
45
41
  let chalk = require("chalk");
46
42
  chalk = __toESM(chalk, 1);
43
+ let envapt = require("envapt");
44
+ let discord_api_types_v10 = require("discord-api-types/v10");
45
+ let _seedcord_utils = require("@seedcord/utils");
46
+ let _discordjs_rest = require("@discordjs/rest");
47
+ let _seedcord_core_node_internal = require("@seedcord/core/node/internal");
48
+ let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
49
+ let _seedcord_types_internal = require("@seedcord/types/internal");
50
+ let _seedcord_event_emitter = require("@seedcord/event-emitter");
47
51
  let envapt_legacy = require("envapt/legacy");
48
52
  let node_path = require("node:path");
49
53
  let _seedcord_core_hmr = require("@seedcord/core/hmr");
50
54
  let _seedcord_utils_node = require("@seedcord/utils/node");
51
- let node_crypto = require("node:crypto");
52
- node_crypto = __toESM(node_crypto, 1);
53
55
 
54
56
  //#region src/logger-bootstrap.ts
55
57
  (0, _seedcord_logger_node.installNodeDefaults)();
@@ -147,190 +149,6 @@ function Gated(...gates) {
147
149
  };
148
150
  }
149
151
 
150
- //#endregion
151
- //#region src/miscellaneous/areRoutes.ts
152
- /**
153
- * Checks if the provided value is an array of strings.
154
- *
155
- * @internal
156
- */
157
- function areRoutes(routes) {
158
- return Array.isArray(routes) && routes.every((r) => typeof r === "string");
159
- }
160
-
161
- //#endregion
162
- //#region src/bot/decorators/Interactions.ts
163
- /**
164
- * Types of select menus supported for routing
165
- */
166
- let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
167
- SelectMenuKind["String"] = "string";
168
- SelectMenuKind["User"] = "user";
169
- SelectMenuKind["Role"] = "role";
170
- SelectMenuKind["Channel"] = "channel";
171
- SelectMenuKind["Mentionable"] = "mentionable";
172
- return SelectMenuKind;
173
- }({});
174
- /**
175
- * Routes one or more slash commands to a {@link SlashHandler}.
176
- *
177
- * Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
178
- * commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
179
- * fewer or more than the handler declares is a compile error. Subcommands use a slash path.
180
- *
181
- * @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
182
- * @decorator
183
- * @example
184
- * ```ts
185
- * \@SlashRoute('ban')
186
- * class BanHandler extends SlashHandler<'ban'> {
187
- * async execute() {
188
- * const target = this.options.getUser('target');
189
- * }
190
- * }
191
- * ```
192
- *
193
- * @example
194
- * ```ts
195
- * \@SlashRoute('ban', 'kick')
196
- * class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
197
- * async execute() {
198
- * await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
199
- * }
200
- * }
201
- * ```
202
- */
203
- function SlashRoute(...routes) {
204
- return function(constructor) {
205
- storeMetadata(_seedcord_core_internal.InteractionRoutes.Slash, routes, constructor);
206
- };
207
- }
208
- /**
209
- * Routes button interactions to handler classes.
210
- *
211
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
212
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
213
- * matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
214
- * reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
215
- *
216
- * @param defs - The customId definition(s) this handler decodes, one per route.
217
- * @decorator
218
- */
219
- function ButtonRoute(...defs) {
220
- return function(constructor) {
221
- storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Button, defs, constructor);
222
- };
223
- }
224
- /**
225
- * Routes modal submissions to handler classes.
226
- *
227
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
228
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
229
- * matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
230
- * on read, the same as a button route.
231
- *
232
- * @param defs - The customId definition(s) this handler decodes, one per route.
233
- * @decorator
234
- */
235
- function ModalRoute(...defs) {
236
- return function(constructor) {
237
- storeComponentRoute(_seedcord_core_internal.InteractionRoutes.Modal, defs, constructor);
238
- };
239
- }
240
- /**
241
- * Routes one or more context-menu commands to a {@link ContextMenuHandler}.
242
- *
243
- * Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
244
- * each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
245
- * handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
246
- * reads `this.target` uniformly with no branch.
247
- *
248
- * @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
249
- * @param names - The command name(s) this handler serves, keyed off the matching registry.
250
- * @decorator
251
- * @example
252
- * ```ts
253
- * \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
254
- * class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
255
- * async execute() {
256
- * const user = this.target;
257
- * }
258
- * }
259
- * ```
260
- */
261
- function ContextMenuRoute(kind, ...names) {
262
- return function(constructor) {
263
- storeMetadata(kind === discord_js.ApplicationCommandType.User ? _seedcord_core_internal.InteractionRoutes.UserContextMenu : _seedcord_core_internal.InteractionRoutes.MessageContextMenu, names, constructor);
264
- };
265
- }
266
- /**
267
- * Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
268
- *
269
- * Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
270
- * over its autocompletable fields, several commands share one handler whose arms span every command's
271
- * fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
272
- * handler declares is a compile error.
273
- *
274
- * @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
275
- * @decorator
276
- * @example
277
- * ```ts
278
- * \@AutocompleteRoute('search')
279
- * class SearchAutocomplete extends AutocompleteHandler<'search'> {
280
- * async execute() {
281
- * await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
282
- * }
283
- * }
284
- * ```
285
- */
286
- function AutocompleteRoute(...routes) {
287
- return function(constructor) {
288
- storeMetadata(_seedcord_core_internal.InteractionRoutes.Autocomplete, routes, constructor);
289
- };
290
- }
291
- /**
292
- * Routes select menu interactions to handler classes.
293
- *
294
- * Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
295
- * generic must list the same definitions, and its second generic argument must be the matching select
296
- * interaction type, or it is a compile error.
297
- *
298
- * @param type - Select menu kind from {@link SelectMenuKind}.
299
- * @param defs - The customId definition(s) this handler decodes, one per route.
300
- * @decorator
301
- *
302
- * @example
303
- * ```typescript
304
- * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
305
- * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
306
- * // handles user select menus minted from AssignId
307
- * }
308
- * ```
309
- */
310
- function SelectMenuRoute(type, ...defs) {
311
- return function(constructor) {
312
- storeComponentRoute({
313
- ["string"]: _seedcord_core_internal.InteractionRoutes.StringMenu,
314
- ["user"]: _seedcord_core_internal.InteractionRoutes.UserMenu,
315
- ["role"]: _seedcord_core_internal.InteractionRoutes.RoleMenu,
316
- ["channel"]: _seedcord_core_internal.InteractionRoutes.ChannelMenu,
317
- ["mentionable"]: _seedcord_core_internal.InteractionRoutes.MentionableMenu
318
- }[type], defs, constructor);
319
- };
320
- }
321
- function storeComponentRoute(route, defs, constructor) {
322
- storeMetadata(route, defs.map((def) => def.prefix), constructor);
323
- Reflect.defineMetadata(_seedcord_core_internal.ComponentDefsKey, defs, constructor);
324
- }
325
- function storeMetadata(route, routes, constructor) {
326
- const key = _seedcord_core_internal.InteractionRouteKeys[route];
327
- const savedRoutes = Reflect.getMetadata(key, constructor);
328
- const existing = areRoutes(savedRoutes) ? savedRoutes : [];
329
- const toStore = Array.isArray(routes) ? routes : [routes];
330
- Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
331
- Reflect.defineMetadata(_seedcord_core_internal.InteractionMetadataKey, true, constructor);
332
- }
333
-
334
152
  //#endregion
335
153
  //#region src/bot/decorators/Middlewares.ts
336
154
  /**
@@ -399,21 +217,6 @@ const IgnoreBots = (0, _seedcord_core.defineGate)("IgnoreBots", (ctx) => {
399
217
  if (ctx.user?.bot) throw new _seedcord_core.Silence("actor is a bot");
400
218
  });
401
219
 
402
- //#endregion
403
- //#region src/bot/notices/utils.ts
404
- function mentionFor(subject) {
405
- if (subject instanceof discord_js.Role) return `<@&${subject.id}>`;
406
- if (subject instanceof discord_js.TextChannel) return `<#${subject.id}>`;
407
- if (subject instanceof discord_js.GuildMember) return `<@${subject.id}>`;
408
- return `\`${subject.name}\``;
409
- }
410
- function labelFor(subject) {
411
- if (subject instanceof discord_js.Role) return "role";
412
- if (subject instanceof discord_js.TextChannel) return "channel";
413
- if (subject instanceof discord_js.GuildMember) return "member";
414
- return "guild";
415
- }
416
-
417
220
  //#endregion
418
221
  //#region src/bot/notices/index.ts
419
222
  var NotNsfw = class extends _seedcord_core_internal.GateNotice {
@@ -421,13 +224,6 @@ var NotNsfw = class extends _seedcord_core_internal.GateNotice {
421
224
  super(message);
422
225
  }
423
226
  };
424
- var MissingRole = class extends _seedcord_core_internal.GateNotice {
425
- role;
426
- constructor(message, role) {
427
- super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
428
- this.role = role;
429
- }
430
- };
431
227
  var RoleHigherThanMe = class extends _seedcord_core.Notice {
432
228
  role;
433
229
  botRole;
@@ -448,30 +244,19 @@ var CannotAssignBotRole = class extends _seedcord_core.Notice {
448
244
  return { components: [new _seedcord_core_internal.NoticeCard("I cannot assign a managed role.").component] };
449
245
  }
450
246
  };
451
- var MissingPermissions = class extends _seedcord_core.Notice {
452
- where;
453
- missingPerms;
454
- constructor(message, where, missingPerms) {
455
- super(message);
456
- this.where = where;
457
- this.missingPerms = missingPerms;
458
- }
459
- render() {
460
- const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
461
- return { components: [new _seedcord_core_internal.NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
462
- }
463
- };
464
247
  var HasDangerousPermissions = class extends _seedcord_core.Notice {
465
- target;
248
+ subject;
466
249
  dangerousPerms;
467
- constructor(message, target, dangerousPerms) {
468
- super(message);
469
- this.target = target;
250
+ customLead;
251
+ constructor(message, subject, dangerousPerms) {
252
+ super(message ?? "A dangerous permission is enabled.");
253
+ this.subject = subject;
470
254
  this.dangerousPerms = dangerousPerms;
255
+ this.customLead = message;
471
256
  }
472
257
  render() {
473
258
  const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
474
- 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] };
475
260
  }
476
261
  };
477
262
  var UserNotFound = class extends _seedcord_core.Notice {
@@ -547,129 +332,6 @@ function Nsfw(options) {
547
332
  });
548
333
  }
549
334
 
550
- //#endregion
551
- //#region src/bot/utilities/permissions/checkPermissions.ts
552
- /**
553
- * Map of permission bits to their prettified human readable names.
554
- */
555
- const PermissionNames = new Map(Object.entries(discord_js.PermissionFlagsBits).map(([key, bit]) => [bit, (0, _seedcord_utils.prettify)(key)]));
556
- function checkPermissions(a, b, c, d, e) {
557
- const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
558
- for: a,
559
- in: b,
560
- scope: c,
561
- inverse: d ?? false,
562
- ...e
563
- } : a;
564
- const Missing = missingCtor ?? MissingPermissions;
565
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
566
- const perms = pIn instanceof discord_js.Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
567
- const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
568
- const present = scope.filter((bit) => perms.has(bit, true));
569
- const presentNames = names(present);
570
- if (inverse) {
571
- 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);
572
- return;
573
- }
574
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
575
- if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
576
- }
577
-
578
- //#endregion
579
- //#region src/bot/gates/catalog/permissions.ts
580
- const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
581
- /**
582
- * Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
583
- *
584
- * Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
585
- *
586
- * @param scope - The permission flag bits the caller must all hold.
587
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
588
- *
589
- * @see {@link Gated}
590
- *
591
- * @example
592
- * ```ts
593
- * import { PermissionFlagsBits } from 'discord.js';
594
- *
595
- * \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
596
- * \@SlashRoute('ban')
597
- * class BanHandler extends SlashHandler<'ban'> {
598
- * async execute() {
599
- * const target = this.options.getUser('target');
600
- * }
601
- * }
602
- * ```
603
- */
604
- function RequirePermissions(scope, options) {
605
- return (0, _seedcord_core.defineGate)("RequirePermissions", (ctx) => {
606
- if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
607
- if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
608
- checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
609
- });
610
- }
611
- /**
612
- * Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
613
- *
614
- * It reads the bot's own member, so it attaches to modal handlers as well.
615
- *
616
- * @param scope - The permission flag bits the bot must all hold.
617
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
618
- *
619
- * @see {@link Gated}
620
- *
621
- * @example
622
- * ```ts
623
- * import { PermissionFlagsBits } from 'discord.js';
624
- *
625
- * \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
626
- * \@ModalRoute(MyModalId)
627
- * class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
628
- * async execute() {
629
- * // ...
630
- * }
631
- * }
632
- * ```
633
- */
634
- function RequireBotPermissions(scope, options) {
635
- return (0, _seedcord_core.defineGate)("RequireBotPermissions", (ctx) => {
636
- const botMember = ctx.guild?.members.me;
637
- if (!ctx.guild || !botMember) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
638
- checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
639
- });
640
- }
641
- /**
642
- * Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
643
- * when the role is missing.
644
- *
645
- * Interaction-only and excludes ModalSubmit.
646
- *
647
- * @param roleId - The role snowflake the caller must hold, read from the member's role cache.
648
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
649
- *
650
- * @see {@link Gated}
651
- *
652
- * @example
653
- * ```ts
654
- * \@Gated(RequireRole('123456789012345678'))
655
- * \@SlashRoute('vip')
656
- * class VipHandler extends SlashHandler<'vip'> {
657
- * async execute() {
658
- * // ...
659
- * }
660
- * }
661
- * ```
662
- */
663
- function RequireRole(roleId, options) {
664
- return (0, _seedcord_core.defineGate)("RequireRole", (ctx) => {
665
- if (!ctx.guild) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message));
666
- if (!ctx.member) throw (0, _seedcord_core_internal.pickNotice)(options?.notInGuild, (message) => new _seedcord_core_internal.NotInGuild(message ?? UNCACHED_MEMBER));
667
- if (ctx.member.roles.cache.has(roleId)) return;
668
- const role = ctx.guild.roles.cache.get(roleId) ?? null;
669
- throw (0, _seedcord_core_internal.pickNotice)(options?.missingRole, (message) => new MissingRole(message, role));
670
- });
671
- }
672
-
673
335
  //#endregion
674
336
  //#region src/bot/injectors/EmojiInjector.ts
675
337
  const emojiStorage = {};
@@ -858,6 +520,37 @@ async function fetchText(client, channelId) {
858
520
  throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
859
521
  }
860
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
+
861
554
  //#endregion
862
555
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
863
556
  /**
@@ -885,8 +578,8 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
885
578
  if (target instanceof discord_js.Guild) {
886
579
  const me = target.members.me;
887
580
  if (!me) {
888
- const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
889
- 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);
890
583
  }
891
584
  checkPermissions(me, target, scope, inverse, errors);
892
585
  return;
@@ -894,8 +587,8 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
894
587
  const me = target.guild.members.me;
895
588
  if (!me) {
896
589
  new _seedcord_logger.Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
897
- const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
898
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
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);
899
592
  }
900
593
  checkPermissions(me, target, scope, inverse, errors);
901
594
  }
@@ -932,26 +625,26 @@ function getBotRole(guild) {
932
625
  * // Check with custom error handling
933
626
  * hasPermsToAssign({
934
627
  * targetRole: role,
935
- * errors: {
936
- * higher: CustomHigherError,
937
- * managed: CustomManagedError,
938
- * missing: CustomMissingPermissionsError
628
+ * noticeOverrides: {
629
+ * higherNotice: CustomHigherNotice,
630
+ * managedNotice: CustomManagedNotice,
631
+ * missingNotice: CustomMissingPermissionsNotice
939
632
  * }
940
633
  * });
941
634
  * ```
942
635
  */
943
636
  function hasPermsToAssign(roleOrOptions) {
944
- const { targetRole, errors } = roleOrOptions instanceof discord_js.Role ? {
637
+ const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof discord_js.Role ? {
945
638
  targetRole: roleOrOptions,
946
- errors: void 0
639
+ noticeOverrides: void 0
947
640
  } : roleOrOptions;
948
- const HigherErr = errors?.higher ?? RoleHigherThanMe;
949
- const ManagedErr = errors?.managed ?? CannotAssignBotRole;
950
- 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;
951
644
  const botRole = getBotRole(targetRole.guild);
952
- if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherErr("Role is higher than me", targetRole, botRole);
953
- if (targetRole.managed) throw new ManagedErr(`Cannot assign managed role ${targetRole.name}`);
954
- 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 });
955
648
  }
956
649
 
957
650
  //#endregion
@@ -1078,115 +771,310 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
1078
771
  await member.roles.set([...updated]);
1079
772
  }
1080
773
 
1081
- //#endregion
1082
- //#region src/bot/harmlessApiCodes.ts
1083
- /**
1084
- * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
1085
- * message. There is nothing to show the user and no bug to report. The controller boundary drops them on
1086
- * the original throw, and {@link ReplySender} drops them on its own send.
1087
- *
1088
- * @internal
1089
- */
1090
- const HARMLESS_API_CODES = new Set([
1091
- discord_js.RESTJSONErrorCodes.UnknownInteraction,
1092
- discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1093
- discord_js.RESTJSONErrorCodes.UnknownMessage
1094
- ]);
1095
-
1096
- //#endregion
1097
- //#region src/miscellaneous/flagsFor.ts
1098
- /**
1099
- * Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
1100
- * IsComponentsV2 flag is always set.
1101
- *
1102
- * @param ephemeral - Whether the reply is ephemeral.
1103
- * @returns The OR'd `MessageFlags` value as a plain number.
1104
- *
1105
- * @internal
1106
- */
1107
- function flagsFor(ephemeral) {
1108
- let flags = discord_js.MessageFlags.IsComponentsV2;
1109
- if (ephemeral) flags |= discord_js.MessageFlags.Ephemeral;
1110
- return flags;
1111
- }
1112
-
1113
774
  //#endregion
1114
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
+ }
1115
782
  /**
1116
- * Sends a {@link ReplyResponse} to an interaction, picking reply, editReply, or followUp from the live
1117
- * acknowledgement state. Logs and drops its own send failures so a dead token never escapes into the
1118
- * controller.
783
+ * Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
784
+ * handler bases and the dispatcher.
1119
785
  */
1120
- var ReplySender = class {
786
+ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
1121
787
  interaction;
1122
- logger = new _seedcord_logger.Logger("ReplySender");
1123
- constructor(interaction) {
788
+ constructor(interaction, routeId) {
789
+ super(routeId, seedState(interaction));
1124
790
  this.interaction = interaction;
1125
791
  }
1126
- /**
1127
- * Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
1128
- * a failed send is logged and dropped and resolves to `undefined`.
1129
- *
1130
- * @param response - The ComponentsV2 reply to show.
1131
- * @param ephemeral - Whether the reply is ephemeral. {@default `true`}
1132
- * @returns The sent {@link Message}, or `undefined` when the send was swallowed.
1133
- */
1134
- async send(response, ephemeral = true) {
1135
- try {
1136
- return await this.dispatch(response, ephemeral);
1137
- } catch (error) {
1138
- this.logSwallowed("send", error);
1139
- return;
1140
- }
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);
1141
798
  }
1142
- /**
1143
- * Edits an already-sent message to a new {@link ReplyResponse}. Used to swap a confirmation prompt for
1144
- * its outcome. Never throws, a failed edit is logged and dropped.
1145
- *
1146
- * @param message - The message to edit (the handle a prior {@link send} returned).
1147
- * @param response - The reply to replace it with.
1148
- */
1149
- async edit(message, response) {
1150
- try {
1151
- await this.interaction.webhook.editMessage(message, this.editBody(response));
1152
- } catch (error) {
1153
- this.logSwallowed("edit", error);
1154
- }
799
+ async writeDefer(opts) {
800
+ await this.interaction.deferReply({ flags: (0, _seedcord_core_internal.deferFlags)(opts) });
1155
801
  }
1156
- async dispatch(response, ephemeral) {
1157
- if (this.interaction.replied) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1158
- if (this.interaction.deferred) {
1159
- if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1160
- return await this.interaction.editReply(this.editBody(response));
1161
- }
1162
- await this.interaction.reply(this.replyOptions(response, ephemeral));
1163
- 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)));
1164
814
  }
1165
- replyOptions(response, ephemeral) {
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();
823
+ }
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);
1166
851
  return {
1167
- components: response.components,
1168
- flags: flagsFor(ephemeral),
1169
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1170
- ...response.files && { files: response.files }
852
+ components: reply.components,
853
+ flags,
854
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
855
+ ...reply.files && { files: [...reply.files] }
1171
856
  };
1172
857
  }
1173
- editBody(response) {
858
+ editOptions(response) {
859
+ const reply = this.serialize(response);
1174
860
  return {
1175
- components: response.components,
861
+ components: reply.components,
1176
862
  flags: discord_js.MessageFlags.IsComponentsV2,
1177
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1178
- ...response.files && { files: response.files }
863
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
864
+ ...reply.files && { files: [...reply.files] }
1179
865
  };
1180
866
  }
1181
- logSwallowed(action, error) {
1182
- if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
1183
- this.logger.debug(`reply ${action} hit harmless code ${error.code}`);
1184
- return;
1185
- }
1186
- 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;
1187
878
  }
1188
879
  };
1189
880
 
881
+ //#endregion
882
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
883
+ /**
884
+ * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
885
+ * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
886
+ * interaction and a generated registry key always match.
887
+ *
888
+ * @internal
889
+ */
890
+ function slashRouteOf(interaction) {
891
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
892
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
893
+ return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
894
+ }
895
+
896
+ //#endregion
897
+ //#region src/miscellaneous/FaultThrottle.ts
898
+ /**
899
+ * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
900
+ * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
901
+ * the map stays small, and a stale entry is evicted on the next read of its key.
902
+ *
903
+ * @internal
904
+ */
905
+ var FaultThrottle = class {
906
+ windowMs;
907
+ now;
908
+ lastReportedAt = /* @__PURE__ */ new Map();
909
+ constructor(windowMs = 6e4, now = () => Date.now()) {
910
+ this.windowMs = windowMs;
911
+ this.now = now;
912
+ }
913
+ /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
914
+ shouldReport(key) {
915
+ const last = this.lastReportedAt.get(key);
916
+ if (last === void 0) return true;
917
+ if (this.now() - last < this.windowMs) return false;
918
+ this.lastReportedAt.delete(key);
919
+ return true;
920
+ }
921
+ markReported(key) {
922
+ this.lastReportedAt.set(key, this.now());
923
+ }
924
+ clear() {
925
+ this.lastReportedAt.clear();
926
+ }
927
+ };
928
+
929
+ //#endregion
930
+ //#region src/miscellaneous/extractErrorResponse.ts
931
+ const logger$2 = new _seedcord_logger.Logger("ErrorsHandling");
932
+ const faultThrottle = new FaultThrottle();
933
+ /**
934
+ * Processes an error into a user-facing reply and routes its report.
935
+ *
936
+ * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
937
+ * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
938
+ * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
939
+ * threaded into both the reply and the bus payload.
940
+ */
941
+ function extractErrorResponse(error, core, origin) {
942
+ const uuid = node_crypto.randomUUID();
943
+ const developerUsername = core.config.notifications?.developerUsername;
944
+ const ctx = developerUsername === void 0 ? { uuid } : {
945
+ uuid,
946
+ developerUsername
947
+ };
948
+ if (error instanceof _seedcord_core.Notice) {
949
+ if (error.report) reportFault(error, core, origin, uuid);
950
+ return {
951
+ uuid,
952
+ response: error.render(ctx)
953
+ };
954
+ }
955
+ reportRawFault(error, core, origin, uuid);
956
+ const override = core.config.errors?.defaultError;
957
+ return {
958
+ uuid,
959
+ response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
960
+ };
961
+ }
962
+ function withThrottle(origin, error, publish) {
963
+ const key = faultKey(origin, error);
964
+ if (!faultThrottle.shouldReport(key)) {
965
+ logger$2.info(`throttled duplicate fault ${key}`);
966
+ return;
967
+ }
968
+ publish();
969
+ faultThrottle.markReported(key);
970
+ }
971
+ function reportFault(denial, core, origin, uuid) {
972
+ withThrottle(origin, denial, () => {
973
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
974
+ if (origin.interaction) core.bus.publish("handledException", {
975
+ denial,
976
+ uuid,
977
+ source: buildInteractionSource(origin.interaction)
978
+ });
979
+ else if (origin.event) core.bus.publish("handledException", {
980
+ denial,
981
+ uuid,
982
+ source: buildEventSource(origin.event, origin)
983
+ });
984
+ else core.bus.publish("unknownException", {
985
+ uuid,
986
+ error: denial,
987
+ ...scalarActors(origin),
988
+ metadata: metadataFor(origin)
989
+ });
990
+ });
991
+ }
992
+ function causeLine(error) {
993
+ const { cause } = error;
994
+ if (!Error.isError(cause)) return "";
995
+ const [first] = cause.message.split("\n");
996
+ return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
997
+ }
998
+ function reportRawFault(error, core, origin, uuid) {
999
+ withThrottle(origin, error, () => {
1000
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1001
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1002
+ core.bus.publish("unknownException", {
1003
+ uuid,
1004
+ error,
1005
+ ...scalarActors(origin),
1006
+ metadata: metadataFor(origin)
1007
+ });
1008
+ });
1009
+ }
1010
+ function scalarActors(origin) {
1011
+ return {
1012
+ guild: origin.guild ? {
1013
+ id: origin.guild.id,
1014
+ name: origin.guild.name
1015
+ } : void 0,
1016
+ user: origin.user ? {
1017
+ id: origin.user.id,
1018
+ username: origin.user.username
1019
+ } : void 0
1020
+ };
1021
+ }
1022
+ function metadataFor(origin) {
1023
+ if (origin.event) return {
1024
+ eventName: origin.event.name,
1025
+ handler: origin.event.handler,
1026
+ args: origin.event.args
1027
+ };
1028
+ return origin.metadata;
1029
+ }
1030
+ function buildEventSource(event, origin) {
1031
+ return {
1032
+ kind: "event",
1033
+ eventName: event.name,
1034
+ handler: event.handler,
1035
+ userId: origin.user?.id ?? null,
1036
+ guildId: origin.guild?.id ?? null,
1037
+ channelId: event.channelId,
1038
+ raw: event.args
1039
+ };
1040
+ }
1041
+ function faultKey(origin, error) {
1042
+ const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
1043
+ if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1044
+ if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1045
+ if (origin.route) return `autocomplete:${origin.route}:${name}`;
1046
+ return `autocomplete:${name}`;
1047
+ }
1048
+ /** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
1049
+ function interactionRoute(interaction) {
1050
+ if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1051
+ if (interaction.isContextMenuCommand()) return interaction.commandName;
1052
+ if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
1053
+ return "interaction";
1054
+ }
1055
+ function buildInteractionSource(interaction) {
1056
+ const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1057
+ const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1058
+ return {
1059
+ kind: "interaction",
1060
+ interactionKind: interactionKind(interaction),
1061
+ command,
1062
+ customId,
1063
+ userId: interaction.user.id,
1064
+ guildId: interaction.guildId,
1065
+ channelId: interaction.channelId,
1066
+ interactionId: interaction.id,
1067
+ raw: interaction
1068
+ };
1069
+ }
1070
+ function interactionKind(interaction) {
1071
+ if (interaction.isChatInputCommand()) return "slash";
1072
+ if (interaction.isContextMenuCommand()) return "context-menu";
1073
+ if (interaction.isButton()) return "button";
1074
+ if (interaction.isAnySelectMenu()) return "select";
1075
+ return "modal";
1076
+ }
1077
+
1190
1078
  //#endregion
1191
1079
  //#region src/bot/confirm/reserved.ts
1192
1080
  const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
@@ -1230,153 +1118,37 @@ async function collectChoice(message, userId, timeoutMs) {
1230
1118
  } catch {
1231
1119
  return null;
1232
1120
  }
1233
- }
1234
- async function settle(sender, interaction, message, outcome) {
1235
- if (outcome === void 0) {
1236
- await interaction.deleteReply(message).catch(() => void 0);
1237
- return;
1238
- }
1239
- await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
1240
- }
1241
- async function getConfirmation(interaction, prompt, options) {
1242
- const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1243
- const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1244
- const sender = new ReplySender(interaction);
1245
- const message = await sender.send(response, ephemeral);
1246
- if (!message) return false;
1247
- const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1248
- if (!winner) {
1249
- await settle(sender, interaction, message, options?.onTimeout);
1250
- return false;
1251
- }
1252
- await winner.deferUpdate().catch(() => void 0);
1253
- const confirmed = winner.customId === CONFIRM_IDS.confirm;
1254
- await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
1255
- return confirmed;
1256
- }
1257
-
1258
- //#endregion
1259
- //#region src/interfaces/Plugin.ts
1260
- /**
1261
- * Base class for Seedcord plugins
1262
- *
1263
- * Extend this class to create plugins that integrate with the Seedcord lifecycle.
1264
- * Plugins have access to the core instance and must implement initialization logic.
1265
- */
1266
- var Plugin = class extends _seedcord_event_emitter.TypedEventEmitter {
1267
- pluggable;
1268
- name = this.constructor.name;
1269
- constructor(pluggable) {
1270
- super();
1271
- this.pluggable = pluggable;
1272
- }
1273
- /**
1274
- * Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
1275
- * @virtual
1276
- */
1277
- onHmr(_event) {
1278
- return Promise.resolve();
1279
- }
1280
- /**
1281
- * Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
1282
- * @param patterns - Glob patterns relative to the project root
1283
- */
1284
- registerCriticalFiles(patterns) {
1285
- (0, _seedcord_core_internal.getDevChannel)()?.send("seedcord:register-critical-files", { patterns });
1286
- }
1287
- /** @internal */
1288
- removeListener(event, listener) {
1289
- return super.removeListener(event, listener);
1290
- }
1291
- /** @internal */
1292
- removeAllListeners(event) {
1293
- return super.removeAllListeners(event);
1294
- }
1295
- };
1296
- /**
1297
- * Base class for objects that can have plugins attached.
1298
- *
1299
- * Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
1300
- * configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
1301
- */
1302
- var Pluggable = class Pluggable extends _seedcord_event_emitter.TypedEventEmitter {
1303
- isInitialized = false;
1304
- shutdown;
1305
- startup;
1306
- plugins = [];
1307
- static PLUGIN_INIT_TIMEOUT_MS = 15e3;
1308
- constructor(shutdown, startup) {
1309
- super();
1310
- this.shutdown = shutdown;
1311
- this.startup = startup;
1312
- }
1313
- /** @internal */
1314
- async init() {
1315
- if (this.isInitialized) return this;
1316
- await this.startup.run();
1317
- this.isInitialized = true;
1318
- return this;
1319
- }
1320
- /**
1321
- * Attaches a plugin to this instance
1322
- *
1323
- * Plugins provide external functionality and are initialized during the specified startup phase.
1324
- * The plugin instance becomes available as a property in `core` wherever it's available.
1325
- *
1326
- * Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
1327
- *
1328
- * @typeParam Key - The property name for accessing the plugin
1329
- * @typeParam Ctor - The plugin constructor type
1330
- * @param key - Property name to access the plugin instance
1331
- * @param Plugin - Plugin constructor class
1332
- * @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
1333
- * @param args - Additional arguments to pass to the plugin constructor
1334
- * @returns This instance with the plugin attached as a typed property
1335
- * @throws A **SeedcordError** When called after initialization or if key already exists
1336
- * @example
1337
- * ```typescript
1338
- * seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
1339
- * ```
1340
- */
1341
- attach(key, Plugin, startupPhase, ...args) {
1342
- if (this.isInitialized) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginAfterInit);
1343
- if (key in this) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CorePluginKeyExists, [key]);
1344
- const instance = new Plugin(this, ...args);
1345
- this.plugins.push(instance);
1346
- const entry = { [key]: instance };
1347
- this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
1348
- instance.logger.utils.initialization(key, "start");
1349
- await instance.init();
1350
- instance.logger.utils.initialization(key, "end");
1351
- }, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
1352
- return Object.assign(this, entry);
1353
- }
1354
- /** @internal */
1355
- removeListener(event, listener) {
1356
- return super.removeListener(event, listener);
1121
+ }
1122
+ async function settle(sender, message, outcome) {
1123
+ if (outcome === void 0) {
1124
+ await sender.delete(message).catch(() => void 0);
1125
+ return;
1357
1126
  }
1358
- /** @internal */
1359
- removeAllListeners(event) {
1360
- return super.removeAllListeners(event);
1127
+ await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
1128
+ }
1129
+ async function getConfirmation(interaction, prompt, options) {
1130
+ const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1131
+ const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1132
+ const sender = new ReplySender(interaction, interactionRoute(interaction));
1133
+ const message = await sender.send(response, { ephemeral });
1134
+ const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1135
+ if (!winner) {
1136
+ await settle(sender, message, options?.onTimeout);
1137
+ return false;
1361
1138
  }
1362
- };
1139
+ await winner.deferUpdate().catch(() => void 0);
1140
+ const confirmed = winner.customId === CONFIRM_IDS.confirm;
1141
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1142
+ return confirmed;
1143
+ }
1363
1144
 
1364
1145
  //#endregion
1365
1146
  //#region src/handlers/BaseHandler.ts
1366
1147
  /**
1367
- * Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
1148
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1149
+ * gate hook.
1368
1150
  */
1369
- var BaseHandler = class {
1370
- core;
1371
- event;
1372
- logger;
1373
- dispatch;
1374
- constructor(event, core, dispatch) {
1375
- this.core = core;
1376
- this.event = event;
1377
- this.logger = new _seedcord_logger.Logger(this.constructor.name);
1378
- this.dispatch = dispatch;
1379
- }
1151
+ var BaseHandler = class extends _seedcord_core.BaseHandler {
1380
1152
  /** @internal */
1381
1153
  getEvent() {
1382
1154
  return this.event;
@@ -1400,36 +1172,49 @@ var InteractionMiddleware = class extends BaseHandler {
1400
1172
  };
1401
1173
 
1402
1174
  //#endregion
1403
- //#region src/handlers/interaction/InteractionHandler.ts
1175
+ //#region src/handlers/RepliableHandler.ts
1404
1176
  /**
1405
- * Shared base the typed interaction handlers extend.
1177
+ * Shared base the repliable interaction handlers extend.
1406
1178
  *
1407
- * Not a public entry point. You should be using {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1408
- * or {@link SelectMenuHandler} instead. This class only carries the repliable-event plumbing those bases share,
1409
- * so DO NOT use it directly.
1179
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1180
+ * {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
1181
+ * to the gateway sender.
1410
1182
  *
1411
- * @typeParam Repliable - The interaction type this handler processes
1183
+ * @typeParam Event - The repliable interaction type this handler processes
1412
1184
  */
1413
- var InteractionHandler = class extends BaseHandler {
1414
- constructor(event, core, dispatch) {
1415
- super(event, core, dispatch);
1185
+ var RepliableHandler = class extends _seedcord_core.RepliableHandler {
1186
+ /** @internal */
1187
+ getEvent() {
1188
+ return this.event;
1189
+ }
1190
+ buildSender(event, _core, routeId) {
1191
+ return new ReplySender(event, routeId);
1416
1192
  }
1417
1193
  };
1418
1194
 
1419
1195
  //#endregion
1420
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1196
+ //#region src/handlers/interaction/InteractionHandler.ts
1421
1197
  /**
1422
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
1423
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
1424
- * interaction and a generated registry key always match.
1198
+ * Shared base the typed interaction handlers extend.
1425
1199
  *
1426
- * @internal
1200
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1201
+ * or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
1202
+ * share.
1203
+ *
1204
+ * @typeParam Repliable - The interaction type this handler processes
1427
1205
  */
1428
- function slashRouteOf(interaction) {
1429
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
1430
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
1431
- return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
1432
- }
1206
+ var InteractionHandler = class extends RepliableHandler {
1207
+ constructor(event, core, dispatch) {
1208
+ super(event, core, dispatch);
1209
+ }
1210
+ /**
1211
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
1212
+ * compile time (Discord forbids a modal in response to a modal).
1213
+ */
1214
+ showModal(modal) {
1215
+ return this.sender.showModal(modal);
1216
+ }
1217
+ };
1433
1218
 
1434
1219
  //#endregion
1435
1220
  //#region src/handlers/interaction/SlashHandler.ts
@@ -1465,7 +1250,7 @@ var SlashHandler = class extends InteractionHandler {
1465
1250
  }
1466
1251
  /**
1467
1252
  * Run the arm for whichever command fired. Use this only when the handler is registered for several
1468
- * routes. A single-route handler reads `this.options` directly instead, match buys nothing there.
1253
+ * routes.
1469
1254
  *
1470
1255
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1471
1256
  * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
@@ -1489,7 +1274,7 @@ var SlashHandler = class extends InteractionHandler {
1489
1274
  */
1490
1275
  async match(arms) {
1491
1276
  const route = slashRouteOf(this.event);
1492
- const arm = arms[route];
1277
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1493
1278
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SlashMatchArmMissing, [route]);
1494
1279
  return await arm(this.options);
1495
1280
  }
@@ -1560,11 +1345,11 @@ var AutocompleteHandler = class extends BaseHandler {
1560
1345
  constructor(event, core, dispatch) {
1561
1346
  super(event, core, dispatch);
1562
1347
  }
1348
+ decodedFocused;
1563
1349
  /**
1564
1350
  * The focused option for this interaction. `value` is always the raw partial string the user is typing,
1565
1351
  * even for an integer or number option, so coerce it yourself when you need a number.
1566
1352
  */
1567
- decodedFocused;
1568
1353
  get focused() {
1569
1354
  if (this.decodedFocused) return this.decodedFocused;
1570
1355
  const raw = this.event.options.getFocused(true);
@@ -1580,7 +1365,7 @@ var AutocompleteHandler = class extends BaseHandler {
1580
1365
  * match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
1581
1366
  *
1582
1367
  * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
1583
- * arm receives the focused partial value and a `respond` pinned to that field's choice value type. The
1368
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
1584
1369
  * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
1585
1370
  * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
1586
1371
  *
@@ -1602,11 +1387,15 @@ var AutocompleteHandler = class extends BaseHandler {
1602
1387
  */
1603
1388
  async match(arms) {
1604
1389
  const { name, value } = this.focused;
1605
- const respond = (choices) => this.event.respond(choices);
1606
- const arm = arms[name];
1390
+ const respond = (choices) => this.respond(choices);
1391
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1607
1392
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1608
1393
  return await arm(value, respond);
1609
1394
  }
1395
+ /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1396
+ async respond(choices) {
1397
+ await this.event.respond(choices);
1398
+ }
1610
1399
  /** The firing command route, for a field whose completion differs per registered command. */
1611
1400
  get route() {
1612
1401
  return slashRouteOf(this.event);
@@ -1614,7 +1403,7 @@ var AutocompleteHandler = class extends BaseHandler {
1614
1403
  /**
1615
1404
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
1616
1405
  * (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
1617
- * types the focused field. Read the focused field from `this.focused`, not here.
1406
+ * types the focused field. The focused field is on `this.focused`.
1618
1407
  */
1619
1408
  get options() {
1620
1409
  return this.event.options;
@@ -1626,25 +1415,25 @@ var AutocompleteHandler = class extends BaseHandler {
1626
1415
  /**
1627
1416
  * Shared base the customId-routed component handlers extend.
1628
1417
  *
1629
- * Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectMenuHandler}, or
1630
- * {@link ModalHandler} instead. This class only carries the customId decode and route-matching plumbing
1631
- * those bases share, so DO NOT use it directly.
1418
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
1419
+ * instead. This class defines the customId decode and the route matching those bases share.
1632
1420
  *
1633
1421
  * @typeParam Event - The component interaction type this handler processes
1634
1422
  * @typeParam Defs - The customId route definitions registered on the concrete handler
1635
1423
  */
1636
1424
  var ComponentHandler = class extends InteractionHandler {
1637
- get registeredDefs() {
1638
- const defs = Reflect.getMetadata(_seedcord_core_internal.ComponentDefsKey, this.constructor);
1639
- if (!defs) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdHandlerRouteMissing, [this.constructor.name]);
1640
- return defs;
1425
+ /** Rewrite the source message this component interaction came from. */
1426
+ update(response) {
1427
+ return this.sender.update(response);
1428
+ }
1429
+ /** Acknowledge the component without changing the source message. */
1430
+ deferUpdate() {
1431
+ return this.sender.deferUpdate();
1641
1432
  }
1642
1433
  decoded;
1643
1434
  get route() {
1644
- if (this.decoded) return this.decoded;
1645
- const decoded = (0, _seedcord_core_internal.decodeFor)(this.registeredDefs, this.event.customId);
1646
- this.decoded = decoded;
1647
- return decoded;
1435
+ this.decoded ??= (0, _seedcord_core_internal.decodeComponentRoute)(this.constructor, this.event.customId);
1436
+ return this.decoded;
1648
1437
  }
1649
1438
  /**
1650
1439
  * The decoded params of the single route this handler is registered for.
@@ -1663,10 +1452,11 @@ var ComponentHandler = class extends InteractionHandler {
1663
1452
  * handler `this.params` is `never`, so match is the only way to read the decoded params.
1664
1453
  *
1665
1454
  * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1666
- * decoded params. The arms must cover every registered def, a missing prefix or an unknown key is a
1667
- * compile error. Decoding runs before any arm, so a stale or corrupt wire throws before an arm body executes.
1455
+ * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1456
+ * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1457
+ * corrupt wire throws before an arm body executes.
1668
1458
  *
1669
- * @param arms - One handler per registered route, keyed by prefix.
1459
+ * @param arms - One callback per registered route, keyed by prefix.
1670
1460
  * @returns The result of the arm that ran.
1671
1461
  *
1672
1462
  * @example
@@ -1675,8 +1465,8 @@ var ComponentHandler = class extends InteractionHandler {
1675
1465
  * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1676
1466
  * async execute() {
1677
1467
  * await this.match({
1678
- * approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
1679
- * reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
1468
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1469
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1680
1470
  * });
1681
1471
  * }
1682
1472
  * }
@@ -1684,7 +1474,7 @@ var ComponentHandler = class extends InteractionHandler {
1684
1474
  */
1685
1475
  async match(arms) {
1686
1476
  const { prefix, params } = this.route;
1687
- const arm = arms[prefix];
1477
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1688
1478
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1689
1479
  return await arm(params);
1690
1480
  }
@@ -1696,8 +1486,9 @@ var ComponentHandler = class extends InteractionHandler {
1696
1486
  * Base class for a button interaction handler.
1697
1487
  *
1698
1488
  * Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
1699
- * generic, then read `this.params` for a single route or `this.match` for several. Passing different
1700
- * definitions to the decorator and the generic is a compile error.
1489
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
1490
+ * members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
1491
+ * and the generic is a compile error.
1701
1492
  *
1702
1493
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1703
1494
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1708,7 +1499,7 @@ var ComponentHandler = class extends InteractionHandler {
1708
1499
  * class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
1709
1500
  * async execute() {
1710
1501
  * const { userId } = this.params;
1711
- * await this.event.reply(`approved <@${userId}>`);
1502
+ * await this.reply(`approved <@${userId}>`);
1712
1503
  * }
1713
1504
  * }
1714
1505
  * ```
@@ -1722,7 +1513,8 @@ var ButtonHandler = class extends ComponentHandler {};
1722
1513
  *
1723
1514
  * Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
1724
1515
  * generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
1725
- * inputs from `this.event.fields`.
1516
+ * inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
1517
+ * response to a modal, so `showModal` fails compilation on this kind.
1726
1518
  *
1727
1519
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1728
1520
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1734,7 +1526,7 @@ var ButtonHandler = class extends ComponentHandler {};
1734
1526
  * async execute() {
1735
1527
  * const { guildId } = this.params;
1736
1528
  * const name = this.event.fields.getTextInputValue('name');
1737
- * await this.event.reply(`saved ${name} for ${guildId}`);
1529
+ * await this.reply(`saved ${name} for ${guildId}`);
1738
1530
  * }
1739
1531
  * }
1740
1532
  * ```
@@ -1748,7 +1540,8 @@ var ModalHandler = class extends ComponentHandler {};
1748
1540
  *
1749
1541
  * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1750
1542
  * so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
1751
- * route or `this.match` for several.
1543
+ * route or `this.match` for several, and reply through the handler members or rewrite the source message
1544
+ * with `this.update`.
1752
1545
  *
1753
1546
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1754
1547
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
@@ -1760,7 +1553,7 @@ var ModalHandler = class extends ComponentHandler {};
1760
1553
  * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1761
1554
  * async execute() {
1762
1555
  * const { roleId } = this.params;
1763
- * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1556
+ * await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1764
1557
  * }
1765
1558
  * }
1766
1559
  * ```
@@ -2026,16 +1819,16 @@ var Paginator = class {
2026
1819
  const loadPage = (ctx, n) => this.page(ctx, n);
2027
1820
  this.Handler = class Nav extends ButtonHandler {
2028
1821
  async execute() {
2029
- await this.event.deferUpdate();
1822
+ await this.deferUpdate();
2030
1823
  const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2031
- await new ReplySender(this.event).edit(this.event.message, response);
1824
+ await this.update(response);
2032
1825
  }
2033
1826
  };
2034
1827
  }
2035
1828
  /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2036
1829
  async start(interaction, core) {
2037
1830
  const response = await this.page(contextOf(interaction, core), 0);
2038
- return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
1831
+ return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
2039
1832
  }
2040
1833
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2041
1834
  async page(ctx, n) {
@@ -2159,19 +1952,184 @@ function Subscribe(subscriber, options) {
2159
1952
  };
2160
1953
  }
2161
1954
 
1955
+ //#endregion
1956
+ //#region src/subscribers/decorators/WebhookUrl.ts
1957
+ /**
1958
+ * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
1959
+ *
1960
+ * An unset variable disables the reporter with a boot warning. A set but malformed value throws
1961
+ * at boot.
1962
+ *
1963
+ * @param envKey - The environment variable name holding the webhook url
1964
+ * @decorator
1965
+ * @example
1966
+ * ```ts
1967
+ * \@Subscribe('memberBanned')
1968
+ * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
1969
+ * class BanLog extends WebhookLog<'memberBanned'> {
1970
+ * report() {
1971
+ * return { components: [new BanCard(this.data).component] };
1972
+ * }
1973
+ * }
1974
+ * ```
1975
+ */
1976
+ function WebhookUrl(envKey) {
1977
+ return function(constructor) {
1978
+ Reflect.defineMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, envKey, constructor);
1979
+ };
1980
+ }
1981
+
1982
+ //#endregion
1983
+ //#region src/miscellaneous/flagsFor.ts
1984
+ /** @internal */
1985
+ function flagsFor(ephemeral) {
1986
+ let flags = discord_api_types_v10.MessageFlags.IsComponentsV2;
1987
+ if (ephemeral) flags |= discord_api_types_v10.MessageFlags.Ephemeral;
1988
+ return flags;
1989
+ }
1990
+
1991
+ //#endregion
1992
+ //#region src/subscribers/bases/webhookHelpers.ts
1993
+ const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
1994
+ function isDiscordWebhookUrl(value) {
1995
+ return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
1996
+ }
1997
+ function jsonAttachment(name, description, data) {
1998
+ const content = (0, _seedcord_utils.filterCirculars)(data);
1999
+ return {
2000
+ name,
2001
+ description,
2002
+ data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2003
+ };
2004
+ }
2005
+ function neutralizeFences(text) {
2006
+ return text.replaceAll("```", "`​`​`");
2007
+ }
2008
+ /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2009
+ function errorReport(error) {
2010
+ let report = error.stack ?? `${error.name}: ${error.message}`;
2011
+ if (Error.isError(error.cause)) {
2012
+ const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2013
+ report += `\n\nCaused by:\n${cause}`;
2014
+ }
2015
+ const stripped = neutralizeFences((0, _seedcord_utils.stripAnsi)(report));
2016
+ return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2017
+ }
2018
+ var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
2019
+ constructor() {
2020
+ super("separator");
2021
+ this.instance.setSpacing(discord_api_types_v10.SeparatorSpacingSize.Small).setDivider(true);
2022
+ }
2023
+ };
2024
+
2025
+ //#endregion
2026
+ //#region src/subscribers/bases/WebhookSender.ts
2027
+ const HTTP_NOT_FOUND = 404;
2028
+ const HTTP_UNAUTHORIZED = 401;
2029
+ /**
2030
+ * Sends component messages to a Discord webhook through `@discordjs/rest`.
2031
+ *
2032
+ * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2033
+ */
2034
+ var WebhookSender = class {
2035
+ rest = new _discordjs_rest.REST();
2036
+ route;
2037
+ constructor(url) {
2038
+ const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2039
+ if (!match?.[1] || !match[2]) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2040
+ this.route = discord_api_types_v10.Routes.webhook(match[1], match[2]);
2041
+ }
2042
+ /**
2043
+ * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2044
+ * Discord and `unreachable` on any other failure.
2045
+ */
2046
+ async verify() {
2047
+ try {
2048
+ await this.rest.get(this.route, { auth: false });
2049
+ return "ok";
2050
+ } catch (error) {
2051
+ return error instanceof _discordjs_rest.DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2052
+ }
2053
+ }
2054
+ async send(options) {
2055
+ const { flags, username, components, files } = options;
2056
+ await this.rest.post(this.route, {
2057
+ auth: false,
2058
+ query: new URLSearchParams({
2059
+ with_components: "true",
2060
+ wait: "true"
2061
+ }),
2062
+ body: {
2063
+ flags,
2064
+ username,
2065
+ components: components.map((component) => component.toJSON()),
2066
+ ...files?.length && { attachments: files.map((file, id) => ({
2067
+ id,
2068
+ filename: file.name,
2069
+ description: file.description
2070
+ })) }
2071
+ },
2072
+ ...files?.length && { files: files.map((file) => ({
2073
+ name: file.name,
2074
+ data: file.data
2075
+ })) }
2076
+ });
2077
+ }
2078
+ };
2079
+
2162
2080
  //#endregion
2163
2081
  //#region src/subscribers/bases/WebhookLog.ts
2164
2082
  /**
2165
- * Abstract webhook logging handler for subscriber events.
2083
+ * Base class for subscribers that deliver their event to a Discord webhook.
2166
2084
  *
2167
- * Extends Subscriber to provide webhook-based logging capabilities.
2168
- * Implementations must define the webhook client to send messages to by implementing the webhook property.
2085
+ * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2086
+ * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2087
+ * the reporter is skipped at registration with a boot warning.
2169
2088
  *
2170
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
2089
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2171
2090
  */
2172
- var WebhookLog = class extends Subscriber {
2173
- constructor(data, core) {
2174
- super(data, core);
2091
+ var WebhookLog = class WebhookLog extends Subscriber {
2092
+ static senders = /* @__PURE__ */ new Map();
2093
+ async execute() {
2094
+ const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2095
+ if (url === null) {
2096
+ this.logger.warn(`${chalk.default.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2097
+ return;
2098
+ }
2099
+ const { username, components, files } = await this.report();
2100
+ try {
2101
+ await WebhookLog.senderFor(url).send({
2102
+ flags: flagsFor(false),
2103
+ username: username ?? this.constructor.name,
2104
+ components,
2105
+ files
2106
+ });
2107
+ } catch (error) {
2108
+ this.logger.error("Failed to send webhook report", error);
2109
+ }
2110
+ }
2111
+ /** @internal */
2112
+ static senderFor(url) {
2113
+ let sender = WebhookLog.senders.get(url);
2114
+ if (!sender) {
2115
+ sender = new WebhookSender(url);
2116
+ WebhookLog.senders.set(url, sender);
2117
+ }
2118
+ return sender;
2119
+ }
2120
+ /** @internal */
2121
+ static envKeyOf(ctor) {
2122
+ const envKey = Reflect.getMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, ctor);
2123
+ if (!envKey) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2124
+ return envKey;
2125
+ }
2126
+ /** @internal Returns null when the env var is unset, throws when set but malformed. */
2127
+ static urlOf(envKey) {
2128
+ if (!envapt.Envapter.has(envKey)) return null;
2129
+ const raw = envapt.Envapter.getRequired(envKey, envapt.Converters.String).trim();
2130
+ if (raw === "") return null;
2131
+ if (!isDiscordWebhookUrl(raw)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2132
+ return raw;
2175
2133
  }
2176
2134
  };
2177
2135
 
@@ -2229,7 +2187,6 @@ function slashRouteLeaves(commands) {
2229
2187
  var CommandRegistry = class {
2230
2188
  core;
2231
2189
  onDeployed;
2232
- name = "Commands";
2233
2190
  logger = new _seedcord_logger.Logger("Commands");
2234
2191
  isInitialised = false;
2235
2192
  globalCommands = [];
@@ -2425,223 +2382,56 @@ function deriveEventActor(args) {
2425
2382
  };
2426
2383
  }
2427
2384
  return {
2428
- guild: null,
2429
- member: null,
2430
- user: null,
2431
- channelId: null
2432
- };
2433
- }
2434
-
2435
- //#endregion
2436
- //#region src/bot/gates/runGates.ts
2437
- function interactionGateContext(interaction, core) {
2438
- const rawMember = interaction.member;
2439
- const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2440
- return {
2441
- kind: "interaction",
2442
- interaction,
2443
- core,
2444
- user: interaction.user,
2445
- guild: interaction.guild,
2446
- member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
2447
- userId: interaction.user.id,
2448
- guildId: interaction.guildId,
2449
- channelId: interaction.channelId,
2450
- memberRoleIds,
2451
- memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2452
- routeId: null
2453
- };
2454
- }
2455
- function eventGateContext(eventName, args, core) {
2456
- const actor = deriveEventActor(args);
2457
- return {
2458
- kind: "event",
2459
- core,
2460
- eventName,
2461
- payload: args,
2462
- user: actor.user,
2463
- guild: actor.guild,
2464
- member: actor.member,
2465
- userId: actor.user?.id ?? null,
2466
- guildId: actor.guild?.id ?? null,
2467
- channelId: actor.channelId,
2468
- memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2469
- memberPermissions: actor.member?.permissions.bitfield ?? null,
2470
- routeId: null
2471
- };
2472
- }
2473
-
2474
- //#endregion
2475
- //#region src/miscellaneous/FaultThrottle.ts
2476
- /**
2477
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
2478
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
2479
- * the map stays small, and a stale entry is evicted on the next read of its key.
2480
- *
2481
- * @internal
2482
- */
2483
- var FaultThrottle = class {
2484
- windowMs;
2485
- now;
2486
- lastReportedAt = /* @__PURE__ */ new Map();
2487
- constructor(windowMs = 6e4, now = () => Date.now()) {
2488
- this.windowMs = windowMs;
2489
- this.now = now;
2490
- }
2491
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
2492
- shouldReport(key) {
2493
- const last = this.lastReportedAt.get(key);
2494
- if (last === void 0) return true;
2495
- if (this.now() - last < this.windowMs) return false;
2496
- this.lastReportedAt.delete(key);
2497
- return true;
2498
- }
2499
- markReported(key) {
2500
- this.lastReportedAt.set(key, this.now());
2501
- }
2502
- clear() {
2503
- this.lastReportedAt.clear();
2504
- }
2505
- };
2506
-
2507
- //#endregion
2508
- //#region src/miscellaneous/extractErrorResponse.ts
2509
- const logger$2 = new _seedcord_logger.Logger("ErrorsHandling");
2510
- /**
2511
- * The single throttle for every fault publish, so a recurring fault reports once per window across both
2512
- * the interaction and event paths. Exported so tests reset it between cases.
2513
- *
2514
- * @internal
2515
- */
2516
- const faultThrottle = new FaultThrottle();
2517
- /**
2518
- * Processes an error into a user-facing reply and routes its report.
2519
- *
2520
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
2521
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
2522
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
2523
- * threaded into both the reply and the bus payload.
2524
- *
2525
- * @internal
2526
- */
2527
- function extractErrorResponse(error, core, origin) {
2528
- const uuid = node_crypto.randomUUID();
2529
- const developerUsername = core.config.notifications?.developerUsername;
2530
- const ctx = developerUsername === void 0 ? { uuid } : {
2531
- uuid,
2532
- developerUsername
2533
- };
2534
- if (error instanceof _seedcord_core.Notice) {
2535
- if (error.report) reportFault(error, core, origin, uuid);
2536
- return {
2537
- uuid,
2538
- response: error.render(ctx)
2539
- };
2540
- }
2541
- reportRawFault(error, core, origin, uuid);
2542
- const override = core.config.errors?.defaultError;
2543
- return {
2544
- uuid,
2545
- response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
2546
- };
2547
- }
2548
- function withThrottle(origin, error, publish) {
2549
- const key = faultKey(origin, error);
2550
- if (!faultThrottle.shouldReport(key)) {
2551
- logger$2.info(`throttled duplicate fault ${key}`);
2552
- return;
2553
- }
2554
- publish();
2555
- faultThrottle.markReported(key);
2556
- }
2557
- function reportFault(denial, core, origin, uuid) {
2558
- withThrottle(origin, denial, () => {
2559
- logger$2.error(`${denial.name}: ${uuid}`, denial);
2560
- if (origin.interaction) core.bus.publish("handledException", {
2561
- denial,
2562
- uuid,
2563
- source: buildInteractionSource(origin.interaction)
2564
- });
2565
- else if (origin.event) core.bus.publish("handledException", {
2566
- denial,
2567
- uuid,
2568
- source: buildEventSource(origin.event, origin)
2569
- });
2570
- else core.bus.publish("unknownException", {
2571
- uuid,
2572
- error: denial,
2573
- guild: origin.guild,
2574
- user: origin.user,
2575
- metadata: metadataFor(origin)
2576
- });
2577
- });
2578
- }
2579
- function reportRawFault(error, core, origin, uuid) {
2580
- withThrottle(origin, error, () => {
2581
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
2582
- else logger$2.error(`${uuid} | ${error.message}`);
2583
- core.bus.publish("unknownException", {
2584
- uuid,
2585
- error,
2586
- guild: origin.guild,
2587
- user: origin.user,
2588
- metadata: metadataFor(origin)
2589
- });
2590
- });
2591
- }
2592
- function metadataFor(origin) {
2593
- if (origin.event) return {
2594
- eventName: origin.event.name,
2595
- handler: origin.event.handler,
2596
- args: origin.event.args
2597
- };
2598
- return origin.metadata;
2599
- }
2600
- function buildEventSource(event, origin) {
2601
- return {
2602
- kind: "event",
2603
- eventName: event.name,
2604
- handler: event.handler,
2605
- userId: origin.user?.id ?? null,
2606
- guildId: origin.guild?.id ?? null,
2607
- channelId: event.channelId,
2608
- raw: event.args
2385
+ guild: null,
2386
+ member: null,
2387
+ user: null,
2388
+ channelId: null
2609
2389
  };
2610
2390
  }
2611
- function faultKey(origin, error) {
2612
- const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
2613
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
2614
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
2615
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
2616
- return `autocomplete:${name}`;
2617
- }
2618
- function interactionRoute(interaction) {
2619
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
2620
- if (interaction.isContextMenuCommand()) return interaction.commandName;
2621
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
2622
- return "interaction";
2623
- }
2624
- function buildInteractionSource(interaction) {
2625
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
2626
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
2391
+
2392
+ //#endregion
2393
+ //#region src/bot/gates/runGates.ts
2394
+ function interactionGateContext(interaction, core) {
2395
+ const rawMember = interaction.member;
2396
+ const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2627
2397
  return {
2628
2398
  kind: "interaction",
2629
- interactionKind: interactionKind(interaction),
2630
- command,
2631
- customId,
2399
+ interaction,
2400
+ core,
2401
+ user: interaction.user,
2402
+ guild: interaction.guild,
2403
+ member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
2632
2404
  userId: interaction.user.id,
2633
2405
  guildId: interaction.guildId,
2634
2406
  channelId: interaction.channelId,
2635
- interactionId: interaction.id,
2636
- raw: interaction
2407
+ memberRoleIds,
2408
+ memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2409
+ appPermissions: interaction.appPermissions.bitfield,
2410
+ memberGuildPermissions: rawMember instanceof discord_js.GuildMember ? rawMember.permissions.bitfield : null,
2411
+ appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
2412
+ routeId: null
2637
2413
  };
2638
2414
  }
2639
- function interactionKind(interaction) {
2640
- if (interaction.isChatInputCommand()) return "slash";
2641
- if (interaction.isContextMenuCommand()) return "context-menu";
2642
- if (interaction.isButton()) return "button";
2643
- if (interaction.isAnySelectMenu()) return "select";
2644
- return "modal";
2415
+ function eventGateContext(eventName, args, core) {
2416
+ const actor = deriveEventActor(args);
2417
+ return {
2418
+ kind: "event",
2419
+ core,
2420
+ eventName,
2421
+ payload: args,
2422
+ user: actor.user,
2423
+ guild: actor.guild,
2424
+ member: actor.member,
2425
+ userId: actor.user?.id ?? null,
2426
+ guildId: actor.guild?.id ?? null,
2427
+ channelId: actor.channelId,
2428
+ memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2429
+ memberPermissions: actor.member?.permissions.bitfield ?? null,
2430
+ appPermissions: null,
2431
+ memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
2432
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
2433
+ routeId: null
2434
+ };
2645
2435
  }
2646
2436
 
2647
2437
  //#endregion
@@ -2656,7 +2446,7 @@ const logger$1 = new _seedcord_logger.Logger("EventBoundary");
2656
2446
  */
2657
2447
  function handleEventFault(caught, eventName, handlerName, args, core) {
2658
2448
  if (caught instanceof _seedcord_core.Silence) {
2659
- if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
2449
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2660
2450
  return;
2661
2451
  }
2662
2452
  if (!Error.isError(caught)) throw caught;
@@ -2692,11 +2482,12 @@ var EventDispatcher = class {
2692
2482
  core;
2693
2483
  logger = new _seedcord_logger.Logger("Events");
2694
2484
  isInitialized = false;
2695
- name = "Events";
2696
2485
  eventMap = /* @__PURE__ */ new Map();
2697
2486
  middlewares = [];
2698
2487
  executedOnceHandlers = /* @__PURE__ */ new Set();
2699
2488
  attachedEvents = /* @__PURE__ */ new Set();
2489
+ inFlight = /* @__PURE__ */ new Set();
2490
+ draining = false;
2700
2491
  loading = false;
2701
2492
  loadedHandlers = [];
2702
2493
  loadedMiddlewares = [];
@@ -2846,7 +2637,7 @@ var EventDispatcher = class {
2846
2637
  };
2847
2638
  if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
2848
2639
  else {
2849
- const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
2640
+ const names = (0, _seedcord_core_internal.areRoutes)(raw) ? raw : typeof raw === "string" ? [raw] : [];
2850
2641
  for (const name of names) register(name, "on");
2851
2642
  }
2852
2643
  const from = (0, _seedcord_utils.formatFilePath)(relativePath);
@@ -2865,15 +2656,22 @@ var EventDispatcher = class {
2865
2656
  const handlerEntries = this.eventMap.get(eventName);
2866
2657
  this.logger.debug(`Attaching ${_seedcord_logger.paint.mint.bold(eventName)} to the client with ${_seedcord_logger.paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2867
2658
  this.core.bot.client.on(eventName, (...args) => {
2659
+ if (this.draining) return;
2868
2660
  this.core.bot.emitSafe("any:event", eventName, ...args);
2869
- (async () => {
2870
- await this.processEvent(eventName, args).catch((err) => {
2871
- this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2872
- this.core.bot.emit("error:unhandled:event", err);
2873
- });
2874
- })();
2661
+ const run = this.processEvent(eventName, args).catch((err) => {
2662
+ this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2663
+ this.core.bot.emit("error:unhandled:event", err);
2664
+ });
2665
+ this.inFlight.add(run);
2666
+ run.finally(() => this.inFlight.delete(run));
2875
2667
  });
2876
2668
  }
2669
+ stopAccepting() {
2670
+ this.draining = true;
2671
+ }
2672
+ drain(timeoutMs) {
2673
+ return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
2674
+ }
2877
2675
  async processEvent(eventName, args) {
2878
2676
  const handlerEntries = this.eventMap.get(eventName);
2879
2677
  if (!handlerEntries || handlerEntries.length === 0) return;
@@ -2901,35 +2699,55 @@ var EventDispatcher = class {
2901
2699
  };
2902
2700
 
2903
2701
  //#endregion
2904
- //#region src/bot/defaults/UnhandledEvent.ts
2702
+ //#region src/bot/defaults/UnhandledRepliable.ts
2905
2703
  /**
2906
- * Default handler for unhandled interactions. A reply failure here rides the controller boundary as a
2907
- * raw fault, so it needs no decorator of its own.
2908
- *
2909
- * @internal
2704
+ * @internal Default handler for unhandled repliable interactions.
2910
2705
  */
2911
- var UnhandledEvent = class extends InteractionHandler {
2706
+ var UnhandledRepliable = class extends InteractionHandler {
2912
2707
  async execute() {
2913
- await this.event.reply({
2914
- content: `Feature not implemented yet.`,
2915
- flags: discord_js.MessageFlags.Ephemeral
2916
- });
2708
+ await this.reply("Feature not implemented yet.");
2709
+ }
2710
+ };
2711
+
2712
+ //#endregion
2713
+ //#region src/bot/defaults/UnhandledAutocomplete.ts
2714
+ /**
2715
+ * @internal Default handler for an autocomplete with no registered handler.
2716
+ */
2717
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
2718
+ async execute() {
2719
+ await this.respond([]);
2917
2720
  }
2918
2721
  };
2919
2722
 
2723
+ //#endregion
2724
+ //#region src/bot/harmlessApiCodes.ts
2725
+ /**
2726
+ * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2727
+ * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2728
+ *
2729
+ * @internal
2730
+ */
2731
+ const HARMLESS_API_CODES = new Set([
2732
+ discord_js.RESTJSONErrorCodes.UnknownInteraction,
2733
+ discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2734
+ discord_js.RESTJSONErrorCodes.UnknownMessage
2735
+ ]);
2736
+
2920
2737
  //#endregion
2921
2738
  //#region src/bot/handleInteractionFault.ts
2922
2739
  const logger = new _seedcord_logger.Logger("InteractionBoundary");
2923
2740
  /**
2924
2741
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2925
- * user reply plus the right bus event, building a {@link ReplySender} lazily so it reads the live
2926
- * acknowledgement state at catch time.
2742
+ * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
2743
+ * sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
2744
+ * boundary builds one from the interaction flags.
2927
2745
  *
2928
2746
  * @internal
2929
2747
  */
2930
- async function handleInteractionFault(caught, interaction, core) {
2748
+ async function handleInteractionFault(caught, interaction, core, sender) {
2931
2749
  if (caught instanceof _seedcord_core.Silence) {
2932
- if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
2750
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2933
2751
  return;
2934
2752
  }
2935
2753
  if (!Error.isError(caught)) throw caught;
@@ -2953,7 +2771,18 @@ async function handleInteractionFault(caught, interaction, core) {
2953
2771
  user: interaction.user,
2954
2772
  metadata: interaction
2955
2773
  });
2956
- await new ReplySender(interaction).send(response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2774
+ await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2775
+ }
2776
+ async function sendGuarded(sender, response, ephemeral) {
2777
+ try {
2778
+ await sender.send(response, { ephemeral });
2779
+ } catch (error) {
2780
+ if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2781
+ logger.debug(`reply send hit harmless code ${error.code}`);
2782
+ return;
2783
+ }
2784
+ logger.error("reply send failed", error);
2785
+ }
2957
2786
  }
2958
2787
 
2959
2788
  //#endregion
@@ -2968,7 +2797,6 @@ var InteractionDispatcher = class {
2968
2797
  core;
2969
2798
  logger = new _seedcord_logger.Logger("Interactions");
2970
2799
  isInitialized = false;
2971
- name = "Interactions";
2972
2800
  slashMap = /* @__PURE__ */ new Map();
2973
2801
  buttonMap = /* @__PURE__ */ new Map();
2974
2802
  modalMap = /* @__PURE__ */ new Map();
@@ -2982,6 +2810,8 @@ var InteractionDispatcher = class {
2982
2810
  autocompleteMap = /* @__PURE__ */ new Map();
2983
2811
  keysToIgnore = /* @__PURE__ */ new Set();
2984
2812
  middlewares = [];
2813
+ inFlight = /* @__PURE__ */ new Set();
2814
+ draining = false;
2985
2815
  loading = false;
2986
2816
  loadedHandlers = [];
2987
2817
  loadedMiddlewares = [];
@@ -3022,7 +2852,7 @@ var InteractionDispatcher = class {
3022
2852
  }
3023
2853
  /**
3024
2854
  * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
3025
- * UnhandledEvent at runtime. A bot may route commands outside the registry, so this does not throw.
2855
+ * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
3026
2856
  *
3027
2857
  * @internal
3028
2858
  */
@@ -3030,7 +2860,7 @@ var InteractionDispatcher = class {
3030
2860
  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.`);
3031
2861
  }
3032
2862
  /**
3033
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
2863
+ * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
3034
2864
  * at runtime. Checked per kind since a user and a message command can share a name.
3035
2865
  *
3036
2866
  * @internal
@@ -3043,7 +2873,7 @@ var InteractionDispatcher = class {
3043
2873
  const artifacts = [];
3044
2874
  for (const [routeType] of this.routeTypes) {
3045
2875
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3046
- if (areRoutes(meta)) artifacts.push({
2876
+ if ((0, _seedcord_core_internal.areRoutes)(meta)) artifacts.push({
3047
2877
  routeType,
3048
2878
  routes: meta
3049
2879
  });
@@ -3138,7 +2968,7 @@ var InteractionDispatcher = class {
3138
2968
  const writes = [];
3139
2969
  for (const [routeType, map] of this.routeTypes) {
3140
2970
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3141
- if (!areRoutes(meta)) continue;
2971
+ if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
3142
2972
  for (const route of meta) {
3143
2973
  const existing = map.get(route);
3144
2974
  if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
@@ -3172,7 +3002,7 @@ var InteractionDispatcher = class {
3172
3002
  }
3173
3003
  for (const [routeType, map] of this.routeTypes) {
3174
3004
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3175
- if (!areRoutes(meta)) continue;
3005
+ if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
3176
3006
  meta.forEach((route) => map.delete(route));
3177
3007
  }
3178
3008
  }
@@ -3182,35 +3012,53 @@ var InteractionDispatcher = class {
3182
3012
  }
3183
3013
  attachToClient() {
3184
3014
  this.core.bot.client.on(discord_js.Events.InteractionCreate, (interaction) => {
3015
+ if (this.draining) return;
3185
3016
  this.core.bot.emitSafe("any:interaction", interaction);
3186
- this.handleInteraction(interaction).catch((err) => {
3017
+ const run = this.handleInteraction(interaction).catch((err) => {
3187
3018
  this.logger.error(`[${_seedcord_logger.paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3188
3019
  this.core.bot.emit("error:unhandled:interaction", err);
3189
3020
  });
3021
+ this.inFlight.add(run);
3022
+ run.finally(() => this.inFlight.delete(run));
3190
3023
  });
3191
3024
  }
3025
+ stopAccepting() {
3026
+ this.draining = true;
3027
+ }
3028
+ drain(timeoutMs) {
3029
+ return (0, _seedcord_core_node_internal.settleWithin)(Promise.allSettled(this.inFlight), timeoutMs);
3030
+ }
3192
3031
  async handleCustomIdInteraction(interaction, getMap, interactionType) {
3193
3032
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3194
3033
  const prefix = (0, _seedcord_core_internal.prefixOf)(interaction.customId);
3195
3034
  if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3196
3035
  await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
3197
3036
  }
3198
- async processInteraction(interaction, extractKey, getHandler) {
3037
+ async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
3199
3038
  const key = extractKey(interaction);
3039
+ let sender;
3200
3040
  try {
3201
3041
  if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3202
3042
  let HandlerCtor = getHandler(key);
3203
3043
  if (!HandlerCtor) {
3204
- this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to UnhandledEvent.`);
3205
- HandlerCtor = UnhandledEvent;
3044
+ this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3045
+ HandlerCtor = fallback;
3206
3046
  }
3207
3047
  this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
3208
3048
  const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor));
3209
3049
  const handler = new HandlerCtor(interaction, this.core, dispatch);
3210
- if (!interaction.isAutocomplete()) await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core));
3050
+ if (handler instanceof RepliableHandler) sender = handler.getSender();
3051
+ if (!interaction.isAutocomplete()) {
3052
+ const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
3053
+ try {
3054
+ await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3055
+ } finally {
3056
+ monitor?.report(dispatch.routeId);
3057
+ }
3058
+ }
3211
3059
  await handler.execute();
3212
3060
  } catch (caught) {
3213
- await handleInteractionFault(caught, interaction, this.core);
3061
+ await handleInteractionFault(caught, interaction, this.core, sender);
3214
3062
  }
3215
3063
  }
3216
3064
  async handleInteraction(interaction) {
@@ -3286,12 +3134,7 @@ var InteractionDispatcher = class {
3286
3134
  }
3287
3135
  async handleAutocomplete(interaction) {
3288
3136
  const route = slashRouteOf(interaction);
3289
- if (!this.autocompleteMap.has(route)) {
3290
- this.logger.warn(`No autocomplete handler for ${_seedcord_logger.paint.sky.bold(route)}.`);
3291
- await interaction.respond([]);
3292
- return;
3293
- }
3294
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
3137
+ await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3295
3138
  }
3296
3139
  };
3297
3140
 
@@ -3312,23 +3155,25 @@ function __decorate(decorators, target, key, desc) {
3312
3155
 
3313
3156
  //#endregion
3314
3157
  //#region src/bot/Bot.ts
3158
+ const DISPATCH_DRAIN_TIMEOUT_MS = 5e3;
3159
+ const DRAIN_TASK_TIMEOUT_MS = 6e3;
3160
+ const UNBIND_TIMEOUT_MS = 2e3;
3161
+ const LOGOUT_TIMEOUT_MS = 2e3;
3315
3162
  /**
3316
- * Discord bot implementation that manages client and controllers
3317
- *
3318
- * Don't instantiate this class directly. Use `core.bot` instead.
3163
+ * The Discord client and its controllers. Access it through `core.bot`.
3319
3164
  */
3320
- var Bot = class extends Plugin {
3165
+ var Bot = class extends _seedcord_event_emitter.TypedEventEmitter {
3321
3166
  /** @internal */
3322
3167
  logger = new _seedcord_logger.Logger("Bot");
3323
3168
  isInitialized = false;
3324
3169
  _client;
3325
3170
  interactions;
3326
3171
  events;
3327
- commands;
3328
3172
  emojiInjector;
3173
+ commands;
3329
3174
  emojis = Emojis;
3330
3175
  mentions = CommandMentions;
3331
- /** @internal For use in dev mode */
3176
+ /** @internal */
3332
3177
  async onHmr(event) {
3333
3178
  if (this.interactions) await this.interactions.onHmr(event);
3334
3179
  if (this.events) await this.events.onHmr(event);
@@ -3336,7 +3181,7 @@ var Bot = class extends Plugin {
3336
3181
  }
3337
3182
  /** @internal */
3338
3183
  constructor(core) {
3339
- super(core);
3184
+ super();
3340
3185
  this._client = new discord_js.Client(core.config.bot.clientOptions);
3341
3186
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3342
3187
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
@@ -3347,12 +3192,24 @@ var Bot = class extends Plugin {
3347
3192
  });
3348
3193
  }
3349
3194
  this.emojiInjector = new EmojiInjector(core);
3350
- core.shutdown.addTask(_seedcord_services.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3195
+ this.registerShutdownTasks(core);
3351
3196
  }
3352
- /**
3353
- * Initializes Discord client and all controllers
3354
- * @internal
3355
- */
3197
+ registerShutdownTasks(core) {
3198
+ core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Unbind, "stop-dispatch", () => {
3199
+ this.stopAccepting();
3200
+ return Promise.resolve();
3201
+ }, UNBIND_TIMEOUT_MS);
3202
+ core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Drain, "drain-dispatch", () => this.drain(), DRAIN_TASK_TIMEOUT_MS);
3203
+ core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Logout, "stop-bot", async () => await this.stop(), LOGOUT_TIMEOUT_MS);
3204
+ }
3205
+ stopAccepting() {
3206
+ this.interactions?.stopAccepting();
3207
+ this.events?.stopAccepting();
3208
+ }
3209
+ async drain() {
3210
+ await Promise.allSettled([this.interactions?.drain(DISPATCH_DRAIN_TIMEOUT_MS), this.events?.drain(DISPATCH_DRAIN_TIMEOUT_MS)]);
3211
+ }
3212
+ /** @internal */
3356
3213
  async init() {
3357
3214
  if (this.isInitialized) return;
3358
3215
  this.isInitialized = true;
@@ -3368,25 +3225,18 @@ var Bot = class extends Plugin {
3368
3225
  this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
3369
3226
  }
3370
3227
  }
3371
- /**
3372
- * Stops the bot and cleans up connections
3373
- * @internal
3374
- */
3228
+ /** @internal */
3375
3229
  async stop() {
3376
3230
  this._client.removeAllListeners();
3377
3231
  await this.logout();
3378
3232
  }
3379
- /**
3380
- * Logs the bot into Discord using the configured token
3381
- */
3382
3233
  async login(token) {
3383
- await this._client.login(token);
3234
+ this._client.once(discord_js.Events.ClientReady, () => this.emit("ready"));
3235
+ this._client.login(token);
3236
+ await this.waitFor("ready");
3384
3237
  this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
3385
3238
  return this;
3386
3239
  }
3387
- /**
3388
- * Logs out and destroys the Discord client connection
3389
- */
3390
3240
  async logout() {
3391
3241
  await this._client.destroy();
3392
3242
  this.logger.info(chalk.default.bold.red("Logged out of Discord!"));
@@ -3394,6 +3244,12 @@ var Bot = class extends Plugin {
3394
3244
  get client() {
3395
3245
  return this._client;
3396
3246
  }
3247
+ /**
3248
+ * The overloads enforce the key/args correlation at compile time only. At runtime this forwards
3249
+ * straight to the underlying TypedEventEmitter.
3250
+ *
3251
+ * @internal
3252
+ */
3397
3253
  emit(event, ...args) {
3398
3254
  return super.emit(event, ...args);
3399
3255
  }
@@ -3406,157 +3262,70 @@ var Bot = class extends Plugin {
3406
3262
  };
3407
3263
  __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3408
3264
 
3409
- //#endregion
3410
- //#region src/subscribers/bases/webhookHelpers.ts
3411
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
3412
- /**
3413
- * Whether a string is a well-formed Discord webhook URL.
3414
- *
3415
- * @internal
3416
- */
3417
- function isDiscordWebhookUrl(value) {
3418
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
3419
- }
3420
- /**
3421
- * Serializes a value to a JSON attachment, made circular-safe first.
3422
- *
3423
- * @internal
3424
- */
3425
- function jsonAttachment(name, description, data) {
3426
- const content = (0, _seedcord_utils.filterCirculars)(data);
3427
- return new discord_js.AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
3428
- name,
3429
- description
3430
- });
3431
- }
3432
- /**
3433
- * The small divider the default webhook reporters place between sections.
3434
- *
3435
- * @internal
3436
- */
3437
- var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
3438
- constructor() {
3439
- super("separator");
3440
- this.instance.setSpacing(discord_js.SeparatorSpacingSize.Small).setDivider(true);
3441
- }
3442
- };
3443
-
3444
3265
  //#endregion
3445
3266
  //#region src/subscribers/default/HandledException.ts
3446
- var _HandledException;
3447
- function webhookUrlValidator$1(raw) {
3448
- if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3449
- const value = raw?.trim() ?? "";
3450
- if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
3451
- if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3452
- return value;
3453
- }
3454
3267
  let HandledException = class HandledException extends WebhookLog {
3455
- static {
3456
- _HandledException = this;
3457
- }
3458
- webhook = new discord_js.WebhookClient({ url: _HandledException.handledExceptionWebhookUrl });
3459
- async execute() {
3460
- const { source } = this.data;
3461
- try {
3462
- await this.webhook.send({
3463
- flags: flagsFor(false),
3464
- withComponents: true,
3465
- username: "Handled Exception",
3466
- components: [new HandledExceptionContainer(this.data).component],
3467
- files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
3468
- });
3469
- } catch (error) {
3470
- this.logger.error("Failed to send handled exception webhook", error);
3471
- }
3268
+ report() {
3269
+ return {
3270
+ username: "Handled Exception",
3271
+ components: [new HandledExceptionContainer(this.data).component],
3272
+ files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3273
+ };
3472
3274
  }
3473
3275
  };
3474
- __decorate([(0, envapt_legacy.Envapt)("HANDLED_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator$1(raw) }), __decorateMetadata("design:type", String)], HandledException, "handledExceptionWebhookUrl", void 0);
3475
- HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
3276
+ HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3476
3277
  var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
3477
3278
  constructor(data) {
3478
3279
  super("container");
3479
3280
  const { denial, uuid, source } = data;
3480
- 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"));
3281
+ this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3481
3282
  }
3482
3283
  };
3483
- /**
3484
- * The summary block of a fault report, narrowed per source kind so an interaction fault shows its
3485
- * command and customId while an event fault shows its event name and handler.
3486
- *
3487
- * @internal
3488
- */
3489
3284
  function faultSummary(denial, source) {
3490
- const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
3285
+ const head = (0, _seedcord_utils.stripAnsi)(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3491
3286
  if (source.kind === "event") return `${head}**Event:** ${source.eventName}\n**Handler:** ${source.handler}\n**User ID:** \`${source.userId ?? "n/a"}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\``;
3492
3287
  return `${head}**Kind:** ${source.interactionKind}\n**Command:** ${source.command ?? "n/a"}\n**CustomId:** ${source.customId ?? "n/a"}\n**User ID:** \`${source.userId}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\`\n**Interaction ID:** \`${source.interactionId}\``;
3493
3288
  }
3494
- /**
3495
- * The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
3496
- * circular object) so building the report never throws.
3497
- *
3498
- * @internal
3499
- */
3500
3289
  function causeStack(denial) {
3501
3290
  const { cause } = denial;
3502
- if (Error.isError(cause)) return cause.stack ?? cause.message;
3503
- if (typeof cause === "string") return cause;
3291
+ if (Error.isError(cause)) return (0, _seedcord_utils.stripAnsi)(cause.stack ?? cause.message);
3292
+ if (typeof cause === "string") return (0, _seedcord_utils.stripAnsi)(cause);
3504
3293
  if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3505
3294
  if (cause !== void 0) try {
3506
3295
  return JSON.stringify(cause);
3507
3296
  } catch {
3508
3297
  return "[unserializable cause]";
3509
3298
  }
3510
- return denial.stack ?? "No cause recorded.";
3299
+ return (0, _seedcord_utils.stripAnsi)(denial.stack ?? "No cause recorded.");
3511
3300
  }
3512
3301
 
3513
3302
  //#endregion
3514
3303
  //#region src/subscribers/default/UnknownException.ts
3515
- var _UnknownException;
3516
- function webhookUrlValidator(raw) {
3517
- if (raw !== null && typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3518
- const value = raw?.trim() ?? "";
3519
- if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
3520
- if (!isDiscordWebhookUrl(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3521
- return value;
3522
- }
3523
3304
  let UnknownException = class UnknownException extends WebhookLog {
3524
- static {
3525
- _UnknownException = this;
3526
- }
3527
- webhook = new discord_js.WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
3528
- async execute() {
3305
+ report() {
3529
3306
  const { metadata } = this.data;
3530
- const metadataFile = metadata ? jsonAttachment("metadata.json", "Metadata associated with the error", metadata) : null;
3531
- try {
3532
- await this.webhook.send({
3533
- flags: flagsFor(false),
3534
- withComponents: true,
3535
- username: "Unknown Exception",
3536
- components: [new UnhandledErrorContainer(this.data).component],
3537
- files: metadataFile ? [metadataFile] : []
3538
- });
3539
- } catch (error) {
3540
- this.logger.error("Failed to send unknown exception webhook", error);
3541
- }
3307
+ return {
3308
+ username: "Unknown Exception",
3309
+ components: [new UnhandledErrorContainer(this.data).component],
3310
+ files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3311
+ };
3542
3312
  }
3543
3313
  };
3544
- __decorate([(0, envapt_legacy.Envapt)("UNKNOWN_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator(raw) }), __decorateMetadata("design:type", String)], UnknownException, "unknownExceptionWebhookUrl", void 0);
3545
- UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
3314
+ UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3546
3315
  var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3547
3316
  constructor(data) {
3548
3317
  super("container");
3549
3318
  const { uuid, error, guild, user, metadata } = data;
3550
- 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}\`\`\``));
3319
+ this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
3551
3320
  this.addTimestampsIfAvailable(error);
3552
3321
  this.addMetadataIfAvailable(metadata);
3553
3322
  }
3554
3323
  addTimestampsIfAvailable(error) {
3555
- if (!(error instanceof discord_js.DiscordAPIError)) return;
3324
+ if (!(error instanceof _discordjs_rest.DiscordAPIError)) return;
3556
3325
  const now = Date.now();
3557
3326
  const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3558
3327
  if (!snowflake) return void 0;
3559
- const interactionTs = Number(discord_js.SnowflakeUtil.deconstruct(snowflake).timestamp);
3328
+ const interactionTs = (0, _seedcord_utils.timestampFromSnowflake)(snowflake);
3560
3329
  const diff = now - interactionTs;
3561
3330
  const seconds = Math.floor(diff / 1e3);
3562
3331
  const millis = diff % 1e3;
@@ -3571,24 +3340,19 @@ var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3571
3340
  //#endregion
3572
3341
  //#region src/subscribers/Bus.ts
3573
3342
  /**
3574
- * Manages application subscribers and event handling
3575
- *
3576
- * Provides a centralized system for registering and executing custom subscribers
3577
- * throughout the application lifecycle. Bus subscribers are loaded from configured directories
3578
- * and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
3579
- * construct it directly.
3343
+ * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3344
+ * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3580
3345
  */
3581
- var Bus = class extends Plugin {
3346
+ var Bus = class extends _seedcord_event_emitter.TypedEventEmitter {
3582
3347
  core;
3583
3348
  logger = new _seedcord_logger.Logger("Subscribers");
3584
- /** @internal */
3585
- name = "Subscribers";
3586
3349
  isInitialized = false;
3587
3350
  subscribersMap = /* @__PURE__ */ new Map();
3588
3351
  executedOnceHandlers = /* @__PURE__ */ new Set();
3352
+ webhookProbes = /* @__PURE__ */ new Map();
3589
3353
  hmrHandler;
3590
3354
  constructor(core) {
3591
- super(core);
3355
+ super();
3592
3356
  this.core = core;
3593
3357
  if (this.core.config.subscribers.path) {
3594
3358
  if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
@@ -3611,7 +3375,6 @@ var Bus = class extends Plugin {
3611
3375
  this.isInitialized = true;
3612
3376
  this.registerSubscriber(UnknownException);
3613
3377
  this.registerSubscriber(HandledException);
3614
- envapt.Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
3615
3378
  const subscribersDir = this.core.config.subscribers.path;
3616
3379
  if (subscribersDir) {
3617
3380
  this.logger.info(chalk.default.bold(subscribersDir));
@@ -3619,8 +3382,19 @@ var Bus = class extends Plugin {
3619
3382
  const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3620
3383
  this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
3621
3384
  }
3385
+ if (!envapt.Envapter.isTest) await this.verifyWebhooks();
3386
+ }
3387
+ /** @internal Exposed for tests. */
3388
+ async verifyWebhooks() {
3389
+ const missing = [];
3390
+ await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3391
+ const result = await WebhookLog.senderFor(url).verify();
3392
+ if (result === "missing") missing.push(...envKeys);
3393
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${_seedcord_logger.paint.sky.bold(envKeys.join(", "))}`);
3394
+ }));
3395
+ if (missing.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3622
3396
  }
3623
- /** @internal For use in dev mode */
3397
+ /** @internal */
3624
3398
  async onHmr(event) {
3625
3399
  if (this.hmrHandler) await this.hmrHandler.handle(event);
3626
3400
  }
@@ -3637,6 +3411,17 @@ var Bus = class extends Plugin {
3637
3411
  }, this.logger);
3638
3412
  }
3639
3413
  registerSubscriber(handler) {
3414
+ if (handler.prototype instanceof WebhookLog) {
3415
+ const envKey = WebhookLog.envKeyOf(handler);
3416
+ const url = WebhookLog.urlOf(envKey);
3417
+ if (url === null) {
3418
+ this.logger.warn(`${chalk.default.bold(handler.name)} disabled, ${_seedcord_logger.paint.sky.bold(envKey)} is not set`);
3419
+ return;
3420
+ }
3421
+ const envKeys = this.webhookProbes.get(url) ?? [];
3422
+ if (!envKeys.includes(envKey)) envKeys.push(envKey);
3423
+ this.webhookProbes.set(url, envKeys);
3424
+ }
3640
3425
  const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
3641
3426
  let handlers = this.subscribersMap.get(options.subscriber);
3642
3427
  if (!handlers) {
@@ -3696,17 +3481,15 @@ var Bus = class extends Plugin {
3696
3481
  //#endregion
3697
3482
  //#region src/version.ts
3698
3483
  /** Package version */
3699
- const version = "0.1.0-next.2";
3484
+ const version = "0.1.0-next.4";
3700
3485
 
3701
3486
  //#endregion
3702
3487
  //#region src/Seedcord.ts
3703
3488
  /**
3704
- * Main Seedcord bot framework class
3705
- *
3706
- * Primary entry point for creating Discord bots with Seedcord.
3707
- * Manages component lifecycle and provides plugin support.
3489
+ * The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
3490
+ * coordinated startup and shutdown. Attach plugins with `attach()`.
3708
3491
  */
3709
- var Seedcord = class Seedcord extends Pluggable {
3492
+ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3710
3493
  config;
3711
3494
  /** @internal */
3712
3495
  [_seedcord_types_internal.SeedcordBrand] = true;
@@ -3714,7 +3497,6 @@ var Seedcord = class Seedcord extends Pluggable {
3714
3497
  augmentTarget = "@seedcord/gateway";
3715
3498
  /** @internal */
3716
3499
  version = version;
3717
- static isInstantiated = false;
3718
3500
  healthCheck;
3719
3501
  hmrManager;
3720
3502
  /** @see {@link CoordinatedShutdown} */
@@ -3728,16 +3510,12 @@ var Seedcord = class Seedcord extends Pluggable {
3728
3510
  /** @see {@link IRateLimiter} */
3729
3511
  rateLimiter;
3730
3512
  /**
3731
- * Creates a new Seedcord instance
3732
- *
3733
3513
  * @param config - Bot configuration including paths and Discord client options
3734
3514
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3735
3515
  */
3736
3516
  constructor(config) {
3737
- if (Seedcord.isInstantiated) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreSingletonViolation);
3738
- Seedcord.isInstantiated = true;
3739
- const shutdown = new _seedcord_services.CoordinatedShutdown(config.shutdownEnabled);
3740
- const startup = new _seedcord_services.CoordinatedStartup();
3517
+ const shutdown = new _seedcord_core_node_internal.CoordinatedShutdown();
3518
+ const startup = new _seedcord_core_node_internal.CoordinatedStartup();
3741
3519
  super(shutdown, startup);
3742
3520
  this.config = config;
3743
3521
  (0, _seedcord_logger_node.installNodeDefaults)(config.logger);
@@ -3749,46 +3527,60 @@ var Seedcord = class Seedcord extends Pluggable {
3749
3527
  this.bus = new Bus(this);
3750
3528
  this.bot = new Bot(this);
3751
3529
  this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
3752
- this.healthCheck = new _seedcord_services.HealthCheck(this.shutdown, config.healthCheck);
3530
+ this.healthCheck = _seedcord_core_node_internal.HealthCheck.fromOption(this.shutdown, config.healthCheck);
3753
3531
  this.registerStartupTasks();
3754
3532
  }
3755
3533
  /** The bot's discord username, populated after login. */
3756
3534
  get username() {
3757
3535
  return this.bot.client.user?.username;
3758
3536
  }
3537
+ pluginCapabilities() {
3538
+ return {
3539
+ client: this.bot.client,
3540
+ token: this.bot.botToken,
3541
+ rest: this.bot.client.rest
3542
+ };
3543
+ }
3759
3544
  static reset() {
3760
- Seedcord.isInstantiated = false;
3545
+ super.reset();
3761
3546
  _seedcord_logger.LoggerChannelRegistry.instance.reset();
3762
3547
  }
3763
3548
  registerStartupTasks() {
3764
3549
  if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
3765
- this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "Bus Initialization", async () => {
3550
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "Bus Initialization", async () => {
3766
3551
  this.bus.logger.utils.initialization("Subscribers", "start");
3767
3552
  await this.bus.init();
3768
3553
  this.bus.logger.utils.initialization("Subscribers", "end");
3769
3554
  });
3770
- this.startup.addTask(_seedcord_services.StartupPhase.Instantiation, "Bot Initialization", async () => {
3555
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Login, "Bot Initialization", async () => {
3771
3556
  this.bot.logger.utils.initialization("Bot", "start");
3772
3557
  await this.bot.init();
3773
3558
  this.bot.logger.utils.initialization("Bot", "end");
3774
3559
  });
3775
- this.startup.addTask(_seedcord_services.StartupPhase.Ready, "Health Check", async () => {
3776
- this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3777
- await this.healthCheck.init();
3778
- this.healthCheck.logger.utils.initialization("HealthCheck", "end");
3560
+ const { healthCheck } = this;
3561
+ if (healthCheck) this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Ready, "Health Check", async () => {
3562
+ healthCheck.logger.utils.initialization("HealthCheck", "start");
3563
+ await healthCheck.init();
3564
+ healthCheck.logger.utils.initialization("HealthCheck", "end");
3779
3565
  });
3780
3566
  }
3781
3567
  /**
3782
3568
  * Starts the bot and runs all initialization tasks
3783
3569
  *
3784
- * @returns This Seedcord instance when fully initialized
3570
+ * @returns This Seedcord instance when initialized
3785
3571
  */
3786
3572
  async start() {
3787
- await super.init();
3573
+ try {
3574
+ await super.init();
3575
+ } catch (caught) {
3576
+ await this.shutdown.run(1, false);
3577
+ Seedcord.reset();
3578
+ throw caught;
3579
+ }
3788
3580
  return this;
3789
3581
  }
3790
3582
  registerHmrAwareModules() {
3791
- this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "HMR Registration", async () => {
3583
+ this.startup.addTask(_seedcord_core_node_internal.StartupPhase.Configuration, "HMR Registration", async () => {
3792
3584
  this.hmrManager.register(this.bot);
3793
3585
  this.hmrManager.register(this.bus);
3794
3586
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
@@ -3800,12 +3592,9 @@ var Seedcord = class Seedcord extends Pluggable {
3800
3592
  //#endregion
3801
3593
  exports.ArraySource = ArraySource;
3802
3594
  exports.AutocompleteHandler = AutocompleteHandler;
3803
- exports.AutocompleteRoute = AutocompleteRoute;
3804
3595
  exports.ButtonHandler = ButtonHandler;
3805
- exports.ButtonRoute = ButtonRoute;
3806
3596
  exports.CommandMentions = CommandMentions;
3807
3597
  exports.ContextMenuHandler = ContextMenuHandler;
3808
- exports.ContextMenuRoute = ContextMenuRoute;
3809
3598
  exports.CursorSource = CursorSource;
3810
3599
  exports.Emojis = Emojis;
3811
3600
  exports.EventHandler = EventHandler;
@@ -3817,31 +3606,17 @@ exports.InteractionMiddleware = InteractionMiddleware;
3817
3606
  exports.Middleware = Middleware;
3818
3607
  exports.MiddlewareType = MiddlewareType;
3819
3608
  exports.ModalHandler = ModalHandler;
3820
- exports.ModalRoute = ModalRoute;
3821
3609
  exports.Nsfw = Nsfw;
3822
3610
  exports.Paginator = Paginator;
3823
- exports.Pluggable = Pluggable;
3824
- exports.Plugin = Plugin;
3825
- Object.defineProperty(exports, 'RegisterCommand', {
3826
- enumerable: true,
3827
- get: function () {
3828
- return _seedcord_core.RegisterCommand;
3829
- }
3830
- });
3831
3611
  exports.RegisterEvent = RegisterEvent;
3832
- exports.ReplySender = ReplySender;
3833
- exports.RequireBotPermissions = RequireBotPermissions;
3834
- exports.RequirePermissions = RequirePermissions;
3835
- exports.RequireRole = RequireRole;
3612
+ exports.RepliableHandler = RepliableHandler;
3836
3613
  exports.Seedcord = Seedcord;
3837
3614
  exports.SelectMenuHandler = SelectMenuHandler;
3838
- exports.SelectMenuKind = SelectMenuKind;
3839
- exports.SelectMenuRoute = SelectMenuRoute;
3840
3615
  exports.SlashHandler = SlashHandler;
3841
- exports.SlashRoute = SlashRoute;
3842
3616
  exports.Subscribe = Subscribe;
3843
3617
  exports.Subscriber = Subscriber;
3844
3618
  exports.WebhookLog = WebhookLog;
3619
+ exports.WebhookUrl = WebhookUrl;
3845
3620
  exports.checkBotPermissions = checkBotPermissions;
3846
3621
  exports.checkPermissions = checkPermissions;
3847
3622
  exports.fetchGuildMember = fetchGuildMember;
@@ -3890,13 +3665,6 @@ Object.keys(_seedcord_rate_limiter).forEach(function (k) {
3890
3665
  });
3891
3666
  });
3892
3667
 
3893
- Object.keys(_seedcord_services).forEach(function (k) {
3894
- if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
3895
- enumerable: true,
3896
- get: function () { return _seedcord_services[k]; }
3897
- });
3898
- });
3899
-
3900
3668
  Object.keys(_seedcord_utils).forEach(function (k) {
3901
3669
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
3902
3670
  enumerable: true,