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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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";
12
- import { TypedEventEmitter } from "@seedcord/event-emitter";
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
+ import chalk from "chalk";
13
+ import { Converters, Envapter } from "envapt";
14
+ import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
15
+ import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
16
+ import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
17
+ import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, settleWithin } from "@seedcord/core/node/internal";
13
18
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
14
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, StartupPhase } from "@seedcord/services";
15
19
  import { SeedcordBrand } from "@seedcord/types/internal";
16
- import { Envapter } from "envapt";
17
- import chalk from "chalk";
20
+ import { TypedEventEmitter } from "@seedcord/event-emitter";
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
- export * from "@seedcord/event-emitter"
32
+ export * from "@seedcord/utils"
33
33
 
34
34
  export * from "@seedcord/rate-limiter"
35
35
 
36
- export * from "@seedcord/services"
36
+ export * from "@seedcord/event-emitter"
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
@@ -1062,115 +753,310 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
1062
753
  await member.roles.set([...updated]);
1063
754
  }
1064
755
 
1065
- //#endregion
1066
- //#region src/bot/harmlessApiCodes.ts
1067
- /**
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
1073
- */
1074
- const HARMLESS_API_CODES = new Set([
1075
- RESTJSONErrorCodes.UnknownInteraction,
1076
- RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1077
- RESTJSONErrorCodes.UnknownMessage
1078
- ]);
1079
-
1080
- //#endregion
1081
- //#region src/miscellaneous/flagsFor.ts
1082
- /**
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.
1088
- *
1089
- * @internal
1090
- */
1091
- function flagsFor(ephemeral) {
1092
- let flags = MessageFlags.IsComponentsV2;
1093
- if (ephemeral) flags |= MessageFlags.Ephemeral;
1094
- return flags;
1095
- }
1096
-
1097
756
  //#endregion
1098
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
+ }
1099
764
  /**
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.
765
+ * Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
766
+ * handler bases and the dispatcher.
1103
767
  */
1104
- var ReplySender = class {
768
+ var ReplySender = class extends BaseReplySender {
1105
769
  interaction;
1106
- logger = new Logger("ReplySender");
1107
- constructor(interaction) {
770
+ constructor(interaction, routeId) {
771
+ super(routeId, seedState(interaction));
1108
772
  this.interaction = interaction;
1109
773
  }
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
- }
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);
1125
780
  }
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
- }
781
+ async writeDefer(opts) {
782
+ await this.interaction.deferReply({ flags: deferFlags(opts) });
1139
783
  }
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();
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));
1148
799
  }
1149
- replyOptions(response, ephemeral) {
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);
1150
833
  return {
1151
- components: response.components,
1152
- flags: flagsFor(ephemeral),
1153
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1154
- ...response.files && { files: response.files }
834
+ components: reply.components,
835
+ flags,
836
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
837
+ ...reply.files && { files: [...reply.files] }
1155
838
  };
1156
839
  }
1157
- editBody(response) {
840
+ editOptions(response) {
841
+ const reply = this.serialize(response);
1158
842
  return {
1159
- components: response.components,
843
+ components: reply.components,
1160
844
  flags: MessageFlags.IsComponentsV2,
1161
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1162
- ...response.files && { files: response.files }
845
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
846
+ ...reply.files && { files: [...reply.files] }
1163
847
  };
1164
848
  }
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);
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;
1171
860
  }
1172
861
  };
1173
862
 
863
+ //#endregion
864
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
865
+ /**
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.
869
+ *
870
+ * @internal
871
+ */
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);
876
+ }
877
+
878
+ //#endregion
879
+ //#region src/miscellaneous/FaultThrottle.ts
880
+ /**
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
886
+ */
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;
894
+ }
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;
902
+ }
903
+ markReported(key) {
904
+ this.lastReportedAt.set(key, this.now());
905
+ }
906
+ clear() {
907
+ this.lastReportedAt.clear();
908
+ }
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);
932
+ return {
933
+ uuid,
934
+ response: error.render(ctx)
935
+ };
936
+ }
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;
949
+ }
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
+ }
1059
+
1174
1060
  //#endregion
1175
1061
  //#region src/bot/confirm/reserved.ts
1176
1062
  const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
@@ -1215,152 +1101,36 @@ 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);
1224
1110
  }
1225
- async function getConfirmation(interaction, prompt, options) {
1226
- const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1227
- 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;
1231
- const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1232
- if (!winner) {
1233
- await settle(sender, interaction, message, options?.onTimeout);
1234
- return false;
1235
- }
1236
- await winner.deferUpdate().catch(() => void 0);
1237
- const confirmed = winner.customId === CONFIRM_IDS.confirm;
1238
- await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
1239
- return confirmed;
1240
- }
1241
-
1242
- //#endregion
1243
- //#region src/interfaces/Plugin.ts
1244
- /**
1245
- * Base class for Seedcord plugins
1246
- *
1247
- * Extend this class to create plugins that integrate with the Seedcord lifecycle.
1248
- * Plugins have access to the core instance and must implement initialization logic.
1249
- */
1250
- var Plugin = class extends TypedEventEmitter {
1251
- pluggable;
1252
- name = this.constructor.name;
1253
- constructor(pluggable) {
1254
- super();
1255
- this.pluggable = pluggable;
1256
- }
1257
- /**
1258
- * Reloads plugin state on an HMR update. The base implementation is a no-op, override it in your plugin.
1259
- * @virtual
1260
- */
1261
- onHmr(_event) {
1262
- return Promise.resolve();
1263
- }
1264
- /**
1265
- * Registers critical file patterns that should trigger a full restart when changed in Dev HMR.
1266
- * @param patterns - Glob patterns relative to the project root
1267
- */
1268
- registerCriticalFiles(patterns) {
1269
- getDevChannel()?.send("seedcord:register-critical-files", { patterns });
1270
- }
1271
- /** @internal */
1272
- removeListener(event, listener) {
1273
- return super.removeListener(event, listener);
1274
- }
1275
- /** @internal */
1276
- removeAllListeners(event) {
1277
- return super.removeAllListeners(event);
1278
- }
1279
- };
1280
- /**
1281
- * Base class for objects that can have plugins attached.
1282
- *
1283
- * Provides plugin attachment capabilities and lifecycle management. Plugins are attached during
1284
- * configuration and initialized during startup. Not constructed directly, the host is a {@link Seedcord}.
1285
- */
1286
- var Pluggable = class Pluggable extends TypedEventEmitter {
1287
- isInitialized = false;
1288
- shutdown;
1289
- startup;
1290
- plugins = [];
1291
- static PLUGIN_INIT_TIMEOUT_MS = 15e3;
1292
- constructor(shutdown, startup) {
1293
- super();
1294
- this.shutdown = shutdown;
1295
- this.startup = startup;
1296
- }
1297
- /** @internal */
1298
- async init() {
1299
- if (this.isInitialized) return this;
1300
- await this.startup.run();
1301
- this.isInitialized = true;
1302
- return this;
1303
- }
1304
- /**
1305
- * Attaches a plugin to this instance
1306
- *
1307
- * Plugins provide external functionality and are initialized during the specified startup phase.
1308
- * The plugin instance becomes available as a property in `core` wherever it's available.
1309
- *
1310
- * Make sure to augment the {@link Core} interface with the plugin type to ensure TypeScript recognizes it and provides intellisense.
1311
- *
1312
- * @typeParam Key - The property name for accessing the plugin
1313
- * @typeParam Ctor - The plugin constructor type
1314
- * @param key - Property name to access the plugin instance
1315
- * @param Plugin - Plugin constructor class
1316
- * @param startupPhase - When during startup to initialize this plugin ({@link StartupPhase})
1317
- * @param args - Additional arguments to pass to the plugin constructor
1318
- * @returns This instance with the plugin attached as a typed property
1319
- * @throws A **SeedcordError** When called after initialization or if key already exists
1320
- * @example
1321
- * ```typescript
1322
- * seedcord.attach('db', Mongo, StartupPhase.Configuration, { uri: 'mongodb://...', name: 'seedcord', dir: ... })
1323
- * ```
1324
- */
1325
- attach(key, Plugin, startupPhase, ...args) {
1326
- if (this.isInitialized) throw new SeedcordError(SeedcordErrorCode.CorePluginAfterInit);
1327
- if (key in this) throw new SeedcordError(SeedcordErrorCode.CorePluginKeyExists, [key]);
1328
- const instance = new Plugin(this, ...args);
1329
- this.plugins.push(instance);
1330
- const entry = { [key]: instance };
1331
- this.startup.addTask(startupPhase, `Plugin:${key}`, async () => {
1332
- instance.logger.utils.initialization(key, "start");
1333
- await instance.init();
1334
- instance.logger.utils.initialization(key, "end");
1335
- }, Pluggable.PLUGIN_INIT_TIMEOUT_MS);
1336
- return Object.assign(this, entry);
1337
- }
1338
- /** @internal */
1339
- removeListener(event, listener) {
1340
- return super.removeListener(event, listener);
1341
- }
1342
- /** @internal */
1343
- removeAllListeners(event) {
1344
- return super.removeAllListeners(event);
1111
+ async function getConfirmation(interaction, prompt, options) {
1112
+ const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1113
+ const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1114
+ const sender = new ReplySender(interaction, interactionRoute(interaction));
1115
+ const message = await sender.send(response, { ephemeral });
1116
+ const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1117
+ if (!winner) {
1118
+ await settle(sender, message, options?.onTimeout);
1119
+ return false;
1345
1120
  }
1346
- };
1121
+ await winner.deferUpdate().catch(() => void 0);
1122
+ const confirmed = winner.customId === CONFIRM_IDS.confirm;
1123
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1124
+ return confirmed;
1125
+ }
1347
1126
 
1348
1127
  //#endregion
1349
1128
  //#region src/handlers/BaseHandler.ts
1350
1129
  /**
1351
- * Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
1130
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1131
+ * gate hook.
1352
1132
  */
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
- }
1133
+ var BaseHandler$1 = class extends BaseHandler {
1364
1134
  /** @internal */
1365
1135
  getEvent() {
1366
1136
  return this.event;
@@ -1377,43 +1147,56 @@ var BaseHandler = class {
1377
1147
  *
1378
1148
  * @typeParam Repliable - The interaction type this middleware processes
1379
1149
  */
1380
- var InteractionMiddleware = class extends BaseHandler {
1150
+ var InteractionMiddleware = class extends BaseHandler$1 {
1381
1151
  constructor(event, core) {
1382
1152
  super(event, core);
1383
1153
  }
1384
1154
  };
1385
1155
 
1386
1156
  //#endregion
1387
- //#region src/handlers/interaction/InteractionHandler.ts
1157
+ //#region src/handlers/RepliableHandler.ts
1388
1158
  /**
1389
- * Shared base the typed interaction handlers extend.
1159
+ * Shared base the repliable interaction handlers extend.
1390
1160
  *
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.
1161
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1162
+ * {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
1163
+ * to the gateway sender.
1394
1164
  *
1395
- * @typeParam Repliable - The interaction type this handler processes
1165
+ * @typeParam Event - The repliable interaction type this handler processes
1396
1166
  */
1397
- var InteractionHandler = class extends BaseHandler {
1398
- constructor(event, core, dispatch) {
1399
- super(event, core, dispatch);
1167
+ var RepliableHandler = class extends RepliableHandler$1 {
1168
+ /** @internal */
1169
+ getEvent() {
1170
+ return this.event;
1171
+ }
1172
+ buildSender(event, _core, routeId) {
1173
+ return new ReplySender(event, routeId);
1400
1174
  }
1401
1175
  };
1402
1176
 
1403
1177
  //#endregion
1404
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1178
+ //#region src/handlers/interaction/InteractionHandler.ts
1405
1179
  /**
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.
1180
+ * Shared base the typed interaction handlers extend.
1409
1181
  *
1410
- * @internal
1182
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1183
+ * or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
1184
+ * share.
1185
+ *
1186
+ * @typeParam Repliable - The interaction type this handler processes
1411
1187
  */
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
- }
1188
+ var InteractionHandler = class extends RepliableHandler {
1189
+ constructor(event, core, dispatch) {
1190
+ super(event, core, dispatch);
1191
+ }
1192
+ /**
1193
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
1194
+ * compile time (Discord forbids a modal in response to a modal).
1195
+ */
1196
+ showModal(modal) {
1197
+ return this.sender.showModal(modal);
1198
+ }
1199
+ };
1417
1200
 
1418
1201
  //#endregion
1419
1202
  //#region src/handlers/interaction/SlashHandler.ts
@@ -1449,7 +1232,7 @@ var SlashHandler = class extends InteractionHandler {
1449
1232
  }
1450
1233
  /**
1451
1234
  * 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.
1235
+ * routes.
1453
1236
  *
1454
1237
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1455
1238
  * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
@@ -1473,7 +1256,7 @@ var SlashHandler = class extends InteractionHandler {
1473
1256
  */
1474
1257
  async match(arms) {
1475
1258
  const route = slashRouteOf(this.event);
1476
- const arm = arms[route];
1259
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1477
1260
  if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1478
1261
  return await arm(this.options);
1479
1262
  }
@@ -1540,15 +1323,15 @@ var ContextMenuHandler = class extends InteractionHandler {
1540
1323
  * }
1541
1324
  * ```
1542
1325
  */
1543
- var AutocompleteHandler = class extends BaseHandler {
1326
+ var AutocompleteHandler = class extends BaseHandler$1 {
1544
1327
  constructor(event, core, dispatch) {
1545
1328
  super(event, core, dispatch);
1546
1329
  }
1330
+ decodedFocused;
1547
1331
  /**
1548
1332
  * The focused option for this interaction. `value` is always the raw partial string the user is typing,
1549
1333
  * even for an integer or number option, so coerce it yourself when you need a number.
1550
1334
  */
1551
- decodedFocused;
1552
1335
  get focused() {
1553
1336
  if (this.decodedFocused) return this.decodedFocused;
1554
1337
  const raw = this.event.options.getFocused(true);
@@ -1564,7 +1347,7 @@ var AutocompleteHandler = class extends BaseHandler {
1564
1347
  * match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
1565
1348
  *
1566
1349
  * 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
1350
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
1568
1351
  * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
1569
1352
  * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
1570
1353
  *
@@ -1586,11 +1369,15 @@ var AutocompleteHandler = class extends BaseHandler {
1586
1369
  */
1587
1370
  async match(arms) {
1588
1371
  const { name, value } = this.focused;
1589
- const respond = (choices) => this.event.respond(choices);
1590
- const arm = arms[name];
1372
+ const respond = (choices) => this.respond(choices);
1373
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1591
1374
  if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1592
1375
  return await arm(value, respond);
1593
1376
  }
1377
+ /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1378
+ async respond(choices) {
1379
+ await this.event.respond(choices);
1380
+ }
1594
1381
  /** The firing command route, for a field whose completion differs per registered command. */
1595
1382
  get route() {
1596
1383
  return slashRouteOf(this.event);
@@ -1598,7 +1385,7 @@ var AutocompleteHandler = class extends BaseHandler {
1598
1385
  /**
1599
1386
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
1600
1387
  * (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.
1388
+ * types the focused field. The focused field is on `this.focused`.
1602
1389
  */
1603
1390
  get options() {
1604
1391
  return this.event.options;
@@ -1610,25 +1397,25 @@ var AutocompleteHandler = class extends BaseHandler {
1610
1397
  /**
1611
1398
  * Shared base the customId-routed component handlers extend.
1612
1399
  *
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.
1400
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
1401
+ * instead. This class defines the customId decode and the route matching those bases share.
1616
1402
  *
1617
1403
  * @typeParam Event - The component interaction type this handler processes
1618
1404
  * @typeParam Defs - The customId route definitions registered on the concrete handler
1619
1405
  */
1620
1406
  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;
1407
+ /** Rewrite the source message this component interaction came from. */
1408
+ update(response) {
1409
+ return this.sender.update(response);
1410
+ }
1411
+ /** Acknowledge the component without changing the source message. */
1412
+ deferUpdate() {
1413
+ return this.sender.deferUpdate();
1625
1414
  }
1626
1415
  decoded;
1627
1416
  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;
1417
+ this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
1418
+ return this.decoded;
1632
1419
  }
1633
1420
  /**
1634
1421
  * The decoded params of the single route this handler is registered for.
@@ -1647,10 +1434,11 @@ var ComponentHandler = class extends InteractionHandler {
1647
1434
  * handler `this.params` is `never`, so match is the only way to read the decoded params.
1648
1435
  *
1649
1436
  * 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.
1437
+ * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1438
+ * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1439
+ * corrupt wire throws before an arm body executes.
1652
1440
  *
1653
- * @param arms - One handler per registered route, keyed by prefix.
1441
+ * @param arms - One callback per registered route, keyed by prefix.
1654
1442
  * @returns The result of the arm that ran.
1655
1443
  *
1656
1444
  * @example
@@ -1659,8 +1447,8 @@ var ComponentHandler = class extends InteractionHandler {
1659
1447
  * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1660
1448
  * async execute() {
1661
1449
  * await this.match({
1662
- * approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
1663
- * reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
1450
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1451
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1664
1452
  * });
1665
1453
  * }
1666
1454
  * }
@@ -1668,7 +1456,7 @@ var ComponentHandler = class extends InteractionHandler {
1668
1456
  */
1669
1457
  async match(arms) {
1670
1458
  const { prefix, params } = this.route;
1671
- const arm = arms[prefix];
1459
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1672
1460
  if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1673
1461
  return await arm(params);
1674
1462
  }
@@ -1680,8 +1468,9 @@ var ComponentHandler = class extends InteractionHandler {
1680
1468
  * Base class for a button interaction handler.
1681
1469
  *
1682
1470
  * 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.
1471
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
1472
+ * members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
1473
+ * and the generic is a compile error.
1685
1474
  *
1686
1475
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1687
1476
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1692,7 +1481,7 @@ var ComponentHandler = class extends InteractionHandler {
1692
1481
  * class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
1693
1482
  * async execute() {
1694
1483
  * const { userId } = this.params;
1695
- * await this.event.reply(`approved <@${userId}>`);
1484
+ * await this.reply(`approved <@${userId}>`);
1696
1485
  * }
1697
1486
  * }
1698
1487
  * ```
@@ -1706,7 +1495,8 @@ var ButtonHandler = class extends ComponentHandler {};
1706
1495
  *
1707
1496
  * Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
1708
1497
  * generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
1709
- * inputs from `this.event.fields`.
1498
+ * inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
1499
+ * response to a modal, so `showModal` fails compilation on this kind.
1710
1500
  *
1711
1501
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1712
1502
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1718,7 +1508,7 @@ var ButtonHandler = class extends ComponentHandler {};
1718
1508
  * async execute() {
1719
1509
  * const { guildId } = this.params;
1720
1510
  * const name = this.event.fields.getTextInputValue('name');
1721
- * await this.event.reply(`saved ${name} for ${guildId}`);
1511
+ * await this.reply(`saved ${name} for ${guildId}`);
1722
1512
  * }
1723
1513
  * }
1724
1514
  * ```
@@ -1732,7 +1522,8 @@ var ModalHandler = class extends ComponentHandler {};
1732
1522
  *
1733
1523
  * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1734
1524
  * 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.
1525
+ * route or `this.match` for several, and reply through the handler members or rewrite the source message
1526
+ * with `this.update`.
1736
1527
  *
1737
1528
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1738
1529
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
@@ -1744,7 +1535,7 @@ var ModalHandler = class extends ComponentHandler {};
1744
1535
  * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1745
1536
  * async execute() {
1746
1537
  * const { roleId } = this.params;
1747
- * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1538
+ * await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1748
1539
  * }
1749
1540
  * }
1750
1541
  * ```
@@ -1775,7 +1566,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1775
1566
  * }
1776
1567
  * ```
1777
1568
  */
1778
- var EventHandler = class extends BaseHandler {
1569
+ var EventHandler = class extends BaseHandler$1 {
1779
1570
  firedEvent;
1780
1571
  constructor(event, core, eventName) {
1781
1572
  super(event, core);
@@ -1828,7 +1619,7 @@ var EventHandler = class extends BaseHandler {
1828
1619
  *
1829
1620
  * @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
1830
1621
  */
1831
- var EventMiddleware = class extends BaseHandler {
1622
+ var EventMiddleware = class extends BaseHandler$1 {
1832
1623
  firedEvent;
1833
1624
  constructor(event, core, eventName) {
1834
1625
  super(event, core);
@@ -2010,16 +1801,16 @@ var Paginator = class {
2010
1801
  const loadPage = (ctx, n) => this.page(ctx, n);
2011
1802
  this.Handler = class Nav extends ButtonHandler {
2012
1803
  async execute() {
2013
- await this.event.deferUpdate();
1804
+ await this.deferUpdate();
2014
1805
  const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2015
- await new ReplySender(this.event).edit(this.event.message, response);
1806
+ await this.update(response);
2016
1807
  }
2017
1808
  };
2018
1809
  }
2019
1810
  /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2020
1811
  async start(interaction, core) {
2021
1812
  const response = await this.page(contextOf(interaction, core), 0);
2022
- return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
1813
+ return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
2023
1814
  }
2024
1815
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2025
1816
  async page(ctx, n) {
@@ -2143,19 +1934,184 @@ function Subscribe(subscriber, options) {
2143
1934
  };
2144
1935
  }
2145
1936
 
1937
+ //#endregion
1938
+ //#region src/subscribers/decorators/WebhookUrl.ts
1939
+ /**
1940
+ * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
1941
+ *
1942
+ * An unset variable disables the reporter with a boot warning. A set but malformed value throws
1943
+ * at boot.
1944
+ *
1945
+ * @param envKey - The environment variable name holding the webhook url
1946
+ * @decorator
1947
+ * @example
1948
+ * ```ts
1949
+ * \@Subscribe('memberBanned')
1950
+ * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
1951
+ * class BanLog extends WebhookLog<'memberBanned'> {
1952
+ * report() {
1953
+ * return { components: [new BanCard(this.data).component] };
1954
+ * }
1955
+ * }
1956
+ * ```
1957
+ */
1958
+ function WebhookUrl(envKey) {
1959
+ return function(constructor) {
1960
+ Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
1961
+ };
1962
+ }
1963
+
1964
+ //#endregion
1965
+ //#region src/miscellaneous/flagsFor.ts
1966
+ /** @internal */
1967
+ function flagsFor(ephemeral) {
1968
+ let flags = MessageFlags$1.IsComponentsV2;
1969
+ if (ephemeral) flags |= MessageFlags$1.Ephemeral;
1970
+ return flags;
1971
+ }
1972
+
1973
+ //#endregion
1974
+ //#region src/subscribers/bases/webhookHelpers.ts
1975
+ const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
1976
+ function isDiscordWebhookUrl(value) {
1977
+ return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
1978
+ }
1979
+ function jsonAttachment(name, description, data) {
1980
+ const content = filterCirculars(data);
1981
+ return {
1982
+ name,
1983
+ description,
1984
+ data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
1985
+ };
1986
+ }
1987
+ function neutralizeFences(text) {
1988
+ return text.replaceAll("```", "`​`​`");
1989
+ }
1990
+ /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
1991
+ function errorReport(error) {
1992
+ let report = error.stack ?? `${error.name}: ${error.message}`;
1993
+ if (Error.isError(error.cause)) {
1994
+ const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
1995
+ report += `\n\nCaused by:\n${cause}`;
1996
+ }
1997
+ const stripped = neutralizeFences(stripAnsi(report));
1998
+ return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
1999
+ }
2000
+ var WebhookSeparator = class extends BuilderComponent {
2001
+ constructor() {
2002
+ super("separator");
2003
+ this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
2004
+ }
2005
+ };
2006
+
2007
+ //#endregion
2008
+ //#region src/subscribers/bases/WebhookSender.ts
2009
+ const HTTP_NOT_FOUND = 404;
2010
+ const HTTP_UNAUTHORIZED = 401;
2011
+ /**
2012
+ * Sends component messages to a Discord webhook through `@discordjs/rest`.
2013
+ *
2014
+ * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2015
+ */
2016
+ var WebhookSender = class {
2017
+ rest = new REST();
2018
+ route;
2019
+ constructor(url) {
2020
+ const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2021
+ if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2022
+ this.route = Routes.webhook(match[1], match[2]);
2023
+ }
2024
+ /**
2025
+ * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2026
+ * Discord and `unreachable` on any other failure.
2027
+ */
2028
+ async verify() {
2029
+ try {
2030
+ await this.rest.get(this.route, { auth: false });
2031
+ return "ok";
2032
+ } catch (error) {
2033
+ return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2034
+ }
2035
+ }
2036
+ async send(options) {
2037
+ const { flags, username, components, files } = options;
2038
+ await this.rest.post(this.route, {
2039
+ auth: false,
2040
+ query: new URLSearchParams({
2041
+ with_components: "true",
2042
+ wait: "true"
2043
+ }),
2044
+ body: {
2045
+ flags,
2046
+ username,
2047
+ components: components.map((component) => component.toJSON()),
2048
+ ...files?.length && { attachments: files.map((file, id) => ({
2049
+ id,
2050
+ filename: file.name,
2051
+ description: file.description
2052
+ })) }
2053
+ },
2054
+ ...files?.length && { files: files.map((file) => ({
2055
+ name: file.name,
2056
+ data: file.data
2057
+ })) }
2058
+ });
2059
+ }
2060
+ };
2061
+
2146
2062
  //#endregion
2147
2063
  //#region src/subscribers/bases/WebhookLog.ts
2148
2064
  /**
2149
- * Abstract webhook logging handler for subscriber events.
2065
+ * Base class for subscribers that deliver their event to a Discord webhook.
2150
2066
  *
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.
2067
+ * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2068
+ * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2069
+ * the reporter is skipped at registration with a boot warning.
2153
2070
  *
2154
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
2071
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2155
2072
  */
2156
- var WebhookLog = class extends Subscriber {
2157
- constructor(data, core) {
2158
- super(data, core);
2073
+ var WebhookLog = class WebhookLog extends Subscriber {
2074
+ static senders = /* @__PURE__ */ new Map();
2075
+ async execute() {
2076
+ const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2077
+ if (url === null) {
2078
+ this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2079
+ return;
2080
+ }
2081
+ const { username, components, files } = await this.report();
2082
+ try {
2083
+ await WebhookLog.senderFor(url).send({
2084
+ flags: flagsFor(false),
2085
+ username: username ?? this.constructor.name,
2086
+ components,
2087
+ files
2088
+ });
2089
+ } catch (error) {
2090
+ this.logger.error("Failed to send webhook report", error);
2091
+ }
2092
+ }
2093
+ /** @internal */
2094
+ static senderFor(url) {
2095
+ let sender = WebhookLog.senders.get(url);
2096
+ if (!sender) {
2097
+ sender = new WebhookSender(url);
2098
+ WebhookLog.senders.set(url, sender);
2099
+ }
2100
+ return sender;
2101
+ }
2102
+ /** @internal */
2103
+ static envKeyOf(ctor) {
2104
+ const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
2105
+ if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2106
+ return envKey;
2107
+ }
2108
+ /** @internal Returns null when the env var is unset, throws when set but malformed. */
2109
+ static urlOf(envKey) {
2110
+ if (!Envapter.has(envKey)) return null;
2111
+ const raw = Envapter.getRequired(envKey, Converters.String).trim();
2112
+ if (raw === "") return null;
2113
+ if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2114
+ return raw;
2159
2115
  }
2160
2116
  };
2161
2117
 
@@ -2213,7 +2169,6 @@ function slashRouteLeaves(commands) {
2213
2169
  var CommandRegistry = class {
2214
2170
  core;
2215
2171
  onDeployed;
2216
- name = "Commands";
2217
2172
  logger = new Logger("Commands");
2218
2173
  isInitialised = false;
2219
2174
  globalCommands = [];
@@ -2401,231 +2356,64 @@ function deriveEventActor(args) {
2401
2356
  user: arg.user,
2402
2357
  channelId: null
2403
2358
  };
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
- };
2524
- }
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;
2359
+ if (arg instanceof User) return {
2360
+ guild: null,
2361
+ member: null,
2362
+ user: arg,
2363
+ channelId: null
2364
+ };
2537
2365
  }
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
2366
  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
2367
+ guild: null,
2368
+ member: null,
2369
+ user: null,
2370
+ channelId: null
2593
2371
  };
2594
2372
  }
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;
2373
+
2374
+ //#endregion
2375
+ //#region src/bot/gates/runGates.ts
2376
+ function interactionGateContext(interaction, core) {
2377
+ const rawMember = interaction.member;
2378
+ const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2611
2379
  return {
2612
2380
  kind: "interaction",
2613
- interactionKind: interactionKind(interaction),
2614
- command,
2615
- customId,
2381
+ interaction,
2382
+ core,
2383
+ user: interaction.user,
2384
+ guild: interaction.guild,
2385
+ member: rawMember instanceof GuildMember ? rawMember : null,
2616
2386
  userId: interaction.user.id,
2617
2387
  guildId: interaction.guildId,
2618
2388
  channelId: interaction.channelId,
2619
- interactionId: interaction.id,
2620
- raw: interaction
2389
+ memberRoleIds,
2390
+ memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2391
+ appPermissions: interaction.appPermissions.bitfield,
2392
+ memberGuildPermissions: rawMember instanceof GuildMember ? rawMember.permissions.bitfield : null,
2393
+ appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
2394
+ routeId: null
2621
2395
  };
2622
2396
  }
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";
2397
+ function eventGateContext(eventName, args, core) {
2398
+ const actor = deriveEventActor(args);
2399
+ return {
2400
+ kind: "event",
2401
+ core,
2402
+ eventName,
2403
+ payload: args,
2404
+ user: actor.user,
2405
+ guild: actor.guild,
2406
+ member: actor.member,
2407
+ userId: actor.user?.id ?? null,
2408
+ guildId: actor.guild?.id ?? null,
2409
+ channelId: actor.channelId,
2410
+ memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2411
+ memberPermissions: actor.member?.permissions.bitfield ?? null,
2412
+ appPermissions: null,
2413
+ memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
2414
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
2415
+ routeId: null
2416
+ };
2629
2417
  }
2630
2418
 
2631
2419
  //#endregion
@@ -2640,7 +2428,7 @@ const logger$1 = new Logger("EventBoundary");
2640
2428
  */
2641
2429
  function handleEventFault(caught, eventName, handlerName, args, core) {
2642
2430
  if (caught instanceof Silence) {
2643
- if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
2431
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2644
2432
  return;
2645
2433
  }
2646
2434
  if (!Error.isError(caught)) throw caught;
@@ -2676,11 +2464,12 @@ var EventDispatcher = class {
2676
2464
  core;
2677
2465
  logger = new Logger("Events");
2678
2466
  isInitialized = false;
2679
- name = "Events";
2680
2467
  eventMap = /* @__PURE__ */ new Map();
2681
2468
  middlewares = [];
2682
2469
  executedOnceHandlers = /* @__PURE__ */ new Set();
2683
2470
  attachedEvents = /* @__PURE__ */ new Set();
2471
+ inFlight = /* @__PURE__ */ new Set();
2472
+ draining = false;
2684
2473
  loading = false;
2685
2474
  loadedHandlers = [];
2686
2475
  loadedMiddlewares = [];
@@ -2849,15 +2638,22 @@ var EventDispatcher = class {
2849
2638
  const handlerEntries = this.eventMap.get(eventName);
2850
2639
  this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2851
2640
  this.core.bot.client.on(eventName, (...args) => {
2641
+ if (this.draining) return;
2852
2642
  this.core.bot.emitSafe("any:event", eventName, ...args);
2853
- (async () => {
2854
- await this.processEvent(eventName, args).catch((err) => {
2855
- this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2856
- this.core.bot.emit("error:unhandled:event", err);
2857
- });
2858
- })();
2643
+ const run = this.processEvent(eventName, args).catch((err) => {
2644
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2645
+ this.core.bot.emit("error:unhandled:event", err);
2646
+ });
2647
+ this.inFlight.add(run);
2648
+ run.finally(() => this.inFlight.delete(run));
2859
2649
  });
2860
2650
  }
2651
+ stopAccepting() {
2652
+ this.draining = true;
2653
+ }
2654
+ drain(timeoutMs) {
2655
+ return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
2656
+ }
2861
2657
  async processEvent(eventName, args) {
2862
2658
  const handlerEntries = this.eventMap.get(eventName);
2863
2659
  if (!handlerEntries || handlerEntries.length === 0) return;
@@ -2885,35 +2681,55 @@ var EventDispatcher = class {
2885
2681
  };
2886
2682
 
2887
2683
  //#endregion
2888
- //#region src/bot/defaults/UnhandledEvent.ts
2684
+ //#region src/bot/defaults/UnhandledRepliable.ts
2889
2685
  /**
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
2686
+ * @internal Default handler for unhandled repliable interactions.
2894
2687
  */
2895
- var UnhandledEvent = class extends InteractionHandler {
2688
+ var UnhandledRepliable = class extends InteractionHandler {
2896
2689
  async execute() {
2897
- await this.event.reply({
2898
- content: `Feature not implemented yet.`,
2899
- flags: MessageFlags.Ephemeral
2900
- });
2690
+ await this.reply("Feature not implemented yet.");
2691
+ }
2692
+ };
2693
+
2694
+ //#endregion
2695
+ //#region src/bot/defaults/UnhandledAutocomplete.ts
2696
+ /**
2697
+ * @internal Default handler for an autocomplete with no registered handler.
2698
+ */
2699
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
2700
+ async execute() {
2701
+ await this.respond([]);
2901
2702
  }
2902
2703
  };
2903
2704
 
2705
+ //#endregion
2706
+ //#region src/bot/harmlessApiCodes.ts
2707
+ /**
2708
+ * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2709
+ * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2710
+ *
2711
+ * @internal
2712
+ */
2713
+ const HARMLESS_API_CODES = new Set([
2714
+ RESTJSONErrorCodes.UnknownInteraction,
2715
+ RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2716
+ RESTJSONErrorCodes.UnknownMessage
2717
+ ]);
2718
+
2904
2719
  //#endregion
2905
2720
  //#region src/bot/handleInteractionFault.ts
2906
2721
  const logger = new Logger("InteractionBoundary");
2907
2722
  /**
2908
2723
  * 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.
2724
+ * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
2725
+ * sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
2726
+ * boundary builds one from the interaction flags.
2911
2727
  *
2912
2728
  * @internal
2913
2729
  */
2914
- async function handleInteractionFault(caught, interaction, core) {
2730
+ async function handleInteractionFault(caught, interaction, core, sender) {
2915
2731
  if (caught instanceof Silence) {
2916
- if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
2732
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
2917
2733
  return;
2918
2734
  }
2919
2735
  if (!Error.isError(caught)) throw caught;
@@ -2937,7 +2753,18 @@ async function handleInteractionFault(caught, interaction, core) {
2937
2753
  user: interaction.user,
2938
2754
  metadata: interaction
2939
2755
  });
2940
- await new ReplySender(interaction).send(response, caught instanceof Notice ? caught.ephemeral : true);
2756
+ await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
2757
+ }
2758
+ async function sendGuarded(sender, response, ephemeral) {
2759
+ try {
2760
+ await sender.send(response, { ephemeral });
2761
+ } catch (error) {
2762
+ if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2763
+ logger.debug(`reply send hit harmless code ${error.code}`);
2764
+ return;
2765
+ }
2766
+ logger.error("reply send failed", error);
2767
+ }
2941
2768
  }
2942
2769
 
2943
2770
  //#endregion
@@ -2952,7 +2779,6 @@ var InteractionDispatcher = class {
2952
2779
  core;
2953
2780
  logger = new Logger("Interactions");
2954
2781
  isInitialized = false;
2955
- name = "Interactions";
2956
2782
  slashMap = /* @__PURE__ */ new Map();
2957
2783
  buttonMap = /* @__PURE__ */ new Map();
2958
2784
  modalMap = /* @__PURE__ */ new Map();
@@ -2966,6 +2792,8 @@ var InteractionDispatcher = class {
2966
2792
  autocompleteMap = /* @__PURE__ */ new Map();
2967
2793
  keysToIgnore = /* @__PURE__ */ new Set();
2968
2794
  middlewares = [];
2795
+ inFlight = /* @__PURE__ */ new Set();
2796
+ draining = false;
2969
2797
  loading = false;
2970
2798
  loadedHandlers = [];
2971
2799
  loadedMiddlewares = [];
@@ -3006,7 +2834,7 @@ var InteractionDispatcher = class {
3006
2834
  }
3007
2835
  /**
3008
2836
  * 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.
2837
+ * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
3010
2838
  *
3011
2839
  * @internal
3012
2840
  */
@@ -3014,7 +2842,7 @@ var InteractionDispatcher = class {
3014
2842
  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
2843
  }
3016
2844
  /**
3017
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
2845
+ * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
3018
2846
  * at runtime. Checked per kind since a user and a message command can share a name.
3019
2847
  *
3020
2848
  * @internal
@@ -3166,35 +2994,53 @@ var InteractionDispatcher = class {
3166
2994
  }
3167
2995
  attachToClient() {
3168
2996
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
2997
+ if (this.draining) return;
3169
2998
  this.core.bot.emitSafe("any:interaction", interaction);
3170
- this.handleInteraction(interaction).catch((err) => {
2999
+ const run = this.handleInteraction(interaction).catch((err) => {
3171
3000
  this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3172
3001
  this.core.bot.emit("error:unhandled:interaction", err);
3173
3002
  });
3003
+ this.inFlight.add(run);
3004
+ run.finally(() => this.inFlight.delete(run));
3174
3005
  });
3175
3006
  }
3007
+ stopAccepting() {
3008
+ this.draining = true;
3009
+ }
3010
+ drain(timeoutMs) {
3011
+ return settleWithin(Promise.allSettled(this.inFlight), timeoutMs);
3012
+ }
3176
3013
  async handleCustomIdInteraction(interaction, getMap, interactionType) {
3177
3014
  if ([...this.keysToIgnore].some((matcher) => matcher.owns(interaction.customId))) return;
3178
3015
  const prefix = prefixOf(interaction.customId);
3179
3016
  if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3180
3017
  await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
3181
3018
  }
3182
- async processInteraction(interaction, extractKey, getHandler) {
3019
+ async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
3183
3020
  const key = extractKey(interaction);
3021
+ let sender;
3184
3022
  try {
3185
3023
  if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3186
3024
  let HandlerCtor = getHandler(key);
3187
3025
  if (!HandlerCtor) {
3188
- this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to UnhandledEvent.`);
3189
- HandlerCtor = UnhandledEvent;
3026
+ this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3027
+ HandlerCtor = fallback;
3190
3028
  }
3191
3029
  this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
3192
3030
  const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
3193
3031
  const handler = new HandlerCtor(interaction, this.core, dispatch);
3194
- if (!interaction.isAutocomplete()) await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core));
3032
+ if (handler instanceof RepliableHandler) sender = handler.getSender();
3033
+ if (!interaction.isAutocomplete()) {
3034
+ const monitor = slowGateMonitor();
3035
+ try {
3036
+ await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3037
+ } finally {
3038
+ monitor?.report(dispatch.routeId);
3039
+ }
3040
+ }
3195
3041
  await handler.execute();
3196
3042
  } catch (caught) {
3197
- await handleInteractionFault(caught, interaction, this.core);
3043
+ await handleInteractionFault(caught, interaction, this.core, sender);
3198
3044
  }
3199
3045
  }
3200
3046
  async handleInteraction(interaction) {
@@ -3270,12 +3116,7 @@ var InteractionDispatcher = class {
3270
3116
  }
3271
3117
  async handleAutocomplete(interaction) {
3272
3118
  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));
3119
+ await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3279
3120
  }
3280
3121
  };
3281
3122
 
@@ -3296,23 +3137,25 @@ function __decorate(decorators, target, key, desc) {
3296
3137
 
3297
3138
  //#endregion
3298
3139
  //#region src/bot/Bot.ts
3140
+ const DISPATCH_DRAIN_TIMEOUT_MS = 5e3;
3141
+ const DRAIN_TASK_TIMEOUT_MS = 6e3;
3142
+ const UNBIND_TIMEOUT_MS = 2e3;
3143
+ const LOGOUT_TIMEOUT_MS = 2e3;
3299
3144
  /**
3300
- * Discord bot implementation that manages client and controllers
3301
- *
3302
- * Don't instantiate this class directly. Use `core.bot` instead.
3145
+ * The Discord client and its controllers. Access it through `core.bot`.
3303
3146
  */
3304
- var Bot = class extends Plugin {
3147
+ var Bot = class extends TypedEventEmitter {
3305
3148
  /** @internal */
3306
3149
  logger = new Logger("Bot");
3307
3150
  isInitialized = false;
3308
3151
  _client;
3309
3152
  interactions;
3310
3153
  events;
3311
- commands;
3312
3154
  emojiInjector;
3155
+ commands;
3313
3156
  emojis = Emojis;
3314
3157
  mentions = CommandMentions;
3315
- /** @internal For use in dev mode */
3158
+ /** @internal */
3316
3159
  async onHmr(event) {
3317
3160
  if (this.interactions) await this.interactions.onHmr(event);
3318
3161
  if (this.events) await this.events.onHmr(event);
@@ -3320,7 +3163,7 @@ var Bot = class extends Plugin {
3320
3163
  }
3321
3164
  /** @internal */
3322
3165
  constructor(core) {
3323
- super(core);
3166
+ super();
3324
3167
  this._client = new Client(core.config.bot.clientOptions);
3325
3168
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3326
3169
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
@@ -3331,12 +3174,24 @@ var Bot = class extends Plugin {
3331
3174
  });
3332
3175
  }
3333
3176
  this.emojiInjector = new EmojiInjector(core);
3334
- core.shutdown.addTask(ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3177
+ this.registerShutdownTasks(core);
3335
3178
  }
3336
- /**
3337
- * Initializes Discord client and all controllers
3338
- * @internal
3339
- */
3179
+ registerShutdownTasks(core) {
3180
+ core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
3181
+ this.stopAccepting();
3182
+ return Promise.resolve();
3183
+ }, UNBIND_TIMEOUT_MS);
3184
+ core.shutdown.addTask(ShutdownPhase.Drain, "drain-dispatch", () => this.drain(), DRAIN_TASK_TIMEOUT_MS);
3185
+ core.shutdown.addTask(ShutdownPhase.Logout, "stop-bot", async () => await this.stop(), LOGOUT_TIMEOUT_MS);
3186
+ }
3187
+ stopAccepting() {
3188
+ this.interactions?.stopAccepting();
3189
+ this.events?.stopAccepting();
3190
+ }
3191
+ async drain() {
3192
+ await Promise.allSettled([this.interactions?.drain(DISPATCH_DRAIN_TIMEOUT_MS), this.events?.drain(DISPATCH_DRAIN_TIMEOUT_MS)]);
3193
+ }
3194
+ /** @internal */
3340
3195
  async init() {
3341
3196
  if (this.isInitialized) return;
3342
3197
  this.isInitialized = true;
@@ -3352,25 +3207,18 @@ var Bot = class extends Plugin {
3352
3207
  this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
3353
3208
  }
3354
3209
  }
3355
- /**
3356
- * Stops the bot and cleans up connections
3357
- * @internal
3358
- */
3210
+ /** @internal */
3359
3211
  async stop() {
3360
3212
  this._client.removeAllListeners();
3361
3213
  await this.logout();
3362
3214
  }
3363
- /**
3364
- * Logs the bot into Discord using the configured token
3365
- */
3366
3215
  async login(token) {
3367
- await this._client.login(token);
3216
+ this._client.once(Events.ClientReady, () => this.emit("ready"));
3217
+ this._client.login(token);
3218
+ await this.waitFor("ready");
3368
3219
  this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
3369
3220
  return this;
3370
3221
  }
3371
- /**
3372
- * Logs out and destroys the Discord client connection
3373
- */
3374
3222
  async logout() {
3375
3223
  await this._client.destroy();
3376
3224
  this.logger.info(chalk.bold.red("Logged out of Discord!"));
@@ -3378,6 +3226,12 @@ var Bot = class extends Plugin {
3378
3226
  get client() {
3379
3227
  return this._client;
3380
3228
  }
3229
+ /**
3230
+ * The overloads enforce the key/args correlation at compile time only. At runtime this forwards
3231
+ * straight to the underlying TypedEventEmitter.
3232
+ *
3233
+ * @internal
3234
+ */
3381
3235
  emit(event, ...args) {
3382
3236
  return super.emit(event, ...args);
3383
3237
  }
@@ -3390,157 +3244,70 @@ var Bot = class extends Plugin {
3390
3244
  };
3391
3245
  __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3392
3246
 
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
3247
  //#endregion
3429
3248
  //#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
3249
  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
- }
3250
+ report() {
3251
+ return {
3252
+ username: "Handled Exception",
3253
+ components: [new HandledExceptionContainer(this.data).component],
3254
+ files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3255
+ };
3456
3256
  }
3457
3257
  };
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);
3258
+ HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3460
3259
  var HandledExceptionContainer = class extends BuilderComponent {
3461
3260
  constructor(data) {
3462
3261
  super("container");
3463
3262
  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"));
3263
+ 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
3264
  }
3466
3265
  };
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
3266
  function faultSummary(denial, source) {
3474
- const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
3267
+ const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3475
3268
  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
3269
  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
3270
  }
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
3271
  function causeStack(denial) {
3485
3272
  const { cause } = denial;
3486
- if (Error.isError(cause)) return cause.stack ?? cause.message;
3487
- if (typeof cause === "string") return cause;
3273
+ if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
3274
+ if (typeof cause === "string") return stripAnsi(cause);
3488
3275
  if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3489
3276
  if (cause !== void 0) try {
3490
3277
  return JSON.stringify(cause);
3491
3278
  } catch {
3492
3279
  return "[unserializable cause]";
3493
3280
  }
3494
- return denial.stack ?? "No cause recorded.";
3281
+ return stripAnsi(denial.stack ?? "No cause recorded.");
3495
3282
  }
3496
3283
 
3497
3284
  //#endregion
3498
3285
  //#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
3286
  let UnknownException = class UnknownException extends WebhookLog {
3508
- static {
3509
- _UnknownException = this;
3510
- }
3511
- webhook = new WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
3512
- async execute() {
3287
+ report() {
3513
3288
  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
- }
3289
+ return {
3290
+ username: "Unknown Exception",
3291
+ components: [new UnhandledErrorContainer(this.data).component],
3292
+ files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3293
+ };
3526
3294
  }
3527
3295
  };
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);
3296
+ UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3530
3297
  var UnhandledErrorContainer = class extends BuilderComponent {
3531
3298
  constructor(data) {
3532
3299
  super("container");
3533
3300
  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}\`\`\``));
3301
+ 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
3302
  this.addTimestampsIfAvailable(error);
3536
3303
  this.addMetadataIfAvailable(metadata);
3537
3304
  }
3538
3305
  addTimestampsIfAvailable(error) {
3539
- if (!(error instanceof DiscordAPIError)) return;
3306
+ if (!(error instanceof DiscordAPIError$1)) return;
3540
3307
  const now = Date.now();
3541
3308
  const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3542
3309
  if (!snowflake) return void 0;
3543
- const interactionTs = Number(SnowflakeUtil.deconstruct(snowflake).timestamp);
3310
+ const interactionTs = timestampFromSnowflake(snowflake);
3544
3311
  const diff = now - interactionTs;
3545
3312
  const seconds = Math.floor(diff / 1e3);
3546
3313
  const millis = diff % 1e3;
@@ -3555,24 +3322,19 @@ var UnhandledErrorContainer = class extends BuilderComponent {
3555
3322
  //#endregion
3556
3323
  //#region src/subscribers/Bus.ts
3557
3324
  /**
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.
3325
+ * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3326
+ * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3564
3327
  */
3565
- var Bus = class extends Plugin {
3328
+ var Bus = class extends TypedEventEmitter {
3566
3329
  core;
3567
3330
  logger = new Logger("Subscribers");
3568
- /** @internal */
3569
- name = "Subscribers";
3570
3331
  isInitialized = false;
3571
3332
  subscribersMap = /* @__PURE__ */ new Map();
3572
3333
  executedOnceHandlers = /* @__PURE__ */ new Set();
3334
+ webhookProbes = /* @__PURE__ */ new Map();
3573
3335
  hmrHandler;
3574
3336
  constructor(core) {
3575
- super(core);
3337
+ super();
3576
3338
  this.core = core;
3577
3339
  if (this.core.config.subscribers.path) {
3578
3340
  if (!Envapter.isDevelopment && !Envapter.isTest) return;
@@ -3595,7 +3357,6 @@ var Bus = class extends Plugin {
3595
3357
  this.isInitialized = true;
3596
3358
  this.registerSubscriber(UnknownException);
3597
3359
  this.registerSubscriber(HandledException);
3598
- Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
3599
3360
  const subscribersDir = this.core.config.subscribers.path;
3600
3361
  if (subscribersDir) {
3601
3362
  this.logger.info(chalk.bold(subscribersDir));
@@ -3603,8 +3364,19 @@ var Bus = class extends Plugin {
3603
3364
  const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3604
3365
  this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
3605
3366
  }
3367
+ if (!Envapter.isTest) await this.verifyWebhooks();
3368
+ }
3369
+ /** @internal Exposed for tests. */
3370
+ async verifyWebhooks() {
3371
+ const missing = [];
3372
+ await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3373
+ const result = await WebhookLog.senderFor(url).verify();
3374
+ if (result === "missing") missing.push(...envKeys);
3375
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
3376
+ }));
3377
+ if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3606
3378
  }
3607
- /** @internal For use in dev mode */
3379
+ /** @internal */
3608
3380
  async onHmr(event) {
3609
3381
  if (this.hmrHandler) await this.hmrHandler.handle(event);
3610
3382
  }
@@ -3621,6 +3393,17 @@ var Bus = class extends Plugin {
3621
3393
  }, this.logger);
3622
3394
  }
3623
3395
  registerSubscriber(handler) {
3396
+ if (handler.prototype instanceof WebhookLog) {
3397
+ const envKey = WebhookLog.envKeyOf(handler);
3398
+ const url = WebhookLog.urlOf(envKey);
3399
+ if (url === null) {
3400
+ this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
3401
+ return;
3402
+ }
3403
+ const envKeys = this.webhookProbes.get(url) ?? [];
3404
+ if (!envKeys.includes(envKey)) envKeys.push(envKey);
3405
+ this.webhookProbes.set(url, envKeys);
3406
+ }
3624
3407
  const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
3625
3408
  let handlers = this.subscribersMap.get(options.subscriber);
3626
3409
  if (!handlers) {
@@ -3680,15 +3463,13 @@ var Bus = class extends Plugin {
3680
3463
  //#endregion
3681
3464
  //#region src/version.ts
3682
3465
  /** Package version */
3683
- const version = "0.1.0-next.2";
3466
+ const version = "0.1.0-next.4";
3684
3467
 
3685
3468
  //#endregion
3686
3469
  //#region src/Seedcord.ts
3687
3470
  /**
3688
- * Main Seedcord bot framework class
3689
- *
3690
- * Primary entry point for creating Discord bots with Seedcord.
3691
- * Manages component lifecycle and provides plugin support.
3471
+ * The gateway bot host. Opens a discord.js gateway session, discovers handlers, and runs
3472
+ * coordinated startup and shutdown. Attach plugins with `attach()`.
3692
3473
  */
3693
3474
  var Seedcord = class Seedcord extends Pluggable {
3694
3475
  config;
@@ -3698,7 +3479,6 @@ var Seedcord = class Seedcord extends Pluggable {
3698
3479
  augmentTarget = "@seedcord/gateway";
3699
3480
  /** @internal */
3700
3481
  version = version;
3701
- static isInstantiated = false;
3702
3482
  healthCheck;
3703
3483
  hmrManager;
3704
3484
  /** @see {@link CoordinatedShutdown} */
@@ -3712,15 +3492,11 @@ var Seedcord = class Seedcord extends Pluggable {
3712
3492
  /** @see {@link IRateLimiter} */
3713
3493
  rateLimiter;
3714
3494
  /**
3715
- * Creates a new Seedcord instance
3716
- *
3717
3495
  * @param config - Bot configuration including paths and Discord client options
3718
3496
  * @throws A **SeedcordError** When attempting to create multiple instances (singleton)
3719
3497
  */
3720
3498
  constructor(config) {
3721
- if (Seedcord.isInstantiated) throw new SeedcordError(SeedcordErrorCode.CoreSingletonViolation);
3722
- Seedcord.isInstantiated = true;
3723
- const shutdown = new CoordinatedShutdown(config.shutdownEnabled);
3499
+ const shutdown = new CoordinatedShutdown();
3724
3500
  const startup = new CoordinatedStartup();
3725
3501
  super(shutdown, startup);
3726
3502
  this.config = config;
@@ -3733,15 +3509,22 @@ var Seedcord = class Seedcord extends Pluggable {
3733
3509
  this.bus = new Bus(this);
3734
3510
  this.bot = new Bot(this);
3735
3511
  this.rateLimiter = config.store ?? new MemoryRateLimiter();
3736
- this.healthCheck = new HealthCheck(this.shutdown, config.healthCheck);
3512
+ this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
3737
3513
  this.registerStartupTasks();
3738
3514
  }
3739
3515
  /** The bot's discord username, populated after login. */
3740
3516
  get username() {
3741
3517
  return this.bot.client.user?.username;
3742
3518
  }
3519
+ pluginCapabilities() {
3520
+ return {
3521
+ client: this.bot.client,
3522
+ token: this.bot.botToken,
3523
+ rest: this.bot.client.rest
3524
+ };
3525
+ }
3743
3526
  static reset() {
3744
- Seedcord.isInstantiated = false;
3527
+ super.reset();
3745
3528
  LoggerChannelRegistry.instance.reset();
3746
3529
  }
3747
3530
  registerStartupTasks() {
@@ -3751,24 +3534,31 @@ var Seedcord = class Seedcord extends Pluggable {
3751
3534
  await this.bus.init();
3752
3535
  this.bus.logger.utils.initialization("Subscribers", "end");
3753
3536
  });
3754
- this.startup.addTask(StartupPhase.Instantiation, "Bot Initialization", async () => {
3537
+ this.startup.addTask(StartupPhase.Login, "Bot Initialization", async () => {
3755
3538
  this.bot.logger.utils.initialization("Bot", "start");
3756
3539
  await this.bot.init();
3757
3540
  this.bot.logger.utils.initialization("Bot", "end");
3758
3541
  });
3759
- this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
3760
- this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3761
- await this.healthCheck.init();
3762
- this.healthCheck.logger.utils.initialization("HealthCheck", "end");
3542
+ const { healthCheck } = this;
3543
+ if (healthCheck) this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
3544
+ healthCheck.logger.utils.initialization("HealthCheck", "start");
3545
+ await healthCheck.init();
3546
+ healthCheck.logger.utils.initialization("HealthCheck", "end");
3763
3547
  });
3764
3548
  }
3765
3549
  /**
3766
3550
  * Starts the bot and runs all initialization tasks
3767
3551
  *
3768
- * @returns This Seedcord instance when fully initialized
3552
+ * @returns This Seedcord instance when initialized
3769
3553
  */
3770
3554
  async start() {
3771
- await super.init();
3555
+ try {
3556
+ await super.init();
3557
+ } catch (caught) {
3558
+ await this.shutdown.run(1, false);
3559
+ Seedcord.reset();
3560
+ throw caught;
3561
+ }
3772
3562
  return this;
3773
3563
  }
3774
3564
  registerHmrAwareModules() {
@@ -3782,5 +3572,5 @@ var Seedcord = class Seedcord extends Pluggable {
3782
3572
  };
3783
3573
 
3784
3574
  //#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 };
3575
+ export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3786
3576
  //# sourceMappingURL=index.mjs.map