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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,35 +1,44 @@
1
1
  import "reflect-metadata";
2
- import { BuilderComponent, CustomId, DispatchContext, Fault, Notice, RegisterCommand, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
3
- import { CommandMetadataKey, ComponentDefsKey, EventMetadataKey, GateNotice, GatedMetadataKey, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, NotInGuild, NoticeCard, PAGE_MAX, SubscribeMetadataKey, decodeFor, pageCursor, pickNotice, prefixOf, routeIdOf, runHandlerGates, setBotColor } from "@seedcord/core/internal";
4
- 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";
2
+ import { installNodeDefaults } from "@seedcord/logger/node";
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";
5
4
  import { SeedcordErrorCode } from "@seedcord/errors";
6
5
  import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
7
- import { filterCirculars, formatFilePath, hasKeys, prettify, traverseDirectory } from "@seedcord/utils";
8
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Logger, ShutdownPhase, StartupPhase } from "@seedcord/services";
9
- import chalk from "chalk";
6
+ import { BaseHandler, BuilderComponent, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, defineGate, paginate } from "@seedcord/core";
7
+ import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
10
8
  import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
11
9
  import { buildSlashRoute, routeLeavesOf } from "@seedcord/utils/internal";
10
+ import { ApplicationCommandType, ButtonStyle, Client, Collection, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, TextChannel, User, chatInputApplicationCommandMention } from "discord.js";
11
+ import * as crypto from "node:crypto";
12
12
  import { TypedEventEmitter } from "@seedcord/event-emitter";
13
- import { existsSync } from "node:fs";
14
- import { resolve } from "node:path";
15
- import { pathToFileURL } from "node:url";
13
+ import chalk from "chalk";
14
+ import { Converters, Envapter } from "envapt";
15
+ import { MessageFlags as MessageFlags$1, Routes, SeparatorSpacingSize } from "discord-api-types/v10";
16
+ import { filterCirculars, formatFilePath, hasKeys, stripAnsi, timestampFromSnowflake } from "@seedcord/utils";
17
+ import { DiscordAPIError as DiscordAPIError$1, REST } from "@discordjs/rest";
18
+ import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, ShutdownPhase, ShutdownPhase as ShutdownPhase$1, StartupPhase, StartupPhase as StartupPhase$1 } from "@seedcord/core/node";
16
19
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
17
- import { SeedcordBrand, wrapHot } from "@seedcord/types/internal";
18
- import { Envapt, Envapter } from "envapt";
19
- import * as crypto from "node:crypto";
20
-
21
- export * from "@seedcord/core"
20
+ import { SeedcordBrand } from "@seedcord/types/internal";
21
+ import { Envapt } from "envapt/legacy";
22
+ import { resolve } from "node:path";
23
+ import { HmrModuleHandler } from "@seedcord/core/hmr";
24
+ import { traverseDirectory } from "@seedcord/utils/node";
22
25
 
23
26
  export * from "@seedcord/errors"
24
27
 
25
- export * from "@seedcord/utils"
28
+ export * from "@seedcord/core"
26
29
 
27
- export * from "@seedcord/services"
30
+ export * from "@seedcord/logger"
28
31
 
29
32
  export * from "@seedcord/event-emitter"
30
33
 
34
+ export * from "@seedcord/utils"
35
+
31
36
  export * from "@seedcord/rate-limiter"
32
37
 
38
+ //#region src/logger-bootstrap.ts
39
+ installNodeDefaults();
40
+
41
+ //#endregion
33
42
  //#region src/bot/decorators/Events.ts
34
43
  /**
35
44
  * Registers an event handler class with one or more Discord.js events.
@@ -122,190 +131,6 @@ function Gated(...gates) {
122
131
  };
123
132
  }
124
133
 
125
- //#endregion
126
- //#region src/miscellaneous/areRoutes.ts
127
- /**
128
- * Checks if the provided value is an array of strings.
129
- *
130
- * @internal
131
- */
132
- function areRoutes(routes) {
133
- return Array.isArray(routes) && routes.every((r) => typeof r === "string");
134
- }
135
-
136
- //#endregion
137
- //#region src/bot/decorators/Interactions.ts
138
- /**
139
- * Types of select menus supported for routing
140
- */
141
- let SelectMenuKind = /* @__PURE__ */ function(SelectMenuKind) {
142
- SelectMenuKind["String"] = "string";
143
- SelectMenuKind["User"] = "user";
144
- SelectMenuKind["Role"] = "role";
145
- SelectMenuKind["Channel"] = "channel";
146
- SelectMenuKind["Mentionable"] = "mentionable";
147
- return SelectMenuKind;
148
- }({});
149
- /**
150
- * Routes one or more slash commands to a {@link SlashHandler}.
151
- *
152
- * Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
153
- * commands branch with `this.match`. The decorator routes and the generic must match exactly, so listing
154
- * fewer or more than the handler declares is a compile error. Subcommands use a slash path.
155
- *
156
- * @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
157
- * @decorator
158
- * @example
159
- * ```ts
160
- * \@SlashRoute('ban')
161
- * class BanHandler extends SlashHandler<'ban'> {
162
- * async execute() {
163
- * const target = this.options.getUser('target');
164
- * }
165
- * }
166
- * ```
167
- *
168
- * @example
169
- * ```ts
170
- * \@SlashRoute('ban', 'kick')
171
- * class ModerationHandler extends SlashHandler<'ban' | 'kick'> {
172
- * async execute() {
173
- * await this.match({ ban: (o) => o.getUser('target'), kick: (o) => o.getUser('member') });
174
- * }
175
- * }
176
- * ```
177
- */
178
- function SlashRoute(...routes) {
179
- return function(constructor) {
180
- storeMetadata(InteractionRoutes.Slash, routes, constructor);
181
- };
182
- }
183
- /**
184
- * Routes button interactions to handler classes.
185
- *
186
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
187
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
188
- * matches the stable prefix, so a wire minted from an older shape still reaches the handler, where
189
- * reading this.params throws StaleCustomId and the controller boundary turns it into a reply.
190
- *
191
- * @param defs - The customId definition(s) this handler decodes, one per route.
192
- * @decorator
193
- */
194
- function ButtonRoute(...defs) {
195
- return function(constructor) {
196
- storeComponentRoute(InteractionRoutes.Button, defs, constructor);
197
- };
198
- }
199
- /**
200
- * Routes modal submissions to handler classes.
201
- *
202
- * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
203
- * generic. Passing different definitions to the decorator and the generic is a compile error. Routing
204
- * matches the stable prefix, so an older-shape wire still reaches the handler and throws StaleCustomId
205
- * on read, the same as a button route.
206
- *
207
- * @param defs - The customId definition(s) this handler decodes, one per route.
208
- * @decorator
209
- */
210
- function ModalRoute(...defs) {
211
- return function(constructor) {
212
- storeComponentRoute(InteractionRoutes.Modal, defs, constructor);
213
- };
214
- }
215
- /**
216
- * Routes one or more context-menu commands to a {@link ContextMenuHandler}.
217
- *
218
- * Pass the kind (`ApplicationCommandType.User` or `ApplicationCommandType.Message`) and the command name(s),
219
- * each checked against that kind's registry, so a typo is a compile error. The same kind must sit on the
220
- * handler's generic, cross-checked both directions. Context menus carry no options, so a multi-name handler
221
- * reads `this.target` uniformly with no branch.
222
- *
223
- * @param kind - The context-menu kind, `ApplicationCommandType.User` or `ApplicationCommandType.Message`.
224
- * @param names - The command name(s) this handler serves, keyed off the matching registry.
225
- * @decorator
226
- * @example
227
- * ```ts
228
- * \@ContextMenuRoute(ApplicationCommandType.User, 'View Profile')
229
- * class ViewProfile extends ContextMenuHandler<ApplicationCommandType.User> {
230
- * async execute() {
231
- * const user = this.target;
232
- * }
233
- * }
234
- * ```
235
- */
236
- function ContextMenuRoute(kind, ...names) {
237
- return function(constructor) {
238
- storeMetadata(kind === ApplicationCommandType.User ? InteractionRoutes.UserContextMenu : InteractionRoutes.MessageContextMenu, names, constructor);
239
- };
240
- }
241
- /**
242
- * Routes one or more commands' autocomplete to an {@link AutocompleteHandler}.
243
- *
244
- * Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
245
- * over its autocompletable fields, several commands share one handler whose arms span every command's
246
- * fields. The decorator routes and the generic must match exactly, so listing fewer or more than the
247
- * handler declares is a compile error.
248
- *
249
- * @param routes - The command route string(s) this handler serves, e.g. `'search'` or `'search', 'find'`.
250
- * @decorator
251
- * @example
252
- * ```ts
253
- * \@AutocompleteRoute('search')
254
- * class SearchAutocomplete extends AutocompleteHandler<'search'> {
255
- * async execute() {
256
- * await this.match({ query: (value, respond) => respond([{ name: value, value }]) });
257
- * }
258
- * }
259
- * ```
260
- */
261
- function AutocompleteRoute(...routes) {
262
- return function(constructor) {
263
- storeMetadata(InteractionRoutes.Autocomplete, routes, constructor);
264
- };
265
- }
266
- /**
267
- * Routes select menu interactions to handler classes.
268
- *
269
- * Pass the select kind and the {@link CustomId} definition(s) this handler decodes. The handler's
270
- * generic must list the same definitions, and its second generic argument must be the matching select
271
- * interaction type, or it is a compile error.
272
- *
273
- * @param type - Select menu kind from {@link SelectMenuKind}.
274
- * @param defs - The customId definition(s) this handler decodes, one per route.
275
- * @decorator
276
- *
277
- * @example
278
- * ```typescript
279
- * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
280
- * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
281
- * // handles user select menus minted from AssignId
282
- * }
283
- * ```
284
- */
285
- function SelectMenuRoute(type, ...defs) {
286
- return function(constructor) {
287
- storeComponentRoute({
288
- ["string"]: InteractionRoutes.StringMenu,
289
- ["user"]: InteractionRoutes.UserMenu,
290
- ["role"]: InteractionRoutes.RoleMenu,
291
- ["channel"]: InteractionRoutes.ChannelMenu,
292
- ["mentionable"]: InteractionRoutes.MentionableMenu
293
- }[type], defs, constructor);
294
- };
295
- }
296
- function storeComponentRoute(route, defs, constructor) {
297
- storeMetadata(route, defs.map((def) => def.prefix), constructor);
298
- Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
299
- }
300
- function storeMetadata(route, routes, constructor) {
301
- const key = InteractionRouteKeys[route];
302
- const savedRoutes = Reflect.getMetadata(key, constructor);
303
- const existing = areRoutes(savedRoutes) ? savedRoutes : [];
304
- const toStore = Array.isArray(routes) ? routes : [routes];
305
- Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
306
- Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
307
- }
308
-
309
134
  //#endregion
310
135
  //#region src/bot/decorators/Middlewares.ts
311
136
  /**
@@ -374,21 +199,6 @@ const IgnoreBots = defineGate("IgnoreBots", (ctx) => {
374
199
  if (ctx.user?.bot) throw new Silence("actor is a bot");
375
200
  });
376
201
 
377
- //#endregion
378
- //#region src/bot/notices/utils.ts
379
- function mentionFor(subject) {
380
- if (subject instanceof Role) return `<@&${subject.id}>`;
381
- if (subject instanceof TextChannel) return `<#${subject.id}>`;
382
- if (subject instanceof GuildMember) return `<@${subject.id}>`;
383
- return `\`${subject.name}\``;
384
- }
385
- function labelFor(subject) {
386
- if (subject instanceof Role) return "role";
387
- if (subject instanceof TextChannel) return "channel";
388
- if (subject instanceof GuildMember) return "member";
389
- return "guild";
390
- }
391
-
392
202
  //#endregion
393
203
  //#region src/bot/notices/index.ts
394
204
  var NotNsfw = class extends GateNotice {
@@ -396,13 +206,6 @@ var NotNsfw = class extends GateNotice {
396
206
  super(message);
397
207
  }
398
208
  };
399
- var MissingRole = class extends GateNotice {
400
- role;
401
- constructor(message, role) {
402
- super(message ?? (role ? `You need the ${role.name} role to use this.` : "You do not have the required role."));
403
- this.role = role;
404
- }
405
- };
406
209
  var RoleHigherThanMe = class extends Notice {
407
210
  role;
408
211
  botRole;
@@ -423,30 +226,19 @@ var CannotAssignBotRole = class extends Notice {
423
226
  return { components: [new NoticeCard("I cannot assign a managed role.").component] };
424
227
  }
425
228
  };
426
- var MissingPermissions = class extends Notice {
427
- where;
428
- missingPerms;
429
- constructor(message, where, missingPerms) {
430
- super(message);
431
- this.where = where;
432
- this.missingPerms = missingPerms;
433
- }
434
- render() {
435
- const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
436
- return { components: [new NoticeCard(`The ${labelFor(this.where)} ${mentionFor(this.where)} is missing the following permission entries:\n\n${bullets}`).component] };
437
- }
438
- };
439
229
  var HasDangerousPermissions = class extends Notice {
440
- target;
230
+ subject;
441
231
  dangerousPerms;
442
- constructor(message, target, dangerousPerms) {
443
- super(message);
444
- this.target = target;
232
+ customLead;
233
+ constructor(message, subject, dangerousPerms) {
234
+ super(message ?? "A dangerous permission is enabled.");
235
+ this.subject = subject;
445
236
  this.dangerousPerms = dangerousPerms;
237
+ this.customLead = message;
446
238
  }
447
239
  render() {
448
240
  const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
449
- 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] };
450
242
  }
451
243
  };
452
244
  var UserNotFound = class extends Notice {
@@ -522,129 +314,6 @@ function Nsfw(options) {
522
314
  });
523
315
  }
524
316
 
525
- //#endregion
526
- //#region src/bot/utilities/permissions/checkPermissions.ts
527
- /**
528
- * Map of permission bits to their prettified human readable names.
529
- */
530
- const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
531
- function checkPermissions(a, b, c, d, e) {
532
- const { for: pFor, in: pIn, scope, inverse = false, missing: missingCtor, dangerous: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
533
- for: a,
534
- in: b,
535
- scope: c,
536
- inverse: d ?? false,
537
- ...e
538
- } : a;
539
- const Missing = missingCtor ?? MissingPermissions;
540
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
541
- const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
542
- const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
543
- const present = scope.filter((bit) => perms.has(bit, true));
544
- const presentNames = names(present);
545
- if (inverse) {
546
- if (present.length > 0) throw new Dangerous(`${pFor instanceof Role ? "Role" : "Member"} has dangerous permissions in this ${pIn instanceof Guild ? "guild" : "channel"}`, pFor, presentNames);
547
- return;
548
- }
549
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
550
- if (missingBits.length > 0) throw new Missing("Missing Any/All/No Permissions", pFor, names(missingBits));
551
- }
552
-
553
- //#endregion
554
- //#region src/bot/gates/catalog/permissions.ts
555
- const UNCACHED_MEMBER = "Your server member data could not be resolved. Try again.";
556
- /**
557
- * Requires the caller to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
558
- *
559
- * Interaction-only and excludes ModalSubmit, since a modal lacks a reliable cached caller member.
560
- *
561
- * @param scope - The permission flag bits the caller must all hold.
562
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
563
- *
564
- * @see {@link Gated}
565
- *
566
- * @example
567
- * ```ts
568
- * import { PermissionFlagsBits } from 'discord.js';
569
- *
570
- * \@Gated(RequirePermissions([PermissionFlagsBits.BanMembers]))
571
- * \@SlashRoute('ban')
572
- * class BanHandler extends SlashHandler<'ban'> {
573
- * async execute() {
574
- * const target = this.options.getUser('target');
575
- * }
576
- * }
577
- * ```
578
- */
579
- function RequirePermissions(scope, options) {
580
- return defineGate("RequirePermissions", (ctx) => {
581
- if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
582
- if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
583
- checkPermissions(ctx.member, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
584
- });
585
- }
586
- /**
587
- * Requires the bot to hold every permission in `scope`, via `checkPermissions`. Refuses outside a guild.
588
- *
589
- * It reads the bot's own member, so it attaches to modal handlers as well.
590
- *
591
- * @param scope - The permission flag bits the bot must all hold.
592
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-permissions one with `missing`.
593
- *
594
- * @see {@link Gated}
595
- *
596
- * @example
597
- * ```ts
598
- * import { PermissionFlagsBits } from 'discord.js';
599
- *
600
- * \@Gated(RequireBotPermissions([PermissionFlagsBits.BanMembers]))
601
- * \@ModalRoute(MyModalId)
602
- * class MyModalHandler extends ModalHandler<[typeof MyModalId]> {
603
- * async execute() {
604
- * // ...
605
- * }
606
- * }
607
- * ```
608
- */
609
- function RequireBotPermissions(scope, options) {
610
- return defineGate("RequireBotPermissions", (ctx) => {
611
- const botMember = ctx.guild?.members.me;
612
- if (!ctx.guild || !botMember) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
613
- checkPermissions(botMember, ctx.guild, scope, false, options?.missing ? { missing: options.missing } : void 0);
614
- });
615
- }
616
- /**
617
- * Requires the caller to have `roleId`, read from the member's role cache. Refuses outside a guild first, then
618
- * when the role is missing.
619
- *
620
- * Interaction-only and excludes ModalSubmit.
621
- *
622
- * @param roleId - The role snowflake the caller must hold, read from the member's role cache.
623
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
624
- *
625
- * @see {@link Gated}
626
- *
627
- * @example
628
- * ```ts
629
- * \@Gated(RequireRole('123456789012345678'))
630
- * \@SlashRoute('vip')
631
- * class VipHandler extends SlashHandler<'vip'> {
632
- * async execute() {
633
- * // ...
634
- * }
635
- * }
636
- * ```
637
- */
638
- function RequireRole(roleId, options) {
639
- return defineGate("RequireRole", (ctx) => {
640
- if (!ctx.guild) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
641
- if (!ctx.member) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? UNCACHED_MEMBER));
642
- if (ctx.member.roles.cache.has(roleId)) return;
643
- const role = ctx.guild.roles.cache.get(roleId) ?? null;
644
- throw pickNotice(options?.missingRole, (message) => new MissingRole(message, role));
645
- });
646
- }
647
-
648
317
  //#endregion
649
318
  //#region src/bot/injectors/EmojiInjector.ts
650
319
  const emojiStorage = {};
@@ -673,7 +342,7 @@ var EmojiInjector = class {
673
342
  async init() {
674
343
  this.clearEmojis();
675
344
  if (!this.core.config.bot.emojis || Object.keys(this.core.config.bot.emojis).length === 0) {
676
- this.logger.debug(chalk.bold.yellow("No emojis configured, skipping emoji injection."));
345
+ this.logger.debug("No emojis configured, skipping emoji injection.");
677
346
  return;
678
347
  }
679
348
  const configEmojis = this.core.config.bot.emojis;
@@ -833,6 +502,37 @@ async function fetchText(client, channelId) {
833
502
  throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
834
503
  }
835
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
+
836
536
  //#endregion
837
537
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
838
538
  /**
@@ -861,7 +561,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
861
561
  const me = target.members.me;
862
562
  if (!me) {
863
563
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
864
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
564
+ throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
865
565
  }
866
566
  checkPermissions(me, target, scope, inverse, errors);
867
567
  return;
@@ -870,7 +570,7 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
870
570
  if (!me) {
871
571
  new Logger("checkBotPermissions").warn(`Bot member is unavailable in guild ${target.guild.id} while checking permissions in channel ${target.id}`);
872
572
  const names = scope.map((bit) => PermissionNames.get(bit) ?? String(bit));
873
- throw new ((errors?.missing) ?? MissingPermissions)("Missing Permissions", target, names);
573
+ throw new ((errors?.missingNotice) ?? MissingPermissions)(void 0, "The bot", names);
874
574
  }
875
575
  checkPermissions(me, target, scope, inverse, errors);
876
576
  }
@@ -907,26 +607,26 @@ function getBotRole(guild) {
907
607
  * // Check with custom error handling
908
608
  * hasPermsToAssign({
909
609
  * targetRole: role,
910
- * errors: {
911
- * higher: CustomHigherError,
912
- * managed: CustomManagedError,
913
- * missing: CustomMissingPermissionsError
610
+ * noticeOverrides: {
611
+ * higherNotice: CustomHigherNotice,
612
+ * managedNotice: CustomManagedNotice,
613
+ * missingNotice: CustomMissingPermissionsNotice
914
614
  * }
915
615
  * });
916
616
  * ```
917
617
  */
918
618
  function hasPermsToAssign(roleOrOptions) {
919
- const { targetRole, errors } = roleOrOptions instanceof Role ? {
619
+ const { targetRole, noticeOverrides: errors } = roleOrOptions instanceof Role ? {
920
620
  targetRole: roleOrOptions,
921
- errors: void 0
621
+ noticeOverrides: void 0
922
622
  } : roleOrOptions;
923
- const HigherErr = errors?.higher ?? RoleHigherThanMe;
924
- const ManagedErr = errors?.managed ?? CannotAssignBotRole;
925
- const Missing = errors?.missing ?? MissingPermissions;
623
+ const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
624
+ const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
625
+ const MissingNotice = errors?.missingNotice ?? MissingPermissions;
926
626
  const botRole = getBotRole(targetRole.guild);
927
- if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherErr("Role is higher than me", targetRole, botRole);
928
- if (targetRole.managed) throw new ManagedErr(`Cannot assign managed role ${targetRole.name}`);
929
- 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 });
930
630
  }
931
631
 
932
632
  //#endregion
@@ -1054,113 +754,308 @@ async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
1054
754
  }
1055
755
 
1056
756
  //#endregion
1057
- //#region src/bot/harmlessApiCodes.ts
757
+ //#region src/bot/ReplySender.ts
758
+ function seedState(interaction) {
759
+ if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
760
+ if (interaction.replied) return "replied";
761
+ if (interaction.deferred) return "deferred-reply";
762
+ return "unacked";
763
+ }
1058
764
  /**
1059
- * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
1060
- * message. There is nothing to show the user and no bug to report. The controller boundary drops them on
1061
- * the original throw, and {@link ReplySender} drops them on its own send.
1062
- *
1063
- * @internal
765
+ * Writes interaction responses through the discord.js interaction. Construction is internal to the repliable
766
+ * handler bases and the dispatcher.
1064
767
  */
1065
- const HARMLESS_API_CODES = new Set([
1066
- RESTJSONErrorCodes.UnknownInteraction,
1067
- RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
1068
- RESTJSONErrorCodes.UnknownMessage
1069
- ]);
768
+ var ReplySender = class extends BaseReplySender {
769
+ interaction;
770
+ constructor(interaction, routeId) {
771
+ super(routeId, seedState(interaction));
772
+ this.interaction = interaction;
773
+ }
774
+ async writeReply(response, opts) {
775
+ const result = await this.interaction.reply({
776
+ ...this.replyOptions(response, sendFlags(opts)),
777
+ withResponse: true
778
+ });
779
+ return this.createdMessage(result);
780
+ }
781
+ async writeDefer(opts) {
782
+ await this.interaction.deferReply({ flags: deferFlags(opts) });
783
+ }
784
+ async writeDeferUpdate() {
785
+ await this.sourceInteraction("deferUpdate").deferUpdate();
786
+ }
787
+ async writeUpdate(response) {
788
+ const result = await this.sourceInteraction("update").update({
789
+ ...this.updateOptions(response),
790
+ withResponse: true
791
+ });
792
+ return this.createdMessage(result);
793
+ }
794
+ async writeFollowUp(response, opts) {
795
+ return await this.interaction.followUp(this.replyOptions(response, sendFlags(opts)));
796
+ }
797
+ async writeEditOriginal(response) {
798
+ return await this.interaction.editReply(this.editOptions(response));
799
+ }
800
+ async writeEditTarget(targetId, response) {
801
+ return await this.interaction.webhook.editMessage(targetId, this.editOptions(response));
802
+ }
803
+ async writeDeleteOriginal() {
804
+ await this.interaction.deleteReply();
805
+ }
806
+ async writeDeleteTarget(targetId) {
807
+ await this.interaction.deleteReply(targetId);
808
+ }
809
+ async writeModal(data) {
810
+ const { interaction } = this;
811
+ if (interaction.isModalSubmit()) this.rejectModalSubmit();
812
+ await interaction.showModal(data);
813
+ }
814
+ guardModalSource() {
815
+ if (this.interaction.isModalSubmit()) this.rejectModalSubmit();
816
+ }
817
+ rejectModalSubmit() {
818
+ throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
819
+ "showModal",
820
+ "this interaction is a modal submit",
821
+ "A modal cannot open another modal.",
822
+ this.routeId
823
+ ]);
824
+ }
825
+ sourceInteraction(method) {
826
+ const { interaction } = this;
827
+ if (interaction.isMessageComponent()) return interaction;
828
+ if (interaction.isModalSubmit() && interaction.isFromMessage()) return interaction;
829
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
830
+ }
831
+ replyOptions(response, flags) {
832
+ const reply = this.serialize(response);
833
+ return {
834
+ components: reply.components,
835
+ flags,
836
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
837
+ ...reply.files && { files: [...reply.files] }
838
+ };
839
+ }
840
+ editOptions(response) {
841
+ const reply = this.serialize(response);
842
+ return {
843
+ components: reply.components,
844
+ flags: MessageFlags.IsComponentsV2,
845
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
846
+ ...reply.files && { files: [...reply.files] }
847
+ };
848
+ }
849
+ updateOptions(response) {
850
+ const reply = this.serialize(response);
851
+ return {
852
+ components: reply.components,
853
+ flags: MessageFlags.IsComponentsV2,
854
+ ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
855
+ ...reply.files && { files: [...reply.files] }
856
+ };
857
+ }
858
+ createdMessage(result) {
859
+ return result.resource?.message ?? void 0;
860
+ }
861
+ };
1070
862
 
1071
863
  //#endregion
1072
- //#region src/miscellaneous/flagsFor.ts
864
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1073
865
  /**
1074
- * Builds the `flags` bitfield for a framework reply. Every framework reply is ComponentsV2, so the
1075
- * IsComponentsV2 flag is always set.
1076
- *
1077
- * @param ephemeral - Whether the reply is ephemeral.
1078
- * @returns The OR'd `MessageFlags` value as a plain number.
866
+ * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
867
+ * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
868
+ * interaction and a generated registry key always match.
1079
869
  *
1080
870
  * @internal
1081
871
  */
1082
- function flagsFor(ephemeral) {
1083
- let flags = MessageFlags.IsComponentsV2;
1084
- if (ephemeral) flags |= MessageFlags.Ephemeral;
1085
- return flags;
872
+ function slashRouteOf(interaction) {
873
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
874
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
875
+ return buildSlashRoute(interaction.commandName, subcommand, group);
1086
876
  }
1087
877
 
1088
878
  //#endregion
1089
- //#region src/bot/ReplySender.ts
879
+ //#region src/miscellaneous/FaultThrottle.ts
1090
880
  /**
1091
- * Sends a {@link ReplyResponse} to an interaction, picking reply, editReply, or followUp from the live
1092
- * acknowledgement state. Logs and drops its own send failures so a dead token never escapes into the
1093
- * controller.
881
+ * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
882
+ * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
883
+ * the map stays small, and a stale entry is evicted on the next read of its key.
884
+ *
885
+ * @internal
1094
886
  */
1095
- var ReplySender = class {
1096
- interaction;
1097
- logger = new Logger("ReplySender");
1098
- constructor(interaction) {
1099
- this.interaction = interaction;
1100
- }
1101
- /**
1102
- * Sends the reply and returns the sent message so a caller can attach a collector to it. Never throws,
1103
- * a failed send is logged and dropped and resolves to `undefined`.
1104
- *
1105
- * @param response - The ComponentsV2 reply to show.
1106
- * @param ephemeral - Whether the reply is ephemeral. {@default `true`}
1107
- * @returns The sent {@link Message}, or `undefined` when the send was swallowed.
1108
- */
1109
- async send(response, ephemeral = true) {
1110
- try {
1111
- return await this.dispatch(response, ephemeral);
1112
- } catch (error) {
1113
- this.logSwallowed("send", error);
1114
- return;
1115
- }
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;
1116
894
  }
1117
- /**
1118
- * Edits an already-sent message to a new {@link ReplyResponse}. Used to swap a confirmation prompt for
1119
- * its outcome. Never throws, a failed edit is logged and dropped.
1120
- *
1121
- * @param message - The message to edit (the handle a prior {@link send} returned).
1122
- * @param response - The reply to replace it with.
1123
- */
1124
- async edit(message, response) {
1125
- try {
1126
- await this.interaction.webhook.editMessage(message, this.editBody(response));
1127
- } catch (error) {
1128
- this.logSwallowed("edit", error);
1129
- }
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;
1130
902
  }
1131
- async dispatch(response, ephemeral) {
1132
- if (this.interaction.replied) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1133
- if (this.interaction.deferred) {
1134
- if (this.interaction.ephemeral === null) return await this.interaction.followUp(this.replyOptions(response, ephemeral));
1135
- return await this.interaction.editReply(this.editBody(response));
1136
- }
1137
- await this.interaction.reply(this.replyOptions(response, ephemeral));
1138
- return await this.interaction.fetchReply();
903
+ markReported(key) {
904
+ this.lastReportedAt.set(key, this.now());
1139
905
  }
1140
- replyOptions(response, ephemeral) {
1141
- return {
1142
- components: response.components,
1143
- flags: flagsFor(ephemeral),
1144
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1145
- ...response.files && { files: response.files }
1146
- };
906
+ clear() {
907
+ this.lastReportedAt.clear();
1147
908
  }
1148
- editBody(response) {
909
+ };
910
+
911
+ //#endregion
912
+ //#region src/miscellaneous/extractErrorResponse.ts
913
+ const logger$2 = new Logger("ErrorsHandling");
914
+ const faultThrottle = new FaultThrottle();
915
+ /**
916
+ * Processes an error into a user-facing reply and routes its report.
917
+ *
918
+ * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
919
+ * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
920
+ * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
921
+ * threaded into both the reply and the bus payload.
922
+ */
923
+ function extractErrorResponse(error, core, origin) {
924
+ const uuid = crypto.randomUUID();
925
+ const developerUsername = core.config.notifications?.developerUsername;
926
+ const ctx = developerUsername === void 0 ? { uuid } : {
927
+ uuid,
928
+ developerUsername
929
+ };
930
+ if (error instanceof Notice) {
931
+ if (error.report) reportFault(error, core, origin, uuid);
1149
932
  return {
1150
- components: response.components,
1151
- flags: MessageFlags.IsComponentsV2,
1152
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
1153
- ...response.files && { files: response.files }
933
+ uuid,
934
+ response: error.render(ctx)
1154
935
  };
1155
936
  }
1156
- logSwallowed(action, error) {
1157
- if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
1158
- this.logger.debug(`reply ${action} hit harmless code ${error.code}`);
1159
- return;
1160
- }
1161
- this.logger.error(`reply ${action} failed`, error);
937
+ reportRawFault(error, core, origin, uuid);
938
+ const override = core.config.errors?.defaultError;
939
+ return {
940
+ uuid,
941
+ response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
942
+ };
943
+ }
944
+ function withThrottle(origin, error, publish) {
945
+ const key = faultKey(origin, error);
946
+ if (!faultThrottle.shouldReport(key)) {
947
+ logger$2.info(`throttled duplicate fault ${key}`);
948
+ return;
1162
949
  }
1163
- };
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
+ }
1164
1059
 
1165
1060
  //#endregion
1166
1061
  //#region src/bot/confirm/reserved.ts
@@ -1206,9 +1101,9 @@ async function collectChoice(message, userId, timeoutMs) {
1206
1101
  return null;
1207
1102
  }
1208
1103
  }
1209
- async function settle(sender, interaction, message, outcome) {
1104
+ async function settle(sender, message, outcome) {
1210
1105
  if (outcome === void 0) {
1211
- await interaction.deleteReply(message).catch(() => void 0);
1106
+ await sender.delete(message).catch(() => void 0);
1212
1107
  return;
1213
1108
  }
1214
1109
  await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
@@ -1216,30 +1111,19 @@ async function settle(sender, interaction, message, outcome) {
1216
1111
  async function getConfirmation(interaction, prompt, options) {
1217
1112
  const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1218
1113
  const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1219
- const sender = new ReplySender(interaction);
1220
- const message = await sender.send(response, ephemeral);
1221
- if (!message) return false;
1114
+ const sender = new ReplySender(interaction, interactionRoute(interaction));
1115
+ const message = await sender.send(response, { ephemeral });
1222
1116
  const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1223
1117
  if (!winner) {
1224
- await settle(sender, interaction, message, options?.onTimeout);
1118
+ await settle(sender, message, options?.onTimeout);
1225
1119
  return false;
1226
1120
  }
1227
1121
  await winner.deferUpdate().catch(() => void 0);
1228
1122
  const confirmed = winner.customId === CONFIRM_IDS.confirm;
1229
- await settle(sender, interaction, message, confirmed ? options?.onConfirm : options?.onCancel);
1123
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1230
1124
  return confirmed;
1231
1125
  }
1232
1126
 
1233
- //#endregion
1234
- //#region src/hmr/devChannel.ts
1235
- let channel;
1236
- function setDevChannel(next) {
1237
- channel = next;
1238
- }
1239
- function getDevChannel() {
1240
- return channel;
1241
- }
1242
-
1243
1127
  //#endregion
1244
1128
  //#region src/interfaces/Plugin.ts
1245
1129
  /**
@@ -1349,19 +1233,10 @@ var Pluggable = class Pluggable extends TypedEventEmitter {
1349
1233
  //#endregion
1350
1234
  //#region src/handlers/BaseHandler.ts
1351
1235
  /**
1352
- * Base class for a Discord event or interaction handler. Don't register handlers directly, use the more specific handler subclasses.
1236
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1237
+ * gate hook.
1353
1238
  */
1354
- var BaseHandler = class {
1355
- core;
1356
- event;
1357
- logger;
1358
- dispatch;
1359
- constructor(event, core, dispatch) {
1360
- this.core = core;
1361
- this.event = event;
1362
- this.logger = new Logger(this.constructor.name);
1363
- this.dispatch = dispatch;
1364
- }
1239
+ var BaseHandler$1 = class extends BaseHandler {
1365
1240
  /** @internal */
1366
1241
  getEvent() {
1367
1242
  return this.event;
@@ -1378,44 +1253,57 @@ var BaseHandler = class {
1378
1253
  *
1379
1254
  * @typeParam Repliable - The interaction type this middleware processes
1380
1255
  */
1381
- var InteractionMiddleware = class extends BaseHandler {
1256
+ var InteractionMiddleware = class extends BaseHandler$1 {
1382
1257
  constructor(event, core) {
1383
1258
  super(event, core);
1384
1259
  }
1385
1260
  };
1386
1261
 
1262
+ //#endregion
1263
+ //#region src/handlers/RepliableHandler.ts
1264
+ /**
1265
+ * Shared base the repliable interaction handlers extend.
1266
+ *
1267
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1268
+ * {@link SelectMenuHandler}, or {@link ContextMenuHandler} instead. This class binds the core reply base
1269
+ * to the gateway sender.
1270
+ *
1271
+ * @typeParam Event - The repliable interaction type this handler processes
1272
+ */
1273
+ var RepliableHandler = class extends RepliableHandler$1 {
1274
+ /** @internal */
1275
+ getEvent() {
1276
+ return this.event;
1277
+ }
1278
+ buildSender(event, _core, routeId) {
1279
+ return new ReplySender(event, routeId);
1280
+ }
1281
+ };
1282
+
1387
1283
  //#endregion
1388
1284
  //#region src/handlers/interaction/InteractionHandler.ts
1389
1285
  /**
1390
1286
  * Shared base the typed interaction handlers extend.
1391
1287
  *
1392
- * Not a public entry point. You should be using {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1393
- * or {@link SelectMenuHandler} instead. This class only carries the repliable-event plumbing those bases share,
1394
- * so DO NOT use it directly.
1288
+ * Not a public entry point. Extend {@link SlashHandler}, {@link ButtonHandler}, {@link ModalHandler},
1289
+ * or {@link SelectMenuHandler} instead. This class adds `showModal` on top of the reply members those bases
1290
+ * share.
1395
1291
  *
1396
1292
  * @typeParam Repliable - The interaction type this handler processes
1397
1293
  */
1398
- var InteractionHandler = class extends BaseHandler {
1294
+ var InteractionHandler = class extends RepliableHandler {
1399
1295
  constructor(event, core, dispatch) {
1400
1296
  super(event, core, dispatch);
1401
1297
  }
1298
+ /**
1299
+ * Open a modal. Must be the initial response to this interaction. The modal kind rejects this call at
1300
+ * compile time (Discord forbids a modal in response to a modal).
1301
+ */
1302
+ showModal(modal) {
1303
+ return this.sender.showModal(modal);
1304
+ }
1402
1305
  };
1403
1306
 
1404
- //#endregion
1405
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
1406
- /**
1407
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
1408
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
1409
- * interaction and a generated registry key always match.
1410
- *
1411
- * @internal
1412
- */
1413
- function slashRouteOf(interaction) {
1414
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
1415
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
1416
- return buildSlashRoute(interaction.commandName, subcommand, group);
1417
- }
1418
-
1419
1307
  //#endregion
1420
1308
  //#region src/handlers/interaction/SlashHandler.ts
1421
1309
  /**
@@ -1450,7 +1338,7 @@ var SlashHandler = class extends InteractionHandler {
1450
1338
  }
1451
1339
  /**
1452
1340
  * Run the arm for whichever command fired. Use this only when the handler is registered for several
1453
- * routes. A single-route handler reads `this.options` directly instead, match buys nothing there.
1341
+ * routes.
1454
1342
  *
1455
1343
  * Provide one arm per registered route, keyed by its route string, and each arm receives that route's
1456
1344
  * own narrowed options. The arms must cover every route in the generic, a missing route or an unknown
@@ -1474,7 +1362,7 @@ var SlashHandler = class extends InteractionHandler {
1474
1362
  */
1475
1363
  async match(arms) {
1476
1364
  const route = slashRouteOf(this.event);
1477
- const arm = arms[route];
1365
+ const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1478
1366
  if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1479
1367
  return await arm(this.options);
1480
1368
  }
@@ -1541,15 +1429,15 @@ var ContextMenuHandler = class extends InteractionHandler {
1541
1429
  * }
1542
1430
  * ```
1543
1431
  */
1544
- var AutocompleteHandler = class extends BaseHandler {
1432
+ var AutocompleteHandler = class extends BaseHandler$1 {
1545
1433
  constructor(event, core, dispatch) {
1546
1434
  super(event, core, dispatch);
1547
1435
  }
1436
+ decodedFocused;
1548
1437
  /**
1549
1438
  * The focused option for this interaction. `value` is always the raw partial string the user is typing,
1550
1439
  * even for an integer or number option, so coerce it yourself when you need a number.
1551
1440
  */
1552
- decodedFocused;
1553
1441
  get focused() {
1554
1442
  if (this.decodedFocused) return this.decodedFocused;
1555
1443
  const raw = this.event.options.getFocused(true);
@@ -1565,7 +1453,7 @@ var AutocompleteHandler = class extends BaseHandler {
1565
1453
  * match is how you read it. There is no single-field shortcut the way slash/component handlers have one.
1566
1454
  *
1567
1455
  * Provide one arm per autocompletable field across the registered commands, keyed by field name. Each
1568
- * arm receives the focused partial value and a `respond` pinned to that field's choice value type. The
1456
+ * arm receives the focused partial value and a `respond` typed to that field's choice value. The
1569
1457
  * arms must cover every autocompletable field, a missing field or an unknown key is a compile error. A
1570
1458
  * focused field with no arm, only reachable from a stale-deployed command, throws at runtime.
1571
1459
  *
@@ -1587,11 +1475,15 @@ var AutocompleteHandler = class extends BaseHandler {
1587
1475
  */
1588
1476
  async match(arms) {
1589
1477
  const { name, value } = this.focused;
1590
- const respond = (choices) => this.event.respond(choices);
1591
- const arm = arms[name];
1478
+ const respond = (choices) => this.respond(choices);
1479
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1592
1480
  if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
1593
1481
  return await arm(value, respond);
1594
1482
  }
1483
+ /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
1484
+ async respond(choices) {
1485
+ await this.event.respond(choices);
1486
+ }
1595
1487
  /** The firing command route, for a field whose completion differs per registered command. */
1596
1488
  get route() {
1597
1489
  return slashRouteOf(this.event);
@@ -1599,7 +1491,7 @@ var AutocompleteHandler = class extends BaseHandler {
1599
1491
  /**
1600
1492
  * The already-entered options on this command, restricted to the kinds Discord resolves during autocomplete
1601
1493
  * (string, integer, number, boolean) with every read nullable, since a sibling is partial while the user
1602
- * types the focused field. Read the focused field from `this.focused`, not here.
1494
+ * types the focused field. The focused field is on `this.focused`.
1603
1495
  */
1604
1496
  get options() {
1605
1497
  return this.event.options;
@@ -1611,25 +1503,25 @@ var AutocompleteHandler = class extends BaseHandler {
1611
1503
  /**
1612
1504
  * Shared base the customId-routed component handlers extend.
1613
1505
  *
1614
- * Not a public entry point. You should be using {@link ButtonHandler}, {@link SelectMenuHandler}, or
1615
- * {@link ModalHandler} instead. This class only carries the customId decode and route-matching plumbing
1616
- * those bases share, so DO NOT use it directly.
1506
+ * Not a public entry point. Extend {@link ButtonHandler}, {@link SelectMenuHandler}, or {@link ModalHandler}
1507
+ * instead. This class defines the customId decode and the route matching those bases share.
1617
1508
  *
1618
1509
  * @typeParam Event - The component interaction type this handler processes
1619
1510
  * @typeParam Defs - The customId route definitions registered on the concrete handler
1620
1511
  */
1621
1512
  var ComponentHandler = class extends InteractionHandler {
1622
- get registeredDefs() {
1623
- const defs = Reflect.getMetadata(ComponentDefsKey, this.constructor);
1624
- if (!defs) throw new SeedcordError(SeedcordErrorCode.CustomIdHandlerRouteMissing, [this.constructor.name]);
1625
- return defs;
1513
+ /** Rewrite the source message this component interaction came from. */
1514
+ update(response) {
1515
+ return this.sender.update(response);
1516
+ }
1517
+ /** Acknowledge the component without changing the source message. */
1518
+ deferUpdate() {
1519
+ return this.sender.deferUpdate();
1626
1520
  }
1627
1521
  decoded;
1628
1522
  get route() {
1629
- if (this.decoded) return this.decoded;
1630
- const decoded = decodeFor(this.registeredDefs, this.event.customId);
1631
- this.decoded = decoded;
1632
- return decoded;
1523
+ this.decoded ??= decodeComponentRoute(this.constructor, this.event.customId);
1524
+ return this.decoded;
1633
1525
  }
1634
1526
  /**
1635
1527
  * The decoded params of the single route this handler is registered for.
@@ -1648,10 +1540,11 @@ var ComponentHandler = class extends InteractionHandler {
1648
1540
  * handler `this.params` is `never`, so match is the only way to read the decoded params.
1649
1541
  *
1650
1542
  * Provide one arm per registered route, keyed by its prefix, and each arm receives that route's own
1651
- * decoded params. The arms must cover every registered def, a missing prefix or an unknown key is a
1652
- * compile error. Decoding runs before any arm, so a stale or corrupt wire throws before an arm body executes.
1543
+ * decoded params. The arms cover every registered route prefix, checked at compile time, and a prefix
1544
+ * unmatched at runtime throws `CustomIdMatchArmMissing`. Decoding runs before any arm, so a stale or
1545
+ * corrupt wire throws before an arm body executes.
1653
1546
  *
1654
- * @param arms - One handler per registered route, keyed by prefix.
1547
+ * @param arms - One callback per registered route, keyed by prefix.
1655
1548
  * @returns The result of the arm that ran.
1656
1549
  *
1657
1550
  * @example
@@ -1660,8 +1553,8 @@ var ComponentHandler = class extends InteractionHandler {
1660
1553
  * class ReviewButtons extends ButtonHandler<[typeof Approve, typeof Reject]> {
1661
1554
  * async execute() {
1662
1555
  * await this.match({
1663
- * approve: ({ userId }) => this.event.reply(`approved <@${userId}>`),
1664
- * reject: ({ userId }) => this.event.reply(`rejected <@${userId}>`)
1556
+ * approve: ({ userId }) => this.reply(`approved <@${userId}>`),
1557
+ * reject: ({ userId }) => this.reply(`rejected <@${userId}>`)
1665
1558
  * });
1666
1559
  * }
1667
1560
  * }
@@ -1669,7 +1562,7 @@ var ComponentHandler = class extends InteractionHandler {
1669
1562
  */
1670
1563
  async match(arms) {
1671
1564
  const { prefix, params } = this.route;
1672
- const arm = arms[prefix];
1565
+ const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1673
1566
  if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1674
1567
  return await arm(params);
1675
1568
  }
@@ -1681,8 +1574,9 @@ var ComponentHandler = class extends InteractionHandler {
1681
1574
  * Base class for a button interaction handler.
1682
1575
  *
1683
1576
  * Register the customId definitions this handler decodes with `@ButtonRoute`, list the same ones in the
1684
- * generic, then read `this.params` for a single route or `this.match` for several. Passing different
1685
- * definitions to the decorator and the generic is a compile error.
1577
+ * generic, then read `this.params` for a single route or `this.match` for several. Reply through the handler
1578
+ * members, or rewrite the source message with `this.update`. Passing different definitions to the decorator
1579
+ * and the generic is a compile error.
1686
1580
  *
1687
1581
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ApproveId]`.
1688
1582
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1693,7 +1587,7 @@ var ComponentHandler = class extends InteractionHandler {
1693
1587
  * class ApproveButton extends ButtonHandler<[typeof ApproveId]> {
1694
1588
  * async execute() {
1695
1589
  * const { userId } = this.params;
1696
- * await this.event.reply(`approved <@${userId}>`);
1590
+ * await this.reply(`approved <@${userId}>`);
1697
1591
  * }
1698
1592
  * }
1699
1593
  * ```
@@ -1707,7 +1601,8 @@ var ButtonHandler = class extends ComponentHandler {};
1707
1601
  *
1708
1602
  * Register the customId definitions this handler decodes with `@ModalRoute`, list the same ones in the
1709
1603
  * generic, then read `this.params` for a single route or `this.match` for several. Read the submitted
1710
- * inputs from `this.event.fields`.
1604
+ * inputs from `this.event.fields`, and reply through the handler members. Discord forbids opening a modal in
1605
+ * response to a modal, so `showModal` fails compilation on this kind.
1711
1606
  *
1712
1607
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1713
1608
  * @typeParam Cache - The interaction cache state, `'cached'` by default.
@@ -1719,7 +1614,7 @@ var ButtonHandler = class extends ComponentHandler {};
1719
1614
  * async execute() {
1720
1615
  * const { guildId } = this.params;
1721
1616
  * const name = this.event.fields.getTextInputValue('name');
1722
- * await this.event.reply(`saved ${name} for ${guildId}`);
1617
+ * await this.reply(`saved ${name} for ${guildId}`);
1723
1618
  * }
1724
1619
  * }
1725
1620
  * ```
@@ -1733,7 +1628,8 @@ var ModalHandler = class extends ComponentHandler {};
1733
1628
  *
1734
1629
  * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1735
1630
  * so `this.event` and `this.event.values` are narrowed to that kind. Read `this.params` for a single
1736
- * route or `this.match` for several.
1631
+ * route or `this.match` for several, and reply through the handler members or rewrite the source message
1632
+ * with `this.update`.
1737
1633
  *
1738
1634
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1739
1635
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
@@ -1745,7 +1641,7 @@ var ModalHandler = class extends ComponentHandler {};
1745
1641
  * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1746
1642
  * async execute() {
1747
1643
  * const { roleId } = this.params;
1748
- * await this.event.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1644
+ * await this.reply(`assigning ${this.event.values.length} member(s) to <@&${roleId}>`);
1749
1645
  * }
1750
1646
  * }
1751
1647
  * ```
@@ -1776,7 +1672,7 @@ var SelectMenuHandler = class extends ComponentHandler {};
1776
1672
  * }
1777
1673
  * ```
1778
1674
  */
1779
- var EventHandler = class extends BaseHandler {
1675
+ var EventHandler = class extends BaseHandler$1 {
1780
1676
  firedEvent;
1781
1677
  constructor(event, core, eventName) {
1782
1678
  super(event, core);
@@ -1829,7 +1725,7 @@ var EventHandler = class extends BaseHandler {
1829
1725
  *
1830
1726
  * @typeParam EventName - One or more `ClientEvents` keys. Defaults to every event (catchall).
1831
1727
  */
1832
- var EventMiddleware = class extends BaseHandler {
1728
+ var EventMiddleware = class extends BaseHandler$1 {
1833
1729
  firedEvent;
1834
1730
  constructor(event, core, eventName) {
1835
1731
  super(event, core);
@@ -2011,16 +1907,16 @@ var Paginator = class {
2011
1907
  const loadPage = (ctx, n) => this.page(ctx, n);
2012
1908
  this.Handler = class Nav extends ButtonHandler {
2013
1909
  async execute() {
2014
- await this.event.deferUpdate();
1910
+ await this.deferUpdate();
2015
1911
  const response = await loadPage(contextOf(this.event, this.core), this.params.page);
2016
- await new ReplySender(this.event).edit(this.event.message, response);
1912
+ await this.update(response);
2017
1913
  }
2018
1914
  };
2019
1915
  }
2020
1916
  /** Render page 0 and send it, picking reply or followUp from the interaction's state. */
2021
1917
  async start(interaction, core) {
2022
1918
  const response = await this.page(contextOf(interaction, core), 0);
2023
- return new ReplySender(interaction).send(response, this.config.ephemeral ?? false);
1919
+ return new ReplySender(interaction, interactionRoute(interaction)).send(response, { ephemeral: this.config.ephemeral ?? false });
2024
1920
  }
2025
1921
  /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
2026
1922
  async page(ctx, n) {
@@ -2145,183 +2041,183 @@ function Subscribe(subscriber, options) {
2145
2041
  }
2146
2042
 
2147
2043
  //#endregion
2148
- //#region src/subscribers/bases/WebhookLog.ts
2044
+ //#region src/subscribers/decorators/WebhookUrl.ts
2149
2045
  /**
2150
- * Abstract webhook logging handler for subscriber events.
2046
+ * Declares the environment variable a {@link WebhookLog} reporter reads its webhook url from.
2151
2047
  *
2152
- * Extends Subscriber to provide webhook-based logging capabilities.
2153
- * Implementations must define the webhook client to send messages to by implementing the webhook property.
2048
+ * An unset variable disables the reporter with a boot warning. A set but malformed value throws
2049
+ * at boot.
2154
2050
  *
2155
- * @typeParam KeyOfSubscribers - The specific subscriber type this handler processes
2051
+ * @param envKey - The environment variable name holding the webhook url
2052
+ * @decorator
2053
+ * @example
2054
+ * ```ts
2055
+ * \@Subscribe('memberBanned')
2056
+ * \@WebhookUrl('BAN_LOG_WEBHOOK_URL')
2057
+ * class BanLog extends WebhookLog<'memberBanned'> {
2058
+ * report() {
2059
+ * return { components: [new BanCard(this.data).component] };
2060
+ * }
2061
+ * }
2062
+ * ```
2156
2063
  */
2157
- var WebhookLog = class extends Subscriber {
2158
- constructor(data, core) {
2159
- super(data, core);
2064
+ function WebhookUrl(envKey) {
2065
+ return function(constructor) {
2066
+ Reflect.defineMetadata(WebhookUrlMetadataKey, envKey, constructor);
2067
+ };
2068
+ }
2069
+
2070
+ //#endregion
2071
+ //#region src/miscellaneous/flagsFor.ts
2072
+ /** @internal */
2073
+ function flagsFor(ephemeral) {
2074
+ let flags = MessageFlags$1.IsComponentsV2;
2075
+ if (ephemeral) flags |= MessageFlags$1.Ephemeral;
2076
+ return flags;
2077
+ }
2078
+
2079
+ //#endregion
2080
+ //#region src/subscribers/bases/webhookHelpers.ts
2081
+ const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
2082
+ function isDiscordWebhookUrl(value) {
2083
+ return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
2084
+ }
2085
+ function jsonAttachment(name, description, data) {
2086
+ const content = filterCirculars(data);
2087
+ return {
2088
+ name,
2089
+ description,
2090
+ data: Buffer.from(JSON.stringify(content, void 0, 2), "utf8")
2091
+ };
2092
+ }
2093
+ function neutralizeFences(text) {
2094
+ return text.replaceAll("```", "`​`​`");
2095
+ }
2096
+ /** An error's stack with its direct cause appended (one level), ANSI-stripped for the webhook payload. */
2097
+ function errorReport(error) {
2098
+ let report = error.stack ?? `${error.name}: ${error.message}`;
2099
+ if (Error.isError(error.cause)) {
2100
+ const cause = error.cause.stack ?? `${error.cause.name}: ${error.cause.message}`;
2101
+ report += `\n\nCaused by:\n${cause}`;
2102
+ }
2103
+ const stripped = neutralizeFences(stripAnsi(report));
2104
+ return stripped.length > 1800 ? `${stripped.slice(0, 1799)}…` : stripped;
2105
+ }
2106
+ var WebhookSeparator = class extends BuilderComponent {
2107
+ constructor() {
2108
+ super("separator");
2109
+ this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
2160
2110
  }
2161
2111
  };
2162
2112
 
2163
2113
  //#endregion
2164
- //#region src/hmr/HmrModuleHandler.ts
2114
+ //#region src/subscribers/bases/WebhookSender.ts
2115
+ const HTTP_NOT_FOUND = 404;
2116
+ const HTTP_UNAUTHORIZED = 401;
2165
2117
  /**
2166
- * Reloads THandler and optional TMiddleware modules on HMR updates.
2167
- *
2168
- * You can either implement a custom `onHmr()` method in your class by extending `HmrAware`, or if it's a plugin, override the existing `onHmr()` with fully custom logic. Or, use this class's `handle()` method inside your `onHmr()` to get standard HMR handling for your modules based on the provided options you give to the constructor.
2118
+ * Sends component messages to a Discord webhook through `@discordjs/rest`.
2169
2119
  *
2170
- * This is only useful during development.
2120
+ * @throws A `SeedcordError` when the url does not carry a webhook id and token.
2171
2121
  */
2172
- var HmrModuleHandler = class {
2173
- store;
2174
- options;
2175
- constructor(options) {
2176
- this.options = {
2177
- ...options,
2178
- logger: options.logger.inChannel("hmr")
2179
- };
2180
- this.store = {
2181
- fileToHandlers: /* @__PURE__ */ new Map(),
2182
- fileToMiddlewares: /* @__PURE__ */ new Map(),
2183
- handlerArtifacts: /* @__PURE__ */ new Map()
2184
- };
2185
- }
2186
- /**
2187
- * Handles an HMR update event by reloading affected modules based on the event details and config. It receives an {@link HmrUpdateEvent}, then checks the type of update.
2188
- *
2189
- * - If it's a deletion, it unloads the module.
2190
- * - If it's an update/creation, it checks if the file is tracked, unloads it if so, then reloads the file if it is in the handlers or middlewares directory and is a valid handler/middleware.
2191
- */
2192
- async handle(event) {
2193
- const { file, affectedModules, type } = event;
2194
- const { logger, handlersDir, middlewaresDir } = this.options;
2195
- if (type === "delete" || type === "deleteDir") {
2196
- this.unload(file);
2197
- return;
2198
- }
2199
- if (type === "update" && !existsSync(file)) {
2200
- if (this.isTracked(file)) {
2201
- logger.info(`Received update for non-existent file: ${formatFilePath(file)}, treating as delete`);
2202
- this.unload(file);
2203
- }
2204
- return;
2205
- }
2206
- const rollbackEnabled = event.rollback ?? true;
2207
- const filesToReload = affectedModules && affectedModules.length > 0 ? affectedModules : [file];
2208
- const absHandlersDir = resolve(process.cwd(), handlersDir);
2209
- const absMiddlewaresDir = middlewaresDir ? resolve(process.cwd(), middlewaresDir) : null;
2210
- for (const fileToReload of filesToReload) {
2211
- const isHandler = fileToReload.startsWith(absHandlersDir);
2212
- const isMiddleware = absMiddlewaresDir ? fileToReload.startsWith(absMiddlewaresDir) : false;
2213
- if (!isHandler && !isMiddleware) continue;
2214
- await this.reloadWithRollback(fileToReload, rollbackEnabled);
2215
- }
2216
- }
2217
- async reloadWithRollback(file, rollbackEnabled) {
2218
- const { logger } = this.options;
2219
- const snapshot = this.snapshotUnits(file);
2220
- this.unload(file);
2221
- if (await this.reloadFile(file)) return;
2222
- this.unload(file);
2223
- if (!rollbackEnabled) return;
2224
- this.restoreUnits(file, snapshot);
2225
- logger.warn(`${chalk.yellow.bold("Rolled back")} ${chalk.gray(formatFilePath(file))} to the last-good version`);
2122
+ var WebhookSender = class {
2123
+ rest = new REST();
2124
+ route;
2125
+ constructor(url) {
2126
+ const match = /\/webhooks\/(\d+)\/([\w$-]+)$/.exec(url);
2127
+ if (!match?.[1] || !match[2]) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, ["The url"]);
2128
+ this.route = Routes.webhook(match[1], match[2]);
2226
2129
  }
2227
2130
  /**
2228
- * Tracks a handler module as being loaded from a specific file.
2229
- * @param file - The file path the handler was loaded from.
2230
- * @param handler - The handler module instance.
2131
+ * Fetches the webhook object, sending no message. Returns `missing` on a 404 or 401 from
2132
+ * Discord and `unreachable` on any other failure.
2231
2133
  */
2232
- trackHandler(file, handler) {
2233
- let set = this.store.fileToHandlers.get(file);
2234
- if (!set) {
2235
- set = /* @__PURE__ */ new Set();
2236
- this.store.fileToHandlers.set(file, set);
2237
- }
2238
- set.add(handler);
2239
- if (this.options.getArtifacts) {
2240
- const artifacts = this.options.getArtifacts(handler);
2241
- this.store.handlerArtifacts.set(handler, artifacts);
2134
+ async verify() {
2135
+ try {
2136
+ await this.rest.get(this.route, { auth: false });
2137
+ return "ok";
2138
+ } catch (error) {
2139
+ return error instanceof DiscordAPIError$1 && (error.status === HTTP_NOT_FOUND || error.status === HTTP_UNAUTHORIZED) ? "missing" : "unreachable";
2242
2140
  }
2243
2141
  }
2244
- /**
2245
- * Tracks a middleware module as being loaded from a specific file.
2246
- * @param file - The file path the middleware was loaded from.
2247
- * @param middleware - The middleware module instance.
2248
- */
2249
- trackMiddleware(file, middleware) {
2250
- let set = this.store.fileToMiddlewares.get(file);
2251
- if (!set) {
2252
- set = /* @__PURE__ */ new Set();
2253
- this.store.fileToMiddlewares.set(file, set);
2254
- }
2255
- set.add(middleware);
2256
- }
2257
- isTracked(file) {
2258
- return this.store.fileToHandlers.has(file) || this.store.fileToMiddlewares.has(file);
2259
- }
2260
- unload(file) {
2261
- const { logger } = this.options;
2262
- const handlers = this.store.fileToHandlers.get(file);
2263
- if (handlers) {
2264
- for (const handler of handlers) {
2265
- const artifacts = this.store.handlerArtifacts.get(handler);
2266
- this.options.unregisterHandler(handler, artifacts);
2267
- this.store.handlerArtifacts.delete(handler);
2268
- const name = handler.name ?? "Handler";
2269
- logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
2270
- }
2271
- this.store.fileToHandlers.delete(file);
2272
- }
2273
- const middlewares = this.store.fileToMiddlewares.get(file);
2274
- if (middlewares) {
2275
- for (const middleware of middlewares) {
2276
- this.options.unregisterMiddleware?.(middleware);
2277
- const name = middleware.name ?? "Middleware";
2278
- logger.info(`${chalk.red.bold("Unloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
2279
- }
2280
- this.store.fileToMiddlewares.delete(file);
2281
- }
2142
+ async send(options) {
2143
+ const { flags, username, components, files } = options;
2144
+ await this.rest.post(this.route, {
2145
+ auth: false,
2146
+ query: new URLSearchParams({
2147
+ with_components: "true",
2148
+ wait: "true"
2149
+ }),
2150
+ body: {
2151
+ flags,
2152
+ username,
2153
+ components: components.map((component) => component.toJSON()),
2154
+ ...files?.length && { attachments: files.map((file, id) => ({
2155
+ id,
2156
+ filename: file.name,
2157
+ description: file.description
2158
+ })) }
2159
+ },
2160
+ ...files?.length && { files: files.map((file) => ({
2161
+ name: file.name,
2162
+ data: file.data
2163
+ })) }
2164
+ });
2282
2165
  }
2283
- async reloadFile(file) {
2284
- const { logger, isHandler, isMiddleware, registerHandler, registerMiddleware } = this.options;
2285
- if (!existsSync(file)) {
2286
- logger.info(`File does not exist, skipping reload: ${formatFilePath(file)}`);
2287
- return false;
2166
+ };
2167
+
2168
+ //#endregion
2169
+ //#region src/subscribers/bases/WebhookLog.ts
2170
+ /**
2171
+ * Base class for subscribers that deliver their event to a Discord webhook.
2172
+ *
2173
+ * Declare the url's environment variable with `@WebhookUrl` and implement {@link report}. Url
2174
+ * resolution, validation, sender reuse, and sending run in the base. When the variable is unset
2175
+ * the reporter is skipped at registration with a boot warning.
2176
+ *
2177
+ * @typeParam KeyOfSubscribers - The subscription key this reporter receives
2178
+ */
2179
+ var WebhookLog = class WebhookLog extends Subscriber {
2180
+ static senders = /* @__PURE__ */ new Map();
2181
+ async execute() {
2182
+ const url = WebhookLog.urlOf(WebhookLog.envKeyOf(this.constructor));
2183
+ if (url === null) {
2184
+ this.logger.warn(`${chalk.bold(this.constructor.name)} has no webhook url set, this reporter is disabled`);
2185
+ return;
2288
2186
  }
2187
+ const { username, components, files } = await this.report();
2289
2188
  try {
2290
- let fileUrl = pathToFileURL(file).href;
2291
- if (process.env.VITEST === "true") fileUrl += `?update=${Date.now()}`;
2292
- const imported = await import(fileUrl);
2293
- for (const val of Object.values(imported)) if (isHandler(val)) {
2294
- registerHandler(val, file);
2295
- this.trackHandler(file, val);
2296
- const name = val.name ?? "Handler";
2297
- logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
2298
- } else if (isMiddleware && registerMiddleware && isMiddleware(val)) {
2299
- registerMiddleware(val, file);
2300
- this.trackMiddleware(file, val);
2301
- const name = val.name ?? "Middleware";
2302
- logger.info(`${chalk.blue.bold("Reloaded")} ${chalk.cyan.bold(name)} from ${chalk.gray(formatFilePath(file))}`);
2303
- }
2304
- return true;
2189
+ await WebhookLog.senderFor(url).send({
2190
+ flags: flagsFor(false),
2191
+ username: username ?? this.constructor.name,
2192
+ components,
2193
+ files
2194
+ });
2305
2195
  } catch (error) {
2306
- logger.error(`Failed to reload file: ${file}`, error);
2307
- return false;
2196
+ this.logger.error("Failed to send webhook report", error);
2308
2197
  }
2309
2198
  }
2310
- snapshotUnits(file) {
2311
- return {
2312
- handlers: [...this.store.fileToHandlers.get(file) ?? []],
2313
- middlewares: [...this.store.fileToMiddlewares.get(file) ?? []]
2314
- };
2315
- }
2316
- restoreUnits(file, snapshot) {
2317
- for (const handler of snapshot.handlers) {
2318
- this.options.registerHandler(handler, file);
2319
- this.trackHandler(file, handler);
2320
- }
2321
- if (this.options.registerMiddleware) for (const middleware of snapshot.middlewares) {
2322
- this.options.registerMiddleware(middleware, file);
2323
- this.trackMiddleware(file, middleware);
2199
+ /** @internal */
2200
+ static senderFor(url) {
2201
+ let sender = WebhookLog.senders.get(url);
2202
+ if (!sender) {
2203
+ sender = new WebhookSender(url);
2204
+ WebhookLog.senders.set(url, sender);
2324
2205
  }
2206
+ return sender;
2207
+ }
2208
+ /** @internal */
2209
+ static envKeyOf(ctor) {
2210
+ const envKey = Reflect.getMetadata(WebhookUrlMetadataKey, ctor);
2211
+ if (!envKey) throw new SeedcordError(SeedcordErrorCode.DecoratorWebhookUrlMissing, [ctor.name]);
2212
+ return envKey;
2213
+ }
2214
+ /** @internal Returns null when the env var is unset, throws when set but malformed. */
2215
+ static urlOf(envKey) {
2216
+ if (!Envapter.has(envKey)) return null;
2217
+ const raw = Envapter.getRequired(envKey, Converters.String).trim();
2218
+ if (raw === "") return null;
2219
+ if (!isDiscordWebhookUrl(raw)) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookUrlInvalid, [envKey]);
2220
+ return raw;
2325
2221
  }
2326
2222
  };
2327
2223
 
@@ -2385,6 +2281,8 @@ var CommandRegistry = class {
2385
2281
  globalCommands = [];
2386
2282
  guildCommands = new Collection();
2387
2283
  ctorToCommand = /* @__PURE__ */ new Map();
2284
+ loading = false;
2285
+ loadedCommands = [];
2388
2286
  hmrHandler;
2389
2287
  pendingEvents = /* @__PURE__ */ new Map();
2390
2288
  constructor(core, onDeployed) {
@@ -2411,16 +2309,33 @@ var CommandRegistry = class {
2411
2309
  this.isInitialised = true;
2412
2310
  const commandsDir = this.core.config.bot.commands.path;
2413
2311
  if (!commandsDir) return;
2414
- this.logger.info(chalk.bold(commandsDir));
2415
- await this.loadCommands(commandsDir);
2416
- this.logger.utils.summary("Loaded commands", {
2417
- global: this.globalCommands.length,
2418
- "guild groups": this.guildCommands.size
2419
- });
2312
+ this.loading = true;
2313
+ this.loadedCommands.length = 0;
2314
+ try {
2315
+ await this.loadCommands(commandsDir);
2316
+ } finally {
2317
+ this.loading = false;
2318
+ }
2319
+ this.reportLoad();
2420
2320
  getDevChannel()?.on("seedcord:refresh-commands", (data) => {
2421
2321
  this.refresh(data.shouldRefresh);
2422
2322
  });
2423
2323
  }
2324
+ reportLoad() {
2325
+ const { utils } = this.logger;
2326
+ utils.summary("Loaded commands", {
2327
+ global: this.globalCommands.length,
2328
+ "guild groups": this.guildCommands.size
2329
+ });
2330
+ let slash = 0;
2331
+ let menu = 0;
2332
+ for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
2333
+ else menu++;
2334
+ utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
2335
+ "slash commands": slash,
2336
+ "context menus": menu
2337
+ })]);
2338
+ }
2424
2339
  /** @internal */
2425
2340
  async refresh(shouldRefresh = true) {
2426
2341
  if (!shouldRefresh) {
@@ -2458,25 +2373,27 @@ var CommandRegistry = class {
2458
2373
  const meta = Reflect.getMetadata(CommandMetadataKey, ctor);
2459
2374
  if (!meta) return;
2460
2375
  const comp = new ctor().component;
2461
- const commandType = comp instanceof SlashCommandBuilder ? "slash command" : "context menu command";
2462
- if (meta.scope === "global") {
2463
- this.globalCommands.push(comp);
2464
- this.logger.utils.registration(ctor.name, rel);
2465
- this.logger.utils.item(`Global ${commandType}: ${chalk.bold.cyan(comp.name)}`);
2466
- } else {
2467
- for (const g of meta.guilds) {
2468
- const arr = this.guildCommands.get(g) ?? [];
2469
- arr.push(comp);
2470
- this.guildCommands.set(g, arr);
2471
- }
2472
- this.logger.utils.registration(ctor.name, rel);
2473
- this.logger.utils.item(`Guild ${commandType}: ${chalk.bold.cyan(comp.name)} for ${chalk.magenta.bold(meta.guilds.length)} guild(s)`);
2376
+ const kind = comp instanceof SlashCommandBuilder ? "slash command" : "context menu";
2377
+ if (meta.scope === "global") this.globalCommands.push(comp);
2378
+ else for (const g of meta.guilds) {
2379
+ const arr = this.guildCommands.get(g) ?? [];
2380
+ arr.push(comp);
2381
+ this.guildCommands.set(g, arr);
2474
2382
  }
2475
2383
  this.ctorToCommand.set(ctor, {
2476
2384
  name: comp.name,
2477
2385
  scope: meta.scope,
2478
2386
  ...meta.scope === "guild" && { guilds: meta.guilds }
2479
2387
  });
2388
+ if (this.loading) {
2389
+ this.loadedCommands.push({
2390
+ name: comp.name,
2391
+ from: formatFilePath(rel),
2392
+ kind
2393
+ });
2394
+ return;
2395
+ }
2396
+ this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
2480
2397
  }
2481
2398
  unregisterCommand(ctor, artifacts) {
2482
2399
  const info = artifacts ?? this.ctorToCommand.get(ctor);
@@ -2503,8 +2420,7 @@ var CommandRegistry = class {
2503
2420
  const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
2504
2421
  if (deployed) result.global = deployed;
2505
2422
  const tag = this.globalCommands.length === 1 ? "command" : "commands";
2506
- this.logger.utils.summary("Configured global", { [tag]: this.globalCommands.length });
2507
- this.logger.utils.item(`${this.globalCommands.map((command) => chalk.bold.cyan(command.name)).join(", ")}`);
2423
+ this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
2508
2424
  }
2509
2425
  for (const [guildId, commands] of this.guildCommands.entries()) {
2510
2426
  const guild = this.core.bot.client.guilds.cache.get(guildId);
@@ -2515,8 +2431,7 @@ var CommandRegistry = class {
2515
2431
  const deployed = await guild.commands.set(commands);
2516
2432
  result.guilds.set(guildId, deployed);
2517
2433
  const tag = commands.length === 1 ? "command" : "commands";
2518
- this.logger.utils.summary(`Configured commands for ${chalk.bold.yellow(guild.name)}`, { [tag]: commands.length });
2519
- this.logger.utils.item(`${commands.map((command) => chalk.bold.cyan(command.name)).join(", ")}`);
2434
+ this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
2520
2435
  }
2521
2436
  this.onDeployed?.(result);
2522
2437
  return result;
@@ -2545,234 +2460,67 @@ function deriveEventActor(args) {
2545
2460
  if (arg instanceof GuildMember) return {
2546
2461
  guild: arg.guild,
2547
2462
  member: arg,
2548
- user: arg.user,
2549
- channelId: null
2550
- };
2551
- if (arg instanceof User) return {
2552
- guild: null,
2553
- member: null,
2554
- user: arg,
2555
- channelId: null
2556
- };
2557
- }
2558
- return {
2559
- guild: null,
2560
- member: null,
2561
- user: null,
2562
- channelId: null
2563
- };
2564
- }
2565
-
2566
- //#endregion
2567
- //#region src/bot/gates/runGates.ts
2568
- function interactionGateContext(interaction, core) {
2569
- const rawMember = interaction.member;
2570
- const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2571
- return {
2572
- kind: "interaction",
2573
- interaction,
2574
- core,
2575
- user: interaction.user,
2576
- guild: interaction.guild,
2577
- member: rawMember instanceof GuildMember ? rawMember : null,
2578
- userId: interaction.user.id,
2579
- guildId: interaction.guildId,
2580
- channelId: interaction.channelId,
2581
- memberRoleIds,
2582
- memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2583
- routeId: null
2584
- };
2585
- }
2586
- function eventGateContext(eventName, args, core) {
2587
- const actor = deriveEventActor(args);
2588
- return {
2589
- kind: "event",
2590
- core,
2591
- eventName,
2592
- payload: args,
2593
- user: actor.user,
2594
- guild: actor.guild,
2595
- member: actor.member,
2596
- userId: actor.user?.id ?? null,
2597
- guildId: actor.guild?.id ?? null,
2598
- channelId: actor.channelId,
2599
- memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2600
- memberPermissions: actor.member?.permissions.bitfield ?? null,
2601
- routeId: null
2602
- };
2603
- }
2604
-
2605
- //#endregion
2606
- //#region src/miscellaneous/FaultThrottle.ts
2607
- /**
2608
- * Drops duplicate fault reports inside a fixed window so a recurring fault (a database outage on a
2609
- * hot path) reports once per window instead of flooding the webhook. Keyed on a structural string, so
2610
- * the map stays small, and a stale entry is evicted on the next read of its key.
2611
- *
2612
- * @internal
2613
- */
2614
- var FaultThrottle = class {
2615
- windowMs;
2616
- now;
2617
- lastReportedAt = /* @__PURE__ */ new Map();
2618
- constructor(windowMs = 6e4, now = () => Date.now()) {
2619
- this.windowMs = windowMs;
2620
- this.now = now;
2621
- }
2622
- /** Evicts the entry for this key if it has aged out, so the map never holds stale keys forever. */
2623
- shouldReport(key) {
2624
- const last = this.lastReportedAt.get(key);
2625
- if (last === void 0) return true;
2626
- if (this.now() - last < this.windowMs) return false;
2627
- this.lastReportedAt.delete(key);
2628
- return true;
2629
- }
2630
- markReported(key) {
2631
- this.lastReportedAt.set(key, this.now());
2632
- }
2633
- clear() {
2634
- this.lastReportedAt.clear();
2635
- }
2636
- };
2637
-
2638
- //#endregion
2639
- //#region src/miscellaneous/extractErrorResponse.ts
2640
- const logger$2 = new Logger("ErrorsHandling");
2641
- /**
2642
- * The single throttle for every fault publish, so a recurring fault reports once per window across both
2643
- * the interaction and event paths. Exported so tests reset it between cases.
2644
- *
2645
- * @internal
2646
- */
2647
- const faultThrottle = new FaultThrottle();
2648
- /**
2649
- * Processes an error into a user-facing reply and routes its report.
2650
- *
2651
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
2652
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
2653
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
2654
- * threaded into both the reply and the bus payload.
2655
- *
2656
- * @internal
2657
- */
2658
- function extractErrorResponse(error, core, origin) {
2659
- const uuid = crypto.randomUUID();
2660
- const developerUsername = core.config.notifications?.developerUsername;
2661
- const ctx = developerUsername === void 0 ? { uuid } : {
2662
- uuid,
2663
- developerUsername
2664
- };
2665
- if (error instanceof Notice) {
2666
- if (error.report) reportFault(error, core, origin, uuid);
2667
- return {
2668
- uuid,
2669
- response: error.render(ctx)
2670
- };
2671
- }
2672
- reportRawFault(error, core, origin, uuid);
2673
- const override = core.config.errors?.defaultError;
2674
- return {
2675
- uuid,
2676
- response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
2677
- };
2678
- }
2679
- function withThrottle(origin, error, publish) {
2680
- const key = faultKey(origin, error);
2681
- if (!faultThrottle.shouldReport(key)) {
2682
- logger$2.info(`throttled duplicate fault ${key}`);
2683
- return;
2463
+ user: arg.user,
2464
+ channelId: null
2465
+ };
2466
+ if (arg instanceof User) return {
2467
+ guild: null,
2468
+ member: null,
2469
+ user: arg,
2470
+ channelId: null
2471
+ };
2684
2472
  }
2685
- publish();
2686
- faultThrottle.markReported(key);
2687
- }
2688
- function reportFault(denial, core, origin, uuid) {
2689
- withThrottle(origin, denial, () => {
2690
- logger$2.error(`${denial.name}: ${uuid}`, denial);
2691
- if (origin.interaction) core.bus.publish("handledException", {
2692
- denial,
2693
- uuid,
2694
- source: buildInteractionSource(origin.interaction)
2695
- });
2696
- else if (origin.event) core.bus.publish("handledException", {
2697
- denial,
2698
- uuid,
2699
- source: buildEventSource(origin.event, origin)
2700
- });
2701
- else core.bus.publish("unknownException", {
2702
- uuid,
2703
- error: denial,
2704
- guild: origin.guild,
2705
- user: origin.user,
2706
- metadata: metadataFor(origin)
2707
- });
2708
- });
2709
- }
2710
- function reportRawFault(error, core, origin, uuid) {
2711
- withThrottle(origin, error, () => {
2712
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
2713
- else logger$2.error(`${uuid} | ${error.message}`);
2714
- core.bus.publish("unknownException", {
2715
- uuid,
2716
- error,
2717
- guild: origin.guild,
2718
- user: origin.user,
2719
- metadata: metadataFor(origin)
2720
- });
2721
- });
2722
- }
2723
- function metadataFor(origin) {
2724
- if (origin.event) return {
2725
- eventName: origin.event.name,
2726
- handler: origin.event.handler,
2727
- args: origin.event.args
2728
- };
2729
- return origin.metadata;
2730
- }
2731
- function buildEventSource(event, origin) {
2732
2473
  return {
2733
- kind: "event",
2734
- eventName: event.name,
2735
- handler: event.handler,
2736
- userId: origin.user?.id ?? null,
2737
- guildId: origin.guild?.id ?? null,
2738
- channelId: event.channelId,
2739
- raw: event.args
2474
+ guild: null,
2475
+ member: null,
2476
+ user: null,
2477
+ channelId: null
2740
2478
  };
2741
2479
  }
2742
- function faultKey(origin, error) {
2743
- const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
2744
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
2745
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
2746
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
2747
- return `autocomplete:${name}`;
2748
- }
2749
- function interactionRoute(interaction) {
2750
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
2751
- if (interaction.isContextMenuCommand()) return interaction.commandName;
2752
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
2753
- return "interaction";
2754
- }
2755
- function buildInteractionSource(interaction) {
2756
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
2757
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
2480
+
2481
+ //#endregion
2482
+ //#region src/bot/gates/runGates.ts
2483
+ function interactionGateContext(interaction, core) {
2484
+ const rawMember = interaction.member;
2485
+ const memberRoleIds = rawMember instanceof GuildMember ? [...rawMember.roles.cache.keys()].filter((id) => id !== interaction.guildId) : rawMember?.roles ?? [];
2758
2486
  return {
2759
2487
  kind: "interaction",
2760
- interactionKind: interactionKind(interaction),
2761
- command,
2762
- customId,
2488
+ interaction,
2489
+ core,
2490
+ user: interaction.user,
2491
+ guild: interaction.guild,
2492
+ member: rawMember instanceof GuildMember ? rawMember : null,
2763
2493
  userId: interaction.user.id,
2764
2494
  guildId: interaction.guildId,
2765
2495
  channelId: interaction.channelId,
2766
- interactionId: interaction.id,
2767
- raw: interaction
2496
+ memberRoleIds,
2497
+ memberPermissions: interaction.memberPermissions?.bitfield ?? null,
2498
+ appPermissions: interaction.appPermissions.bitfield,
2499
+ memberGuildPermissions: rawMember instanceof GuildMember ? rawMember.permissions.bitfield : null,
2500
+ appGuildPermissions: interaction.guild?.members.me?.permissions.bitfield ?? null,
2501
+ routeId: null
2768
2502
  };
2769
2503
  }
2770
- function interactionKind(interaction) {
2771
- if (interaction.isChatInputCommand()) return "slash";
2772
- if (interaction.isContextMenuCommand()) return "context-menu";
2773
- if (interaction.isButton()) return "button";
2774
- if (interaction.isAnySelectMenu()) return "select";
2775
- return "modal";
2504
+ function eventGateContext(eventName, args, core) {
2505
+ const actor = deriveEventActor(args);
2506
+ return {
2507
+ kind: "event",
2508
+ core,
2509
+ eventName,
2510
+ payload: args,
2511
+ user: actor.user,
2512
+ guild: actor.guild,
2513
+ member: actor.member,
2514
+ userId: actor.user?.id ?? null,
2515
+ guildId: actor.guild?.id ?? null,
2516
+ channelId: actor.channelId,
2517
+ memberRoleIds: actor.member ? [...actor.member.roles.cache.keys()].filter((id) => id !== actor.guild?.id) : [],
2518
+ memberPermissions: actor.member?.permissions.bitfield ?? null,
2519
+ appPermissions: null,
2520
+ memberGuildPermissions: actor.member?.permissions.bitfield ?? null,
2521
+ appGuildPermissions: actor.guild?.members.me?.permissions.bitfield ?? null,
2522
+ routeId: null
2523
+ };
2776
2524
  }
2777
2525
 
2778
2526
  //#endregion
@@ -2787,7 +2535,7 @@ const logger$1 = new Logger("EventBoundary");
2787
2535
  */
2788
2536
  function handleEventFault(caught, eventName, handlerName, args, core) {
2789
2537
  if (caught instanceof Silence) {
2790
- if (caught.reason !== void 0) logger$1.debug(`Silence: ${caught.reason}`);
2538
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger$1.debug(`Silence: ${caught.reason}`);
2791
2539
  return;
2792
2540
  }
2793
2541
  if (!Error.isError(caught)) throw caught;
@@ -2828,6 +2576,9 @@ var EventDispatcher = class {
2828
2576
  middlewares = [];
2829
2577
  executedOnceHandlers = /* @__PURE__ */ new Set();
2830
2578
  attachedEvents = /* @__PURE__ */ new Set();
2579
+ loading = false;
2580
+ loadedHandlers = [];
2581
+ loadedMiddlewares = [];
2831
2582
  hmrHandler;
2832
2583
  constructor(core) {
2833
2584
  this.core = core;
@@ -2857,20 +2608,29 @@ var EventDispatcher = class {
2857
2608
  this.isInitialized = true;
2858
2609
  const handlersDir = this.core.config.bot.events.path;
2859
2610
  if (!handlersDir) return;
2860
- this.logger.info(chalk.bold(handlersDir));
2861
2611
  const middlewareDir = hasKeys(this.core.config.bot.events, ["middlewares"]) ? this.core.config.bot.events.middlewares : void 0;
2862
- if (middlewareDir) {
2863
- this.logger.info(`${chalk.bold(middlewareDir)} ${chalk.gray("(middlewares)")}`);
2864
- await this.loadMiddlewares(middlewareDir);
2612
+ this.loading = true;
2613
+ this.loadedHandlers.length = 0;
2614
+ this.loadedMiddlewares.length = 0;
2615
+ try {
2616
+ if (middlewareDir) await this.loadMiddlewares(middlewareDir);
2617
+ await this.loadHandlers(handlersDir);
2618
+ } finally {
2619
+ this.loading = false;
2865
2620
  }
2866
- await this.loadHandlers(handlersDir);
2867
2621
  this.attachToClient();
2868
- this.logger.info(`${chalk.bold.green("Loaded event handlers:")}`);
2869
- const eventList = [`${chalk.magenta.bold(this.middlewares.length)} middlewares`];
2870
- this.eventMap.forEach((handlers, eventName) => {
2871
- eventList.push(`${chalk.magenta.bold(handlers.length)} ${eventName}`);
2622
+ this.reportLoad();
2623
+ }
2624
+ reportLoad() {
2625
+ const { utils } = this.logger;
2626
+ utils.summary("Loaded", {
2627
+ "event handlers": this.loadedHandlers.length,
2628
+ middlewares: this.loadedMiddlewares.length
2872
2629
  });
2873
- this.logger.utils.list(eventList);
2630
+ if (this.loadedMiddlewares.length > 0) utils.block("Loaded event middlewares", utils.entries(this.loadedMiddlewares));
2631
+ const perEvent = {};
2632
+ for (const [event, handlers] of this.eventMap) perEvent[event] = handlers.length;
2633
+ utils.block("Loaded event handlers", [...utils.entries(this.loadedHandlers), ...utils.counts(perEvent)]);
2874
2634
  }
2875
2635
  async loadHandlers(dir) {
2876
2636
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -2920,7 +2680,14 @@ var EventDispatcher = class {
2920
2680
  ...metadata.events && { events: metadata.events }
2921
2681
  });
2922
2682
  this.middlewares.sort((a, b) => a.priority - b.priority);
2923
- this.logger.utils.registration(`${middlewareCtor.name} ${chalk.gray(`(${metadata.priority})`)}`, relativePath, "event middleware");
2683
+ if (this.loading) {
2684
+ this.loadedMiddlewares.push({
2685
+ name: `${middlewareCtor.name} (${metadata.priority})`,
2686
+ from: formatFilePath(relativePath)
2687
+ });
2688
+ return;
2689
+ }
2690
+ this.logger.utils.registration(`${middlewareCtor.name} ${paint.mute(`(${metadata.priority})`)}`, relativePath, "event middleware");
2924
2691
  }
2925
2692
  async runMiddlewares(eventName, args) {
2926
2693
  for (const { ctor, events } of this.middlewares) {
@@ -2961,7 +2728,12 @@ var EventDispatcher = class {
2961
2728
  const names = areRoutes(raw) ? raw : typeof raw === "string" ? [raw] : [];
2962
2729
  for (const name of names) register(name, "on");
2963
2730
  }
2964
- this.logger.utils.registration(handlerClass.name, relativePath);
2731
+ const from = formatFilePath(relativePath);
2732
+ if (this.loading) this.loadedHandlers.push({
2733
+ name: handlerClass.name,
2734
+ from
2735
+ });
2736
+ else this.logger.utils.registration(handlerClass.name, from);
2965
2737
  }
2966
2738
  attachToClient() {
2967
2739
  for (const [eventName] of this.eventMap) this.attachListener(eventName);
@@ -2970,12 +2742,12 @@ var EventDispatcher = class {
2970
2742
  if (this.attachedEvents.has(eventName)) return;
2971
2743
  this.attachedEvents.add(eventName);
2972
2744
  const handlerEntries = this.eventMap.get(eventName);
2973
- this.logger.debug(`Attaching ${chalk.bold.green(eventName)} to the client with ${chalk.gray(handlerEntries?.length ?? 0)} handler(s)`);
2745
+ this.logger.debug(`Attaching ${paint.mint.bold(eventName)} to the client with ${paint.mute(handlerEntries?.length ?? 0)} handler(s)`);
2974
2746
  this.core.bot.client.on(eventName, (...args) => {
2975
2747
  this.core.bot.emitSafe("any:event", eventName, ...args);
2976
2748
  (async () => {
2977
2749
  await this.processEvent(eventName, args).catch((err) => {
2978
- this.logger.error(`[${chalk.bold.red("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2750
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
2979
2751
  this.core.bot.emit("error:unhandled:event", err);
2980
2752
  });
2981
2753
  })();
@@ -2997,7 +2769,7 @@ var EventDispatcher = class {
2997
2769
  }
2998
2770
  async processHandler(eventName, ctor, args) {
2999
2771
  try {
3000
- this.logger.debug(`Processing ${chalk.bold.green(eventName)} with ${chalk.gray(ctor.name)}`);
2772
+ this.logger.debug(`Processing ${paint.mint.bold(eventName)} with ${paint.mute(ctor.name)}`);
3001
2773
  const handler = new ctor(args, this.core, eventName);
3002
2774
  await runHandlerGates(ctor, eventGateContext(eventName, args, this.core));
3003
2775
  await handler.execute();
@@ -3008,35 +2780,55 @@ var EventDispatcher = class {
3008
2780
  };
3009
2781
 
3010
2782
  //#endregion
3011
- //#region src/bot/defaults/UnhandledEvent.ts
2783
+ //#region src/bot/defaults/UnhandledRepliable.ts
3012
2784
  /**
3013
- * Default handler for unhandled interactions. A reply failure here rides the controller boundary as a
3014
- * raw fault, so it needs no decorator of its own.
3015
- *
3016
- * @internal
2785
+ * @internal Default handler for unhandled repliable interactions.
3017
2786
  */
3018
- var UnhandledEvent = class extends InteractionHandler {
2787
+ var UnhandledRepliable = class extends InteractionHandler {
3019
2788
  async execute() {
3020
- await this.event.reply({
3021
- content: `Feature not implemented yet.`,
3022
- flags: MessageFlags.Ephemeral
3023
- });
2789
+ await this.reply("Feature not implemented yet.");
2790
+ }
2791
+ };
2792
+
2793
+ //#endregion
2794
+ //#region src/bot/defaults/UnhandledAutocomplete.ts
2795
+ /**
2796
+ * @internal Default handler for an autocomplete with no registered handler.
2797
+ */
2798
+ var UnhandledAutocomplete = class extends AutocompleteHandler {
2799
+ async execute() {
2800
+ await this.respond([]);
3024
2801
  }
3025
2802
  };
3026
2803
 
2804
+ //#endregion
2805
+ //#region src/bot/harmlessApiCodes.ts
2806
+ /**
2807
+ * Discord API error codes that mean a dead end on an interaction, a dead token, a double ack, or a gone
2808
+ * message. Nothing to show the user and no bug to report, so the boundary's fault-card send drops them.
2809
+ *
2810
+ * @internal
2811
+ */
2812
+ const HARMLESS_API_CODES = new Set([
2813
+ RESTJSONErrorCodes.UnknownInteraction,
2814
+ RESTJSONErrorCodes.InteractionHasAlreadyBeenAcknowledged,
2815
+ RESTJSONErrorCodes.UnknownMessage
2816
+ ]);
2817
+
3027
2818
  //#endregion
3028
2819
  //#region src/bot/handleInteractionFault.ts
3029
2820
  const logger = new Logger("InteractionBoundary");
3030
2821
  /**
3031
2822
  * The interaction controller boundary. Sorts a throw from any handler-lifecycle phase into the right
3032
- * user reply plus the right bus event, building a {@link ReplySender} lazily so it reads the live
3033
- * acknowledgement state at catch time.
2823
+ * user reply plus the right bus event. When a handler is built its sender is passed in, so the boundary
2824
+ * sends from the current ack state. A middleware or pre-construction throw arrives with no sender, so the
2825
+ * boundary builds one from the interaction flags.
3034
2826
  *
3035
2827
  * @internal
3036
2828
  */
3037
- async function handleInteractionFault(caught, interaction, core) {
2829
+ async function handleInteractionFault(caught, interaction, core, sender) {
3038
2830
  if (caught instanceof Silence) {
3039
- if (caught.reason !== void 0) logger.debug(`Silence: ${caught.reason}`);
2831
+ if (caught.reason !== void 0 && (core.config.errors?.logSilences ?? true)) logger.debug(`Silence: ${caught.reason}`);
3040
2832
  return;
3041
2833
  }
3042
2834
  if (!Error.isError(caught)) throw caught;
@@ -3060,7 +2852,18 @@ async function handleInteractionFault(caught, interaction, core) {
3060
2852
  user: interaction.user,
3061
2853
  metadata: interaction
3062
2854
  });
3063
- await new ReplySender(interaction).send(response, caught instanceof Notice ? caught.ephemeral : true);
2855
+ await sendGuarded(sender ?? new ReplySender(interaction, interactionRoute(interaction)), response, caught instanceof Notice ? caught.ephemeral : true);
2856
+ }
2857
+ async function sendGuarded(sender, response, ephemeral) {
2858
+ try {
2859
+ await sender.send(response, { ephemeral });
2860
+ } catch (error) {
2861
+ if (error instanceof DiscordAPIError && HARMLESS_API_CODES.has(error.code)) {
2862
+ logger.debug(`reply send hit harmless code ${error.code}`);
2863
+ return;
2864
+ }
2865
+ logger.error("reply send failed", error);
2866
+ }
3064
2867
  }
3065
2868
 
3066
2869
  //#endregion
@@ -3089,6 +2892,9 @@ var InteractionDispatcher = class {
3089
2892
  autocompleteMap = /* @__PURE__ */ new Map();
3090
2893
  keysToIgnore = /* @__PURE__ */ new Set();
3091
2894
  middlewares = [];
2895
+ loading = false;
2896
+ loadedHandlers = [];
2897
+ loadedMiddlewares = [];
3092
2898
  hmrHandler;
3093
2899
  routeTypes = [
3094
2900
  [InteractionRoutes.Slash, this.slashMap],
@@ -3126,22 +2932,22 @@ var InteractionDispatcher = class {
3126
2932
  }
3127
2933
  /**
3128
2934
  * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
3129
- * UnhandledEvent at runtime. A bot may route commands outside the registry, so this does not throw.
2935
+ * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
3130
2936
  *
3131
2937
  * @internal
3132
2938
  */
3133
2939
  warnUnhandledRoutes(commandLeaves) {
3134
- for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${chalk.bold.cyan(route)} has no registered ${chalk.bold("@SlashRoute")} handler.`);
2940
+ for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${paint.sky.bold(route)} has no registered ${chalk.bold("@SlashRoute")} handler.`);
3135
2941
  }
3136
2942
  /**
3137
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledEvent
2943
+ * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
3138
2944
  * at runtime. Checked per kind since a user and a message command can share a name.
3139
2945
  *
3140
2946
  * @internal
3141
2947
  */
3142
2948
  warnUnhandledContextMenuRoutes(leaves) {
3143
- for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${chalk.bold.cyan(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
3144
- for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${chalk.bold.cyan(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
2949
+ for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
2950
+ for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${paint.sky.bold(name)} has no registered ${chalk.bold("@ContextMenuRoute")} handler.`);
3145
2951
  }
3146
2952
  getArtifacts(handlerClass) {
3147
2953
  const artifacts = [];
@@ -3159,29 +2965,39 @@ var InteractionDispatcher = class {
3159
2965
  this.isInitialized = true;
3160
2966
  const handlersDir = this.core.config.bot.interactions.path;
3161
2967
  if (!handlersDir) return;
3162
- this.logger.info(chalk.bold(handlersDir));
3163
2968
  const middlewareDir = hasKeys(this.core.config.bot.interactions, ["middlewares"]) ? this.core.config.bot.interactions.middlewares : void 0;
3164
- if (middlewareDir) {
3165
- this.logger.info(`${chalk.bold(middlewareDir)} ${chalk.gray("(middlewares)")}`);
3166
- await this.loadMiddlewares(middlewareDir);
2969
+ this.loading = true;
2970
+ this.loadedHandlers.length = 0;
2971
+ this.loadedMiddlewares.length = 0;
2972
+ try {
2973
+ if (middlewareDir) await this.loadMiddlewares(middlewareDir);
2974
+ await this.loadHandlers(handlersDir);
2975
+ } finally {
2976
+ this.loading = false;
3167
2977
  }
3168
- await this.loadHandlers(handlersDir);
3169
2978
  this.attachToClient();
3170
- this.logger.info(`${chalk.bold.green("Loaded interaction handlers:")}`);
3171
- this.logger.utils.list([
3172
- `${chalk.magenta.bold(this.middlewares.length)} middlewares`,
3173
- `${chalk.magenta.bold(this.slashMap.size)} slash commands`,
3174
- `${chalk.magenta.bold(this.buttonMap.size)} buttons`,
3175
- `${chalk.magenta.bold(this.modalMap.size)} modals`,
3176
- `${chalk.magenta.bold(this.stringSelectMap.size)} string selects`,
3177
- `${chalk.magenta.bold(this.userSelectMap.size)} user selects`,
3178
- `${chalk.magenta.bold(this.roleSelectMap.size)} role selects`,
3179
- `${chalk.magenta.bold(this.channelSelectMap.size)} channel selects`,
3180
- `${chalk.magenta.bold(this.mentionableSelectMap.size)} mentionable selects`,
3181
- `${chalk.magenta.bold(this.messageContextMenuMap.size)} message context menus`,
3182
- `${chalk.magenta.bold(this.userContextMenuMap.size)} user context menus`,
3183
- `${chalk.magenta.bold(this.autocompleteMap.size)} autocomplete`
3184
- ]);
2979
+ this.reportLoad();
2980
+ }
2981
+ reportLoad() {
2982
+ const { utils } = this.logger;
2983
+ utils.summary("Loaded", {
2984
+ "interaction handlers": this.loadedHandlers.length,
2985
+ middlewares: this.loadedMiddlewares.length
2986
+ });
2987
+ if (this.loadedMiddlewares.length > 0) utils.block("Loaded middlewares", utils.entries(this.loadedMiddlewares));
2988
+ utils.block("Loaded interaction handlers", [...utils.entries(this.loadedHandlers), ...utils.counts({
2989
+ slash: this.slashMap.size,
2990
+ buttons: this.buttonMap.size,
2991
+ modals: this.modalMap.size,
2992
+ "string selects": this.stringSelectMap.size,
2993
+ "user selects": this.userSelectMap.size,
2994
+ "role selects": this.roleSelectMap.size,
2995
+ "channel selects": this.channelSelectMap.size,
2996
+ "mentionable selects": this.mentionableSelectMap.size,
2997
+ "message menus": this.messageContextMenuMap.size,
2998
+ "user menus": this.userContextMenuMap.size,
2999
+ autocomplete: this.autocompleteMap.size
3000
+ })]);
3185
3001
  }
3186
3002
  async loadHandlers(dir) {
3187
3003
  await traverseDirectory(dir, (fullPath, relativePath, imported) => {
@@ -3211,7 +3027,14 @@ var InteractionDispatcher = class {
3211
3027
  priority: metadata.priority
3212
3028
  });
3213
3029
  this.middlewares.sort((a, b) => a.priority - b.priority);
3214
- this.logger.utils.registration(`${middlewareCtor.name} ${chalk.gray(`(${metadata.priority})`)}`, relativePath, "middleware");
3030
+ if (this.loading) {
3031
+ this.loadedMiddlewares.push({
3032
+ name: `${middlewareCtor.name} (${metadata.priority})`,
3033
+ from: formatFilePath(relativePath)
3034
+ });
3035
+ return;
3036
+ }
3037
+ this.logger.utils.registration(`${middlewareCtor.name} ${paint.mute(`(${metadata.priority})`)}`, relativePath, "middleware");
3215
3038
  }
3216
3039
  isHandlerClass(obj) {
3217
3040
  if (typeof obj !== "function") return false;
@@ -3235,9 +3058,15 @@ var InteractionDispatcher = class {
3235
3058
  ]);
3236
3059
  writes.push([map, route]);
3237
3060
  }
3238
- this.logger.utils.registration(handlerClass.name, formatFilePath(relativePath));
3239
3061
  }
3062
+ if (writes.length === 0) return;
3240
3063
  for (const [map, route] of writes) map.set(route, handlerClass);
3064
+ const from = formatFilePath(relativePath);
3065
+ if (this.loading) this.loadedHandlers.push({
3066
+ name: handlerClass.name,
3067
+ from
3068
+ });
3069
+ else this.logger.utils.registration(handlerClass.name, from);
3241
3070
  }
3242
3071
  /** @internal For use in dev mode */
3243
3072
  async onHmr(event) {
@@ -3265,7 +3094,7 @@ var InteractionDispatcher = class {
3265
3094
  this.core.bot.client.on(Events.InteractionCreate, (interaction) => {
3266
3095
  this.core.bot.emitSafe("any:interaction", interaction);
3267
3096
  this.handleInteraction(interaction).catch((err) => {
3268
- this.logger.error(`[${chalk.bold.red("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3097
+ this.logger.error(`[${paint.coral.bold("UNHANDLED ERROR AT ROOT")}] ${err.name}`, err.stack);
3269
3098
  this.core.bot.emit("error:unhandled:interaction", err);
3270
3099
  });
3271
3100
  });
@@ -3276,22 +3105,31 @@ var InteractionDispatcher = class {
3276
3105
  if (!prefix) return this.logger.warn(`${interactionType} has invalid customId: ${interaction.customId}`);
3277
3106
  await this.processInteraction(interaction, () => prefix, (key) => getMap().get(key));
3278
3107
  }
3279
- async processInteraction(interaction, extractKey, getHandler) {
3108
+ async processInteraction(interaction, extractKey, getHandler, fallback = UnhandledRepliable) {
3280
3109
  const key = extractKey(interaction);
3110
+ let sender;
3281
3111
  try {
3282
3112
  if (!interaction.isAutocomplete()) for (const { ctor } of this.middlewares) await new ctor(interaction, this.core).execute();
3283
3113
  let HandlerCtor = getHandler(key);
3284
3114
  if (!HandlerCtor) {
3285
- this.logger.warn(`No handler found for key ${chalk.bold.cyan(key)}. Falling back to UnhandledEvent.`);
3286
- HandlerCtor = UnhandledEvent;
3115
+ this.logger.warn(`No handler found for key ${paint.sky.bold(key)}. Falling back to ${fallback.name}.`);
3116
+ HandlerCtor = fallback;
3287
3117
  }
3288
- this.logger.debug(`Processing ${chalk.bold.green(key)} with ${chalk.gray(HandlerCtor.name)}`);
3118
+ this.logger.debug(`Processing ${paint.mint.bold(key)} with ${paint.mute(HandlerCtor.name)}`);
3289
3119
  const dispatch = new DispatchContext(routeIdOf(HandlerCtor));
3290
3120
  const handler = new HandlerCtor(interaction, this.core, dispatch);
3291
- if (!interaction.isAutocomplete()) await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core));
3121
+ if (handler instanceof RepliableHandler) sender = handler.getSender();
3122
+ if (!interaction.isAutocomplete()) {
3123
+ const monitor = slowGateMonitor();
3124
+ try {
3125
+ await runHandlerGates(HandlerCtor, interactionGateContext(interaction, this.core), dispatch.routeId ?? void 0, monitor?.observe);
3126
+ } finally {
3127
+ monitor?.report(dispatch.routeId);
3128
+ }
3129
+ }
3292
3130
  await handler.execute();
3293
3131
  } catch (caught) {
3294
- await handleInteractionFault(caught, interaction, this.core);
3132
+ await handleInteractionFault(caught, interaction, this.core, sender);
3295
3133
  }
3296
3134
  }
3297
3135
  async handleInteraction(interaction) {
@@ -3367,12 +3205,7 @@ var InteractionDispatcher = class {
3367
3205
  }
3368
3206
  async handleAutocomplete(interaction) {
3369
3207
  const route = slashRouteOf(interaction);
3370
- if (!this.autocompleteMap.has(route)) {
3371
- this.logger.warn(`No autocomplete handler for ${chalk.bold.cyan(route)}.`);
3372
- await interaction.respond([]);
3373
- return;
3374
- }
3375
- await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key));
3208
+ await this.processInteraction(interaction, () => route, (key) => this.autocompleteMap.get(key), UnhandledAutocomplete);
3376
3209
  }
3377
3210
  };
3378
3211
 
@@ -3428,7 +3261,7 @@ var Bot = class extends Plugin {
3428
3261
  });
3429
3262
  }
3430
3263
  this.emojiInjector = new EmojiInjector(core);
3431
- core.shutdown.addTask(ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3264
+ core.shutdown.addTask(ShutdownPhase$1.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3432
3265
  }
3433
3266
  /**
3434
3267
  * Initializes Discord client and all controllers
@@ -3461,7 +3294,9 @@ var Bot = class extends Plugin {
3461
3294
  * Logs the bot into Discord using the configured token
3462
3295
  */
3463
3296
  async login(token) {
3464
- await this._client.login(token);
3297
+ this._client.once(Events.ClientReady, () => this.emit("ready"));
3298
+ this._client.login(token);
3299
+ await this.waitFor("ready");
3465
3300
  this.logger.info(`Logged in as ${chalk.bold.magenta(this._client.user?.username)}!`);
3466
3301
  return this;
3467
3302
  }
@@ -3487,197 +3322,70 @@ var Bot = class extends Plugin {
3487
3322
  };
3488
3323
  __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3489
3324
 
3490
- //#endregion
3491
- //#region src/hmr/HmrManager.ts
3492
- var HmrManager = class {
3493
- logger = new Logger("HMR", { channel: "hmr" });
3494
- listeners = /* @__PURE__ */ new Set();
3495
- constructor() {}
3496
- /** @internal */
3497
- init() {
3498
- if (!import.meta.hot) return;
3499
- setDevChannel(wrapHot(import.meta.hot));
3500
- if (Envapter.isDevelopment || Envapter.isTest) {
3501
- this.logger.info("Enabled");
3502
- getDevChannel()?.on("seedcord:hmr", (payload) => {
3503
- const affected = payload.affectedModules?.length ?? 0;
3504
- this.logger.info(`${chalk.bold("1")} module changed, ${chalk.bold(affected)} affected modules`);
3505
- if (payload.affectedModules) this.logger.utils.list(payload.affectedModules.map((mod) => formatFilePath(mod)), "Affected modules: ");
3506
- this.handleUpdate(payload);
3507
- });
3508
- }
3509
- }
3510
- /** @internal */
3511
- register(listener) {
3512
- this.listeners.add(listener);
3513
- }
3514
- /** @internal */
3515
- unregister(listener) {
3516
- this.listeners.delete(listener);
3517
- }
3518
- async handleUpdate(event) {
3519
- const promises = [...this.listeners].map(async (listener) => {
3520
- try {
3521
- await listener.onHmr(event);
3522
- } catch (error) {
3523
- this.logger.error(`Error handling HMR update in ${chalk.bold(listener.name)}: `, error);
3524
- }
3525
- });
3526
- await Promise.all(promises);
3527
- }
3528
- };
3529
-
3530
- //#endregion
3531
- //#region src/subscribers/bases/webhookHelpers.ts
3532
- const DISCORD_WEBHOOK_REGEX = new RegExp(String.raw`^https?:\/\/(?:canary\.|ptb\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w$-]+$`);
3533
- /**
3534
- * Whether a string is a well-formed Discord webhook URL.
3535
- *
3536
- * @internal
3537
- */
3538
- function isDiscordWebhookUrl(value) {
3539
- return URL.canParse(value) && DISCORD_WEBHOOK_REGEX.test(value);
3540
- }
3541
- /**
3542
- * Serializes a value to a JSON attachment, made circular-safe first.
3543
- *
3544
- * @internal
3545
- */
3546
- function jsonAttachment(name, description, data) {
3547
- const content = filterCirculars(data);
3548
- return new AttachmentBuilder(Buffer.from(JSON.stringify(content, void 0, 2), "utf8"), {
3549
- name,
3550
- description
3551
- });
3552
- }
3553
- /**
3554
- * The small divider the default webhook reporters place between sections.
3555
- *
3556
- * @internal
3557
- */
3558
- var WebhookSeparator = class extends BuilderComponent {
3559
- constructor() {
3560
- super("separator");
3561
- this.instance.setSpacing(SeparatorSpacingSize.Small).setDivider(true);
3562
- }
3563
- };
3564
-
3565
3325
  //#endregion
3566
3326
  //#region src/subscribers/default/HandledException.ts
3567
- var _HandledException;
3568
- function webhookUrlValidator$1(raw) {
3569
- if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3570
- const value = raw?.trim() ?? "";
3571
- if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookMissing);
3572
- if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigHandledExceptionWebhookInvalid);
3573
- return value;
3574
- }
3575
3327
  let HandledException = class HandledException extends WebhookLog {
3576
- static {
3577
- _HandledException = this;
3578
- }
3579
- webhook = new WebhookClient({ url: _HandledException.handledExceptionWebhookUrl });
3580
- async execute() {
3581
- const { source } = this.data;
3582
- try {
3583
- await this.webhook.send({
3584
- flags: flagsFor(false),
3585
- withComponents: true,
3586
- username: "Handled Exception",
3587
- components: [new HandledExceptionContainer(this.data).component],
3588
- files: [jsonAttachment("source.json", "The raw source the fault came from", source.raw)]
3589
- });
3590
- } catch (error) {
3591
- this.logger.error("Failed to send handled exception webhook", error);
3592
- }
3328
+ report() {
3329
+ return {
3330
+ username: "Handled Exception",
3331
+ components: [new HandledExceptionContainer(this.data).component],
3332
+ files: [jsonAttachment("source.json", "The raw source the fault came from", this.data.source.raw)]
3333
+ };
3593
3334
  }
3594
3335
  };
3595
- __decorate([Envapt("HANDLED_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator$1(raw) }), __decorateMetadata("design:type", String)], HandledException, "handledExceptionWebhookUrl", void 0);
3596
- HandledException = _HandledException = __decorate([Subscribe("handledException")], HandledException);
3336
+ HandledException = __decorate([Subscribe("handledException"), WebhookUrl("HANDLED_EXCEPTION_WEBHOOK_URL")], HandledException);
3597
3337
  var HandledExceptionContainer = class extends BuilderComponent {
3598
3338
  constructor(data) {
3599
3339
  super("container");
3600
3340
  const { denial, uuid, source } = data;
3601
- this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${causeStack(denial)}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3341
+ this.instance.addTextDisplayComponents((text) => text.setContent(faultSummary(denial, source))).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${neutralizeFences(causeStack(denial))}\`\`\``)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent("### Source")).addFileComponents((file) => file.setURL("attachment://source.json"));
3602
3342
  }
3603
3343
  };
3604
- /**
3605
- * The summary block of a fault report, narrowed per source kind so an interaction fault shows its
3606
- * command and customId while an event fault shows its event name and handler.
3607
- *
3608
- * @internal
3609
- */
3610
3344
  function faultSummary(denial, source) {
3611
- const head = `### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`;
3345
+ const head = stripAnsi(`### A handled fault was reported: \`${denial.name}\`\n**Message:** ${denial.message}\n`);
3612
3346
  if (source.kind === "event") return `${head}**Event:** ${source.eventName}\n**Handler:** ${source.handler}\n**User ID:** \`${source.userId ?? "n/a"}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\``;
3613
3347
  return `${head}**Kind:** ${source.interactionKind}\n**Command:** ${source.command ?? "n/a"}\n**CustomId:** ${source.customId ?? "n/a"}\n**User ID:** \`${source.userId}\`\n**Guild ID:** \`${source.guildId ?? "n/a"}\`\n**Channel ID:** \`${source.channelId ?? "n/a"}\`\n**Interaction ID:** \`${source.interactionId}\``;
3614
3348
  }
3615
- /**
3616
- * The text shown for a denial's cause in the report. Tolerates a non-serializable cause (a bigint or a
3617
- * circular object) so building the report never throws.
3618
- *
3619
- * @internal
3620
- */
3621
3349
  function causeStack(denial) {
3622
3350
  const { cause } = denial;
3623
- if (Error.isError(cause)) return cause.stack ?? cause.message;
3624
- if (typeof cause === "string") return cause;
3351
+ if (Error.isError(cause)) return stripAnsi(cause.stack ?? cause.message);
3352
+ if (typeof cause === "string") return stripAnsi(cause);
3625
3353
  if (typeof cause === "bigint" || typeof cause === "symbol" || typeof cause === "function") return cause.toString();
3626
3354
  if (cause !== void 0) try {
3627
3355
  return JSON.stringify(cause);
3628
3356
  } catch {
3629
3357
  return "[unserializable cause]";
3630
3358
  }
3631
- return denial.stack ?? "No cause recorded.";
3359
+ return stripAnsi(denial.stack ?? "No cause recorded.");
3632
3360
  }
3633
3361
 
3634
3362
  //#endregion
3635
3363
  //#region src/subscribers/default/UnknownException.ts
3636
- var _UnknownException;
3637
- function webhookUrlValidator(raw) {
3638
- if (raw !== null && typeof raw !== "string") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3639
- const value = raw?.trim() ?? "";
3640
- if (value === "") throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookMissing);
3641
- if (!isDiscordWebhookUrl(value)) throw new SeedcordError(SeedcordErrorCode.ConfigUnknownExceptionWebhookInvalid);
3642
- return value;
3643
- }
3644
3364
  let UnknownException = class UnknownException extends WebhookLog {
3645
- static {
3646
- _UnknownException = this;
3647
- }
3648
- webhook = new WebhookClient({ url: _UnknownException.unknownExceptionWebhookUrl });
3649
- async execute() {
3365
+ report() {
3650
3366
  const { metadata } = this.data;
3651
- const metadataFile = metadata ? jsonAttachment("metadata.json", "Metadata associated with the error", metadata) : null;
3652
- try {
3653
- await this.webhook.send({
3654
- flags: flagsFor(false),
3655
- withComponents: true,
3656
- username: "Unknown Exception",
3657
- components: [new UnhandledErrorContainer(this.data).component],
3658
- files: metadataFile ? [metadataFile] : []
3659
- });
3660
- } catch (error) {
3661
- this.logger.error("Failed to send unknown exception webhook", error);
3662
- }
3367
+ return {
3368
+ username: "Unknown Exception",
3369
+ components: [new UnhandledErrorContainer(this.data).component],
3370
+ files: metadata ? [jsonAttachment("metadata.json", "Metadata associated with the error", metadata)] : void 0
3371
+ };
3663
3372
  }
3664
3373
  };
3665
- __decorate([Envapt("UNKNOWN_EXCEPTION_WEBHOOK_URL", { converter: (raw) => webhookUrlValidator(raw) }), __decorateMetadata("design:type", String)], UnknownException, "unknownExceptionWebhookUrl", void 0);
3666
- UnknownException = _UnknownException = __decorate([Subscribe("unknownException")], UnknownException);
3374
+ UnknownException = __decorate([Subscribe("unknownException"), WebhookUrl("UNKNOWN_EXCEPTION_WEBHOOK_URL")], UnknownException);
3667
3375
  var UnhandledErrorContainer = class extends BuilderComponent {
3668
3376
  constructor(data) {
3669
3377
  super("container");
3670
3378
  const { uuid, error, guild, user, metadata } = data;
3671
- this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${error.stack}\`\`\``));
3379
+ this.instance.addTextDisplayComponents((text) => text.setContent(`### An unknown exception was thrown\n**Guild ID:** \`${guild?.id ?? "Not used in a guild"}\`\n**Guild Name:** ${guild?.name ?? "Not used in a guild"}\n**User ID:** \`${user?.id ?? "Missing user info"}\`\n**Username:** ${user?.username ?? "Missing user info"}\n`)).addSeparatorComponents(new WebhookSeparator().component).addTextDisplayComponents((text) => text.setContent(`### UUID \`${uuid}\`\n\`\`\`${errorReport(error)}\`\`\``));
3672
3380
  this.addTimestampsIfAvailable(error);
3673
3381
  this.addMetadataIfAvailable(metadata);
3674
3382
  }
3675
3383
  addTimestampsIfAvailable(error) {
3676
- if (!(error instanceof DiscordAPIError)) return;
3384
+ if (!(error instanceof DiscordAPIError$1)) return;
3677
3385
  const now = Date.now();
3678
3386
  const snowflake = error.url.match(/\/interactions\/(\d+)\//)?.[1];
3679
3387
  if (!snowflake) return void 0;
3680
- const interactionTs = Number(SnowflakeUtil.deconstruct(snowflake).timestamp);
3388
+ const interactionTs = timestampFromSnowflake(snowflake);
3681
3389
  const diff = now - interactionTs;
3682
3390
  const seconds = Math.floor(diff / 1e3);
3683
3391
  const millis = diff % 1e3;
@@ -3692,12 +3400,8 @@ var UnhandledErrorContainer = class extends BuilderComponent {
3692
3400
  //#endregion
3693
3401
  //#region src/subscribers/Bus.ts
3694
3402
  /**
3695
- * Manages application subscribers and event handling
3696
- *
3697
- * Provides a centralized system for registering and executing custom subscribers
3698
- * throughout the application lifecycle. Bus subscribers are loaded from configured directories
3699
- * and can be triggered programmatically or by framework events. Accessed via `core.bus`. Do not
3700
- * construct it directly.
3403
+ * Registers and dispatches subscribers. Subscribers load from the configured directories and run
3404
+ * on a programmatic or framework-event publish. Accessed via `core.bus`. Do not construct it directly.
3701
3405
  */
3702
3406
  var Bus = class extends Plugin {
3703
3407
  core;
@@ -3707,6 +3411,7 @@ var Bus = class extends Plugin {
3707
3411
  isInitialized = false;
3708
3412
  subscribersMap = /* @__PURE__ */ new Map();
3709
3413
  executedOnceHandlers = /* @__PURE__ */ new Set();
3414
+ webhookProbes = /* @__PURE__ */ new Map();
3710
3415
  hmrHandler;
3711
3416
  constructor(core) {
3712
3417
  super(core);
@@ -3732,7 +3437,6 @@ var Bus = class extends Plugin {
3732
3437
  this.isInitialized = true;
3733
3438
  this.registerSubscriber(UnknownException);
3734
3439
  this.registerSubscriber(HandledException);
3735
- Envapter.require("UNKNOWN_EXCEPTION_WEBHOOK_URL", "HANDLED_EXCEPTION_WEBHOOK_URL");
3736
3440
  const subscribersDir = this.core.config.subscribers.path;
3737
3441
  if (subscribersDir) {
3738
3442
  this.logger.info(chalk.bold(subscribersDir));
@@ -3740,8 +3444,19 @@ var Bus = class extends Plugin {
3740
3444
  const totalSubscribers = [...this.subscribersMap.values()].reduce((acc, handlers) => acc + handlers.length, 0);
3741
3445
  this.logger.utils.list([`${chalk.bold.magenta(totalSubscribers)} subscribers`], chalk.bold.green("Loaded"));
3742
3446
  }
3447
+ if (!Envapter.isTest) await this.verifyWebhooks();
3448
+ }
3449
+ /** @internal Exposed for tests. */
3450
+ async verifyWebhooks() {
3451
+ const missing = [];
3452
+ await Promise.all([...this.webhookProbes].map(async ([url, envKeys]) => {
3453
+ const result = await WebhookLog.senderFor(url).verify();
3454
+ if (result === "missing") missing.push(...envKeys);
3455
+ else if (result === "unreachable") this.logger.warn(`could not verify webhook at ${paint.sky.bold(envKeys.join(", "))}`);
3456
+ }));
3457
+ if (missing.length > 0) throw new SeedcordError(SeedcordErrorCode.ConfigWebhookNotFound, [missing.join(", ")]);
3743
3458
  }
3744
- /** @internal For use in dev mode */
3459
+ /** @internal */
3745
3460
  async onHmr(event) {
3746
3461
  if (this.hmrHandler) await this.hmrHandler.handle(event);
3747
3462
  }
@@ -3758,6 +3473,17 @@ var Bus = class extends Plugin {
3758
3473
  }, this.logger);
3759
3474
  }
3760
3475
  registerSubscriber(handler) {
3476
+ if (handler.prototype instanceof WebhookLog) {
3477
+ const envKey = WebhookLog.envKeyOf(handler);
3478
+ const url = WebhookLog.urlOf(envKey);
3479
+ if (url === null) {
3480
+ this.logger.warn(`${chalk.bold(handler.name)} disabled, ${paint.sky.bold(envKey)} is not set`);
3481
+ return;
3482
+ }
3483
+ const envKeys = this.webhookProbes.get(url) ?? [];
3484
+ if (!envKeys.includes(envKey)) envKeys.push(envKey);
3485
+ this.webhookProbes.set(url, envKeys);
3486
+ }
3761
3487
  const options = Reflect.getMetadata(SubscribeMetadataKey, handler);
3762
3488
  let handlers = this.subscribersMap.get(options.subscriber);
3763
3489
  if (!handlers) {
@@ -3817,7 +3543,7 @@ var Bus = class extends Plugin {
3817
3543
  //#endregion
3818
3544
  //#region src/version.ts
3819
3545
  /** Package version */
3820
- const version = "0.1.0-next.1";
3546
+ const version = "0.1.0-next.3";
3821
3547
 
3822
3548
  //#endregion
3823
3549
  //#region src/Seedcord.ts
@@ -3836,6 +3562,8 @@ var Seedcord = class Seedcord extends Pluggable {
3836
3562
  /** @internal */
3837
3563
  version = version;
3838
3564
  static isInstantiated = false;
3565
+ healthCheck;
3566
+ hmrManager;
3839
3567
  /** @see {@link CoordinatedShutdown} */
3840
3568
  shutdown;
3841
3569
  /** @see {@link CoordinatedStartup} */
@@ -3846,10 +3574,6 @@ var Seedcord = class Seedcord extends Pluggable {
3846
3574
  bot;
3847
3575
  /** @see {@link IRateLimiter} */
3848
3576
  rateLimiter;
3849
- /** @see {@link HealthCheck} */
3850
- healthCheck;
3851
- /** @see {@link HmrManager} */
3852
- hmrManager;
3853
3577
  /**
3854
3578
  * Creates a new Seedcord instance
3855
3579
  *
@@ -3863,6 +3587,7 @@ var Seedcord = class Seedcord extends Pluggable {
3863
3587
  const startup = new CoordinatedStartup();
3864
3588
  super(shutdown, startup);
3865
3589
  this.config = config;
3590
+ installNodeDefaults(config.logger);
3866
3591
  setBotColor(config.botColor);
3867
3592
  this.shutdown = shutdown;
3868
3593
  this.startup = startup;
@@ -3874,27 +3599,27 @@ var Seedcord = class Seedcord extends Pluggable {
3874
3599
  this.healthCheck = new HealthCheck(this.shutdown, config.healthCheck);
3875
3600
  this.registerStartupTasks();
3876
3601
  }
3877
- /** @internal */
3602
+ /** The bot's discord username, populated after login. */
3603
+ get username() {
3604
+ return this.bot.client.user?.username;
3605
+ }
3878
3606
  static reset() {
3879
3607
  Seedcord.isInstantiated = false;
3608
+ LoggerChannelRegistry.instance.reset();
3880
3609
  }
3881
- /**
3882
- * Registers default startup tasks
3883
- * @internal
3884
- */
3885
3610
  registerStartupTasks() {
3886
3611
  if (Envapter.isDevelopment || Envapter.isTest) this.registerHmrAwareModules();
3887
- this.startup.addTask(StartupPhase.Configuration, "Bus Initialization", async () => {
3612
+ this.startup.addTask(StartupPhase$1.Configuration, "Bus Initialization", async () => {
3888
3613
  this.bus.logger.utils.initialization("Subscribers", "start");
3889
3614
  await this.bus.init();
3890
3615
  this.bus.logger.utils.initialization("Subscribers", "end");
3891
3616
  });
3892
- this.startup.addTask(StartupPhase.Instantiation, "Bot Initialization", async () => {
3617
+ this.startup.addTask(StartupPhase$1.Instantiation, "Bot Initialization", async () => {
3893
3618
  this.bot.logger.utils.initialization("Bot", "start");
3894
3619
  await this.bot.init();
3895
3620
  this.bot.logger.utils.initialization("Bot", "end");
3896
3621
  });
3897
- this.startup.addTask(StartupPhase.Ready, "Health Check", async () => {
3622
+ this.startup.addTask(StartupPhase$1.Ready, "Health Check", async () => {
3898
3623
  this.healthCheck.logger.utils.initialization("HealthCheck", "start");
3899
3624
  await this.healthCheck.init();
3900
3625
  this.healthCheck.logger.utils.initialization("HealthCheck", "end");
@@ -3910,7 +3635,7 @@ var Seedcord = class Seedcord extends Pluggable {
3910
3635
  return this;
3911
3636
  }
3912
3637
  registerHmrAwareModules() {
3913
- this.startup.addTask(StartupPhase.Configuration, "HMR Registration", async () => {
3638
+ this.startup.addTask(StartupPhase$1.Configuration, "HMR Registration", async () => {
3914
3639
  this.hmrManager.register(this.bot);
3915
3640
  this.hmrManager.register(this.bus);
3916
3641
  for (const plugin of this.plugins) this.hmrManager.register(plugin);
@@ -3920,5 +3645,5 @@ var Seedcord = class Seedcord extends Pluggable {
3920
3645
  };
3921
3646
 
3922
3647
  //#endregion
3923
- export { ArraySource, AutocompleteHandler, AutocompleteRoute, ButtonHandler, ButtonRoute, CommandMentions, ContextMenuHandler, ContextMenuRoute, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, HmrModuleHandler, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, ModalRoute, Nsfw, Paginator, Pluggable, Plugin, RegisterCommand, RegisterEvent, ReplySender, RequireBotPermissions, RequirePermissions, RequireRole, Seedcord, SelectMenuHandler, SelectMenuKind, SelectMenuRoute, SlashHandler, SlashRoute, Subscribe, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3648
+ export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Pluggable, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, ShutdownPhase, SlashHandler, StartupPhase, Subscribe, Subscriber, WebhookLog, WebhookUrl, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
3924
3649
  //# sourceMappingURL=index.mjs.map