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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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");
38
+ let discord_js = require("discord.js");
39
+ let node_crypto = require("node:crypto");
40
+ node_crypto = __toESM(node_crypto, 1);
40
41
  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");
45
42
  let chalk = require("chalk");
46
43
  chalk = __toESM(chalk, 1);
44
+ let envapt = require("envapt");
45
+ let discord_api_types_v10 = require("discord-api-types/v10");
46
+ let _seedcord_utils = require("@seedcord/utils");
47
+ let _discordjs_rest = require("@discordjs/rest");
48
+ let _seedcord_core_node = require("@seedcord/core/node");
49
+ let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
50
+ let _seedcord_types_internal = require("@seedcord/types/internal");
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
@@ -1079,113 +772,308 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
1079
772
  }
1080
773
 
1081
774
  //#endregion
1082
- //#region src/bot/harmlessApiCodes.ts
775
+ //#region src/bot/ReplySender.ts
776
+ function seedState(interaction) {
777
+ if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
778
+ if (interaction.replied) return "replied";
779
+ if (interaction.deferred) return "deferred-reply";
780
+ return "unacked";
781
+ }
1083
782
  /**
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
783
+ * Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
784
+ * handler bases and the dispatcher.
1089
785
  */
1090
- const HARMLESS_API_CODES = new Set([
1091
- discord_js.RESTJSONErrorCodes.UnknownInteraction,
1092
- discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1093
- discord_js.RESTJSONErrorCodes.UnknownMessage
1094
- ]);
786
+ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
787
+ interaction;
788
+ constructor(interaction, routeId) {
789
+ super(routeId, seedState(interaction));
790
+ this.interaction = interaction;
791
+ }
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);
798
+ }
799
+ async writeDefer(opts) {
800
+ await this.interaction.deferReply({ flags: (0, _seedcord_core_internal.deferFlags)(opts) });
801
+ }
802
+ async writeDeferUpdate() {
803
+ await this.sourceInteraction("deferUpdate").deferUpdate();
804
+ }
805
+ async writeUpdate(response) {
806
+ const result = await this.sourceInteraction("update").update({
807
+ ...this.updateOptions(response),
808
+ withResponse: true
809
+ });
810
+ return this.createdMessage(result);
811
+ }
812
+ async writeFollowUp(response, opts) {
813
+ return await this.interaction.followUp(this.replyOptions(response, (0, _seedcord_core_internal.sendFlags)(opts)));
814
+ }
815
+ async writeEditOriginal(response) {
816
+ return await this.interaction.editReply(this.editOptions(response));
817
+ }
818
+ async writeEditTarget(targetId, response) {
819
+ return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
820
+ }
821
+ async writeDeleteOriginal() {
822
+ await this.interaction.deleteReply();
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);
851
+ return {
852
+ components: reply.components,
853
+ flags,
854
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
855
+ ...reply.files && { files: [...reply.files] }
856
+ };
857
+ }
858
+ editOptions(response) {
859
+ const reply = this.serialize(response);
860
+ return {
861
+ components: reply.components,
862
+ flags: discord_js.MessageFlags.IsComponentsV2,
863
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
864
+ ...reply.files && { files: [...reply.files] }
865
+ };
866
+ }
867
+ updateOptions(response) {
868
+ const reply = this.serialize(response);
869
+ return {
870
+ components: reply.components,
871
+ flags: discord_js.MessageFlags.IsComponentsV2,
872
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
873
+ ...reply.files && { files: [...reply.files] }
874
+ };
875
+ }
876
+ createdMessage(result) {
877
+ return result.resource?.message ?? void 0;
878
+ }
879
+ };
1095
880
 
1096
881
  //#endregion
1097
- //#region src/miscellaneous/flagsFor.ts
882
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1098
883
  /**
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.
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.
1104
887
  *
1105
888
  * @internal
1106
889
  */
1107
- function flagsFor(ephemeral) {
1108
- let flags = discord_js.MessageFlags.IsComponentsV2;
1109
- if (ephemeral) flags |= discord_js.MessageFlags.Ephemeral;
1110
- return flags;
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);
1111
894
  }
1112
895
 
1113
896
  //#endregion
1114
- //#region src/bot/ReplySender.ts
897
+ //#region src/miscellaneous/FaultThrottle.ts
1115
898
  /**
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.
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
1119
904
  */
1120
- var ReplySender = class {
1121
- interaction;
1122
- logger = new _seedcord_logger.Logger("ReplySender");
1123
- constructor(interaction) {
1124
- this.interaction = interaction;
1125
- }
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
- }
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;
1141
912
  }
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
- }
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;
1155
920
  }
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();
921
+ markReported(key) {
922
+ this.lastReportedAt.set(key, this.now());
1164
923
  }
1165
- replyOptions(response, ephemeral) {
1166
- return {
1167
- components: response.components,
1168
- flags: flagsFor(ephemeral),
1169
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1170
- ...response.files && { files: response.files }
1171
- };
924
+ clear() {
925
+ this.lastReportedAt.clear();
1172
926
  }
1173
- editBody(response) {
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);
1174
950
  return {
1175
- components: response.components,
1176
- flags: discord_js.MessageFlags.IsComponentsV2,
1177
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1178
- ...response.files && { files: response.files }
951
+ uuid,
952
+ response: error.render(ctx)
1179
953
  };
1180
954
  }
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);
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;
1187
967
  }
1188
- };
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
+ }
1189
1077
 
1190
1078
  //#endregion
1191
1079
  //#region src/bot/confirm/reserved.ts
@@ -1231,9 +1119,9 @@ async function collectChoice(message, userId, timeoutMs) {
1231
1119
  return null;
1232
1120
  }
1233
1121
  }
1234
- async function settle(sender, interaction, message, outcome) {
1122
+ async function settle(sender, message, outcome) {
1235
1123
  if (outcome === void 0) {
1236
- await interaction.deleteReply(message).catch(() => void 0);
1124
+ await sender.delete(message).catch(() => void 0);
1237
1125
  return;
1238
1126
  }
1239
1127
  await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
@@ -1241,17 +1129,16 @@ async function settle(sender, interaction, message, outcome) {
1241
1129
  async function getConfirmation(interaction, prompt, options) {
1242
1130
  const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1243
1131
  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;
1132
+ const sender = new ReplySender(interaction, interactionRoute(interaction));
1133
+ const message = await sender.send(response, { ephemeral });
1247
1134
  const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1248
1135
  if (!winner) {
1249
- await settle(sender, interaction, message, options?.onTimeout);
1136
+ await settle(sender, message, options?.onTimeout);
1250
1137
  return false;
1251
1138
  }
1252
1139
  await winner.deferUpdate().catch(() => void 0);
1253
1140
  const confirmed = winner.customId === CONFIRM_IDS.confirm;
1254
- await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
1141
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1255
1142
  return confirmed;
1256
1143
  }
1257
1144
 
@@ -1364,19 +1251,10 @@ var Pluggable = class Pluggable extends _seedcord_event_emitter.TypedEventEmitte
1364
1251
  //#endregion
1365
1252
  //#region src/handlers/BaseHandler.ts
1366
1253
  /**
1367
- * Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
1254
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1255
+ * gate hook.
1368
1256
  */
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
- }
1257
+ var BaseHandler = class extends _seedcord_core.BaseHandler {
1380
1258
  /** @internal */
1381
1259
  getEvent() {
1382
1260
  return this.event;
@@ -1400,36 +1278,49 @@ var InteractionMiddleware = class extends BaseHandler {
1400
1278
  };
1401
1279
 
1402
1280
  //#endregion
1403
- //#region src/handlers/interaction/InteractionHandler.ts
1281
+ //#region src/handlers/RepliableHandler.ts
1404
1282
  /**
1405
- * Shared base the typed interaction handlers extend.
1283
+ * Shared base the repliable interaction handlers extend.
1406
1284
  *
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.
1285
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1286
+ * {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
1287
+ * to the gateway sender.
1410
1288
  *
1411
- * @typeParam Repliable - The interaction type this handler processes
1289
+ * @typeParam Event - The repliable interaction type this handler processes
1412
1290
  */
1413
- var InteractionHandler = class extends BaseHandler {
1414
- constructor(event, core, dispatch) {
1415
- super(event, core, dispatch);
1291
+ var RepliableHandler = class extends _seedcord_core.RepliableHandler {
1292
+ /** @internal */
1293
+ getEvent() {
1294
+ return this.event;
1295
+ }
1296
+ buildSender(event, _core, routeId) {
1297
+ return new ReplySender(event, routeId);
1416
1298
  }
1417
1299
  };
1418
1300
 
1419
1301
  //#endregion
1420
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1302
+ //#region src/handlers/interaction/InteractionHandler.ts
1421
1303
  /**
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.
1304
+ * Shared base the typed interaction handlers extend.
1425
1305
  *
1426
- * @internal
1306
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1307
+ * or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
1308
+ * share.
1309
+ *
1310
+ * @typeParam Repliable - The interaction type this handler processes
1427
1311
  */
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
- }
1312
+ var InteractionHandler = class extends RepliableHandler {
1313
+ constructor(event, core, dispatch) {
1314
+ super(event, core, dispatch);
1315
+ }
1316
+ /**
1317
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
1318
+ * compile time (Discord forbids a modal in response to a modal).
1319
+ */
1320
+ showModal(modal) {
1321
+ return this.sender.showModal(modal);
1322
+ }
1323
+ };
1433
1324
 
1434
1325
  //#endregion
1435
1326
  //#region src/handlers/interaction/SlashHandler.ts
@@ -1465,7 +1356,7 @@ var SlashHandler = class extends InteractionHandler {
1465
1356
  }
1466
1357
  /**
1467
1358
  * 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.
1359
+ * routes.
1469
1360
  *
1470
1361
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1471
1362
  * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
@@ -1489,7 +1380,7 @@ var SlashHandler = class extends InteractionHandler {
1489
1380
  */
1490
1381
  async match(arms) {
1491
1382
  const route = slashRouteOf(this.event);
1492
- const arm = arms[route];
1383
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1493
1384
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.SlashMatchArmMissing, [route]);
1494
1385
  return await arm(this.options);
1495
1386
  }
@@ -1560,11 +1451,11 @@ var AutocompleteHandler = class extends BaseHandler {
1560
1451
  constructor(event, core, dispatch) {
1561
1452
  super(event, core, dispatch);
1562
1453
  }
1454
+ decodedFocused;
1563
1455
  /**
1564
1456
  * The focused option for this interaction. `value` is always the raw partial string the user is typing,
1565
1457
  * even for an integer or number option, so coerce it yourself when you need a number.
1566
1458
  */
1567
- decodedFocused;
1568
1459
  get focused() {
1569
1460
  if (this.decodedFocused) return this.decodedFocused;
1570
1461
  const raw = this.event.options.getFocused(true);
@@ -1580,7 +1471,7 @@ var AutocompleteHandler = class extends BaseHandler {
1580
1471
  * match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
1581
1472
  *
1582
1473
  * 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
1474
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
1584
1475
  * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
1585
1476
  * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
1586
1477
  *
@@ -1602,11 +1493,15 @@ var AutocompleteHandler = class extends BaseHandler {
1602
1493
  */
1603
1494
  async match(arms) {
1604
1495
  const { name, value } = this.focused;
1605
- const respond = (choices) => this.event.respond(choices);
1606
- const arm = arms[name];
1496
+ const respond = (choices) => this.respond(choices);
1497
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1607
1498
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1608
1499
  return await arm(value, respond);
1609
1500
  }
1501
+ /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1502
+ async respond(choices) {
1503
+ await this.event.respond(choices);
1504
+ }
1610
1505
  /** The firing command route, for a field whose completion differs per registered command. */
1611
1506
  get route() {
1612
1507
  return slashRouteOf(this.event);
@@ -1614,7 +1509,7 @@ var AutocompleteHandler = class extends BaseHandler {
1614
1509
  /**
1615
1510
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
1616
1511
  * (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.
1512
+ * types the focused field. The focused field is on `this.focused`.
1618
1513
  */
1619
1514
  get options() {
1620
1515
  return this.event.options;
@@ -1626,25 +1521,25 @@ var AutocompleteHandler = class extends BaseHandler {
1626
1521
  /**
1627
1522
  * Shared base the customId-routed component handlers extend.
1628
1523
  *
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.
1524
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
1525
+ * instead. This class defines the customId decode and the route matching those bases share.
1632
1526
  *
1633
1527
  * @typeParam Event - The component interaction type this handler processes
1634
1528
  * @typeParam Defs - The customId route definitions registered on the concrete handler
1635
1529
  */
1636
1530
  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;
1531
+ /** Rewrite the source message this component interaction came from. */
1532
+ update(response) {
1533
+ return this.sender.update(response);
1534
+ }
1535
+ /** Acknowledge the component without changing the source message. */
1536
+ deferUpdate() {
1537
+ return this.sender.deferUpdate();
1641
1538
  }
1642
1539
  decoded;
1643
1540
  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;
1541
+ this.decoded ??= (0, _seedcord_core_internal.decodeComponentRoute)(this.constructor, this.event.customId);
1542
+ return this.decoded;
1648
1543
  }
1649
1544
  /**
1650
1545
  * The decoded params of the single route this handler is registered for.
@@ -1663,10 +1558,11 @@ var ComponentHandler = class extends InteractionHandler {
1663
1558
  * handler `this.params` is `never`, so match is the only way to read the decoded params.
1664
1559
  *
1665
1560
  * 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.
1561
+ * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1562
+ * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1563
+ * corrupt wire throws before an arm body executes.
1668
1564
  *
1669
- * @param arms - One handler per registered route, keyed by prefix.
1565
+ * @param arms - One callback per registered route, keyed by prefix.
1670
1566
  * @returns The result of the arm that ran.
1671
1567
  *
1672
1568
  * @example
@@ -1675,8 +1571,8 @@ var ComponentHandler = class extends InteractionHandler {
1675
1571
  * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1676
1572
  * async execute() {
1677
1573
  * await this.match({
1678
- * approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
1679
- * reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
1574
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1575
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1680
1576
  * });
1681
1577
  * }
1682
1578
  * }
@@ -1684,7 +1580,7 @@ var ComponentHandler = class extends InteractionHandler {
1684
1580
  */
1685
1581
  async match(arms) {
1686
1582
  const { prefix, params } = this.route;
1687
- const arm = arms[prefix];
1583
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1688
1584
  if (!arm) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1689
1585
  return await arm(params);
1690
1586
  }
@@ -1696,8 +1592,9 @@ var ComponentHandler = class extends InteractionHandler {
1696
1592
  * Base class for a button interaction handler.
1697
1593
  *
1698
1594
  * 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.
1595
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
1596
+ * members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
1597
+ * and the generic is a compile error.
1701
1598
  *
1702
1599
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1703
1600
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1708,7 +1605,7 @@ var ComponentHandler = class extends InteractionHandler {
1708
1605
  * class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
1709
1606
  * async execute() {
1710
1607
  * const { userId } = this.params;
1711
- * await this.event.reply(`approved <@${userId}>`);
1608
+ * await this.reply(`approved <@${userId}>`);
1712
1609
  * }
1713
1610
  * }
1714
1611
  * ```
@@ -1722,7 +1619,8 @@ var ButtonHandler = class extends ComponentHandler {};
1722
1619
  *
1723
1620
  * Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
1724
1621
  * generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
1725
- * inputs from `this.event.fields`.
1622
+ * inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
1623
+ * response to a modal, so `showModal` fails compilation on this kind.
1726
1624
  *
1727
1625
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1728
1626
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1734,7 +1632,7 @@ var ButtonHandler = class extends ComponentHandler {};
1734
1632
  * async execute() {
1735
1633
  * const { guildId } = this.params;
1736
1634
  * const name = this.event.fields.getTextInputValue('name');
1737
- * await this.event.reply(`saved ${name} for ${guildId}`);
1635
+ * await this.reply(`saved ${name} for ${guildId}`);
1738
1636
  * }
1739
1637
  * }
1740
1638
  * ```
@@ -1748,7 +1646,8 @@ var ModalHandler = class extends ComponentHandler {};
1748
1646
  *
1749
1647
  * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1750
1648
  * 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.
1649
+ * route or `this.match` for several, and reply through the handler members or rewrite the source message
1650
+ * with `this.update`.
1752
1651
  *
1753
1652
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1754
1653
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
@@ -1760,7 +1659,7 @@ var ModalHandler = class extends ComponentHandler {};
1760
1659
  * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1761
1660
  * async execute() {
1762
1661
  * const { roleId } = this.params;
1763
- * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1662
+ * await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1764
1663
  * }
1765
1664
  * }
1766
1665
  * ```
@@ -2026,16 +1925,16 @@ var Paginator = class {
2026
1925
  const loadPage = (ctx, n) => this.page(ctx, n);
2027
1926
  this.Handler = class Nav extends ButtonHandler {
2028
1927
  async execute() {
2029
- await this.event.deferUpdate();
1928
+ await this.deferUpdate();
2030
1929
  const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2031
- await new ReplySender(this.event).edit(this.event.message, response);
1930
+ await this.update(response);
2032
1931
  }
2033
1932
  };
2034
1933
  }
2035
1934
  /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2036
1935
  async start(interaction, core) {
2037
1936
  const response = await this.page(contextOf(interaction, core), 0);
2038
- return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
1937
+ return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
2039
1938
  }
2040
1939
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2041
1940
  async page(ctx, n) {
@@ -2159,19 +2058,184 @@ function Subscribe(subscriber, options) {
2159
2058
  };
2160
2059
  }
2161
2060
 
2061
+ //#endregion
2062
+ //#region src/subscribers/decorators/WebhookUrl.ts
2063
+ /**
2064
+ * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
2065
+ *
2066
+ * An unset variable disables the reporter with a boot warning. A set but malformed value throws
2067
+ * at boot.
2068
+ *
2069
+ * @param envKey - The environment variable name holding the webhook url
2070
+ * @decorator
2071
+ * @example
2072
+ * ```ts
2073
+ * \@Subscribe('memberBanned')
2074
+ * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
2075
+ * class BanLog extends WebhookLog<'memberBanned'> {
2076
+ * report() {
2077
+ * return { components: [new BanCard(this.data).component] };
2078
+ * }
2079
+ * }
2080
+ * ```
2081
+ */
2082
+ function WebhookUrl(envKey) {
2083
+ return function(constructor) {
2084
+ Reflect.defineMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, envKey, constructor);
2085
+ };
2086
+ }
2087
+
2088
+ //#endregion
2089
+ //#region src/miscellaneous/flagsFor.ts
2090
+ /** @internal */
2091
+ function flagsFor(ephemeral) {
2092
+ let flags = discord_api_types_v10.MessageFlags.IsComponentsV2;
2093
+ if (ephemeral) flags |= discord_api_types_v10.MessageFlags.Ephemeral;
2094
+ return flags;
2095
+ }
2096
+
2097
+ //#endregion
2098
+ //#region src/subscribers/bases/webhookHelpers.ts
2099
+ const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
2100
+ function isDiscordWebhookUrl(value) {
2101
+ return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
2102
+ }
2103
+ function jsonAttachment(name, description, data) {
2104
+ const content = (0, _seedcord_utils.filterCirculars)(data);
2105
+ return {
2106
+ name,
2107
+ description,
2108
+ data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2109
+ };
2110
+ }
2111
+ function neutralizeFences(text) {
2112
+ return text.replaceAll("```", "`​`​`");
2113
+ }
2114
+ /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2115
+ function errorReport(error) {
2116
+ let report = error.stack ?? `${error.name}: ${error.message}`;
2117
+ if (Error.isError(error.cause)) {
2118
+ const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2119
+ report += `\n\nCaused by:\n${cause}`;
2120
+ }
2121
+ const stripped = neutralizeFences((0, _seedcord_utils.stripAnsi)(report));
2122
+ return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2123
+ }
2124
+ var WebhookSeparator = class extends _seedcord_core.BuilderComponent {
2125
+ constructor() {
2126
+ super("separator");
2127
+ this.instance.setSpacing(discord_api_types_v10.SeparatorSpacingSize.Small).setDivider(true);
2128
+ }
2129
+ };
2130
+
2131
+ //#endregion
2132
+ //#region src/subscribers/bases/WebhookSender.ts
2133
+ const HTTP_NOT_FOUND = 404;
2134
+ const HTTP_UNAUTHORIZED = 401;
2135
+ /**
2136
+ * Sends component messages to a Discord webhook through `@discordjs/rest`.
2137
+ *
2138
+ * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2139
+ */
2140
+ var WebhookSender = class {
2141
+ rest = new _discordjs_rest.REST();
2142
+ route;
2143
+ constructor(url) {
2144
+ const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2145
+ if (!match?.[1] || !match[2]) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2146
+ this.route = discord_api_types_v10.Routes.webhook(match[1], match[2]);
2147
+ }
2148
+ /**
2149
+ * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2150
+ * Discord and `unreachable` on any other failure.
2151
+ */
2152
+ async verify() {
2153
+ try {
2154
+ await this.rest.get(this.route, { auth: false });
2155
+ return "ok";
2156
+ } catch (error) {
2157
+ return error instanceof _discordjs_rest.DiscordAPIError && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2158
+ }
2159
+ }
2160
+ async send(options) {
2161
+ const { flags, username, components, files } = options;
2162
+ await this.rest.post(this.route, {
2163
+ auth: false,
2164
+ query: new URLSearchParams({
2165
+ with_components: "true",
2166
+ wait: "true"
2167
+ }),
2168
+ body: {
2169
+ flags,
2170
+ username,
2171
+ components: components.map((component) => component.toJSON()),
2172
+ ...files?.length && { attachments: files.map((file, id) => ({
2173
+ id,
2174
+ filename: file.name,
2175
+ description: file.description
2176
+ })) }
2177
+ },
2178
+ ...files?.length && { files: files.map((file) => ({
2179
+ name: file.name,
2180
+ data: file.data
2181
+ })) }
2182
+ });
2183
+ }
2184
+ };
2185
+
2162
2186
  //#endregion
2163
2187
  //#region src/subscribers/bases/WebhookLog.ts
2164
2188
  /**
2165
- * Abstract webhook logging handler for subscriber events.
2189
+ * Base class for subscribers that deliver their event to a Discord webhook.
2166
2190
  *
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.
2191
+ * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2192
+ * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2193
+ * the reporter is skipped at registration with a boot warning.
2169
2194
  *
2170
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
2195
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2171
2196
  */
2172
- var WebhookLog = class extends Subscriber {
2173
- constructor(data, core) {
2174
- super(data, core);
2197
+ var WebhookLog = class WebhookLog extends Subscriber {
2198
+ static senders = /* @__PURE__ */ new Map();
2199
+ async execute() {
2200
+ const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2201
+ if (url === null) {
2202
+ this.logger.warn(`${chalk.default.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2203
+ return;
2204
+ }
2205
+ const { username, components, files } = await this.report();
2206
+ try {
2207
+ await WebhookLog.senderFor(url).send({
2208
+ flags: flagsFor(false),
2209
+ username: username ?? this.constructor.name,
2210
+ components,
2211
+ files
2212
+ });
2213
+ } catch (error) {
2214
+ this.logger.error("Failed to send webhook report", error);
2215
+ }
2216
+ }
2217
+ /** @internal */
2218
+ static senderFor(url) {
2219
+ let sender = WebhookLog.senders.get(url);
2220
+ if (!sender) {
2221
+ sender = new WebhookSender(url);
2222
+ WebhookLog.senders.set(url, sender);
2223
+ }
2224
+ return sender;
2225
+ }
2226
+ /** @internal */
2227
+ static envKeyOf(ctor) {
2228
+ const envKey = Reflect.getMetadata(_seedcord_core_internal.WebhookUrlMetadataKey, ctor);
2229
+ if (!envKey) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2230
+ return envKey;
2231
+ }
2232
+ /** @internal Returns null when the env var is unset, throws when set but malformed. */
2233
+ static urlOf(envKey) {
2234
+ if (!envapt.Envapter.has(envKey)) return null;
2235
+ const raw = envapt.Envapter.getRequired(envKey, envapt.Converters.String).trim();
2236
+ if (raw === "") return null;
2237
+ if (!isDiscordWebhookUrl(raw)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2238
+ return raw;
2175
2239
  }
2176
2240
  };
2177
2241
 
@@ -2402,246 +2466,79 @@ var CommandRegistry = class {
2402
2466
 
2403
2467
  //#endregion
2404
2468
  //#region src/miscellaneous/deriveEventActor.ts
2405
- function deriveEventActor(args) {
2406
- const tuple = Array.isArray(args) ? args : [args];
2407
- for (const arg of tuple) {
2408
- if (arg instanceof discord_js.Message) return {
2409
- guild: arg.guild,
2410
- member: arg.member,
2411
- user: arg.author,
2412
- channelId: arg.channelId
2413
- };
2414
- if (arg instanceof discord_js.GuildMember) return {
2415
- guild: arg.guild,
2416
- member: arg,
2417
- user: arg.user,
2418
- channelId: null
2419
- };
2420
- if (arg instanceof discord_js.User) return {
2421
- guild: null,
2422
- member: null,
2423
- user: arg,
2424
- channelId: null
2425
- };
2426
- }
2427
- 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)
2469
+ function deriveEventActor(args) {
2470
+ const tuple = Array.isArray(args) ? args : [args];
2471
+ for (const arg of tuple) {
2472
+ if (arg instanceof discord_js.Message) return {
2473
+ guild: arg.guild,
2474
+ member: arg.member,
2475
+ user: arg.author,
2476
+ channelId: arg.channelId
2477
+ };
2478
+ if (arg instanceof discord_js.GuildMember) return {
2479
+ guild: arg.guild,
2480
+ member: arg,
2481
+ user: arg.user,
2482
+ channelId: null
2483
+ };
2484
+ if (arg instanceof discord_js.User) return {
2485
+ guild: null,
2486
+ member: null,
2487
+ user: arg,
2488
+ channelId: null
2539
2489
  };
2540
2490
  }
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
2491
  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
2492
+ guild: null,
2493
+ member: null,
2494
+ user: null,
2495
+ channelId: null
2609
2496
  };
2610
2497
  }
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;
2498
+
2499
+ //#endregion
2500
+ //#region src/bot/gates/runGates.ts
2501
+ function interactionGateContext(interaction, core) {
2502
+ const rawMember = interaction.member;
2503
+ const memberRoleIds = rawMember instanceof discord_js.GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2627
2504
  return {
2628
2505
  kind: "interaction",
2629
- interactionKind: interactionKind(interaction),
2630
- command,
2631
- customId,
2506
+ interaction,
2507
+ core,
2508
+ user: interaction.user,
2509
+ guild: interaction.guild,
2510
+ member: rawMember instanceof discord_js.GuildMember ? rawMember : null,
2632
2511
  userId: interaction.user.id,
2633
2512
  guildId: interaction.guildId,
2634
2513
  channelId: interaction.channelId,
2635
- interactionId: interaction.id,
2636
- raw: interaction
2514
+ memberRoleIds,
2515
+ memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2516
+ appPermissions: interaction.appPermissions.bitfield,
2517
+ memberGuildPermissions: rawMember instanceof discord_js.GuildMember ? rawMember.permissions.bitfield : null,
2518
+ appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
2519
+ routeId: null
2637
2520
  };
2638
2521
  }
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";
2522
+ function eventGateContext(eventName, args, core) {
2523
+ const actor = deriveEventActor(args);
2524
+ return {
2525
+ kind: "event",
2526
+ core,
2527
+ eventName,
2528
+ payload: args,
2529
+ user: actor.user,
2530
+ guild: actor.guild,
2531
+ member: actor.member,
2532
+ userId: actor.user?.id ?? null,
2533
+ guildId: actor.guild?.id ?? null,
2534
+ channelId: actor.channelId,
2535
+ memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2536
+ memberPermissions: actor.member?.permissions.bitfield ?? null,
2537
+ appPermissions: null,
2538
+ memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
2539
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
2540
+ routeId: null
2541
+ };
2645
2542
  }
2646
2543
 
2647
2544
  //#endregion
@@ -2656,7 +2553,7 @@ const logger$1 = new _seedcord_logger.Logger("EventBoundary");
2656
2553
  */
2657
2554
  function handleEventFault(caught, eventName, handlerName, args, core) {
2658
2555
  if (caught instanceof _seedcord_core.Silence) {
2659
- if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
2556
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2660
2557
  return;
2661
2558
  }
2662
2559
  if (!Error.isError(caught)) throw caught;
@@ -2846,7 +2743,7 @@ var EventDispatcher = class {
2846
2743
  };
2847
2744
  if (Array.isArray(raw)) for (const entry of raw) register(entry.event, entry.frequency);
2848
2745
  else {
2849
- const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
2746
+ const names = (0, _seedcord_core_internal.areRoutes)(raw) ? raw : typeof raw === "string" ? [raw] : [];
2850
2747
  for (const name of names) register(name, "on");
2851
2748
  }
2852
2749
  const from = (0, _seedcord_utils.formatFilePath)(relativePath);
@@ -2901,35 +2798,55 @@ var EventDispatcher = class {
2901
2798
  };
2902
2799
 
2903
2800
  //#endregion
2904
- //#region src/bot/defaults/UnhandledEvent.ts
2801
+ //#region src/bot/defaults/UnhandledRepliable.ts
2905
2802
  /**
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
2803
+ * @internal Default handler for unhandled repliable interactions.
2910
2804
  */
2911
- var UnhandledEvent = class extends InteractionHandler {
2805
+ var UnhandledRepliable = class extends InteractionHandler {
2912
2806
  async execute() {
2913
- await this.event.reply({
2914
- content: `Feature not implemented yet.`,
2915
- flags: discord_js.MessageFlags.Ephemeral
2916
- });
2807
+ await this.reply("Feature not implemented yet.");
2808
+ }
2809
+ };
2810
+
2811
+ //#endregion
2812
+ //#region src/bot/defaults/UnhandledAutocomplete.ts
2813
+ /**
2814
+ * @internal Default handler for an autocomplete with no registered handler.
2815
+ */
2816
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
2817
+ async execute() {
2818
+ await this.respond([]);
2917
2819
  }
2918
2820
  };
2919
2821
 
2822
+ //#endregion
2823
+ //#region src/bot/harmlessApiCodes.ts
2824
+ /**
2825
+ * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2826
+ * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2827
+ *
2828
+ * @internal
2829
+ */
2830
+ const HARMLESS_API_CODES = new Set([
2831
+ discord_js.RESTJSONErrorCodes.UnknownInteraction,
2832
+ discord_js.RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2833
+ discord_js.RESTJSONErrorCodes.UnknownMessage
2834
+ ]);
2835
+
2920
2836
  //#endregion
2921
2837
  //#region src/bot/handleInteractionFault.ts
2922
2838
  const logger = new _seedcord_logger.Logger("InteractionBoundary");
2923
2839
  /**
2924
2840
  * 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.
2841
+ * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
2842
+ * sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
2843
+ * boundary builds one from the interaction flags.
2927
2844
  *
2928
2845
  * @internal
2929
2846
  */
2930
- async function handleInteractionFault(caught, interaction, core) {
2847
+ async function handleInteractionFault(caught, interaction, core, sender) {
2931
2848
  if (caught instanceof _seedcord_core.Silence) {
2932
- if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
2849
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2933
2850
  return;
2934
2851
  }
2935
2852
  if (!Error.isError(caught)) throw caught;
@@ -2953,7 +2870,18 @@ async function handleInteractionFault(caught, interaction, core) {
2953
2870
  user: interaction.user,
2954
2871
  metadata: interaction
2955
2872
  });
2956
- await new ReplySender(interaction).send(response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2873
+ await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof _seedcord_core.Notice ? caught.ephemeral : true);
2874
+ }
2875
+ async function sendGuarded(sender, response, ephemeral) {
2876
+ try {
2877
+ await sender.send(response, { ephemeral });
2878
+ } catch (error) {
2879
+ if (error instanceof discord_js.DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2880
+ logger.debug(`reply send hit harmless code ${error.code}`);
2881
+ return;
2882
+ }
2883
+ logger.error("reply send failed", error);
2884
+ }
2957
2885
  }
2958
2886
 
2959
2887
  //#endregion
@@ -3022,7 +2950,7 @@ var InteractionDispatcher = class {
3022
2950
  }
3023
2951
  /**
3024
2952
  * 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.
2953
+ * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
3026
2954
  *
3027
2955
  * @internal
3028
2956
  */
@@ -3030,7 +2958,7 @@ var InteractionDispatcher = class {
3030
2958
  for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${_seedcord_logger.paint.sky.bold(route)} has no registered ${chalk.default.bold("@SlashRoute")} handler.`);
3031
2959
  }
3032
2960
  /**
3033
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
2961
+ * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
3034
2962
  * at runtime. Checked per kind since a user and a message command can share a name.
3035
2963
  *
3036
2964
  * @internal
@@ -3043,7 +2971,7 @@ var InteractionDispatcher = class {
3043
2971
  const artifacts = [];
3044
2972
  for (const [routeType] of this.routeTypes) {
3045
2973
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3046
- if (areRoutes(meta)) artifacts.push({
2974
+ if ((0, _seedcord_core_internal.areRoutes)(meta)) artifacts.push({
3047
2975
  routeType,
3048
2976
  routes: meta
3049
2977
  });
@@ -3138,7 +3066,7 @@ var InteractionDispatcher = class {
3138
3066
  const writes = [];
3139
3067
  for (const [routeType, map] of this.routeTypes) {
3140
3068
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3141
- if (!areRoutes(meta)) continue;
3069
+ if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
3142
3070
  for (const route of meta) {
3143
3071
  const existing = map.get(route);
3144
3072
  if (existing && existing !== handlerClass) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.InteractionDuplicateRoute, [
@@ -3172,7 +3100,7 @@ var InteractionDispatcher = class {
3172
3100
  }
3173
3101
  for (const [routeType, map] of this.routeTypes) {
3174
3102
  const meta = Reflect.getMetadata(_seedcord_core_internal.InteractionRouteKeys[routeType], handlerClass);
3175
- if (!areRoutes(meta)) continue;
3103
+ if (!(0, _seedcord_core_internal.areRoutes)(meta)) continue;
3176
3104
  meta.forEach((route) => map.delete(route));
3177
3105
  }
3178
3106
  }
@@ -3195,22 +3123,31 @@ var InteractionDispatcher = class {
3195
3123
  if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3196
3124
  await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
3197
3125
  }
3198
- async processInteraction(interaction, extractKey, getHandler) {
3126
+ async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
3199
3127
  const key = extractKey(interaction);
3128
+ let sender;
3200
3129
  try {
3201
3130
  if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3202
3131
  let HandlerCtor = getHandler(key);
3203
3132
  if (!HandlerCtor) {
3204
- this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to UnhandledEvent.`);
3205
- HandlerCtor = UnhandledEvent;
3133
+ this.logger.warn(`No handler found for key ${_seedcord_logger.paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3134
+ HandlerCtor = fallback;
3206
3135
  }
3207
3136
  this.logger.debug(`Processing ${_seedcord_logger.paint.mint.bold(key)} with ${_seedcord_logger.paint.mute(HandlerCtor.name)}`);
3208
3137
  const dispatch = new _seedcord_core.DispatchContext((0, _seedcord_core_internal.routeIdOf)(HandlerCtor));
3209
3138
  const handler = new HandlerCtor(interaction, this.core, dispatch);
3210
- if (!interaction.isAutocomplete()) await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core));
3139
+ if (handler instanceof RepliableHandler) sender = handler.getSender();
3140
+ if (!interaction.isAutocomplete()) {
3141
+ const monitor = (0, _seedcord_core_internal.slowGateMonitor)();
3142
+ try {
3143
+ await (0, _seedcord_core_internal.runHandlerGates)(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3144
+ } finally {
3145
+ monitor?.report(dispatch.routeId);
3146
+ }
3147
+ }
3211
3148
  await handler.execute();
3212
3149
  } catch (caught) {
3213
- await handleInteractionFault(caught, interaction, this.core);
3150
+ await handleInteractionFault(caught, interaction, this.core, sender);
3214
3151
  }
3215
3152
  }
3216
3153
  async handleInteraction(interaction) {
@@ -3286,12 +3223,7 @@ var InteractionDispatcher = class {
3286
3223
  }
3287
3224
  async handleAutocomplete(interaction) {
3288
3225
  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));
3226
+ await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3295
3227
  }
3296
3228
  };
3297
3229
 
@@ -3347,7 +3279,7 @@ var Bot = class extends Plugin {
3347
3279
  });
3348
3280
  }
3349
3281
  this.emojiInjector = new EmojiInjector(core);
3350
- core.shutdown.addTask(_seedcord_services.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3282
+ core.shutdown.addTask(_seedcord_core_node.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3351
3283
  }
3352
3284
  /**
3353
3285
  * Initializes Discord client and all controllers
@@ -3380,7 +3312,9 @@ var Bot = class extends Plugin {
3380
3312
  * Logs the bot into Discord using the configured token
3381
3313
  */
3382
3314
  async login(token) {
3383
- await this._client.login(token);
3315
+ this._client.once(discord_js.Events.ClientReady, () => this.emit("ready"));
3316
+ this._client.login(token);
3317
+ await this.waitFor("ready");
3384
3318
  this.logger.info(`Logged in as ${chalk.default.bold.magenta(this._client.user?.username)}!`);
3385
3319
  return this;
3386
3320
  }
@@ -3406,157 +3340,70 @@ var Bot = class extends Plugin {
3406
3340
  };
3407
3341
  __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3408
3342
 
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
3343
  //#endregion
3445
3344
  //#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
3345
  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
- }
3346
+ report() {
3347
+ return {
3348
+ username: "Handled Exception",
3349
+ components: [new HandledExceptionContainer(this.data).component],
3350
+ files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3351
+ };
3472
3352
  }
3473
3353
  };
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);
3354
+ HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3476
3355
  var HandledExceptionContainer = class extends _seedcord_core.BuilderComponent {
3477
3356
  constructor(data) {
3478
3357
  super("container");
3479
3358
  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"));
3359
+ this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3481
3360
  }
3482
3361
  };
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
3362
  function faultSummary(denial, source) {
3490
- const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
3363
+ const head = (0, _seedcord_utils.stripAnsi)(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3491
3364
  if (source.kind === "event") return `${head}**Event:** ${source.eventName}\n**Handler:** ${source.handler}\n**User ID:** \`${source.userId ?? "n/a"}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\``;
3492
3365
  return `${head}**Kind:** ${source.interactionKind}\n**Command:** ${source.command ?? "n/a"}\n**CustomId:** ${source.customId ?? "n/a"}\n**User ID:** \`${source.userId}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\`\n**Interaction ID:** \`${source.interactionId}\``;
3493
3366
  }
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
3367
  function causeStack(denial) {
3501
3368
  const { cause } = denial;
3502
- if (Error.isError(cause)) return cause.stack ?? cause.message;
3503
- if (typeof cause === "string") return cause;
3369
+ if (Error.isError(cause)) return (0, _seedcord_utils.stripAnsi)(cause.stack ?? cause.message);
3370
+ if (typeof cause === "string") return (0, _seedcord_utils.stripAnsi)(cause);
3504
3371
  if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3505
3372
  if (cause !== void 0) try {
3506
3373
  return JSON.stringify(cause);
3507
3374
  } catch {
3508
3375
  return "[unserializable cause]";
3509
3376
  }
3510
- return denial.stack ?? "No cause recorded.";
3377
+ return (0, _seedcord_utils.stripAnsi)(denial.stack ?? "No cause recorded.");
3511
3378
  }
3512
3379
 
3513
3380
  //#endregion
3514
3381
  //#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
3382
  let UnknownException = class UnknownException extends WebhookLog {
3524
- static {
3525
- _UnknownException = this;
3526
- }
3527
- webhook = new discord_js.WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
3528
- async execute() {
3383
+ report() {
3529
3384
  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
- }
3385
+ return {
3386
+ username: "Unknown Exception",
3387
+ components: [new UnhandledErrorContainer(this.data).component],
3388
+ files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3389
+ };
3542
3390
  }
3543
3391
  };
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);
3392
+ UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3546
3393
  var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3547
3394
  constructor(data) {
3548
3395
  super("container");
3549
3396
  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}\`\`\``));
3397
+ this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
3551
3398
  this.addTimestampsIfAvailable(error);
3552
3399
  this.addMetadataIfAvailable(metadata);
3553
3400
  }
3554
3401
  addTimestampsIfAvailable(error) {
3555
- if (!(error instanceof discord_js.DiscordAPIError)) return;
3402
+ if (!(error instanceof _discordjs_rest.DiscordAPIError)) return;
3556
3403
  const now = Date.now();
3557
3404
  const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3558
3405
  if (!snowflake) return void 0;
3559
- const interactionTs = Number(discord_js.SnowflakeUtil.deconstruct(snowflake).timestamp);
3406
+ const interactionTs = (0, _seedcord_utils.timestampFromSnowflake)(snowflake);
3560
3407
  const diff = now - interactionTs;
3561
3408
  const seconds = Math.floor(diff / 1e3);
3562
3409
  const millis = diff % 1e3;
@@ -3571,12 +3418,8 @@ var UnhandledErrorContainer = class extends _seedcord_core.BuilderComponent {
3571
3418
  //#endregion
3572
3419
  //#region src/subscribers/Bus.ts
3573
3420
  /**
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.
3421
+ * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3422
+ * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3580
3423
  */
3581
3424
  var Bus = class extends Plugin {
3582
3425
  core;
@@ -3586,6 +3429,7 @@ var Bus = class extends Plugin {
3586
3429
  isInitialized = false;
3587
3430
  subscribersMap = /* @__PURE__ */ new Map();
3588
3431
  executedOnceHandlers = /* @__PURE__ */ new Set();
3432
+ webhookProbes = /* @__PURE__ */ new Map();
3589
3433
  hmrHandler;
3590
3434
  constructor(core) {
3591
3435
  super(core);
@@ -3611,7 +3455,6 @@ var Bus = class extends Plugin {
3611
3455
  this.isInitialized = true;
3612
3456
  this.registerSubscriber(UnknownException);
3613
3457
  this.registerSubscriber(HandledException);
3614
- envapt.Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
3615
3458
  const subscribersDir = this.core.config.subscribers.path;
3616
3459
  if (subscribersDir) {
3617
3460
  this.logger.info(chalk.default.bold(subscribersDir));
@@ -3619,8 +3462,19 @@ var Bus = class extends Plugin {
3619
3462
  const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3620
3463
  this.logger.utils.list([`${chalk.default.bold.magenta(totalSubscribers)} subscribers`], chalk.default.bold.green("Loaded"));
3621
3464
  }
3465
+ if (!envapt.Envapter.isTest) await this.verifyWebhooks();
3466
+ }
3467
+ /** @internal Exposed for tests. */
3468
+ async verifyWebhooks() {
3469
+ const missing = [];
3470
+ await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3471
+ const result = await WebhookLog.senderFor(url).verify();
3472
+ if (result === "missing") missing.push(...envKeys);
3473
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${_seedcord_logger.paint.sky.bold(envKeys.join(", "))}`);
3474
+ }));
3475
+ if (missing.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3622
3476
  }
3623
- /** @internal For use in dev mode */
3477
+ /** @internal */
3624
3478
  async onHmr(event) {
3625
3479
  if (this.hmrHandler) await this.hmrHandler.handle(event);
3626
3480
  }
@@ -3637,6 +3491,17 @@ var Bus = class extends Plugin {
3637
3491
  }, this.logger);
3638
3492
  }
3639
3493
  registerSubscriber(handler) {
3494
+ if (handler.prototype instanceof WebhookLog) {
3495
+ const envKey = WebhookLog.envKeyOf(handler);
3496
+ const url = WebhookLog.urlOf(envKey);
3497
+ if (url === null) {
3498
+ this.logger.warn(`${chalk.default.bold(handler.name)} disabled, ${_seedcord_logger.paint.sky.bold(envKey)} is not set`);
3499
+ return;
3500
+ }
3501
+ const envKeys = this.webhookProbes.get(url) ?? [];
3502
+ if (!envKeys.includes(envKey)) envKeys.push(envKey);
3503
+ this.webhookProbes.set(url, envKeys);
3504
+ }
3640
3505
  const options = Reflect.getMetadata(_seedcord_core_internal.SubscribeMetadataKey, handler);
3641
3506
  let handlers = this.subscribersMap.get(options.subscriber);
3642
3507
  if (!handlers) {
@@ -3696,7 +3561,7 @@ var Bus = class extends Plugin {
3696
3561
  //#endregion
3697
3562
  //#region src/version.ts
3698
3563
  /** Package version */
3699
- const version = "0.1.0-next.2";
3564
+ const version = "0.1.0-next.3";
3700
3565
 
3701
3566
  //#endregion
3702
3567
  //#region src/Seedcord.ts
@@ -3736,8 +3601,8 @@ var Seedcord = class Seedcord extends Pluggable {
3736
3601
  constructor(config) {
3737
3602
  if (Seedcord.isInstantiated) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreSingletonViolation);
3738
3603
  Seedcord.isInstantiated = true;
3739
- const shutdown = new _seedcord_services.CoordinatedShutdown(config.shutdownEnabled);
3740
- const startup = new _seedcord_services.CoordinatedStartup();
3604
+ const shutdown = new _seedcord_core_node.CoordinatedShutdown(config.shutdownEnabled);
3605
+ const startup = new _seedcord_core_node.CoordinatedStartup();
3741
3606
  super(shutdown, startup);
3742
3607
  this.config = config;
3743
3608
  (0, _seedcord_logger_node.installNodeDefaults)(config.logger);
@@ -3749,7 +3614,7 @@ var Seedcord = class Seedcord extends Pluggable {
3749
3614
  this.bus = new Bus(this);
3750
3615
  this.bot = new Bot(this);
3751
3616
  this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
3752
- this.healthCheck = new _seedcord_services.HealthCheck(this.shutdown, config.healthCheck);
3617
+ this.healthCheck = new _seedcord_core_node.HealthCheck(this.shutdown, config.healthCheck);
3753
3618
  this.registerStartupTasks();
3754
3619
  }
3755
3620
  /** The bot's discord username, populated after login. */
@@ -3762,17 +3627,17 @@ var Seedcord = class Seedcord extends Pluggable {
3762
3627
  }
3763
3628
  registerStartupTasks() {
3764
3629
  if (envapt.Envapter.isDevelopment || envapt.Envapter.isTest) this.registerHmrAwareModules();
3765
- this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "Bus Initialization", async () => {
3630
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Configuration, "Bus Initialization", async () => {
3766
3631
  this.bus.logger.utils.initialization("Subscribers", "start");
3767
3632
  await this.bus.init();
3768
3633
  this.bus.logger.utils.initialization("Subscribers", "end");
3769
3634
  });
3770
- this.startup.addTask(_seedcord_services.StartupPhase.Instantiation, "Bot Initialization", async () => {
3635
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Instantiation, "Bot Initialization", async () => {
3771
3636
  this.bot.logger.utils.initialization("Bot", "start");
3772
3637
  await this.bot.init();
3773
3638
  this.bot.logger.utils.initialization("Bot", "end");
3774
3639
  });
3775
- this.startup.addTask(_seedcord_services.StartupPhase.Ready, "Health Check", async () => {
3640
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Ready, "Health Check", async () => {
3776
3641
  this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3777
3642
  await this.healthCheck.init();
3778
3643
  this.healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3788,7 +3653,7 @@ var Seedcord = class Seedcord extends Pluggable {
3788
3653
  return this;
3789
3654
  }
3790
3655
  registerHmrAwareModules() {
3791
- this.startup.addTask(_seedcord_services.StartupPhase.Configuration, "HMR Registration", async () => {
3656
+ this.startup.addTask(_seedcord_core_node.StartupPhase.Configuration, "HMR Registration", async () => {
3792
3657
  this.hmrManager.register(this.bot);
3793
3658
  this.hmrManager.register(this.bus);
3794
3659
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
@@ -3800,12 +3665,9 @@ var Seedcord = class Seedcord extends Pluggable {
3800
3665
  //#endregion
3801
3666
  exports.ArraySource = ArraySource;
3802
3667
  exports.AutocompleteHandler = AutocompleteHandler;
3803
- exports.AutocompleteRoute = AutocompleteRoute;
3804
3668
  exports.ButtonHandler = ButtonHandler;
3805
- exports.ButtonRoute = ButtonRoute;
3806
3669
  exports.CommandMentions = CommandMentions;
3807
3670
  exports.ContextMenuHandler = ContextMenuHandler;
3808
- exports.ContextMenuRoute = ContextMenuRoute;
3809
3671
  exports.CursorSource = CursorSource;
3810
3672
  exports.Emojis = Emojis;
3811
3673
  exports.EventHandler = EventHandler;
@@ -3817,31 +3679,31 @@ exports.InteractionMiddleware = InteractionMiddleware;
3817
3679
  exports.Middleware = Middleware;
3818
3680
  exports.MiddlewareType = MiddlewareType;
3819
3681
  exports.ModalHandler = ModalHandler;
3820
- exports.ModalRoute = ModalRoute;
3821
3682
  exports.Nsfw = Nsfw;
3822
3683
  exports.Paginator = Paginator;
3823
3684
  exports.Pluggable = Pluggable;
3824
3685
  exports.Plugin = Plugin;
3825
- Object.defineProperty(exports, 'RegisterCommand', {
3686
+ exports.RegisterEvent = RegisterEvent;
3687
+ exports.RepliableHandler = RepliableHandler;
3688
+ exports.Seedcord = Seedcord;
3689
+ exports.SelectMenuHandler = SelectMenuHandler;
3690
+ Object.defineProperty(exports, 'ShutdownPhase', {
3826
3691
  enumerable: true,
3827
3692
  get: function () {
3828
- return _seedcord_core.RegisterCommand;
3693
+ return _seedcord_core_node.ShutdownPhase;
3829
3694
  }
3830
3695
  });
3831
- exports.RegisterEvent = RegisterEvent;
3832
- exports.ReplySender = ReplySender;
3833
- exports.RequireBotPermissions = RequireBotPermissions;
3834
- exports.RequirePermissions = RequirePermissions;
3835
- exports.RequireRole = RequireRole;
3836
- exports.Seedcord = Seedcord;
3837
- exports.SelectMenuHandler = SelectMenuHandler;
3838
- exports.SelectMenuKind = SelectMenuKind;
3839
- exports.SelectMenuRoute = SelectMenuRoute;
3840
3696
  exports.SlashHandler = SlashHandler;
3841
- exports.SlashRoute = SlashRoute;
3697
+ Object.defineProperty(exports, 'StartupPhase', {
3698
+ enumerable: true,
3699
+ get: function () {
3700
+ return _seedcord_core_node.StartupPhase;
3701
+ }
3702
+ });
3842
3703
  exports.Subscribe = Subscribe;
3843
3704
  exports.Subscriber = Subscriber;
3844
3705
  exports.WebhookLog = WebhookLog;
3706
+ exports.WebhookUrl = WebhookUrl;
3845
3707
  exports.checkBotPermissions = checkBotPermissions;
3846
3708
  exports.checkPermissions = checkPermissions;
3847
3709
  exports.fetchGuildMember = fetchGuildMember;
@@ -3890,13 +3752,6 @@ Object.keys(_seedcord_rate_limiter).forEach(function (k) {
3890
3752
  });
3891
3753
  });
3892
3754
 
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
3755
  Object.keys(_seedcord_utils).forEach(function (k) {
3901
3756
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
3902
3757
  enumerable: true,