@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.mjs CHANGED
@@ -1,39 +1,39 @@
1
1
  import "reflect-metadata";
2
2
  import { installNodeDefaults } from "@seedcord/logger/node";
3
- import { BuilderComponent, CustomId, DispatchContext, Fault, Notice, RegisterCommand, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
4
- import { CommandMetadataKey, ComponentDefsKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NotInGuild, NoticeCard, PAGE_MAX, SubscribeMetadataKey, decodeFor, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, setBotColor } from "@seedcord/core/internal";
5
- import { ApplicationCommandType, AttachmentBuilder, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, SeparatorSpacingSize, SnowflakeUtil, TextChannel, User, WebhookClient, chatInputApplicationCommandMention } from "discord.js";
3
+ import { BaseReplySender, CommandMetadataKey, EventMetadataKey, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, SubscribeMetadataKey, WebhookUrlMetadataKey, areRoutes, decodeComponentRoute, deferFlags, getDevChannel, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
6
4
  import { SeedcordErrorCode } from "@seedcord/errors";
7
5
  import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
8
- import { filterCirculars, formatFilePath, hasKeys, prettify } from "@seedcord/utils";
6
+ import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
9
7
  import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
10
8
  import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
11
9
  import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
10
+ import { ApplicationCommandType, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, TextChannel, User, chatInputApplicationCommandMention } from "discord.js";
11
+ import * as crypto from "node:crypto";
12
12
  import { TypedEventEmitter } from "@seedcord/event-emitter";
13
+ import chalk from "chalk";
14
+ import { Converters, Envapter } from "envapt";
15
+ import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
16
+ import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
17
+ import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
18
+ import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, ShutdownPhase as ShutdownPhase$1, StartupPhase, StartupPhase as StartupPhase$1 } from "@seedcord/core/node";
13
19
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
14
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, StartupPhase } from "@seedcord/services";
15
20
  import { SeedcordBrand } from "@seedcord/types/internal";
16
- import { Envapter } from "envapt";
17
- import chalk from "chalk";
18
21
  import { Envapt } from "envapt/legacy";
19
22
  import { resolve } from "node:path";
20
23
  import { HmrModuleHandler } from "@seedcord/core/hmr";
21
24
  import { traverseDirectory } from "@seedcord/utils/node";
22
- import * as crypto from "node:crypto";
23
-
24
- export * from "@seedcord/core"
25
25
 
26
26
  export * from "@seedcord/errors"
27
27
 
28
- export * from "@seedcord/utils"
28
+ export * from "@seedcord/core"
29
29
 
30
30
  export * from "@seedcord/logger"
31
31
 
32
32
  export * from "@seedcord/event-emitter"
33
33
 
34
- export * from "@seedcord/rate-limiter"
34
+ export * from "@seedcord/utils"
35
35
 
36
- export * from "@seedcord/services"
36
+ export * from "@seedcord/rate-limiter"
37
37
 
38
38
  //#region src/logger-bootstrap.ts
39
39
  installNodeDefaults();
@@ -131,190 +131,6 @@ function Gated(...gates) {
131
131
  };
132
132
  }
133
133
 
134
- //#endregion
135
- //#region src/miscellaneous/areRoutes.ts
136
- /**
137
- * Checks if the provided value is an array of strings.
138
- *
139
- * @internal
140
- */
141
- function areRoutes(routes) {
142
- return Array.isArray(routes) && routes.every((r) => typeof r === "string");
143
- }
144
-
145
- //#endregion
146
- //#region src/bot/decorators/Interactions.ts
147
- /**
148
- * Types of select menus supported for routing
149
- */
150
- let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
151
- SelectMenuKind["String"] = "string";
152
- SelectMenuKind["User"] = "user";
153
- SelectMenuKind["Role"] = "role";
154
- SelectMenuKind["Channel"] = "channel";
155
- SelectMenuKind["Mentionable"] = "mentionable";
156
- return SelectMenuKind;
157
- }({});
158
- /**
159
- * Routes one or more slash commands to a {@link SlashHandler}.
160
- *
161
- * Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
162
- * commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
163
- * fewer or more than the handler declares is a compile error. Subcommands use a slash path.
164
- *
165
- * @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
166
- * @decorator
167
- * @example
168
- * ```ts
169
- * \@SlashRoute('ban')
170
- * class BanHandler extends SlashHandler<'ban'> {
171
- * async execute() {
172
- * const target = this.options.getUser('target');
173
- * }
174
- * }
175
- * ```
176
- *
177
- * @example
178
- * ```ts
179
- * \@SlashRoute('ban', 'kick')
180
- * class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
181
- * async execute() {
182
- * await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
183
- * }
184
- * }
185
- * ```
186
- */
187
- function SlashRoute(...routes) {
188
- return function(constructor) {
189
- storeMetadata(InteractionRoutes.Slash, routes, constructor);
190
- };
191
- }
192
- /**
193
- * Routes button interactions to handler classes.
194
- *
195
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
196
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
197
- * matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
198
- * reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
199
- *
200
- * @param defs - The customId definition(s) this handler decodes, one per route.
201
- * @decorator
202
- */
203
- function ButtonRoute(...defs) {
204
- return function(constructor) {
205
- storeComponentRoute(InteractionRoutes.Button, defs, constructor);
206
- };
207
- }
208
- /**
209
- * Routes modal submissions 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 an older-shape wire still reaches the handler and throws StaleCustomId
214
- * on read, the same as a button route.
215
- *
216
- * @param defs - The customId definition(s) this handler decodes, one per route.
217
- * @decorator
218
- */
219
- function ModalRoute(...defs) {
220
- return function(constructor) {
221
- storeComponentRoute(InteractionRoutes.Modal, defs, constructor);
222
- };
223
- }
224
- /**
225
- * Routes one or more context-menu commands to a {@link ContextMenuHandler}.
226
- *
227
- * Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
228
- * each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
229
- * handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
230
- * reads `this.target` uniformly with no branch.
231
- *
232
- * @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
233
- * @param names - The command name(s) this handler serves, keyed off the matching registry.
234
- * @decorator
235
- * @example
236
- * ```ts
237
- * \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
238
- * class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
239
- * async execute() {
240
- * const user = this.target;
241
- * }
242
- * }
243
- * ```
244
- */
245
- function ContextMenuRoute(kind, ...names) {
246
- return function(constructor) {
247
- storeMetadata(kind === ApplicationCommandType.User ? InteractionRoutes.UserContextMenu : InteractionRoutes.MessageContextMenu, names, constructor);
248
- };
249
- }
250
- /**
251
- * Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
252
- *
253
- * Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
254
- * over its autocompletable fields, several commands share one handler whose arms span every command's
255
- * fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
256
- * handler declares is a compile error.
257
- *
258
- * @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
259
- * @decorator
260
- * @example
261
- * ```ts
262
- * \@AutocompleteRoute('search')
263
- * class SearchAutocomplete extends AutocompleteHandler<'search'> {
264
- * async execute() {
265
- * await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
266
- * }
267
- * }
268
- * ```
269
- */
270
- function AutocompleteRoute(...routes) {
271
- return function(constructor) {
272
- storeMetadata(InteractionRoutes.Autocomplete, routes, constructor);
273
- };
274
- }
275
- /**
276
- * Routes select menu interactions to handler classes.
277
- *
278
- * Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
279
- * generic must list the same definitions, and its second generic argument must be the matching select
280
- * interaction type, or it is a compile error.
281
- *
282
- * @param type - Select menu kind from {@link SelectMenuKind}.
283
- * @param defs - The customId definition(s) this handler decodes, one per route.
284
- * @decorator
285
- *
286
- * @example
287
- * ```typescript
288
- * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
289
- * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
290
- * // handles user select menus minted from AssignId
291
- * }
292
- * ```
293
- */
294
- function SelectMenuRoute(type, ...defs) {
295
- return function(constructor) {
296
- storeComponentRoute({
297
- ["string"]: InteractionRoutes.StringMenu,
298
- ["user"]: InteractionRoutes.UserMenu,
299
- ["role"]: InteractionRoutes.RoleMenu,
300
- ["channel"]: InteractionRoutes.ChannelMenu,
301
- ["mentionable"]: InteractionRoutes.MentionableMenu
302
- }[type], defs, constructor);
303
- };
304
- }
305
- function storeComponentRoute(route, defs, constructor) {
306
- storeMetadata(route, defs.map((def) => def.prefix), constructor);
307
- Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
308
- }
309
- function storeMetadata(route, routes, constructor) {
310
- const key = InteractionRouteKeys[route];
311
- const savedRoutes = Reflect.getMetadata(key, constructor);
312
- const existing = areRoutes(savedRoutes) ? savedRoutes : [];
313
- const toStore = Array.isArray(routes) ? routes : [routes];
314
- Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
315
- Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
316
- }
317
-
318
134
  //#endregion
319
135
  //#region src/bot/decorators/Middlewares.ts
320
136
  /**
@@ -383,21 +199,6 @@ const IgnoreBots = defineGate("IgnoreBots", (ctx) => {
383
199
  if (ctx.user?.bot) throw new Silence("actor is a bot");
384
200
  });
385
201
 
386
- //#endregion
387
- //#region src/bot/notices/utils.ts
388
- function mentionFor(subject) {
389
- if (subject instanceof Role) return `<@&${subject.id}>`;
390
- if (subject instanceof TextChannel) return `<#${subject.id}>`;
391
- if (subject instanceof GuildMember) return `<@${subject.id}>`;
392
- return `\`${subject.name}\``;
393
- }
394
- function labelFor(subject) {
395
- if (subject instanceof Role) return "role";
396
- if (subject instanceof TextChannel) return "channel";
397
- if (subject instanceof GuildMember) return "member";
398
- return "guild";
399
- }
400
-
401
202
  //#endregion
402
203
  //#region src/bot/notices/index.ts
403
204
  var NotNsfw = class extends GateNotice {
@@ -405,13 +206,6 @@ var NotNsfw = class extends GateNotice {
405
206
  super(message);
406
207
  }
407
208
  };
408
- var MissingRole = class extends GateNotice {
409
- role;
410
- constructor(message, role) {
411
- super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
412
- this.role = role;
413
- }
414
- };
415
209
  var RoleHigherThanMe = class extends Notice {
416
210
  role;
417
211
  botRole;
@@ -432,30 +226,19 @@ var CannotAssignBotRole = class extends Notice {
432
226
  return { components: [new NoticeCard("I cannot assign a managed role.").component] };
433
227
  }
434
228
  };
435
- var MissingPermissions = class extends Notice {
436
- where;
437
- missingPerms;
438
- constructor(message, where, missingPerms) {
439
- super(message);
440
- this.where = where;
441
- this.missingPerms = missingPerms;
442
- }
443
- render() {
444
- const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
445
- return { components: [new NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
446
- }
447
- };
448
229
  var HasDangerousPermissions = class extends Notice {
449
- target;
230
+ subject;
450
231
  dangerousPerms;
451
- constructor(message, target, dangerousPerms) {
452
- super(message);
453
- this.target = target;
232
+ customLead;
233
+ constructor(message, subject, dangerousPerms) {
234
+ super(message ?? "A dangerous permission is enabled.");
235
+ this.subject = subject;
454
236
  this.dangerousPerms = dangerousPerms;
237
+ this.customLead = message;
455
238
  }
456
239
  render() {
457
240
  const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
458
- return { components: [new NoticeCard(`The ${labelFor(this.target)} ${mentionFor(this.target)} has the following permission entries that must not be enabled:\n\n${bullets}`).component] };
241
+ return { components: [new NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
459
242
  }
460
243
  };
461
244
  var UserNotFound = class extends Notice {
@@ -531,129 +314,6 @@ function Nsfw(options) {
531
314
  });
532
315
  }
533
316
 
534
- //#endregion
535
- //#region src/bot/utilities/permissions/checkPermissions.ts
536
- /**
537
- * Map of permission bits to their prettified human readable names.
538
- */
539
- const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
540
- function checkPermissions(a, b, c, d, e) {
541
- const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
542
- for: a,
543
- in: b,
544
- scope: c,
545
- inverse: d ?? false,
546
- ...e
547
- } : a;
548
- const Missing = missingCtor ?? MissingPermissions;
549
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
550
- const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
551
- const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
552
- const present = scope.filter((bit) => perms.has(bit, true));
553
- const presentNames = names(present);
554
- if (inverse) {
555
- if (present.length > 0) throw new Dangerous(`${pFor instanceof Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof Guild ? "guild" : "channel"}`, pFor, presentNames);
556
- return;
557
- }
558
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
559
- if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
560
- }
561
-
562
- //#endregion
563
- //#region src/bot/gates/catalog/permissions.ts
564
- const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
565
- /**
566
- * Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
567
- *
568
- * Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
569
- *
570
- * @param scope - The permission flag bits the caller must all hold.
571
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
572
- *
573
- * @see {@link Gated}
574
- *
575
- * @example
576
- * ```ts
577
- * import { PermissionFlagsBits } from 'discord.js';
578
- *
579
- * \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
580
- * \@SlashRoute('ban')
581
- * class BanHandler extends SlashHandler<'ban'> {
582
- * async execute() {
583
- * const target = this.options.getUser('target');
584
- * }
585
- * }
586
- * ```
587
- */
588
- function RequirePermissions(scope, options) {
589
- return defineGate("RequirePermissions", (ctx) => {
590
- if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
591
- if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
592
- checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
593
- });
594
- }
595
- /**
596
- * Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
597
- *
598
- * It reads the bot's own member, so it attaches to modal handlers as well.
599
- *
600
- * @param scope - The permission flag bits the bot must all hold.
601
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
602
- *
603
- * @see {@link Gated}
604
- *
605
- * @example
606
- * ```ts
607
- * import { PermissionFlagsBits } from 'discord.js';
608
- *
609
- * \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
610
- * \@ModalRoute(MyModalId)
611
- * class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
612
- * async execute() {
613
- * // ...
614
- * }
615
- * }
616
- * ```
617
- */
618
- function RequireBotPermissions(scope, options) {
619
- return defineGate("RequireBotPermissions", (ctx) => {
620
- const botMember = ctx.guild?.members.me;
621
- if (!ctx.guild || !botMember) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
622
- checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
623
- });
624
- }
625
- /**
626
- * Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
627
- * when the role is missing.
628
- *
629
- * Interaction-only and excludes ModalSubmit.
630
- *
631
- * @param roleId - The role snowflake the caller must hold, read from the member's role cache.
632
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
633
- *
634
- * @see {@link Gated}
635
- *
636
- * @example
637
- * ```ts
638
- * \@Gated(RequireRole('123456789012345678'))
639
- * \@SlashRoute('vip')
640
- * class VipHandler extends SlashHandler<'vip'> {
641
- * async execute() {
642
- * // ...
643
- * }
644
- * }
645
- * ```
646
- */
647
- function RequireRole(roleId, options) {
648
- return defineGate("RequireRole", (ctx) => {
649
- if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
650
- if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
651
- if (ctx.member.roles.cache.has(roleId)) return;
652
- const role = ctx.guild.roles.cache.get(roleId) ?? null;
653
- throw pickNotice(options?.missingRole, (message) => new MissingRole(message, role));
654
- });
655
- }
656
-
657
317
  //#endregion
658
318
  //#region src/bot/injectors/EmojiInjector.ts
659
319
  const emojiStorage = {};
@@ -842,6 +502,37 @@ async function fetchText(client, channelId) {
842
502
  throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
843
503
  }
844
504
 
505
+ //#endregion
506
+ //#region src/bot/notices/utils.ts
507
+ function mentionFor(subject) {
508
+ if (subject instanceof Role) return `<@&${subject.id}>`;
509
+ return `<@${subject.id}>`;
510
+ }
511
+
512
+ //#endregion
513
+ //#region src/bot/utilities/permissions/checkPermissions.ts
514
+ function checkPermissions(a, b, c, d, e) {
515
+ const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
516
+ for: a,
517
+ in: b,
518
+ scope: c,
519
+ inverse: d ?? false,
520
+ ...e
521
+ } : a;
522
+ const Missing = missingCtor ?? MissingPermissions;
523
+ const Dangerous = dangerousCtor ?? HasDangerousPermissions;
524
+ const subject = mentionFor(pFor);
525
+ const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
526
+ const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
527
+ if (inverse) {
528
+ const present = scope.filter((bit) => perms.has(bit, true));
529
+ if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
530
+ return;
531
+ }
532
+ const missingBits = scope.filter((bit) => !perms.has(bit, true));
533
+ if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
534
+ }
535
+
845
536
  //#endregion
846
537
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
847
538
  /**
@@ -870,7 +561,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
870
561
  const me = target.members.me;
871
562
  if (!me) {
872
563
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
873
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
564
+ throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
874
565
  }
875
566
  checkPermissions(me, target, scope, inverse, errors);
876
567
  return;
@@ -879,7 +570,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
879
570
  if (!me) {
880
571
  new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
881
572
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
882
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
573
+ throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
883
574
  }
884
575
  checkPermissions(me, target, scope, inverse, errors);
885
576
  }
@@ -916,26 +607,26 @@ function getBotRole(guild) {
916
607
  * // Check with custom error handling
917
608
  * hasPermsToAssign({
918
609
  * targetRole: role,
919
- * errors: {
920
- * higher: CustomHigherError,
921
- * managed: CustomManagedError,
922
- * missing: CustomMissingPermissionsError
610
+ * noticeOverrides: {
611
+ * higherNotice: CustomHigherNotice,
612
+ * managedNotice: CustomManagedNotice,
613
+ * missingNotice: CustomMissingPermissionsNotice
923
614
  * }
924
615
  * });
925
616
  * ```
926
617
  */
927
618
  function hasPermsToAssign(roleOrOptions) {
928
- const { targetRole, errors } = roleOrOptions instanceof Role ? {
619
+ const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof Role ? {
929
620
  targetRole: roleOrOptions,
930
- errors: void 0
621
+ noticeOverrides: void 0
931
622
  } : roleOrOptions;
932
- const HigherErr = errors?.higher ?? RoleHigherThanMe;
933
- const ManagedErr = errors?.managed ?? CannotAssignBotRole;
934
- const Missing = errors?.missing ?? MissingPermissions;
623
+ const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
624
+ const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
625
+ const MissingNotice = errors?.missingNotice ?? MissingPermissions;
935
626
  const botRole = getBotRole(targetRole.guild);
936
- if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherErr("Role is higher than me", targetRole, botRole);
937
- if (targetRole.managed) throw new ManagedErr(`Cannot assign managed role ${targetRole.name}`);
938
- checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missing: Missing });
627
+ if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
628
+ if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
629
+ checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
939
630
  }
940
631
 
941
632
  //#endregion
@@ -1063,113 +754,308 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
1063
754
  }
1064
755
 
1065
756
  //#endregion
1066
- //#region src/bot/harmlessApiCodes.ts
757
+ //#region src/bot/ReplySender.ts
758
+ function seedState(interaction) {
759
+ if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
760
+ if (interaction.replied) return "replied";
761
+ if (interaction.deferred) return "deferred-reply";
762
+ return "unacked";
763
+ }
1067
764
  /**
1068
- * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
1069
- * message. There is nothing to show the user and no bug to report. The controller boundary drops them on
1070
- * the original throw, and {@link ReplySender} drops them on its own send.
1071
- *
1072
- * @internal
765
+ * Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
766
+ * handler bases and the dispatcher.
1073
767
  */
1074
- const HARMLESS_API_CODES = new Set([
1075
- RESTJSONErrorCodes.UnknownInteraction,
1076
- RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1077
- RESTJSONErrorCodes.UnknownMessage
1078
- ]);
768
+ var ReplySender = class extends BaseReplySender {
769
+ interaction;
770
+ constructor(interaction, routeId) {
771
+ super(routeId, seedState(interaction));
772
+ this.interaction = interaction;
773
+ }
774
+ async writeReply(response, opts) {
775
+ const result = await this.interaction.reply({
776
+ ...this.replyOptions(response, sendFlags(opts)),
777
+ withResponse: true
778
+ });
779
+ return this.createdMessage(result);
780
+ }
781
+ async writeDefer(opts) {
782
+ await this.interaction.deferReply({ flags: deferFlags(opts) });
783
+ }
784
+ async writeDeferUpdate() {
785
+ await this.sourceInteraction("deferUpdate").deferUpdate();
786
+ }
787
+ async writeUpdate(response) {
788
+ const result = await this.sourceInteraction("update").update({
789
+ ...this.updateOptions(response),
790
+ withResponse: true
791
+ });
792
+ return this.createdMessage(result);
793
+ }
794
+ async writeFollowUp(response, opts) {
795
+ return await this.interaction.followUp(this.replyOptions(response, sendFlags(opts)));
796
+ }
797
+ async writeEditOriginal(response) {
798
+ return await this.interaction.editReply(this.editOptions(response));
799
+ }
800
+ async writeEditTarget(targetId, response) {
801
+ return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
802
+ }
803
+ async writeDeleteOriginal() {
804
+ await this.interaction.deleteReply();
805
+ }
806
+ async writeDeleteTarget(targetId) {
807
+ await this.interaction.deleteReply(targetId);
808
+ }
809
+ async writeModal(data) {
810
+ const { interaction } = this;
811
+ if (interaction.isModalSubmit()) this.rejectModalSubmit();
812
+ await interaction.showModal(data);
813
+ }
814
+ guardModalSource() {
815
+ if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
816
+ }
817
+ rejectModalSubmit() {
818
+ throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
819
+ "showModal",
820
+ "this interaction is a modal submit",
821
+ "A modal cannot open another modal.",
822
+ this.routeId
823
+ ]);
824
+ }
825
+ sourceInteraction(method) {
826
+ const { interaction } = this;
827
+ if (interaction.isMessageComponent()) return interaction;
828
+ if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
829
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
830
+ }
831
+ replyOptions(response, flags) {
832
+ const reply = this.serialize(response);
833
+ return {
834
+ components: reply.components,
835
+ flags,
836
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
837
+ ...reply.files && { files: [...reply.files] }
838
+ };
839
+ }
840
+ editOptions(response) {
841
+ const reply = this.serialize(response);
842
+ return {
843
+ components: reply.components,
844
+ flags: MessageFlags.IsComponentsV2,
845
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
846
+ ...reply.files && { files: [...reply.files] }
847
+ };
848
+ }
849
+ updateOptions(response) {
850
+ const reply = this.serialize(response);
851
+ return {
852
+ components: reply.components,
853
+ flags: MessageFlags.IsComponentsV2,
854
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
855
+ ...reply.files && { files: [...reply.files] }
856
+ };
857
+ }
858
+ createdMessage(result) {
859
+ return result.resource?.message ?? void 0;
860
+ }
861
+ };
1079
862
 
1080
863
  //#endregion
1081
- //#region src/miscellaneous/flagsFor.ts
864
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1082
865
  /**
1083
- * Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
1084
- * IsComponentsV2 flag is always set.
1085
- *
1086
- * @param ephemeral - Whether the reply is ephemeral.
1087
- * @returns The OR'd `MessageFlags` value as a plain number.
866
+ * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
867
+ * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
868
+ * interaction and a generated registry key always match.
1088
869
  *
1089
870
  * @internal
1090
871
  */
1091
- function flagsFor(ephemeral) {
1092
- let flags = MessageFlags.IsComponentsV2;
1093
- if (ephemeral) flags |= MessageFlags.Ephemeral;
1094
- return flags;
872
+ function slashRouteOf(interaction) {
873
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
874
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
875
+ return buildSlashRoute(interaction.commandName, subcommand, group);
1095
876
  }
1096
877
 
1097
878
  //#endregion
1098
- //#region src/bot/ReplySender.ts
879
+ //#region src/miscellaneous/FaultThrottle.ts
1099
880
  /**
1100
- * Sends a {@link ReplyResponse} to an interaction, picking reply, editReply, or followUp from the live
1101
- * acknowledgement state. Logs and drops its own send failures so a dead token never escapes into the
1102
- * controller.
881
+ * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
882
+ * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
883
+ * the map stays small, and a stale entry is evicted on the next read of its key.
884
+ *
885
+ * @internal
1103
886
  */
1104
- var ReplySender = class {
1105
- interaction;
1106
- logger = new Logger("ReplySender");
1107
- constructor(interaction) {
1108
- this.interaction = interaction;
1109
- }
1110
- /**
1111
- * Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
1112
- * a failed send is logged and dropped and resolves to `undefined`.
1113
- *
1114
- * @param response - The ComponentsV2 reply to show.
1115
- * @param ephemeral - Whether the reply is ephemeral. {@default `true`}
1116
- * @returns The sent {@link Message}, or `undefined` when the send was swallowed.
1117
- */
1118
- async send(response, ephemeral = true) {
1119
- try {
1120
- return await this.dispatch(response, ephemeral);
1121
- } catch (error) {
1122
- this.logSwallowed("send", error);
1123
- return;
1124
- }
887
+ var FaultThrottle = class {
888
+ windowMs;
889
+ now;
890
+ lastReportedAt = /* @__PURE__ */ new Map();
891
+ constructor(windowMs = 6e4, now = () => Date.now()) {
892
+ this.windowMs = windowMs;
893
+ this.now = now;
1125
894
  }
1126
- /**
1127
- * Edits an already-sent message to a new {@link ReplyResponse}. Used to swap a confirmation prompt for
1128
- * its outcome. Never throws, a failed edit is logged and dropped.
1129
- *
1130
- * @param message - The message to edit (the handle a prior {@link send} returned).
1131
- * @param response - The reply to replace it with.
1132
- */
1133
- async edit(message, response) {
1134
- try {
1135
- await this.interaction.webhook.editMessage(message, this.editBody(response));
1136
- } catch (error) {
1137
- this.logSwallowed("edit", error);
1138
- }
895
+ /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
896
+ shouldReport(key) {
897
+ const last = this.lastReportedAt.get(key);
898
+ if (last === void 0) return true;
899
+ if (this.now() - last < this.windowMs) return false;
900
+ this.lastReportedAt.delete(key);
901
+ return true;
1139
902
  }
1140
- async dispatch(response, ephemeral) {
1141
- if (this.interaction.replied) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1142
- if (this.interaction.deferred) {
1143
- if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1144
- return await this.interaction.editReply(this.editBody(response));
1145
- }
1146
- await this.interaction.reply(this.replyOptions(response, ephemeral));
1147
- return await this.interaction.fetchReply();
903
+ markReported(key) {
904
+ this.lastReportedAt.set(key, this.now());
1148
905
  }
1149
- replyOptions(response, ephemeral) {
1150
- return {
1151
- components: response.components,
1152
- flags: flagsFor(ephemeral),
1153
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1154
- ...response.files && { files: response.files }
1155
- };
906
+ clear() {
907
+ this.lastReportedAt.clear();
1156
908
  }
1157
- editBody(response) {
909
+ };
910
+
911
+ //#endregion
912
+ //#region src/miscellaneous/extractErrorResponse.ts
913
+ const logger$2 = new Logger("ErrorsHandling");
914
+ const faultThrottle = new FaultThrottle();
915
+ /**
916
+ * Processes an error into a user-facing reply and routes its report.
917
+ *
918
+ * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
919
+ * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
920
+ * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
921
+ * threaded into both the reply and the bus payload.
922
+ */
923
+ function extractErrorResponse(error, core, origin) {
924
+ const uuid = crypto.randomUUID();
925
+ const developerUsername = core.config.notifications?.developerUsername;
926
+ const ctx = developerUsername === void 0 ? { uuid } : {
927
+ uuid,
928
+ developerUsername
929
+ };
930
+ if (error instanceof Notice) {
931
+ if (error.report) reportFault(error, core, origin, uuid);
1158
932
  return {
1159
- components: response.components,
1160
- flags: MessageFlags.IsComponentsV2,
1161
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1162
- ...response.files && { files: response.files }
933
+ uuid,
934
+ response: error.render(ctx)
1163
935
  };
1164
936
  }
1165
- logSwallowed(action, error) {
1166
- if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
1167
- this.logger.debug(`reply ${action} hit harmless code ${error.code}`);
1168
- return;
1169
- }
1170
- this.logger.error(`reply ${action} failed`, error);
937
+ reportRawFault(error, core, origin, uuid);
938
+ const override = core.config.errors?.defaultError;
939
+ return {
940
+ uuid,
941
+ response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
942
+ };
943
+ }
944
+ function withThrottle(origin, error, publish) {
945
+ const key = faultKey(origin, error);
946
+ if (!faultThrottle.shouldReport(key)) {
947
+ logger$2.info(`throttled duplicate fault ${key}`);
948
+ return;
1171
949
  }
1172
- };
950
+ publish();
951
+ faultThrottle.markReported(key);
952
+ }
953
+ function reportFault(denial, core, origin, uuid) {
954
+ withThrottle(origin, denial, () => {
955
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
956
+ if (origin.interaction) core.bus.publish("handledException", {
957
+ denial,
958
+ uuid,
959
+ source: buildInteractionSource(origin.interaction)
960
+ });
961
+ else if (origin.event) core.bus.publish("handledException", {
962
+ denial,
963
+ uuid,
964
+ source: buildEventSource(origin.event, origin)
965
+ });
966
+ else core.bus.publish("unknownException", {
967
+ uuid,
968
+ error: denial,
969
+ ...scalarActors(origin),
970
+ metadata: metadataFor(origin)
971
+ });
972
+ });
973
+ }
974
+ function causeLine(error) {
975
+ const { cause } = error;
976
+ if (!Error.isError(cause)) return "";
977
+ const [first] = cause.message.split("\n");
978
+ return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
979
+ }
980
+ function reportRawFault(error, core, origin, uuid) {
981
+ withThrottle(origin, error, () => {
982
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
983
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
984
+ core.bus.publish("unknownException", {
985
+ uuid,
986
+ error,
987
+ ...scalarActors(origin),
988
+ metadata: metadataFor(origin)
989
+ });
990
+ });
991
+ }
992
+ function scalarActors(origin) {
993
+ return {
994
+ guild: origin.guild ? {
995
+ id: origin.guild.id,
996
+ name: origin.guild.name
997
+ } : void 0,
998
+ user: origin.user ? {
999
+ id: origin.user.id,
1000
+ username: origin.user.username
1001
+ } : void 0
1002
+ };
1003
+ }
1004
+ function metadataFor(origin) {
1005
+ if (origin.event) return {
1006
+ eventName: origin.event.name,
1007
+ handler: origin.event.handler,
1008
+ args: origin.event.args
1009
+ };
1010
+ return origin.metadata;
1011
+ }
1012
+ function buildEventSource(event, origin) {
1013
+ return {
1014
+ kind: "event",
1015
+ eventName: event.name,
1016
+ handler: event.handler,
1017
+ userId: origin.user?.id ?? null,
1018
+ guildId: origin.guild?.id ?? null,
1019
+ channelId: event.channelId,
1020
+ raw: event.args
1021
+ };
1022
+ }
1023
+ function faultKey(origin, error) {
1024
+ const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
1025
+ if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1026
+ if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1027
+ if (origin.route) return `autocomplete:${origin.route}:${name}`;
1028
+ return `autocomplete:${name}`;
1029
+ }
1030
+ /** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
1031
+ function interactionRoute(interaction) {
1032
+ if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1033
+ if (interaction.isContextMenuCommand()) return interaction.commandName;
1034
+ if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
1035
+ return "interaction";
1036
+ }
1037
+ function buildInteractionSource(interaction) {
1038
+ const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1039
+ const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1040
+ return {
1041
+ kind: "interaction",
1042
+ interactionKind: interactionKind(interaction),
1043
+ command,
1044
+ customId,
1045
+ userId: interaction.user.id,
1046
+ guildId: interaction.guildId,
1047
+ channelId: interaction.channelId,
1048
+ interactionId: interaction.id,
1049
+ raw: interaction
1050
+ };
1051
+ }
1052
+ function interactionKind(interaction) {
1053
+ if (interaction.isChatInputCommand()) return "slash";
1054
+ if (interaction.isContextMenuCommand()) return "context-menu";
1055
+ if (interaction.isButton()) return "button";
1056
+ if (interaction.isAnySelectMenu()) return "select";
1057
+ return "modal";
1058
+ }
1173
1059
 
1174
1060
  //#endregion
1175
1061
  //#region src/bot/confirm/reserved.ts
@@ -1215,9 +1101,9 @@ async function collectChoice(message, userId, timeoutMs) {
1215
1101
  return null;
1216
1102
  }
1217
1103
  }
1218
- async function settle(sender, interaction, message, outcome) {
1104
+ async function settle(sender, message, outcome) {
1219
1105
  if (outcome === void 0) {
1220
- await interaction.deleteReply(message).catch(() => void 0);
1106
+ await sender.delete(message).catch(() => void 0);
1221
1107
  return;
1222
1108
  }
1223
1109
  await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
@@ -1225,17 +1111,16 @@ async function settle(sender, interaction, message, outcome) {
1225
1111
  async function getConfirmation(interaction, prompt, options) {
1226
1112
  const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1227
1113
  const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1228
- const sender = new ReplySender(interaction);
1229
- const message = await sender.send(response, ephemeral);
1230
- if (!message) return false;
1114
+ const sender = new ReplySender(interaction, interactionRoute(interaction));
1115
+ const message = await sender.send(response, { ephemeral });
1231
1116
  const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1232
1117
  if (!winner) {
1233
- await settle(sender, interaction, message, options?.onTimeout);
1118
+ await settle(sender, message, options?.onTimeout);
1234
1119
  return false;
1235
1120
  }
1236
1121
  await winner.deferUpdate().catch(() => void 0);
1237
1122
  const confirmed = winner.customId === CONFIRM_IDS.confirm;
1238
- await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
1123
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1239
1124
  return confirmed;
1240
1125
  }
1241
1126
 
@@ -1348,19 +1233,10 @@ var Pluggable = class Pluggable extends TypedEventEmitter {
1348
1233
  //#endregion
1349
1234
  //#region src/handlers/BaseHandler.ts
1350
1235
  /**
1351
- * Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
1236
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1237
+ * gate hook.
1352
1238
  */
1353
- var BaseHandler = class {
1354
- core;
1355
- event;
1356
- logger;
1357
- dispatch;
1358
- constructor(event, core, dispatch) {
1359
- this.core = core;
1360
- this.event = event;
1361
- this.logger = new Logger(this.constructor.name);
1362
- this.dispatch = dispatch;
1363
- }
1239
+ var BaseHandler$1 = class extends BaseHandler {
1364
1240
  /** @internal */
1365
1241
  getEvent() {
1366
1242
  return this.event;
@@ -1377,43 +1253,56 @@ var BaseHandler = class {
1377
1253
  *
1378
1254
  * @typeParam Repliable - The interaction type this middleware processes
1379
1255
  */
1380
- var InteractionMiddleware = class extends BaseHandler {
1256
+ var InteractionMiddleware = class extends BaseHandler$1 {
1381
1257
  constructor(event, core) {
1382
1258
  super(event, core);
1383
1259
  }
1384
1260
  };
1385
1261
 
1386
1262
  //#endregion
1387
- //#region src/handlers/interaction/InteractionHandler.ts
1263
+ //#region src/handlers/RepliableHandler.ts
1388
1264
  /**
1389
- * Shared base the typed interaction handlers extend.
1265
+ * Shared base the repliable interaction handlers extend.
1390
1266
  *
1391
- * Not a public entry point. You should be using {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1392
- * or {@link SelectMenuHandler} instead. This class only carries the repliable-event plumbing those bases share,
1393
- * so DO NOT use it directly.
1267
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1268
+ * {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
1269
+ * to the gateway sender.
1394
1270
  *
1395
- * @typeParam Repliable - The interaction type this handler processes
1271
+ * @typeParam Event - The repliable interaction type this handler processes
1396
1272
  */
1397
- var InteractionHandler = class extends BaseHandler {
1398
- constructor(event, core, dispatch) {
1399
- super(event, core, dispatch);
1273
+ var RepliableHandler = class extends RepliableHandler$1 {
1274
+ /** @internal */
1275
+ getEvent() {
1276
+ return this.event;
1277
+ }
1278
+ buildSender(event, _core, routeId) {
1279
+ return new ReplySender(event, routeId);
1400
1280
  }
1401
1281
  };
1402
1282
 
1403
1283
  //#endregion
1404
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1284
+ //#region src/handlers/interaction/InteractionHandler.ts
1405
1285
  /**
1406
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
1407
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
1408
- * interaction and a generated registry key always match.
1286
+ * Shared base the typed interaction handlers extend.
1409
1287
  *
1410
- * @internal
1288
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1289
+ * or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
1290
+ * share.
1291
+ *
1292
+ * @typeParam Repliable - The interaction type this handler processes
1411
1293
  */
1412
- function slashRouteOf(interaction) {
1413
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
1414
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
1415
- return buildSlashRoute(interaction.commandName, subcommand, group);
1416
- }
1294
+ var InteractionHandler = class extends RepliableHandler {
1295
+ constructor(event, core, dispatch) {
1296
+ super(event, core, dispatch);
1297
+ }
1298
+ /**
1299
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
1300
+ * compile time (Discord forbids a modal in response to a modal).
1301
+ */
1302
+ showModal(modal) {
1303
+ return this.sender.showModal(modal);
1304
+ }
1305
+ };
1417
1306
 
1418
1307
  //#endregion
1419
1308
  //#region src/handlers/interaction/SlashHandler.ts
@@ -1449,7 +1338,7 @@ var SlashHandler = class extends InteractionHandler {
1449
1338
  }
1450
1339
  /**
1451
1340
  * Run the arm for whichever command fired. Use this only when the handler is registered for several
1452
- * routes. A single-route handler reads `this.options` directly instead, match buys nothing there.
1341
+ * routes.
1453
1342
  *
1454
1343
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1455
1344
  * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
@@ -1473,7 +1362,7 @@ var SlashHandler = class extends InteractionHandler {
1473
1362
  */
1474
1363
  async match(arms) {
1475
1364
  const route = slashRouteOf(this.event);
1476
- const arm = arms[route];
1365
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1477
1366
  if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1478
1367
  return await arm(this.options);
1479
1368
  }
@@ -1540,15 +1429,15 @@ var ContextMenuHandler = class extends InteractionHandler {
1540
1429
  * }
1541
1430
  * ```
1542
1431
  */
1543
- var AutocompleteHandler = class extends BaseHandler {
1432
+ var AutocompleteHandler = class extends BaseHandler$1 {
1544
1433
  constructor(event, core, dispatch) {
1545
1434
  super(event, core, dispatch);
1546
1435
  }
1436
+ decodedFocused;
1547
1437
  /**
1548
1438
  * The focused option for this interaction. `value` is always the raw partial string the user is typing,
1549
1439
  * even for an integer or number option, so coerce it yourself when you need a number.
1550
1440
  */
1551
- decodedFocused;
1552
1441
  get focused() {
1553
1442
  if (this.decodedFocused) return this.decodedFocused;
1554
1443
  const raw = this.event.options.getFocused(true);
@@ -1564,7 +1453,7 @@ var AutocompleteHandler = class extends BaseHandler {
1564
1453
  * match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
1565
1454
  *
1566
1455
  * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
1567
- * arm receives the focused partial value and a `respond` pinned to that field's choice value type. The
1456
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
1568
1457
  * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
1569
1458
  * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
1570
1459
  *
@@ -1586,11 +1475,15 @@ var AutocompleteHandler = class extends BaseHandler {
1586
1475
  */
1587
1476
  async match(arms) {
1588
1477
  const { name, value } = this.focused;
1589
- const respond = (choices) => this.event.respond(choices);
1590
- const arm = arms[name];
1478
+ const respond = (choices) => this.respond(choices);
1479
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1591
1480
  if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1592
1481
  return await arm(value, respond);
1593
1482
  }
1483
+ /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1484
+ async respond(choices) {
1485
+ await this.event.respond(choices);
1486
+ }
1594
1487
  /** The firing command route, for a field whose completion differs per registered command. */
1595
1488
  get route() {
1596
1489
  return slashRouteOf(this.event);
@@ -1598,7 +1491,7 @@ var AutocompleteHandler = class extends BaseHandler {
1598
1491
  /**
1599
1492
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
1600
1493
  * (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
1601
- * types the focused field. Read the focused field from `this.focused`, not here.
1494
+ * types the focused field. The focused field is on `this.focused`.
1602
1495
  */
1603
1496
  get options() {
1604
1497
  return this.event.options;
@@ -1610,25 +1503,25 @@ var AutocompleteHandler = class extends BaseHandler {
1610
1503
  /**
1611
1504
  * Shared base the customId-routed component handlers extend.
1612
1505
  *
1613
- * Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectMenuHandler}, or
1614
- * {@link ModalHandler} instead. This class only carries the customId decode and route-matching plumbing
1615
- * those bases share, so DO NOT use it directly.
1506
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
1507
+ * instead. This class defines the customId decode and the route matching those bases share.
1616
1508
  *
1617
1509
  * @typeParam Event - The component interaction type this handler processes
1618
1510
  * @typeParam Defs - The customId route definitions registered on the concrete handler
1619
1511
  */
1620
1512
  var ComponentHandler = class extends InteractionHandler {
1621
- get registeredDefs() {
1622
- const defs = Reflect.getMetadata(ComponentDefsKey, this.constructor);
1623
- if (!defs) throw new SeedcordError(SeedcordErrorCode.CustomIdHandlerRouteMissing, [this.constructor.name]);
1624
- return defs;
1513
+ /** Rewrite the source message this component interaction came from. */
1514
+ update(response) {
1515
+ return this.sender.update(response);
1516
+ }
1517
+ /** Acknowledge the component without changing the source message. */
1518
+ deferUpdate() {
1519
+ return this.sender.deferUpdate();
1625
1520
  }
1626
1521
  decoded;
1627
1522
  get route() {
1628
- if (this.decoded) return this.decoded;
1629
- const decoded = decodeFor(this.registeredDefs, this.event.customId);
1630
- this.decoded = decoded;
1631
- return decoded;
1523
+ this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
1524
+ return this.decoded;
1632
1525
  }
1633
1526
  /**
1634
1527
  * The decoded params of the single route this handler is registered for.
@@ -1647,10 +1540,11 @@ var ComponentHandler = class extends InteractionHandler {
1647
1540
  * handler `this.params` is `never`, so match is the only way to read the decoded params.
1648
1541
  *
1649
1542
  * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1650
- * decoded params. The arms must cover every registered def, a missing prefix or an unknown key is a
1651
- * compile error. Decoding runs before any arm, so a stale or corrupt wire throws before an arm body executes.
1543
+ * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1544
+ * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1545
+ * corrupt wire throws before an arm body executes.
1652
1546
  *
1653
- * @param arms - One handler per registered route, keyed by prefix.
1547
+ * @param arms - One callback per registered route, keyed by prefix.
1654
1548
  * @returns The result of the arm that ran.
1655
1549
  *
1656
1550
  * @example
@@ -1659,8 +1553,8 @@ var ComponentHandler = class extends InteractionHandler {
1659
1553
  * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1660
1554
  * async execute() {
1661
1555
  * await this.match({
1662
- * approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
1663
- * reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
1556
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1557
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1664
1558
  * });
1665
1559
  * }
1666
1560
  * }
@@ -1668,7 +1562,7 @@ var ComponentHandler = class extends InteractionHandler {
1668
1562
  */
1669
1563
  async match(arms) {
1670
1564
  const { prefix, params } = this.route;
1671
- const arm = arms[prefix];
1565
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1672
1566
  if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1673
1567
  return await arm(params);
1674
1568
  }
@@ -1680,8 +1574,9 @@ var ComponentHandler = class extends InteractionHandler {
1680
1574
  * Base class for a button interaction handler.
1681
1575
  *
1682
1576
  * Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
1683
- * generic, then read `this.params` for a single route or `this.match` for several. Passing different
1684
- * definitions to the decorator and the generic is a compile error.
1577
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
1578
+ * members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
1579
+ * and the generic is a compile error.
1685
1580
  *
1686
1581
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1687
1582
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1692,7 +1587,7 @@ var ComponentHandler = class extends InteractionHandler {
1692
1587
  * class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
1693
1588
  * async execute() {
1694
1589
  * const { userId } = this.params;
1695
- * await this.event.reply(`approved <@${userId}>`);
1590
+ * await this.reply(`approved <@${userId}>`);
1696
1591
  * }
1697
1592
  * }
1698
1593
  * ```
@@ -1706,7 +1601,8 @@ var ButtonHandler = class extends ComponentHandler {};
1706
1601
  *
1707
1602
  * Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
1708
1603
  * generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
1709
- * inputs from `this.event.fields`.
1604
+ * inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
1605
+ * response to a modal, so `showModal` fails compilation on this kind.
1710
1606
  *
1711
1607
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1712
1608
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1718,7 +1614,7 @@ var ButtonHandler = class extends ComponentHandler {};
1718
1614
  * async execute() {
1719
1615
  * const { guildId } = this.params;
1720
1616
  * const name = this.event.fields.getTextInputValue('name');
1721
- * await this.event.reply(`saved ${name} for ${guildId}`);
1617
+ * await this.reply(`saved ${name} for ${guildId}`);
1722
1618
  * }
1723
1619
  * }
1724
1620
  * ```
@@ -1732,7 +1628,8 @@ var ModalHandler = class extends ComponentHandler {};
1732
1628
  *
1733
1629
  * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1734
1630
  * so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
1735
- * route or `this.match` for several.
1631
+ * route or `this.match` for several, and reply through the handler members or rewrite the source message
1632
+ * with `this.update`.
1736
1633
  *
1737
1634
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1738
1635
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
@@ -1744,7 +1641,7 @@ var ModalHandler = class extends ComponentHandler {};
1744
1641
  * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1745
1642
  * async execute() {
1746
1643
  * const { roleId } = this.params;
1747
- * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1644
+ * await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1748
1645
  * }
1749
1646
  * }
1750
1647
  * ```
@@ -1775,7 +1672,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1775
1672
  * }
1776
1673
  * ```
1777
1674
  */
1778
- var EventHandler = class extends BaseHandler {
1675
+ var EventHandler = class extends BaseHandler$1 {
1779
1676
  firedEvent;
1780
1677
  constructor(event, core, eventName) {
1781
1678
  super(event, core);
@@ -1828,7 +1725,7 @@ var EventHandler = class extends BaseHandler {
1828
1725
  *
1829
1726
  * @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
1830
1727
  */
1831
- var EventMiddleware = class extends BaseHandler {
1728
+ var EventMiddleware = class extends BaseHandler$1 {
1832
1729
  firedEvent;
1833
1730
  constructor(event, core, eventName) {
1834
1731
  super(event, core);
@@ -2010,16 +1907,16 @@ var Paginator = class {
2010
1907
  const loadPage = (ctx, n) => this.page(ctx, n);
2011
1908
  this.Handler = class Nav extends ButtonHandler {
2012
1909
  async execute() {
2013
- await this.event.deferUpdate();
1910
+ await this.deferUpdate();
2014
1911
  const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2015
- await new ReplySender(this.event).edit(this.event.message, response);
1912
+ await this.update(response);
2016
1913
  }
2017
1914
  };
2018
1915
  }
2019
1916
  /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2020
1917
  async start(interaction, core) {
2021
1918
  const response = await this.page(contextOf(interaction, core), 0);
2022
- return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
1919
+ return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
2023
1920
  }
2024
1921
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2025
1922
  async page(ctx, n) {
@@ -2143,19 +2040,184 @@ function Subscribe(subscriber, options) {
2143
2040
  };
2144
2041
  }
2145
2042
 
2043
+ //#endregion
2044
+ //#region src/subscribers/decorators/WebhookUrl.ts
2045
+ /**
2046
+ * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
2047
+ *
2048
+ * An unset variable disables the reporter with a boot warning. A set but malformed value throws
2049
+ * at boot.
2050
+ *
2051
+ * @param envKey - The environment variable name holding the webhook url
2052
+ * @decorator
2053
+ * @example
2054
+ * ```ts
2055
+ * \@Subscribe('memberBanned')
2056
+ * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
2057
+ * class BanLog extends WebhookLog<'memberBanned'> {
2058
+ * report() {
2059
+ * return { components: [new BanCard(this.data).component] };
2060
+ * }
2061
+ * }
2062
+ * ```
2063
+ */
2064
+ function WebhookUrl(envKey) {
2065
+ return function(constructor) {
2066
+ Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
2067
+ };
2068
+ }
2069
+
2070
+ //#endregion
2071
+ //#region src/miscellaneous/flagsFor.ts
2072
+ /** @internal */
2073
+ function flagsFor(ephemeral) {
2074
+ let flags = MessageFlags$1.IsComponentsV2;
2075
+ if (ephemeral) flags |= MessageFlags$1.Ephemeral;
2076
+ return flags;
2077
+ }
2078
+
2079
+ //#endregion
2080
+ //#region src/subscribers/bases/webhookHelpers.ts
2081
+ const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
2082
+ function isDiscordWebhookUrl(value) {
2083
+ return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
2084
+ }
2085
+ function jsonAttachment(name, description, data) {
2086
+ const content = filterCirculars(data);
2087
+ return {
2088
+ name,
2089
+ description,
2090
+ data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2091
+ };
2092
+ }
2093
+ function neutralizeFences(text) {
2094
+ return text.replaceAll("```", "`​`​`");
2095
+ }
2096
+ /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2097
+ function errorReport(error) {
2098
+ let report = error.stack ?? `${error.name}: ${error.message}`;
2099
+ if (Error.isError(error.cause)) {
2100
+ const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2101
+ report += `\n\nCaused by:\n${cause}`;
2102
+ }
2103
+ const stripped = neutralizeFences(stripAnsi(report));
2104
+ return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2105
+ }
2106
+ var WebhookSeparator = class extends BuilderComponent {
2107
+ constructor() {
2108
+ super("separator");
2109
+ this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
2110
+ }
2111
+ };
2112
+
2113
+ //#endregion
2114
+ //#region src/subscribers/bases/WebhookSender.ts
2115
+ const HTTP_NOT_FOUND = 404;
2116
+ const HTTP_UNAUTHORIZED = 401;
2117
+ /**
2118
+ * Sends component messages to a Discord webhook through `@discordjs/rest`.
2119
+ *
2120
+ * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2121
+ */
2122
+ var WebhookSender = class {
2123
+ rest = new REST();
2124
+ route;
2125
+ constructor(url) {
2126
+ const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2127
+ if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2128
+ this.route = Routes.webhook(match[1], match[2]);
2129
+ }
2130
+ /**
2131
+ * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2132
+ * Discord and `unreachable` on any other failure.
2133
+ */
2134
+ async verify() {
2135
+ try {
2136
+ await this.rest.get(this.route, { auth: false });
2137
+ return "ok";
2138
+ } catch (error) {
2139
+ return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2140
+ }
2141
+ }
2142
+ async send(options) {
2143
+ const { flags, username, components, files } = options;
2144
+ await this.rest.post(this.route, {
2145
+ auth: false,
2146
+ query: new URLSearchParams({
2147
+ with_components: "true",
2148
+ wait: "true"
2149
+ }),
2150
+ body: {
2151
+ flags,
2152
+ username,
2153
+ components: components.map((component) => component.toJSON()),
2154
+ ...files?.length && { attachments: files.map((file, id) => ({
2155
+ id,
2156
+ filename: file.name,
2157
+ description: file.description
2158
+ })) }
2159
+ },
2160
+ ...files?.length && { files: files.map((file) => ({
2161
+ name: file.name,
2162
+ data: file.data
2163
+ })) }
2164
+ });
2165
+ }
2166
+ };
2167
+
2146
2168
  //#endregion
2147
2169
  //#region src/subscribers/bases/WebhookLog.ts
2148
2170
  /**
2149
- * Abstract webhook logging handler for subscriber events.
2171
+ * Base class for subscribers that deliver their event to a Discord webhook.
2150
2172
  *
2151
- * Extends Subscriber to provide webhook-based logging capabilities.
2152
- * Implementations must define the webhook client to send messages to by implementing the webhook property.
2173
+ * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2174
+ * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2175
+ * the reporter is skipped at registration with a boot warning.
2153
2176
  *
2154
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
2177
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2155
2178
  */
2156
- var WebhookLog = class extends Subscriber {
2157
- constructor(data, core) {
2158
- super(data, core);
2179
+ var WebhookLog = class WebhookLog extends Subscriber {
2180
+ static senders = /* @__PURE__ */ new Map();
2181
+ async execute() {
2182
+ const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2183
+ if (url === null) {
2184
+ this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2185
+ return;
2186
+ }
2187
+ const { username, components, files } = await this.report();
2188
+ try {
2189
+ await WebhookLog.senderFor(url).send({
2190
+ flags: flagsFor(false),
2191
+ username: username ?? this.constructor.name,
2192
+ components,
2193
+ files
2194
+ });
2195
+ } catch (error) {
2196
+ this.logger.error("Failed to send webhook report", error);
2197
+ }
2198
+ }
2199
+ /** @internal */
2200
+ static senderFor(url) {
2201
+ let sender = WebhookLog.senders.get(url);
2202
+ if (!sender) {
2203
+ sender = new WebhookSender(url);
2204
+ WebhookLog.senders.set(url, sender);
2205
+ }
2206
+ return sender;
2207
+ }
2208
+ /** @internal */
2209
+ static envKeyOf(ctor) {
2210
+ const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
2211
+ if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2212
+ return envKey;
2213
+ }
2214
+ /** @internal Returns null when the env var is unset, throws when set but malformed. */
2215
+ static urlOf(envKey) {
2216
+ if (!Envapter.has(envKey)) return null;
2217
+ const raw = Envapter.getRequired(envKey, Converters.String).trim();
2218
+ if (raw === "") return null;
2219
+ if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2220
+ return raw;
2159
2221
  }
2160
2222
  };
2161
2223
 
@@ -2371,261 +2433,94 @@ var CommandRegistry = class {
2371
2433
  const tag = commands.length === 1 ? "command" : "commands";
2372
2434
  this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
2373
2435
  }
2374
- this.onDeployed?.(result);
2375
- return result;
2376
- }
2377
- /** The deduplicated slash route keys across every global and guild command. @internal */
2378
- routeLeaves() {
2379
- return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2380
- }
2381
- /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2382
- contextMenuLeaves() {
2383
- return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2384
- }
2385
- };
2386
-
2387
- //#endregion
2388
- //#region src/miscellaneous/deriveEventActor.ts
2389
- function deriveEventActor(args) {
2390
- const tuple = Array.isArray(args) ? args : [args];
2391
- for (const arg of tuple) {
2392
- if (arg instanceof Message) return {
2393
- guild: arg.guild,
2394
- member: arg.member,
2395
- user: arg.author,
2396
- channelId: arg.channelId
2397
- };
2398
- if (arg instanceof GuildMember) return {
2399
- guild: arg.guild,
2400
- member: arg,
2401
- user: arg.user,
2402
- channelId: null
2403
- };
2404
- if (arg instanceof User) return {
2405
- guild: null,
2406
- member: null,
2407
- user: arg,
2408
- channelId: null
2409
- };
2410
- }
2411
- return {
2412
- guild: null,
2413
- member: null,
2414
- user: null,
2415
- channelId: null
2416
- };
2417
- }
2418
-
2419
- //#endregion
2420
- //#region src/bot/gates/runGates.ts
2421
- function interactionGateContext(interaction, core) {
2422
- const rawMember = interaction.member;
2423
- const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2424
- return {
2425
- kind: "interaction",
2426
- interaction,
2427
- core,
2428
- user: interaction.user,
2429
- guild: interaction.guild,
2430
- member: rawMember instanceof GuildMember ? rawMember : null,
2431
- userId: interaction.user.id,
2432
- guildId: interaction.guildId,
2433
- channelId: interaction.channelId,
2434
- memberRoleIds,
2435
- memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2436
- routeId: null
2437
- };
2438
- }
2439
- function eventGateContext(eventName, args, core) {
2440
- const actor = deriveEventActor(args);
2441
- return {
2442
- kind: "event",
2443
- core,
2444
- eventName,
2445
- payload: args,
2446
- user: actor.user,
2447
- guild: actor.guild,
2448
- member: actor.member,
2449
- userId: actor.user?.id ?? null,
2450
- guildId: actor.guild?.id ?? null,
2451
- channelId: actor.channelId,
2452
- memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2453
- memberPermissions: actor.member?.permissions.bitfield ?? null,
2454
- routeId: null
2455
- };
2456
- }
2457
-
2458
- //#endregion
2459
- //#region src/miscellaneous/FaultThrottle.ts
2460
- /**
2461
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
2462
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
2463
- * the map stays small, and a stale entry is evicted on the next read of its key.
2464
- *
2465
- * @internal
2466
- */
2467
- var FaultThrottle = class {
2468
- windowMs;
2469
- now;
2470
- lastReportedAt = /* @__PURE__ */ new Map();
2471
- constructor(windowMs = 6e4, now = () => Date.now()) {
2472
- this.windowMs = windowMs;
2473
- this.now = now;
2474
- }
2475
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
2476
- shouldReport(key) {
2477
- const last = this.lastReportedAt.get(key);
2478
- if (last === void 0) return true;
2479
- if (this.now() - last < this.windowMs) return false;
2480
- this.lastReportedAt.delete(key);
2481
- return true;
2482
- }
2483
- markReported(key) {
2484
- this.lastReportedAt.set(key, this.now());
2485
- }
2486
- clear() {
2487
- this.lastReportedAt.clear();
2488
- }
2489
- };
2490
-
2491
- //#endregion
2492
- //#region src/miscellaneous/extractErrorResponse.ts
2493
- const logger$2 = new Logger("ErrorsHandling");
2494
- /**
2495
- * The single throttle for every fault publish, so a recurring fault reports once per window across both
2496
- * the interaction and event paths. Exported so tests reset it between cases.
2497
- *
2498
- * @internal
2499
- */
2500
- const faultThrottle = new FaultThrottle();
2501
- /**
2502
- * Processes an error into a user-facing reply and routes its report.
2503
- *
2504
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
2505
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
2506
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
2507
- * threaded into both the reply and the bus payload.
2508
- *
2509
- * @internal
2510
- */
2511
- function extractErrorResponse(error, core, origin) {
2512
- const uuid = crypto.randomUUID();
2513
- const developerUsername = core.config.notifications?.developerUsername;
2514
- const ctx = developerUsername === void 0 ? { uuid } : {
2515
- uuid,
2516
- developerUsername
2517
- };
2518
- if (error instanceof Notice) {
2519
- if (error.report) reportFault(error, core, origin, uuid);
2520
- return {
2521
- uuid,
2522
- response: error.render(ctx)
2523
- };
2436
+ this.onDeployed?.(result);
2437
+ return result;
2524
2438
  }
2525
- reportRawFault(error, core, origin, uuid);
2526
- const override = core.config.errors?.defaultError;
2527
- return {
2528
- uuid,
2529
- response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
2530
- };
2531
- }
2532
- function withThrottle(origin, error, publish) {
2533
- const key = faultKey(origin, error);
2534
- if (!faultThrottle.shouldReport(key)) {
2535
- logger$2.info(`throttled duplicate fault ${key}`);
2536
- return;
2439
+ /** The deduplicated slash route keys across every global and guild command. @internal */
2440
+ routeLeaves() {
2441
+ return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2442
+ }
2443
+ /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2444
+ contextMenuLeaves() {
2445
+ return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2446
+ }
2447
+ };
2448
+
2449
+ //#endregion
2450
+ //#region src/miscellaneous/deriveEventActor.ts
2451
+ function deriveEventActor(args) {
2452
+ const tuple = Array.isArray(args) ? args : [args];
2453
+ for (const arg of tuple) {
2454
+ if (arg instanceof Message) return {
2455
+ guild: arg.guild,
2456
+ member: arg.member,
2457
+ user: arg.author,
2458
+ channelId: arg.channelId
2459
+ };
2460
+ if (arg instanceof GuildMember) return {
2461
+ guild: arg.guild,
2462
+ member: arg,
2463
+ user: arg.user,
2464
+ channelId: null
2465
+ };
2466
+ if (arg instanceof User) return {
2467
+ guild: null,
2468
+ member: null,
2469
+ user: arg,
2470
+ channelId: null
2471
+ };
2537
2472
  }
2538
- publish();
2539
- faultThrottle.markReported(key);
2540
- }
2541
- function reportFault(denial, core, origin, uuid) {
2542
- withThrottle(origin, denial, () => {
2543
- logger$2.error(`${denial.name}: ${uuid}`, denial);
2544
- if (origin.interaction) core.bus.publish("handledException", {
2545
- denial,
2546
- uuid,
2547
- source: buildInteractionSource(origin.interaction)
2548
- });
2549
- else if (origin.event) core.bus.publish("handledException", {
2550
- denial,
2551
- uuid,
2552
- source: buildEventSource(origin.event, origin)
2553
- });
2554
- else core.bus.publish("unknownException", {
2555
- uuid,
2556
- error: denial,
2557
- guild: origin.guild,
2558
- user: origin.user,
2559
- metadata: metadataFor(origin)
2560
- });
2561
- });
2562
- }
2563
- function reportRawFault(error, core, origin, uuid) {
2564
- withThrottle(origin, error, () => {
2565
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
2566
- else logger$2.error(`${uuid} | ${error.message}`);
2567
- core.bus.publish("unknownException", {
2568
- uuid,
2569
- error,
2570
- guild: origin.guild,
2571
- user: origin.user,
2572
- metadata: metadataFor(origin)
2573
- });
2574
- });
2575
- }
2576
- function metadataFor(origin) {
2577
- if (origin.event) return {
2578
- eventName: origin.event.name,
2579
- handler: origin.event.handler,
2580
- args: origin.event.args
2581
- };
2582
- return origin.metadata;
2583
- }
2584
- function buildEventSource(event, origin) {
2585
2473
  return {
2586
- kind: "event",
2587
- eventName: event.name,
2588
- handler: event.handler,
2589
- userId: origin.user?.id ?? null,
2590
- guildId: origin.guild?.id ?? null,
2591
- channelId: event.channelId,
2592
- raw: event.args
2474
+ guild: null,
2475
+ member: null,
2476
+ user: null,
2477
+ channelId: null
2593
2478
  };
2594
2479
  }
2595
- function faultKey(origin, error) {
2596
- const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
2597
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
2598
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
2599
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
2600
- return `autocomplete:${name}`;
2601
- }
2602
- function interactionRoute(interaction) {
2603
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
2604
- if (interaction.isContextMenuCommand()) return interaction.commandName;
2605
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
2606
- return "interaction";
2607
- }
2608
- function buildInteractionSource(interaction) {
2609
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
2610
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
2480
+
2481
+ //#endregion
2482
+ //#region src/bot/gates/runGates.ts
2483
+ function interactionGateContext(interaction, core) {
2484
+ const rawMember = interaction.member;
2485
+ const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2611
2486
  return {
2612
2487
  kind: "interaction",
2613
- interactionKind: interactionKind(interaction),
2614
- command,
2615
- customId,
2488
+ interaction,
2489
+ core,
2490
+ user: interaction.user,
2491
+ guild: interaction.guild,
2492
+ member: rawMember instanceof GuildMember ? rawMember : null,
2616
2493
  userId: interaction.user.id,
2617
2494
  guildId: interaction.guildId,
2618
2495
  channelId: interaction.channelId,
2619
- interactionId: interaction.id,
2620
- raw: interaction
2496
+ memberRoleIds,
2497
+ memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2498
+ appPermissions: interaction.appPermissions.bitfield,
2499
+ memberGuildPermissions: rawMember instanceof GuildMember ? rawMember.permissions.bitfield : null,
2500
+ appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
2501
+ routeId: null
2621
2502
  };
2622
2503
  }
2623
- function interactionKind(interaction) {
2624
- if (interaction.isChatInputCommand()) return "slash";
2625
- if (interaction.isContextMenuCommand()) return "context-menu";
2626
- if (interaction.isButton()) return "button";
2627
- if (interaction.isAnySelectMenu()) return "select";
2628
- return "modal";
2504
+ function eventGateContext(eventName, args, core) {
2505
+ const actor = deriveEventActor(args);
2506
+ return {
2507
+ kind: "event",
2508
+ core,
2509
+ eventName,
2510
+ payload: args,
2511
+ user: actor.user,
2512
+ guild: actor.guild,
2513
+ member: actor.member,
2514
+ userId: actor.user?.id ?? null,
2515
+ guildId: actor.guild?.id ?? null,
2516
+ channelId: actor.channelId,
2517
+ memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2518
+ memberPermissions: actor.member?.permissions.bitfield ?? null,
2519
+ appPermissions: null,
2520
+ memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
2521
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
2522
+ routeId: null
2523
+ };
2629
2524
  }
2630
2525
 
2631
2526
  //#endregion
@@ -2640,7 +2535,7 @@ const logger$1 = new Logger("EventBoundary");
2640
2535
  */
2641
2536
  function handleEventFault(caught, eventName, handlerName, args, core) {
2642
2537
  if (caught instanceof Silence) {
2643
- if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
2538
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2644
2539
  return;
2645
2540
  }
2646
2541
  if (!Error.isError(caught)) throw caught;
@@ -2885,35 +2780,55 @@ var EventDispatcher = class {
2885
2780
  };
2886
2781
 
2887
2782
  //#endregion
2888
- //#region src/bot/defaults/UnhandledEvent.ts
2783
+ //#region src/bot/defaults/UnhandledRepliable.ts
2889
2784
  /**
2890
- * Default handler for unhandled interactions. A reply failure here rides the controller boundary as a
2891
- * raw fault, so it needs no decorator of its own.
2892
- *
2893
- * @internal
2785
+ * @internal Default handler for unhandled repliable interactions.
2894
2786
  */
2895
- var UnhandledEvent = class extends InteractionHandler {
2787
+ var UnhandledRepliable = class extends InteractionHandler {
2896
2788
  async execute() {
2897
- await this.event.reply({
2898
- content: `Feature not implemented yet.`,
2899
- flags: MessageFlags.Ephemeral
2900
- });
2789
+ await this.reply("Feature not implemented yet.");
2790
+ }
2791
+ };
2792
+
2793
+ //#endregion
2794
+ //#region src/bot/defaults/UnhandledAutocomplete.ts
2795
+ /**
2796
+ * @internal Default handler for an autocomplete with no registered handler.
2797
+ */
2798
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
2799
+ async execute() {
2800
+ await this.respond([]);
2901
2801
  }
2902
2802
  };
2903
2803
 
2804
+ //#endregion
2805
+ //#region src/bot/harmlessApiCodes.ts
2806
+ /**
2807
+ * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2808
+ * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2809
+ *
2810
+ * @internal
2811
+ */
2812
+ const HARMLESS_API_CODES = new Set([
2813
+ RESTJSONErrorCodes.UnknownInteraction,
2814
+ RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2815
+ RESTJSONErrorCodes.UnknownMessage
2816
+ ]);
2817
+
2904
2818
  //#endregion
2905
2819
  //#region src/bot/handleInteractionFault.ts
2906
2820
  const logger = new Logger("InteractionBoundary");
2907
2821
  /**
2908
2822
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
2909
- * user reply plus the right bus event, building a {@link ReplySender} lazily so it reads the live
2910
- * acknowledgement state at catch time.
2823
+ * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
2824
+ * sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
2825
+ * boundary builds one from the interaction flags.
2911
2826
  *
2912
2827
  * @internal
2913
2828
  */
2914
- async function handleInteractionFault(caught, interaction, core) {
2829
+ async function handleInteractionFault(caught, interaction, core, sender) {
2915
2830
  if (caught instanceof Silence) {
2916
- if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
2831
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2917
2832
  return;
2918
2833
  }
2919
2834
  if (!Error.isError(caught)) throw caught;
@@ -2937,7 +2852,18 @@ async function handleInteractionFault(caught, interaction, core) {
2937
2852
  user: interaction.user,
2938
2853
  metadata: interaction
2939
2854
  });
2940
- await new ReplySender(interaction).send(response, caught instanceof Notice ? caught.ephemeral : true);
2855
+ await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
2856
+ }
2857
+ async function sendGuarded(sender, response, ephemeral) {
2858
+ try {
2859
+ await sender.send(response, { ephemeral });
2860
+ } catch (error) {
2861
+ if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2862
+ logger.debug(`reply send hit harmless code ${error.code}`);
2863
+ return;
2864
+ }
2865
+ logger.error("reply send failed", error);
2866
+ }
2941
2867
  }
2942
2868
 
2943
2869
  //#endregion
@@ -3006,7 +2932,7 @@ var InteractionDispatcher = class {
3006
2932
  }
3007
2933
  /**
3008
2934
  * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
3009
- * UnhandledEvent at runtime. A bot may route commands outside the registry, so this does not throw.
2935
+ * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
3010
2936
  *
3011
2937
  * @internal
3012
2938
  */
@@ -3014,7 +2940,7 @@ var InteractionDispatcher = class {
3014
2940
  for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${paint.sky.bold(route)} has no registered ${chalk.bold("@SlashRoute")} handler.`);
3015
2941
  }
3016
2942
  /**
3017
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
2943
+ * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
3018
2944
  * at runtime. Checked per kind since a user and a message command can share a name.
3019
2945
  *
3020
2946
  * @internal
@@ -3179,22 +3105,31 @@ var InteractionDispatcher = class {
3179
3105
  if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3180
3106
  await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
3181
3107
  }
3182
- async processInteraction(interaction, extractKey, getHandler) {
3108
+ async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
3183
3109
  const key = extractKey(interaction);
3110
+ let sender;
3184
3111
  try {
3185
3112
  if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3186
3113
  let HandlerCtor = getHandler(key);
3187
3114
  if (!HandlerCtor) {
3188
- this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to UnhandledEvent.`);
3189
- HandlerCtor = UnhandledEvent;
3115
+ this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3116
+ HandlerCtor = fallback;
3190
3117
  }
3191
3118
  this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
3192
3119
  const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
3193
3120
  const handler = new HandlerCtor(interaction, this.core, dispatch);
3194
- if (!interaction.isAutocomplete()) await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core));
3121
+ if (handler instanceof RepliableHandler) sender = handler.getSender();
3122
+ if (!interaction.isAutocomplete()) {
3123
+ const monitor = slowGateMonitor();
3124
+ try {
3125
+ await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3126
+ } finally {
3127
+ monitor?.report(dispatch.routeId);
3128
+ }
3129
+ }
3195
3130
  await handler.execute();
3196
3131
  } catch (caught) {
3197
- await handleInteractionFault(caught, interaction, this.core);
3132
+ await handleInteractionFault(caught, interaction, this.core, sender);
3198
3133
  }
3199
3134
  }
3200
3135
  async handleInteraction(interaction) {
@@ -3270,12 +3205,7 @@ var InteractionDispatcher = class {
3270
3205
  }
3271
3206
  async handleAutocomplete(interaction) {
3272
3207
  const route = slashRouteOf(interaction);
3273
- if (!this.autocompleteMap.has(route)) {
3274
- this.logger.warn(`No autocomplete handler for ${paint.sky.bold(route)}.`);
3275
- await interaction.respond([]);
3276
- return;
3277
- }
3278
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
3208
+ await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3279
3209
  }
3280
3210
  };
3281
3211
 
@@ -3331,7 +3261,7 @@ var Bot = class extends Plugin {
3331
3261
  });
3332
3262
  }
3333
3263
  this.emojiInjector = new EmojiInjector(core);
3334
- core.shutdown.addTask(ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3264
+ core.shutdown.addTask(ShutdownPhase$1.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3335
3265
  }
3336
3266
  /**
3337
3267
  * Initializes Discord client and all controllers
@@ -3364,7 +3294,9 @@ var Bot = class extends Plugin {
3364
3294
  * Logs the bot into Discord using the configured token
3365
3295
  */
3366
3296
  async login(token) {
3367
- await this._client.login(token);
3297
+ this._client.once(Events.ClientReady, () => this.emit("ready"));
3298
+ this._client.login(token);
3299
+ await this.waitFor("ready");
3368
3300
  this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
3369
3301
  return this;
3370
3302
  }
@@ -3390,157 +3322,70 @@ var Bot = class extends Plugin {
3390
3322
  };
3391
3323
  __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3392
3324
 
3393
- //#endregion
3394
- //#region src/subscribers/bases/webhookHelpers.ts
3395
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
3396
- /**
3397
- * Whether a string is a well-formed Discord webhook URL.
3398
- *
3399
- * @internal
3400
- */
3401
- function isDiscordWebhookUrl(value) {
3402
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
3403
- }
3404
- /**
3405
- * Serializes a value to a JSON attachment, made circular-safe first.
3406
- *
3407
- * @internal
3408
- */
3409
- function jsonAttachment(name, description, data) {
3410
- const content = filterCirculars(data);
3411
- return new AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
3412
- name,
3413
- description
3414
- });
3415
- }
3416
- /**
3417
- * The small divider the default webhook reporters place between sections.
3418
- *
3419
- * @internal
3420
- */
3421
- var WebhookSeparator = class extends BuilderComponent {
3422
- constructor() {
3423
- super("separator");
3424
- this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
3425
- }
3426
- };
3427
-
3428
3325
  //#endregion
3429
3326
  //#region src/subscribers/default/HandledException.ts
3430
- var _HandledException;
3431
- function webhookUrlValidator$1(raw) {
3432
- if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3433
- const value = raw?.trim() ?? "";
3434
- if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
3435
- if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3436
- return value;
3437
- }
3438
3327
  let HandledException = class HandledException extends WebhookLog {
3439
- static {
3440
- _HandledException = this;
3441
- }
3442
- webhook = new WebhookClient({ url: _HandledException.handledExceptionWebhookUrl });
3443
- async execute() {
3444
- const { source } = this.data;
3445
- try {
3446
- await this.webhook.send({
3447
- flags: flagsFor(false),
3448
- withComponents: true,
3449
- username: "Handled Exception",
3450
- components: [new HandledExceptionContainer(this.data).component],
3451
- files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
3452
- });
3453
- } catch (error) {
3454
- this.logger.error("Failed to send handled exception webhook", error);
3455
- }
3328
+ report() {
3329
+ return {
3330
+ username: "Handled Exception",
3331
+ components: [new HandledExceptionContainer(this.data).component],
3332
+ files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3333
+ };
3456
3334
  }
3457
3335
  };
3458
- __decorate([Envapt("HANDLED_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator$1(raw) }), __decorateMetadata("design:type", String)], HandledException, "handledExceptionWebhookUrl", void 0);
3459
- HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
3336
+ HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3460
3337
  var HandledExceptionContainer = class extends BuilderComponent {
3461
3338
  constructor(data) {
3462
3339
  super("container");
3463
3340
  const { denial, uuid, source } = data;
3464
- 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"));
3341
+ 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"));
3465
3342
  }
3466
3343
  };
3467
- /**
3468
- * The summary block of a fault report, narrowed per source kind so an interaction fault shows its
3469
- * command and customId while an event fault shows its event name and handler.
3470
- *
3471
- * @internal
3472
- */
3473
3344
  function faultSummary(denial, source) {
3474
- const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
3345
+ const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3475
3346
  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"}\``;
3476
3347
  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}\``;
3477
3348
  }
3478
- /**
3479
- * The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
3480
- * circular object) so building the report never throws.
3481
- *
3482
- * @internal
3483
- */
3484
3349
  function causeStack(denial) {
3485
3350
  const { cause } = denial;
3486
- if (Error.isError(cause)) return cause.stack ?? cause.message;
3487
- if (typeof cause === "string") return cause;
3351
+ if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
3352
+ if (typeof cause === "string") return stripAnsi(cause);
3488
3353
  if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3489
3354
  if (cause !== void 0) try {
3490
3355
  return JSON.stringify(cause);
3491
3356
  } catch {
3492
3357
  return "[unserializable cause]";
3493
3358
  }
3494
- return denial.stack ?? "No cause recorded.";
3359
+ return stripAnsi(denial.stack ?? "No cause recorded.");
3495
3360
  }
3496
3361
 
3497
3362
  //#endregion
3498
3363
  //#region src/subscribers/default/UnknownException.ts
3499
- var _UnknownException;
3500
- function webhookUrlValidator(raw) {
3501
- if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3502
- const value = raw?.trim() ?? "";
3503
- if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
3504
- if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3505
- return value;
3506
- }
3507
3364
  let UnknownException = class UnknownException extends WebhookLog {
3508
- static {
3509
- _UnknownException = this;
3510
- }
3511
- webhook = new WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
3512
- async execute() {
3365
+ report() {
3513
3366
  const { metadata } = this.data;
3514
- const metadataFile = metadata ? jsonAttachment("metadata.json", "Metadata associated with the error", metadata) : null;
3515
- try {
3516
- await this.webhook.send({
3517
- flags: flagsFor(false),
3518
- withComponents: true,
3519
- username: "Unknown Exception",
3520
- components: [new UnhandledErrorContainer(this.data).component],
3521
- files: metadataFile ? [metadataFile] : []
3522
- });
3523
- } catch (error) {
3524
- this.logger.error("Failed to send unknown exception webhook", error);
3525
- }
3367
+ return {
3368
+ username: "Unknown Exception",
3369
+ components: [new UnhandledErrorContainer(this.data).component],
3370
+ files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3371
+ };
3526
3372
  }
3527
3373
  };
3528
- __decorate([Envapt("UNKNOWN_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator(raw) }), __decorateMetadata("design:type", String)], UnknownException, "unknownExceptionWebhookUrl", void 0);
3529
- UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
3374
+ UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3530
3375
  var UnhandledErrorContainer = class extends BuilderComponent {
3531
3376
  constructor(data) {
3532
3377
  super("container");
3533
3378
  const { uuid, error, guild, user, metadata } = data;
3534
- 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}\`\`\``));
3379
+ 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)}\`\`\``));
3535
3380
  this.addTimestampsIfAvailable(error);
3536
3381
  this.addMetadataIfAvailable(metadata);
3537
3382
  }
3538
3383
  addTimestampsIfAvailable(error) {
3539
- if (!(error instanceof DiscordAPIError)) return;
3384
+ if (!(error instanceof DiscordAPIError$1)) return;
3540
3385
  const now = Date.now();
3541
3386
  const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3542
3387
  if (!snowflake) return void 0;
3543
- const interactionTs = Number(SnowflakeUtil.deconstruct(snowflake).timestamp);
3388
+ const interactionTs = timestampFromSnowflake(snowflake);
3544
3389
  const diff = now - interactionTs;
3545
3390
  const seconds = Math.floor(diff / 1e3);
3546
3391
  const millis = diff % 1e3;
@@ -3555,12 +3400,8 @@ var UnhandledErrorContainer = class extends BuilderComponent {
3555
3400
  //#endregion
3556
3401
  //#region src/subscribers/Bus.ts
3557
3402
  /**
3558
- * Manages application subscribers and event handling
3559
- *
3560
- * Provides a centralized system for registering and executing custom subscribers
3561
- * throughout the application lifecycle. Bus subscribers are loaded from configured directories
3562
- * and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
3563
- * construct it directly.
3403
+ * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3404
+ * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3564
3405
  */
3565
3406
  var Bus = class extends Plugin {
3566
3407
  core;
@@ -3570,6 +3411,7 @@ var Bus = class extends Plugin {
3570
3411
  isInitialized = false;
3571
3412
  subscribersMap = /* @__PURE__ */ new Map();
3572
3413
  executedOnceHandlers = /* @__PURE__ */ new Set();
3414
+ webhookProbes = /* @__PURE__ */ new Map();
3573
3415
  hmrHandler;
3574
3416
  constructor(core) {
3575
3417
  super(core);
@@ -3595,7 +3437,6 @@ var Bus = class extends Plugin {
3595
3437
  this.isInitialized = true;
3596
3438
  this.registerSubscriber(UnknownException);
3597
3439
  this.registerSubscriber(HandledException);
3598
- Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
3599
3440
  const subscribersDir = this.core.config.subscribers.path;
3600
3441
  if (subscribersDir) {
3601
3442
  this.logger.info(chalk.bold(subscribersDir));
@@ -3603,8 +3444,19 @@ var Bus = class extends Plugin {
3603
3444
  const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3604
3445
  this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
3605
3446
  }
3447
+ if (!Envapter.isTest) await this.verifyWebhooks();
3448
+ }
3449
+ /** @internal Exposed for tests. */
3450
+ async verifyWebhooks() {
3451
+ const missing = [];
3452
+ await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3453
+ const result = await WebhookLog.senderFor(url).verify();
3454
+ if (result === "missing") missing.push(...envKeys);
3455
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
3456
+ }));
3457
+ if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3606
3458
  }
3607
- /** @internal For use in dev mode */
3459
+ /** @internal */
3608
3460
  async onHmr(event) {
3609
3461
  if (this.hmrHandler) await this.hmrHandler.handle(event);
3610
3462
  }
@@ -3621,6 +3473,17 @@ var Bus = class extends Plugin {
3621
3473
  }, this.logger);
3622
3474
  }
3623
3475
  registerSubscriber(handler) {
3476
+ if (handler.prototype instanceof WebhookLog) {
3477
+ const envKey = WebhookLog.envKeyOf(handler);
3478
+ const url = WebhookLog.urlOf(envKey);
3479
+ if (url === null) {
3480
+ this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
3481
+ return;
3482
+ }
3483
+ const envKeys = this.webhookProbes.get(url) ?? [];
3484
+ if (!envKeys.includes(envKey)) envKeys.push(envKey);
3485
+ this.webhookProbes.set(url, envKeys);
3486
+ }
3624
3487
  const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
3625
3488
  let handlers = this.subscribersMap.get(options.subscriber);
3626
3489
  if (!handlers) {
@@ -3680,7 +3543,7 @@ var Bus = class extends Plugin {
3680
3543
  //#endregion
3681
3544
  //#region src/version.ts
3682
3545
  /** Package version */
3683
- const version = "0.1.0-next.2";
3546
+ const version = "0.1.0-next.3";
3684
3547
 
3685
3548
  //#endregion
3686
3549
  //#region src/Seedcord.ts
@@ -3746,17 +3609,17 @@ var Seedcord = class Seedcord extends Pluggable {
3746
3609
  }
3747
3610
  registerStartupTasks() {
3748
3611
  if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
3749
- this.startup.addTask(StartupPhase.Configuration, "Bus Initialization", async () => {
3612
+ this.startup.addTask(StartupPhase$1.Configuration, "Bus Initialization", async () => {
3750
3613
  this.bus.logger.utils.initialization("Subscribers", "start");
3751
3614
  await this.bus.init();
3752
3615
  this.bus.logger.utils.initialization("Subscribers", "end");
3753
3616
  });
3754
- this.startup.addTask(StartupPhase.Instantiation, "Bot Initialization", async () => {
3617
+ this.startup.addTask(StartupPhase$1.Instantiation, "Bot Initialization", async () => {
3755
3618
  this.bot.logger.utils.initialization("Bot", "start");
3756
3619
  await this.bot.init();
3757
3620
  this.bot.logger.utils.initialization("Bot", "end");
3758
3621
  });
3759
- this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
3622
+ this.startup.addTask(StartupPhase$1.Ready, "Health Check", async () => {
3760
3623
  this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3761
3624
  await this.healthCheck.init();
3762
3625
  this.healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3772,7 +3635,7 @@ var Seedcord = class Seedcord extends Pluggable {
3772
3635
  return this;
3773
3636
  }
3774
3637
  registerHmrAwareModules() {
3775
- this.startup.addTask(StartupPhase.Configuration, "HMR Registration", async () => {
3638
+ this.startup.addTask(StartupPhase$1.Configuration, "HMR Registration", async () => {
3776
3639
  this.hmrManager.register(this.bot);
3777
3640
  this.hmrManager.register(this.bus);
3778
3641
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
@@ -3782,5 +3645,5 @@ var Seedcord = class Seedcord extends Pluggable {
3782
3645
  };
3783
3646
 
3784
3647
  //#endregion
3785
- export { ArraySource, AutocompleteHandler, AutocompleteRoute, ButtonHandler, ButtonRoute, CommandMentions, ContextMenuHandler, ContextMenuRoute, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, ModalRoute, Nsfw, Paginator, Pluggable, Plugin, RegisterCommand, RegisterEvent, ReplySender, RequireBotPermissions, RequirePermissions, RequireRole, Seedcord, SelectMenuHandler, SelectMenuKind, SelectMenuRoute, SlashHandler, SlashRoute, Subscribe, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3648
+ export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Pluggable, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, ShutdownPhase, SlashHandler, StartupPhase, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3786
3649
  //# sourceMappingURL=index.mjs.map