@seedcord/gateway 0.1.0-next.5 → 0.1.0

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,25 +1,24 @@
1
1
  import "reflect-metadata";
2
2
  import { installNodeDefaults } from "@seedcord/logger/node";
3
- import { BaseReplySender, CommandMetadataKey, EventMetadataKey, FaultThrottle, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PAGE_MAX, PermissionNames, PublishDefault, areRoutes, asError, decodeComponentRoute, deferFlags, dispatchedPayload, getDevChannel, outcomeFor, pageCursor, pickNotice, prefixOf, queuedMsFor, reportedWrite, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
3
+ import { BaseReplySender, CommandInjector, EventMetadataKey, FaultThrottle, GateNotice, GatedMetadataKey, HmrManager, InteractionMetadataKey, InteractionRouteKeys, InteractionRoutes, MiddlewareMetadataKey, MissingPermissions, NoticeCard, PermissionNames, PublishDefault, accessorStore, areRoutes, asError, clearStore, decodeComponentRoute, deferFlags, dispatchedPayload, guardedAccessor, isEmojiTuple, outcomeFor, pickNotice, prefixOf, queuedMsFor, reportedWrite, routeIdOf, runHandlerGates, sendFlags, setBotColor, slowGateMonitor } from "@seedcord/core/internal";
4
4
  import { SeedcordErrorCode } from "@seedcord/errors";
5
- import { SeedcordError, SeedcordRangeError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
- import { BaseHandler, BuilderComponent, Bus, CustomId, DispatchContext, Fault, Notice, RepliableHandler as RepliableHandler$1, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, defineGate, paginate } from "@seedcord/core";
5
+ import { SeedcordError, SeedcordTypeError, validateDiscordToken } from "@seedcord/errors/internal";
6
+ import { ArraySource as ArraySource$1, BaseHandler, BuilderComponent, Bus, CursorSource as CursorSource$1, CustomId, DispatchContext, Fault, Notice, PaginatorBase, RepliableHandler as RepliableHandler$1, RowComponent, Silence, Subscriber as Subscriber$1, WebhookLog as WebhookLog$1, assertPermissions, defineGate } from "@seedcord/core";
7
7
  import { Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
8
- import { ActionRowBuilder, ButtonBuilder, ContextMenuCommandBuilder, SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
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";
8
+ import { ButtonStyle, Client, ComponentType, DiscordAPIError, Events, Guild, GuildMember, Message, MessageFlags, PermissionFlagsBits, RESTJSONErrorCodes, Role, User } from "discord.js";
9
+ import { ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
10
+ import { buildSlashRoute } from "@seedcord/utils/internal";
12
11
  import { Plugin as Plugin$1 } from "@seedcord/core/plugin";
13
- import { CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, SubscriberLoader, settleWithin } from "@seedcord/core/node/internal";
12
+ import { CommandRegistry, CoordinatedShutdown, CoordinatedStartup, HealthCheck, Pluggable, ShutdownPhase, StartupPhase, SubscriberLoader, settleWithin } from "@seedcord/core/node/internal";
14
13
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
15
14
  import { SeedcordBrand } from "@seedcord/types/internal";
16
15
  import { Envapter } from "envapt";
17
16
  import chalk from "chalk";
18
17
  import { Envapt } from "envapt/legacy";
19
- import { resolve } from "node:path";
20
18
  import { HmrModuleHandler } from "@seedcord/core/hmr";
21
19
  import { formatFilePath, hasKeys } from "@seedcord/utils";
22
20
  import { traverseDirectory } from "@seedcord/utils/node";
21
+ import * as crypto from "node:crypto";
23
22
 
24
23
  export * from "@seedcord/errors"
25
24
 
@@ -224,59 +223,6 @@ var CannotAssignBotRole = class extends Notice {
224
223
  return { components: [new NoticeCard("I cannot assign a managed role.").component] };
225
224
  }
226
225
  };
227
- var HasDangerousPermissions = class extends Notice {
228
- subject;
229
- dangerousPerms;
230
- customLead;
231
- constructor(message, subject, dangerousPerms) {
232
- super(message ?? "A dangerous permission is enabled.");
233
- this.subject = subject;
234
- this.dangerousPerms = dangerousPerms;
235
- this.customLead = message;
236
- }
237
- render() {
238
- const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
239
- return { components: [new NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
240
- }
241
- };
242
- var UserNotFound = class extends Notice {
243
- userArg;
244
- constructor(userArg) {
245
- super(`User not found: ${userArg}`);
246
- this.userArg = userArg;
247
- }
248
- render() {
249
- return { components: [new NoticeCard(`User probably doesn't exist or was deleted.\n**User Argument:** \`${this.userArg}\`\nPlease check the user ID and try again. Only pass valid user IDs as the argument.`, "User Not Found").component] };
250
- }
251
- };
252
- var UserNotInGuild = class extends Notice {
253
- constructor(message = "User is not in the guild.") {
254
- super(message);
255
- }
256
- render() {
257
- return { components: [new NoticeCard(this.message).component] };
258
- }
259
- };
260
- var RoleDoesNotExist = class extends Notice {
261
- roleId;
262
- constructor(message, roleId) {
263
- super(message);
264
- this.roleId = roleId;
265
- }
266
- render() {
267
- return { components: [new NoticeCard(`The role with ID \`${this.roleId}\` does not exist.`).component] };
268
- }
269
- };
270
- var CouldNotFindChannel = class extends Notice {
271
- channelId;
272
- constructor(message, channelId) {
273
- super(message);
274
- this.channelId = channelId;
275
- }
276
- render() {
277
- return { components: [new NoticeCard(`Could not find channel with ID \`${this.channelId}\`. It could also be that the channel is not a text channel.`).component] };
278
- }
279
- };
280
226
 
281
227
  //#endregion
282
228
  //#region src/bot/gates/catalog/Nsfw.ts
@@ -314,20 +260,15 @@ function Nsfw(options) {
314
260
 
315
261
  //#endregion
316
262
  //#region src/bot/injectors/EmojiInjector.ts
317
- const emojiStorage = {};
318
- function isEmojiTuple(v) {
319
- return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string";
320
- }
263
+ const emojiStorage = accessorStore();
321
264
  /**
322
- * Global emoji mappings object
323
- *
324
- * @see {@link EmojiMap}
265
+ * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
266
+ * A read before that throws.
325
267
  */
326
- const Emojis = emojiStorage;
268
+ const Emojis = guardedAccessor("Emojis", emojiStorage);
327
269
  /**
328
- * Loads and injects emojis based on bot configuration.
329
- *
330
- * For emojis injected from specific guilds, ensure that the Guilds intent is provided in client options.
270
+ * Loads and injects emojis based on bot configuration. A guild emoji requires the Guilds intent in the client
271
+ * options.
331
272
  *
332
273
  * @internal
333
274
  */
@@ -380,126 +321,10 @@ var EmojiInjector = class {
380
321
  emojiStorage[key] = appEmoji;
381
322
  }
382
323
  clearEmojis() {
383
- for (const key of Object.keys(emojiStorage)) Reflect.deleteProperty(emojiStorage, key);
384
- }
385
- };
386
-
387
- //#endregion
388
- //#region src/bot/injectors/CommandMentionInjector.ts
389
- const mentionStorage = {};
390
- /**
391
- * Ready-to-send command mention strings, keyed by {@link SlashOptionRegistry}.
392
- *
393
- * A value is a clickable `</name:id>` when the command's id resolved from the deploy, otherwise the plain
394
- * `/name` text. Populated by {@link CommandMentionInjector} after each deploy.
395
- */
396
- const CommandMentions = mentionStorage;
397
- const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
398
- /**
399
- * Turns each deployed slash route into a clickable command mention exposed through {@link CommandMentions}.
400
- *
401
- * Consumes the ids {@link DeployResult} carries, so it runs after a deploy rather than resolving from a name
402
- * cache (discord.js offers none for command ids). A global command is clickable everywhere by its app-wide id.
403
- * A guild command deployed to one guild is clickable by that guild's id. A command deployed to two or more
404
- * guilds mints a different id per guild, so one accessor key cannot resolve it, and it falls back to plain
405
- * `/name` text with one warn. On a non-owning shard a guild command also falls back to plain text.
406
- *
407
- * @internal
408
- */
409
- var CommandMentionInjector = class {
410
- core;
411
- logger = new Logger("CommandMentions", { channel: "bot" });
412
- constructor(core) {
413
- this.core = core;
414
- }
415
- inject(deploy) {
416
- this.clear();
417
- const globalIds = this.indexByName(deploy.global);
418
- const guildIds = this.indexGuilds(deploy.guilds);
419
- const warned = /* @__PURE__ */ new Set();
420
- let clickable = 0;
421
- for (const command of this.allSlashBuilders()) {
422
- const id = this.resolveId(command.name, globalIds, guildIds, warned);
423
- for (const leaf of routeLeavesOf(command.toJSON())) {
424
- mentionStorage[leaf.route] = id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route);
425
- if (id) clickable++;
426
- }
427
- }
428
- this.logger.utils.summary("Linked mentions", {
429
- clickable,
430
- plain: Object.keys(mentionStorage).length - clickable
431
- });
432
- }
433
- allSlashBuilders() {
434
- const registry = this.core.bot.commands;
435
- if (!registry) return [];
436
- const slash = [...registry.globalCommands, ...registry.guildCommands.values()].flat().filter((command) => command instanceof SlashCommandBuilder);
437
- return [...new Set(slash)];
438
- }
439
- indexByName(collection) {
440
- const map = /* @__PURE__ */ new Map();
441
- for (const command of collection.values()) map.set(command.name, command.id);
442
- return map;
443
- }
444
- indexGuilds(guilds) {
445
- const map = /* @__PURE__ */ new Map();
446
- for (const collection of guilds.values()) for (const command of collection.values()) {
447
- const existing = map.get(command.name);
448
- if (existing === void 0) map.set(command.name, command.id);
449
- else if (existing !== command.id) map.set(command.name, MULTI_GUILD);
450
- }
451
- return map;
452
- }
453
- resolveId(name, globalIds, guildIds, warned) {
454
- const global = globalIds.get(name);
455
- if (global) return global;
456
- const guild = guildIds.get(name);
457
- if (guild === void 0) return void 0;
458
- if (guild === MULTI_GUILD) {
459
- if (!warned.has(name)) {
460
- this.logger.warn(`${name} is deployed to multiple guilds, falling back to plain text (not clickable).`);
461
- warned.add(name);
462
- }
463
- return;
464
- }
465
- return guild;
466
- }
467
- toMention(route, id) {
468
- const [name, middle, sub] = route.split("/");
469
- if (name && middle && sub) return chatInputApplicationCommandMention(name, middle, sub, id);
470
- if (name && middle) return chatInputApplicationCommandMention(name, middle, id);
471
- return chatInputApplicationCommandMention(name ?? route, id);
472
- }
473
- toPlain(route) {
474
- return `/${route.replaceAll("/", " ")}`;
475
- }
476
- clear() {
477
- for (const key of Object.keys(mentionStorage)) Reflect.deleteProperty(mentionStorage, key);
324
+ clearStore(emojiStorage);
478
325
  }
479
326
  };
480
327
 
481
- //#endregion
482
- //#region src/bot/utilities/channels/fetchText.ts
483
- /**
484
- * Fetches and validates a text channel. Refuses when the channel doesn't exist or isn't a text channel.
485
- *
486
- * @param client - The Discord client instance
487
- * @param channelId - Channel ID or TextChannel instance
488
- * @returns Promise resolving to the text channel
489
- */
490
- async function fetchText(client, channelId) {
491
- if (channelId instanceof TextChannel) return channelId;
492
- let channel = client.channels.cache.get(channelId);
493
- if (!channel) try {
494
- channel = await client.channels.fetch(channelId);
495
- } catch (err) {
496
- if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownChannel) throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
497
- throw err;
498
- }
499
- if (channel?.isTextBased()) return channel;
500
- throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
501
- }
502
-
503
328
  //#endregion
504
329
  //#region src/bot/notices/utils.ts
505
330
  function mentionFor(subject) {
@@ -510,48 +335,37 @@ function mentionFor(subject) {
510
335
  //#endregion
511
336
  //#region src/bot/utilities/permissions/checkPermissions.ts
512
337
  function checkPermissions(a, b, c, d, e) {
513
- const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof Role || a instanceof GuildMember ? {
338
+ const { for: pFor, in: pIn, scope, inverse = false, missingNotice, dangerousNotice } = a instanceof Role || a instanceof GuildMember ? {
514
339
  for: a,
515
340
  in: b,
516
341
  scope: c,
517
342
  inverse: d ?? false,
518
343
  ...e
519
344
  } : a;
520
- const Missing = missingCtor ?? MissingPermissions;
521
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
522
- const subject = mentionFor(pFor);
523
345
  const perms = pIn instanceof Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
524
- const names = (bits) => bits.map((bit) => PermissionNames.get(bit) ?? String(bit));
525
- if (inverse) {
526
- const present = scope.filter((bit) => perms.has(bit, true));
527
- if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
528
- return;
529
- }
530
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
531
- if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
346
+ assertPermissions({
347
+ subject: mentionFor(pFor),
348
+ permissions: perms.bitfield,
349
+ scope,
350
+ inverse,
351
+ ...missingNotice && { missingNotice },
352
+ ...dangerousNotice && { dangerousNotice }
353
+ });
532
354
  }
533
355
 
534
356
  //#endregion
535
357
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
536
358
  /**
537
- * Checks if the bot has required permissions in a {@link Guild} or {@link TextChannel}. Refuses when a
538
- * required permission is missing.
539
- *
540
- * @param target - Guild or text channel to check in
541
- * @param scope - Permission bits to validate
542
- * @param inverse - Whether to check for absence of the given permissions
543
- * @param errors - Optional custom error constructors
359
+ * Checks the bot's own permissions in a {@link Guild} or {@link TextChannel}. Refuses when a permission in
360
+ * `scope` is missing, or with `inverse` when one is present. When the bot member is uncached, the refusal
361
+ * reports every permission in `scope` as missing.
544
362
  *
545
363
  * @example
546
364
  * ```ts
547
365
  * import { PermissionFlagsBits } from 'discord.js';
548
366
  * import { checkBotPermissions } from '@seedcord/gateway';
549
367
  *
550
- * // Check if the bot has SendMessages and ViewChannel permissions in a text channel
551
- * checkBotPermissions(textChannel, [
552
- * PermissionFlagsBits.SendMessages,
553
- * PermissionFlagsBits.ViewChannel
554
- * ]);
368
+ * checkBotPermissions(textChannel, [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel]);
555
369
  * ```
556
370
  */
557
371
  function checkBotPermissions(target, scope, inverse = false, errors) {
@@ -574,26 +388,17 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
574
388
  }
575
389
 
576
390
  //#endregion
577
- //#region src/bot/utilities/roles/getBotRole.ts
578
- /**
579
- * Gets the bot's managed role in a guild.
580
- *
581
- * @param guild - The guild to get the bot role from
582
- * @returns The bot's managed role in the guild
583
- * @throws A **SeedcordError** if the client user is unavailable or if the bot role is missing
584
- */
585
- function getBotRole(guild) {
391
+ //#region src/bot/utilities/permissions/hasPermsToAssign.ts
392
+ function botRoleOf(guild) {
586
393
  const botRole = guild.roles.botRoleFor(guild.client.user);
587
394
  if (!botRole) throw new SeedcordError(SeedcordErrorCode.CoreBotRoleMissing, [guild.id]);
588
395
  return botRole;
589
396
  }
590
-
591
- //#endregion
592
- //#region src/bot/utilities/permissions/hasPermsToAssign.ts
593
397
  /**
594
- * Validates if the bot can assign a target role. Refuses when the target role is above the bot's, is managed, or the bot lacks Manage Roles.
398
+ * Validates that the bot can assign a target role. Refuses when the target role is above the bot's, is managed, or the bot lacks Manage Roles.
595
399
  *
596
400
  * @param roleOrOptions - Target role or complete options for the check
401
+ * @throws A `Notice` on refusal, or a `SeedcordError` when the bot has no role in the guild's role cache.
597
402
  *
598
403
  * @example
599
404
  * ```ts
@@ -621,138 +426,123 @@ function hasPermsToAssign(roleOrOptions) {
621
426
  const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
622
427
  const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
623
428
  const MissingNotice = errors?.missingNotice ?? MissingPermissions;
624
- const botRole = getBotRole(targetRole.guild);
429
+ const botRole = botRoleOf(targetRole.guild);
625
430
  if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
626
431
  if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
627
432
  checkBotPermissions(targetRole.guild, [PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
628
433
  }
629
434
 
630
435
  //#endregion
631
- //#region src/bot/utilities/roles/fetchRole.ts
632
- function isUnknownRole(err) {
633
- return err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownRole;
634
- }
635
- async function fetchRoleInGuild(guild, roleId) {
636
- const cached = guild.roles.cache.get(roleId);
637
- if (cached) return cached;
638
- try {
639
- return await guild.roles.fetch(roleId);
640
- } catch (err) {
641
- if (isUnknownRole(err)) throw new RoleDoesNotExist("Role not found in specified guild", roleId);
642
- throw err;
643
- }
644
- }
645
- async function scanGuildsForRole(client, roleId) {
646
- const cached = client.guilds.cache.map((guild) => guild.roles.cache.get(roleId)).find(Boolean);
647
- if (cached) return cached;
648
- for (const guild of client.guilds.cache.values()) try {
649
- const role = await guild.roles.fetch(roleId);
650
- if (role) return role;
651
- } catch (err) {
652
- if (isUnknownRole(err)) continue;
653
- throw err;
654
- }
655
- return null;
656
- }
436
+ //#region src/bot/confirm/reserved.ts
437
+ const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
657
438
  /**
658
- * Fetches a role by ID from a client or guild. Refuses when the role doesn't exist.
439
+ * The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
440
+ * in-process collector reads them. Do not mint a real component from it.
659
441
  *
660
- * @param clientOrGuild - Discord client or guild instance
661
- * @param roleId - The role ID to fetch
662
- * @returns Promise resolving to the role
442
+ * @internal
663
443
  */
664
- async function fetchRole(clientOrGuild, roleId) {
665
- if (!roleId) throw new RoleDoesNotExist("Role ID is null or undefined", roleId);
666
- const role = clientOrGuild instanceof Guild ? await fetchRoleInGuild(clientOrGuild, roleId) : await scanGuildsForRole(clientOrGuild, roleId);
667
- if (!role) throw new RoleDoesNotExist("Role not found", roleId);
668
- return role;
669
- }
444
+ const CONFIRM_DEF = new CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
670
445
 
671
446
  //#endregion
672
- //#region src/bot/utilities/users/fetchGuildMember.ts
673
- /**
674
- * Fetches a guild member by user ID. Refuses when the user is not in the guild.
675
- *
676
- * @param guild - The guild to fetch the member from
677
- * @param userId - The Discord user ID
678
- * @returns Promise resolving to the guild member
679
- */
680
- async function fetchGuildMember(guild, userId) {
681
- let user = guild.members.cache.get(userId);
682
- user ??= await guild.members.fetch(userId).catch((err) => {
683
- if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownMember) throw new UserNotInGuild(`User with ID ${userId} not found in guild`);
684
- throw err;
685
- });
686
- return user;
447
+ //#region src/bot/confirm/getConfirmation.ts
448
+ const DEFAULT_TIMEOUT_MS = 3e4;
449
+ const CONFIRM_IDS = {
450
+ confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
451
+ cancel: CONFIRM_DEF.encode({ choice: "cancel" })
452
+ };
453
+ var ConfirmButtons = class extends RowComponent {
454
+ constructor(options) {
455
+ super("button");
456
+ this.instance.addComponents(new ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? ButtonStyle.Danger), new ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(ButtonStyle.Secondary));
457
+ }
458
+ };
459
+ var ConfirmPromptCard = class extends BuilderComponent {
460
+ constructor(text, options) {
461
+ super("container");
462
+ this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
463
+ }
464
+ };
465
+ function defaultPrompt(text, options) {
466
+ return { components: [new ConfirmPromptCard(text, options).component] };
687
467
  }
688
-
689
- //#endregion
690
- //#region src/bot/utilities/users/fetchManyGuildMembers.ts
691
- /**
692
- * Fetches multiple guild members by user IDs.
693
- *
694
- * @param guild - The guild to fetch members from
695
- * @param userIds - Array of Discord user IDs
696
- * @returns Promise resolving to array of successfully fetched members
697
- */
698
- async function fetchManyGuildMembers(guild, userIds) {
699
- return (await Promise.allSettled(userIds.map((userId) => fetchGuildMember(guild, userId)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
468
+ async function collectChoice(message, userId, timeoutMs) {
469
+ try {
470
+ return await message.awaitMessageComponent({
471
+ componentType: ComponentType.Button,
472
+ time: timeoutMs,
473
+ filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
474
+ });
475
+ } catch {
476
+ return null;
477
+ }
700
478
  }
701
-
702
- //#endregion
703
- //#region src/bot/utilities/users/fetchUser.ts
704
- /**
705
- * Fetches a Discord user by ID. Refuses when the user doesn't exist, with `options.throwAs` or the default.
706
- *
707
- * @param client - The Discord client instance
708
- * @param userId - The Discord user ID
709
- * @param options - Optional overrides, including the {@link FetchUserOptions.throwAs} notice
710
- * @returns Promise resolving to the user
711
- */
712
- async function fetchUser(client, userId, options) {
713
- const Throw = options?.throwAs ?? UserNotFound;
714
- let user = client.users.cache.get(userId);
715
- user ??= await client.users.fetch(userId).catch((err) => {
716
- if (err instanceof DiscordAPIError && err.code === RESTJSONErrorCodes.UnknownUser) throw new Throw(userId);
717
- throw err;
718
- });
719
- return user;
479
+ async function settle(sender, message, outcome) {
480
+ if (outcome === void 0) {
481
+ await sender.delete(message).catch(() => void 0);
482
+ return;
483
+ }
484
+ await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
485
+ }
486
+ async function getConfirmation(handler, prompt, options) {
487
+ const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
488
+ const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
489
+ const interaction = handler.getEvent();
490
+ const sender = handler.getSender();
491
+ const message = await sender.send(response, { ephemeral });
492
+ const winner = await collectChoice(message, interaction.user.id, timeoutMs);
493
+ if (!winner) {
494
+ await settle(sender, message, options?.onTimeout);
495
+ return false;
496
+ }
497
+ await winner.deferUpdate().catch(() => void 0);
498
+ const confirmed = winner.customId === CONFIRM_IDS.confirm;
499
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
500
+ return confirmed;
720
501
  }
721
502
 
722
503
  //#endregion
723
- //#region src/bot/utilities/users/fetchManyUsers.ts
504
+ //#region src/handlers/BaseHandler.ts
724
505
  /**
725
- * Fetches multiple Discord users by IDs.
726
- *
727
- * @param client - The Discord client instance
728
- * @param userIds - Array of Discord user IDs
729
- * @param options - Optional overrides threaded to each {@link fetchUser} call
730
- * @returns Promise resolving to array of successfully fetched users
506
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
507
+ * gate hook.
731
508
  */
732
- async function fetchManyUsers(client, userIds, options) {
733
- return (await Promise.allSettled(userIds.map((userId) => fetchUser(client, userId, options)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
734
- }
509
+ var BaseHandler$1 = class extends BaseHandler {
510
+ /** @internal */
511
+ getEvent() {
512
+ return this.event;
513
+ }
514
+ };
735
515
 
736
516
  //#endregion
737
- //#region src/bot/utilities/users/updateMemberRoles.ts
517
+ //#region src/handlers/interaction/InteractionMiddleware.ts
738
518
  /**
739
- * Updates a guild member's roles by adding and removing specified roles.
519
+ * Base class for interaction middleware
740
520
  *
741
- * @param rolesToAdd - Array of role IDs to add to the member
742
- * @param rolesToRemove - Array of role IDs to remove from the member
743
- * @param member - The guild member to update
744
- * @returns Promise that resolves when roles are updated
521
+ * Middleware runs before interaction handlers and can modify behavior or block execution.
522
+ * Unlike handlers, middleware should not send responses directly.
523
+ *
524
+ * @typeParam Repliable - The interaction type this middleware processes
745
525
  */
746
- async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
747
- const current = new Set(member.roles.cache.map((r) => r.id));
748
- const toAdd = new Set(rolesToAdd);
749
- const toRemove = new Set(rolesToRemove);
750
- const updated = current.union(toAdd).difference(toRemove);
751
- await member.roles.set([...updated]);
752
- }
526
+ var InteractionMiddleware = class extends BaseHandler$1 {
527
+ constructor(event, core) {
528
+ super(event, core, void 0, "interactions");
529
+ }
530
+ };
753
531
 
754
532
  //#endregion
755
533
  //#region src/bot/ReplySender.ts
534
+ function isReplyFile(file) {
535
+ return typeof file === "object" && "data" in file && ArrayBuffer.isView(file.data);
536
+ }
537
+ function toDjsFile(file) {
538
+ if (!isReplyFile(file)) return file;
539
+ return {
540
+ attachment: Buffer.from(file.data),
541
+ name: file.name,
542
+ ...file.description !== void 0 && { description: file.description },
543
+ ...file.title !== void 0 && { title: file.title }
544
+ };
545
+ }
756
546
  function seedState(interaction) {
757
547
  if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
758
548
  if (interaction.replied) return "replied";
@@ -835,7 +625,7 @@ var ReplySender = class extends BaseReplySender {
835
625
  components: reply.components,
836
626
  flags,
837
627
  ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
838
- ...reply.files && { files: [...reply.files] }
628
+ ...reply.files && { files: reply.files.map(toDjsFile) }
839
629
  };
840
630
  }
841
631
  editOptions(response) {
@@ -844,7 +634,7 @@ var ReplySender = class extends BaseReplySender {
844
634
  components: reply.components,
845
635
  flags: MessageFlags.IsComponentsV2,
846
636
  ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
847
- ...reply.files && { files: [...reply.files] }
637
+ ...reply.files && { files: reply.files.map(toDjsFile) }
848
638
  };
849
639
  }
850
640
  updateOptions(response) {
@@ -853,7 +643,7 @@ var ReplySender = class extends BaseReplySender {
853
643
  components: reply.components,
854
644
  flags: MessageFlags.IsComponentsV2,
855
645
  ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
856
- ...reply.files && { files: [...reply.files] }
646
+ ...reply.files && { files: reply.files.map(toDjsFile) }
857
647
  };
858
648
  }
859
649
  createdMessage(result) {
@@ -861,266 +651,6 @@ var ReplySender = class extends BaseReplySender {
861
651
  }
862
652
  };
863
653
 
864
- //#endregion
865
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
866
- /**
867
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
868
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
869
- * interaction and a generated registry key always match.
870
- *
871
- * @internal
872
- */
873
- function slashRouteOf(interaction) {
874
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
875
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
876
- return buildSlashRoute(interaction.commandName, subcommand, group);
877
- }
878
-
879
- //#endregion
880
- //#region src/miscellaneous/extractErrorResponse.ts
881
- const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
882
- /**
883
- * Processes an error into a user-facing reply and routes its report.
884
- *
885
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
886
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
887
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
888
- * threaded into both the reply and the bus payload.
889
- */
890
- function extractErrorResponse(error, core, origin) {
891
- const uuid = crypto.randomUUID();
892
- const developerUsername = core.config.notifications?.developerUsername;
893
- const ctx = developerUsername === void 0 ? { uuid } : {
894
- uuid,
895
- developerUsername
896
- };
897
- if (error instanceof Notice) {
898
- if (error.report) reportFault(error, core, origin, uuid);
899
- return {
900
- uuid,
901
- response: error.render(ctx)
902
- };
903
- }
904
- reportRawFault(error, core, origin, uuid);
905
- const override = core.config.errors?.defaultError;
906
- return {
907
- uuid,
908
- response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
909
- };
910
- }
911
- function withThrottle(core, origin, error, publish) {
912
- const throttle = FaultThrottle.for(core);
913
- const key = faultKey(origin, error);
914
- if (!throttle.shouldReport(key)) {
915
- logger$2.debug(`throttled duplicate fault ${key}`);
916
- return;
917
- }
918
- publish();
919
- throttle.markReported(key);
920
- }
921
- function reportFault(denial, core, origin, uuid) {
922
- logger$2.error(`${denial.name}: ${uuid}`, denial);
923
- withThrottle(core, origin, denial, () => {
924
- if (origin.interaction) core.bus[PublishDefault]("handledException", {
925
- denial,
926
- uuid,
927
- source: buildInteractionSource(origin.interaction)
928
- });
929
- else if (origin.event) core.bus[PublishDefault]("handledException", {
930
- denial,
931
- uuid,
932
- source: buildEventSource(origin.event, origin)
933
- });
934
- else core.bus[PublishDefault]("unknownException", {
935
- uuid,
936
- error: denial,
937
- ...scalarActors(origin),
938
- metadata: metadataFor(origin)
939
- });
940
- });
941
- }
942
- function causeLine(error) {
943
- const { cause } = error;
944
- if (!Error.isError(cause)) return "";
945
- const [first] = cause.message.split("\n");
946
- return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
947
- }
948
- function reportRawFault(error, core, origin, uuid) {
949
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
950
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
951
- withThrottle(core, origin, error, () => {
952
- core.bus[PublishDefault]("unknownException", {
953
- uuid,
954
- error,
955
- ...scalarActors(origin),
956
- metadata: metadataFor(origin)
957
- });
958
- });
959
- }
960
- function scalarActors(origin) {
961
- return {
962
- guild: origin.guild ? {
963
- id: origin.guild.id,
964
- name: origin.guild.name
965
- } : void 0,
966
- user: origin.user ? {
967
- id: origin.user.id,
968
- username: origin.user.username
969
- } : void 0
970
- };
971
- }
972
- function metadataFor(origin) {
973
- if (origin.event) return {
974
- eventName: origin.event.name,
975
- handler: origin.event.handler,
976
- args: origin.event.args
977
- };
978
- return origin.metadata;
979
- }
980
- function buildEventSource(event, origin) {
981
- return {
982
- kind: "event",
983
- eventName: event.name,
984
- handler: event.handler,
985
- userId: origin.user?.id ?? null,
986
- guildId: origin.guild?.id ?? null,
987
- channelId: event.channelId,
988
- raw: event.args
989
- };
990
- }
991
- function faultKey(origin, error) {
992
- const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
993
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
994
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
995
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
996
- return `autocomplete:${name}`;
997
- }
998
- /** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
999
- function interactionRoute(interaction) {
1000
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1001
- if (interaction.isContextMenuCommand()) return interaction.commandName;
1002
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
1003
- return "interaction";
1004
- }
1005
- function buildInteractionSource(interaction) {
1006
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1007
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1008
- return {
1009
- kind: "interaction",
1010
- interactionKind: interactionKind(interaction),
1011
- command,
1012
- customId,
1013
- userId: interaction.user.id,
1014
- guildId: interaction.guildId,
1015
- channelId: interaction.channelId,
1016
- interactionId: interaction.id,
1017
- raw: interaction
1018
- };
1019
- }
1020
- function interactionKind(interaction) {
1021
- if (interaction.isChatInputCommand()) return "slash";
1022
- if (interaction.isContextMenuCommand()) return "context-menu";
1023
- if (interaction.isButton()) return "button";
1024
- if (interaction.isAnySelectMenu()) return "select";
1025
- return "modal";
1026
- }
1027
-
1028
- //#endregion
1029
- //#region src/bot/confirm/reserved.ts
1030
- const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
1031
- /**
1032
- * The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
1033
- * in-process collector reads them. Do not mint a real component from it.
1034
- *
1035
- * @internal
1036
- */
1037
- const CONFIRM_DEF = new CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
1038
-
1039
- //#endregion
1040
- //#region src/bot/confirm/getConfirmation.ts
1041
- const DEFAULT_TIMEOUT_MS = 3e4;
1042
- const CONFIRM_IDS = {
1043
- confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
1044
- cancel: CONFIRM_DEF.encode({ choice: "cancel" })
1045
- };
1046
- var ConfirmButtons = class extends RowComponent {
1047
- constructor(options) {
1048
- super("button");
1049
- this.instance.addComponents(new ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? ButtonStyle.Danger), new ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(ButtonStyle.Secondary));
1050
- }
1051
- };
1052
- var ConfirmPromptCard = class extends BuilderComponent {
1053
- constructor(text, options) {
1054
- super("container");
1055
- this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
1056
- }
1057
- };
1058
- function defaultPrompt(text, options) {
1059
- return { components: [new ConfirmPromptCard(text, options).component] };
1060
- }
1061
- async function collectChoice(message, userId, timeoutMs) {
1062
- try {
1063
- return await message.awaitMessageComponent({
1064
- componentType: ComponentType.Button,
1065
- time: timeoutMs,
1066
- filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
1067
- });
1068
- } catch {
1069
- return null;
1070
- }
1071
- }
1072
- async function settle(sender, message, outcome) {
1073
- if (outcome === void 0) {
1074
- await sender.delete(message).catch(() => void 0);
1075
- return;
1076
- }
1077
- await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
1078
- }
1079
- async function getConfirmation(interaction, prompt, options) {
1080
- const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1081
- const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1082
- const sender = new ReplySender(interaction, interactionRoute(interaction));
1083
- const message = await sender.send(response, { ephemeral });
1084
- const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1085
- if (!winner) {
1086
- await settle(sender, message, options?.onTimeout);
1087
- return false;
1088
- }
1089
- await winner.deferUpdate().catch(() => void 0);
1090
- const confirmed = winner.customId === CONFIRM_IDS.confirm;
1091
- await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1092
- return confirmed;
1093
- }
1094
-
1095
- //#endregion
1096
- //#region src/handlers/BaseHandler.ts
1097
- /**
1098
- * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1099
- * gate hook.
1100
- */
1101
- var BaseHandler$1 = class extends BaseHandler {
1102
- /** @internal */
1103
- getEvent() {
1104
- return this.event;
1105
- }
1106
- };
1107
-
1108
- //#endregion
1109
- //#region src/handlers/interaction/InteractionMiddleware.ts
1110
- /**
1111
- * Base class for interaction middleware
1112
- *
1113
- * Middleware runs before interaction handlers and can modify behavior or block execution.
1114
- * Unlike handlers, middleware should not send responses directly.
1115
- *
1116
- * @typeParam Repliable - The interaction type this middleware processes
1117
- */
1118
- var InteractionMiddleware = class extends BaseHandler$1 {
1119
- constructor(event, core) {
1120
- super(event, core, void 0, "interactions");
1121
- }
1122
- };
1123
-
1124
654
  //#endregion
1125
655
  //#region src/handlers/RepliableHandler.ts
1126
656
  /**
@@ -1166,6 +696,21 @@ var InteractionHandler = class extends RepliableHandler {
1166
696
  }
1167
697
  };
1168
698
 
699
+ //#endregion
700
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
701
+ /**
702
+ * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
703
+ * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
704
+ * interaction and a generated registry key always match.
705
+ *
706
+ * @internal
707
+ */
708
+ function slashRouteOf(interaction) {
709
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
710
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
711
+ return buildSlashRoute(interaction.commandName, subcommand, group);
712
+ }
713
+
1169
714
  //#endregion
1170
715
  //#region src/handlers/interaction/SlashHandler.ts
1171
716
  /**
@@ -1485,7 +1030,22 @@ var ButtonHandler = class extends ComponentHandler {};
1485
1030
  * }
1486
1031
  * ```
1487
1032
  */
1488
- var ModalHandler = class extends ComponentHandler {};
1033
+ var ModalHandler = class extends ComponentHandler {
1034
+ /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1035
+ update(response) {
1036
+ this.ensureSourceMessage("update");
1037
+ return super.update(response);
1038
+ }
1039
+ /** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
1040
+ deferUpdate() {
1041
+ this.ensureSourceMessage("deferUpdate");
1042
+ return super.deferUpdate();
1043
+ }
1044
+ ensureSourceMessage(method) {
1045
+ if (this.event.isFromMessage()) return;
1046
+ throw new SeedcordError(SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1047
+ }
1048
+ };
1489
1049
 
1490
1050
  //#endregion
1491
1051
  //#region src/handlers/interaction/components/SelectMenuHandler.ts
@@ -1599,138 +1159,14 @@ var EventMiddleware = class extends BaseHandler$1 {
1599
1159
  }
1600
1160
  /**
1601
1161
  * The event that fired this middleware. Read it on a catchall or multi-event middleware, where `this.event`
1602
- * is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
1603
- * `this.event` directly and does not need this.
1604
- */
1605
- get eventName() {
1606
- if (this.firedEvent === void 0) throw new SeedcordError(SeedcordErrorCode.EventMiddlewareNameUnavailable);
1607
- return this.firedEvent;
1608
- }
1609
- };
1610
-
1611
- //#endregion
1612
- //#region src/pagination/controls.ts
1613
- const DEFAULT_LABEL = {
1614
- first: "First",
1615
- prev: "Prev",
1616
- indicator: "",
1617
- next: "Next",
1618
- last: "Last"
1619
- };
1620
- const CONTROL_SLOT = {
1621
- first: 0,
1622
- prev: 1,
1623
- indicator: 2,
1624
- next: 3,
1625
- last: 4
1626
- };
1627
- const MAX_BUTTONS_PER_ROW = 5;
1628
- var Controls = class {
1629
- cursor;
1630
- view;
1631
- constructor(cursor, view) {
1632
- this.cursor = cursor;
1633
- this.view = view;
1634
- }
1635
- button(key, cosmetics) {
1636
- const { target, disabled } = this.navState(key);
1637
- const defaultLabel = key === "indicator" ? this.indicatorText() : DEFAULT_LABEL[key];
1638
- const label = cosmetics?.label ?? (key === "indicator" || !cosmetics?.emoji ? defaultLabel : void 0);
1639
- const button = new ButtonBuilder().setCustomId(this.cursor.encode({
1640
- page: Math.min(Math.max(0, target), PAGE_MAX),
1641
- slot: CONTROL_SLOT[key]
1642
- })).setStyle(cosmetics?.style ?? ButtonStyle.Secondary).setDisabled(disabled);
1643
- if (label !== void 0) button.setLabel(label);
1644
- if (cosmetics?.emoji) button.setEmoji(cosmetics.emoji);
1645
- return button;
1646
- }
1647
- navState(key) {
1648
- const { page, totalPages, hasPrev, hasNext } = this.view;
1649
- const knownTotal = totalPages !== void 0;
1650
- return {
1651
- target: {
1652
- first: 0,
1653
- prev: page - 1,
1654
- indicator: page,
1655
- next: page + 1,
1656
- last: knownTotal ? totalPages - 1 : page
1657
- }[key],
1658
- disabled: {
1659
- first: !hasPrev,
1660
- prev: !hasPrev,
1661
- indicator: true,
1662
- next: !hasNext || page + 1 > PAGE_MAX,
1663
- last: !hasNext || !knownTotal || totalPages - 1 > PAGE_MAX
1664
- }[key]
1665
- };
1666
- }
1667
- row(...keys) {
1668
- if (keys.length === 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationEmptyControls);
1669
- if (keys.length > MAX_BUTTONS_PER_ROW) throw new SeedcordRangeError(SeedcordErrorCode.PaginationTooManyControls, [keys.length]);
1670
- const seen = /* @__PURE__ */ new Set();
1671
- for (const key of keys) {
1672
- if (seen.has(key)) throw new SeedcordTypeError(SeedcordErrorCode.PaginationDuplicateControls, [key]);
1673
- seen.add(key);
1674
- }
1675
- return new ActionRowBuilder().addComponents(keys.map((key) => this.button(key)));
1676
- }
1677
- indicatorText() {
1678
- const { page, totalPages } = this.view;
1679
- return totalPages === void 0 ? `Page ${page + 1}` : `Page ${page + 1} of ${totalPages}`;
1680
- }
1681
- };
1682
-
1683
- //#endregion
1684
- //#region src/pagination/render.ts
1685
- var PageContainer = class extends BuilderComponent {
1686
- constructor(view, renderItem, controlRow) {
1687
- super("container");
1688
- const base = view.page * view.perPage;
1689
- let lines = [];
1690
- const flush = () => {
1691
- if (lines.length === 0) return;
1692
- this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(lines.join("\n")));
1693
- lines = [];
1694
- };
1695
- view.items.forEach((item, offset) => {
1696
- const rendered = renderItem(item, base + offset);
1697
- if (typeof rendered === "string") {
1698
- lines.push(rendered);
1699
- return;
1700
- }
1701
- flush();
1702
- this.instance.addSectionComponents(rendered.component);
1703
- });
1704
- flush();
1705
- this.instance.addActionRowComponents(controlRow);
1162
+ * is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
1163
+ * `this.event` directly and does not need this.
1164
+ */
1165
+ get eventName() {
1166
+ if (this.firedEvent === void 0) throw new SeedcordError(SeedcordErrorCode.EventMiddlewareNameUnavailable);
1167
+ return this.firedEvent;
1706
1168
  }
1707
1169
  };
1708
- function toReplyResponse(renderable) {
1709
- if (typeof renderable === "string") return { components: [new TextDisplayBuilder().setContent(renderable)] };
1710
- if (Array.isArray(renderable)) return { components: renderable };
1711
- return renderable;
1712
- }
1713
- const ARRAY_KEYS = [
1714
- "first",
1715
- "prev",
1716
- "indicator",
1717
- "next",
1718
- "last"
1719
- ];
1720
- const CURSOR_KEYS = [
1721
- "prev",
1722
- "indicator",
1723
- "next"
1724
- ];
1725
- /**
1726
- * Build a page's V2 reply. A `render` override builds the whole tree, otherwise the default container lists
1727
- * the items and appends the controls (all five for a known total, prev/indicator/next for a cursor source).
1728
- */
1729
- function renderPage(view, cursor, config) {
1730
- const controls = new Controls(cursor, view);
1731
- if (config.render) return toReplyResponse(config.render(view, controls));
1732
- return { components: [new PageContainer(view, config.renderItem ?? String, controls.row(...view.totalPages === void 0 ? CURSOR_KEYS : ARRAY_KEYS)).component] };
1733
- }
1734
1170
 
1735
1171
  //#endregion
1736
1172
  //#region src/pagination/Paginator.ts
@@ -1739,7 +1175,7 @@ function contextOf(interaction, core) {
1739
1175
  interaction,
1740
1176
  user: interaction.user,
1741
1177
  guild: interaction.guild,
1742
- ...core && { core }
1178
+ core
1743
1179
  };
1744
1180
  }
1745
1181
  /**
@@ -1761,15 +1197,11 @@ function contextOf(interaction, core) {
1761
1197
  * export class BansNav extends Bans.Handler {}
1762
1198
  * ```
1763
1199
  */
1764
- var Paginator = class {
1765
- /** The page cursor, pass it to your `@ButtonRoute`. */
1766
- cursor;
1200
+ var Paginator = class extends PaginatorBase {
1767
1201
  /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
1768
1202
  Handler;
1769
- config;
1770
1203
  constructor(config) {
1771
- this.cursor = pageCursor(config.prefix);
1772
- this.config = config;
1204
+ super(config);
1773
1205
  const loadPage = (ctx, n) => this.page(ctx, n);
1774
1206
  this.Handler = class Nav extends ButtonHandler {
1775
1207
  async execute() {
@@ -1780,7 +1212,8 @@ var Paginator = class {
1780
1212
  };
1781
1213
  }
1782
1214
  /**
1783
- * Render page 0 and send it through the handler's sender, picking reply or followUp from its ack state.
1215
+ * Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
1216
+ * ack state.
1784
1217
  *
1785
1218
  * @param handler - The handler starting the paginator, normally `this`.
1786
1219
  */
@@ -1789,34 +1222,18 @@ var Paginator = class {
1789
1222
  const response = await this.page(contextOf(interaction, handler.core), 0);
1790
1223
  return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1791
1224
  }
1792
- /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1793
- async page(ctx, n) {
1794
- return renderPage(await this.config.source.page(ctx, n), this.cursor, this.config);
1795
- }
1796
1225
  };
1797
1226
 
1798
1227
  //#endregion
1799
1228
  //#region src/pagination/sources.ts
1800
- const DEFAULT_PER_PAGE = 10;
1801
1229
  /**
1802
1230
  * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
1803
- * page via {@link paginate}, so the real total is known (a real last button, "Page X of Y"). Cache inside
1804
- * your loader if the load is expensive.
1231
+ * page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
1232
+ * load is expensive.
1805
1233
  *
1806
1234
  * @typeParam Item - The item type, inferred from the loader's return.
1807
1235
  */
1808
- var ArraySource = class {
1809
- load;
1810
- perPage;
1811
- constructor(load, opts) {
1812
- this.load = load;
1813
- this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
1814
- if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
1815
- }
1816
- async page(ctx, n) {
1817
- return paginate(await this.load(ctx), n, this.perPage);
1818
- }
1819
- };
1236
+ var ArraySource = class extends ArraySource$1 {};
1820
1237
  /**
1821
1238
  * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1822
1239
  * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
@@ -1825,26 +1242,7 @@ var ArraySource = class {
1825
1242
  *
1826
1243
  * @typeParam Item - The item type, inferred from the fetcher's slice.
1827
1244
  */
1828
- var CursorSource = class {
1829
- fetch;
1830
- perPage;
1831
- constructor(fetch, opts) {
1832
- this.fetch = fetch;
1833
- this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
1834
- if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
1835
- }
1836
- async page(ctx, n) {
1837
- const page = Math.max(0, Math.trunc(n));
1838
- const { items, hasNext } = await this.fetch(ctx, page, this.perPage);
1839
- return {
1840
- items: [...items],
1841
- page,
1842
- perPage: this.perPage,
1843
- hasPrev: page > 0,
1844
- hasNext
1845
- };
1846
- }
1847
- };
1245
+ var CursorSource = class extends CursorSource$1 {};
1848
1246
 
1849
1247
  //#endregion
1850
1248
  //#region src/subscribers/index.ts
@@ -1868,236 +1266,12 @@ var WebhookLog = class extends WebhookLog$1 {};
1868
1266
  * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
1869
1267
  *
1870
1268
  * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1871
- * `this.core` carries the shared members only.
1269
+ * `this.core` is typed to the shared members only.
1872
1270
  *
1873
- * @typeParam Opts - The transport, runtime, and capabilities the plugin declares.
1271
+ * @typeParam Opts - The transport and runtime the plugin declares.
1874
1272
  */
1875
1273
  var Plugin = class extends Plugin$1 {};
1876
1274
 
1877
- //#endregion
1878
- //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
1879
- /**
1880
- * Collects the names of every registered context-menu command across a set of command builders, split into a
1881
- * user set and a message set. The same builder may appear more than once (a guild command is pushed once per
1882
- * guild), and slash commands carry no context-menu name, so both collapse here. The split mirrors the
1883
- * controller's two name-keyed maps, so the boot guard checks each name against its own kind.
1884
- *
1885
- * @internal
1886
- */
1887
- function contextMenuLeaves(commands) {
1888
- const user = /* @__PURE__ */ new Set();
1889
- const message = /* @__PURE__ */ new Set();
1890
- for (const command of commands) {
1891
- if (!(command instanceof ContextMenuCommandBuilder)) continue;
1892
- if (command.type === ApplicationCommandType.User) user.add(command.name);
1893
- else message.add(command.name);
1894
- }
1895
- return {
1896
- user,
1897
- message
1898
- };
1899
- }
1900
-
1901
- //#endregion
1902
- //#region src/bot/utilities/miscellaneous/slashRouteLeaves.ts
1903
- /**
1904
- * Collects the route keys of every executable slash leaf across a set of command builders, deduplicated. The
1905
- * same builder may appear more than once (a guild command is pushed once per guild), and context-menu
1906
- * commands carry no slash route, so both collapse here. Route keys come from the same {@link routeLeavesOf}
1907
- * that `seedcord codegen` reads, so the boot-time check compares against the generated registry's keys.
1908
- *
1909
- * @internal
1910
- */
1911
- function slashRouteLeaves(commands) {
1912
- const leaves = /* @__PURE__ */ new Set();
1913
- for (const command of commands) {
1914
- if (!(command instanceof SlashCommandBuilder)) continue;
1915
- for (const leaf of routeLeavesOf(command.toJSON())) leaves.add(leaf.route);
1916
- }
1917
- return leaves;
1918
- }
1919
-
1920
- //#endregion
1921
- //#region src/bot/controllers/CommandRegistry.ts
1922
- /**
1923
- * Manages Discord application command registration and deployment.
1924
- *
1925
- * Scans command directories, builds command structures, and registers both global and guild-scoped commands
1926
- * to Discord's API. Accessed via `core.bot.commands`. Do not construct it directly.
1927
- */
1928
- var CommandRegistry = class {
1929
- core;
1930
- onDeployed;
1931
- logger = new Logger("Commands", { channel: "commands" });
1932
- isInitialised = false;
1933
- globalCommands = [];
1934
- guildCommands = new Collection();
1935
- ctorToCommand = /* @__PURE__ */ new Map();
1936
- loading = false;
1937
- loadedCommands = [];
1938
- hmrHandler;
1939
- pendingEvents = /* @__PURE__ */ new Map();
1940
- constructor(core, onDeployed) {
1941
- this.core = core;
1942
- this.onDeployed = onDeployed;
1943
- const commandsDir = this.core.config.bot.commands.path;
1944
- if (!commandsDir) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
1945
- if (!Envapter.isDevelopment && !Envapter.isTest) return;
1946
- this.hmrHandler = new HmrModuleHandler({
1947
- handlersDir: commandsDir,
1948
- isHandler: this.isCommandClass.bind(this),
1949
- registerHandler: this.registerCommand.bind(this),
1950
- unregisterHandler: this.unregisterCommand.bind(this),
1951
- getArtifacts: this.getArtifacts.bind(this),
1952
- logger: this.logger
1953
- });
1954
- }
1955
- getArtifacts(ctor) {
1956
- return this.ctorToCommand.get(ctor);
1957
- }
1958
- /** @internal */
1959
- async init() {
1960
- if (this.isInitialised) return;
1961
- this.isInitialised = true;
1962
- const commandsDir = this.core.config.bot.commands.path;
1963
- if (!commandsDir) return;
1964
- this.loading = true;
1965
- this.loadedCommands.length = 0;
1966
- try {
1967
- await this.loadCommands(commandsDir);
1968
- } finally {
1969
- this.loading = false;
1970
- }
1971
- this.reportLoad();
1972
- getDevChannel()?.on("seedcord:refresh-commands", (data) => {
1973
- this.refresh(data.shouldRefresh);
1974
- });
1975
- }
1976
- reportLoad() {
1977
- const { utils } = this.logger;
1978
- utils.summary("Loaded commands", {
1979
- global: this.globalCommands.length,
1980
- "guild groups": this.guildCommands.size
1981
- });
1982
- let slash = 0;
1983
- let menu = 0;
1984
- for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
1985
- else menu++;
1986
- utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
1987
- "slash commands": slash,
1988
- "context menus": menu
1989
- })]);
1990
- }
1991
- /** @internal */
1992
- async refresh(shouldRefresh = true) {
1993
- if (!shouldRefresh) {
1994
- this.logger.info(chalk.italic("Command refresh cancelled."));
1995
- this.pendingEvents.clear();
1996
- return;
1997
- }
1998
- this.logger.info(chalk.italic("Refreshing commands..."));
1999
- for (const event of this.pendingEvents.values()) await this.hmrHandler?.handle(event);
2000
- this.pendingEvents.clear();
2001
- await this.setCommands();
2002
- }
2003
- /** @internal */
2004
- async onHmr(event) {
2005
- const commandsDir = this.core.config.bot.commands.path;
2006
- if (commandsDir && event.file.startsWith(resolve(process.cwd(), commandsDir))) {
2007
- this.pendingEvents.delete(event.file);
2008
- this.pendingEvents.set(event.file, event);
2009
- getDevChannel()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => formatFilePath(f)) });
2010
- } else await this.hmrHandler?.handle(event);
2011
- }
2012
- async loadCommands(dir) {
2013
- await traverseDirectory(dir, (fullPath, rel, mod) => {
2014
- for (const exported of Object.values(mod)) if (this.isCommandClass(exported)) {
2015
- this.registerCommand(exported, rel);
2016
- this.hmrHandler?.trackHandler(fullPath, exported);
2017
- }
2018
- });
2019
- }
2020
- isCommandClass(obj) {
2021
- if (typeof obj !== "function") return false;
2022
- return obj.prototype instanceof BuilderComponent && Reflect.hasMetadata(CommandMetadataKey, obj);
2023
- }
2024
- registerCommand(ctor, rel) {
2025
- const meta = Reflect.getMetadata(CommandMetadataKey, ctor);
2026
- if (!meta) return;
2027
- const comp = new ctor().component;
2028
- const kind = comp instanceof SlashCommandBuilder ? "slash command" : "context menu";
2029
- if (meta.scope === "global") this.globalCommands.push(comp);
2030
- else for (const g of meta.guilds) {
2031
- const arr = this.guildCommands.get(g) ?? [];
2032
- arr.push(comp);
2033
- this.guildCommands.set(g, arr);
2034
- }
2035
- this.ctorToCommand.set(ctor, {
2036
- name: comp.name,
2037
- scope: meta.scope,
2038
- ...meta.scope === "guild" && { guilds: meta.guilds }
2039
- });
2040
- if (this.loading) {
2041
- this.loadedCommands.push({
2042
- name: comp.name,
2043
- from: formatFilePath(rel),
2044
- kind
2045
- });
2046
- return;
2047
- }
2048
- this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
2049
- }
2050
- unregisterCommand(ctor, artifacts) {
2051
- const info = artifacts ?? this.ctorToCommand.get(ctor);
2052
- if (!info) return;
2053
- if (info.scope === "global") {
2054
- const idx = this.globalCommands.findIndex((c) => c.name === info.name);
2055
- if (idx !== -1) this.globalCommands.splice(idx, 1);
2056
- } else for (const g of info.guilds ?? []) {
2057
- const arr = this.guildCommands.get(g);
2058
- if (arr) {
2059
- const idx = arr.findIndex((c) => c.name === info.name);
2060
- if (idx !== -1) arr.splice(idx, 1);
2061
- }
2062
- }
2063
- this.ctorToCommand.delete(ctor);
2064
- }
2065
- /** @internal */
2066
- async setCommands() {
2067
- const result = {
2068
- global: new Collection(),
2069
- guilds: new Collection()
2070
- };
2071
- if (this.globalCommands.length > 0) {
2072
- const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
2073
- if (deployed) result.global = deployed;
2074
- const tag = this.globalCommands.length === 1 ? "command" : "commands";
2075
- this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
2076
- }
2077
- for (const [guildId, commands] of this.guildCommands.entries()) {
2078
- const guild = this.core.bot.client.guilds.cache.get(guildId);
2079
- if (!guild) {
2080
- this.logger.warn(`Guild with ID ${guildId} not found, skipping command registration.`);
2081
- continue;
2082
- }
2083
- const deployed = await guild.commands.set(commands);
2084
- result.guilds.set(guildId, deployed);
2085
- const tag = commands.length === 1 ? "command" : "commands";
2086
- this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
2087
- }
2088
- this.onDeployed?.(result);
2089
- return result;
2090
- }
2091
- /** The deduplicated slash route keys across every global and guild command. @internal */
2092
- routeLeaves() {
2093
- return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2094
- }
2095
- /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2096
- contextMenuLeaves() {
2097
- return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2098
- }
2099
- };
2100
-
2101
1275
  //#endregion
2102
1276
  //#region src/miscellaneous/deriveEventActor.ts
2103
1277
  function deriveEventActor(args) {
@@ -2175,6 +1349,154 @@ function eventGateContext(eventName, args, core) {
2175
1349
  };
2176
1350
  }
2177
1351
 
1352
+ //#endregion
1353
+ //#region src/miscellaneous/extractErrorResponse.ts
1354
+ const logger$2 = new Logger("ErrorsHandling", { channel: "errors" });
1355
+ /**
1356
+ * Processes an error into a user-facing reply and routes its report.
1357
+ *
1358
+ * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
1359
+ * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
1360
+ * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
1361
+ * threaded into both the reply and the bus payload.
1362
+ */
1363
+ function extractErrorResponse(error, core, origin) {
1364
+ const uuid = crypto.randomUUID();
1365
+ const developerUsername = core.config.notifications?.developerUsername;
1366
+ const ctx = developerUsername === void 0 ? { uuid } : {
1367
+ uuid,
1368
+ developerUsername
1369
+ };
1370
+ if (error instanceof Notice) {
1371
+ if (error.report) reportFault(error, core, origin, uuid);
1372
+ return {
1373
+ uuid,
1374
+ response: error.render(ctx)
1375
+ };
1376
+ }
1377
+ reportRawFault(error, core, origin, uuid);
1378
+ const override = core.config.errors?.defaultError;
1379
+ return {
1380
+ uuid,
1381
+ response: override ? new override(uuid).render(ctx) : new Fault().render(ctx)
1382
+ };
1383
+ }
1384
+ function withThrottle(core, origin, error, publish) {
1385
+ const throttle = FaultThrottle.for(core);
1386
+ const key = faultKey(origin, error);
1387
+ if (!throttle.shouldReport(key)) {
1388
+ logger$2.debug(`throttled duplicate fault ${key}`);
1389
+ return;
1390
+ }
1391
+ publish();
1392
+ throttle.markReported(key);
1393
+ }
1394
+ function reportFault(denial, core, origin, uuid) {
1395
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
1396
+ withThrottle(core, origin, denial, () => {
1397
+ if (origin.interaction) core.bus[PublishDefault]("handledException", {
1398
+ denial,
1399
+ uuid,
1400
+ source: buildInteractionSource(origin.interaction)
1401
+ });
1402
+ else if (origin.event) core.bus[PublishDefault]("handledException", {
1403
+ denial,
1404
+ uuid,
1405
+ source: buildEventSource(origin.event, origin)
1406
+ });
1407
+ else core.bus[PublishDefault]("unknownException", {
1408
+ uuid,
1409
+ error: denial,
1410
+ ...scalarActors(origin),
1411
+ metadata: metadataFor(origin)
1412
+ });
1413
+ });
1414
+ }
1415
+ function causeLine(error) {
1416
+ const { cause } = error;
1417
+ if (!Error.isError(cause)) return "";
1418
+ const [first] = cause.message.split("\n");
1419
+ return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
1420
+ }
1421
+ function reportRawFault(error, core, origin, uuid) {
1422
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1423
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1424
+ withThrottle(core, origin, error, () => {
1425
+ core.bus[PublishDefault]("unknownException", {
1426
+ uuid,
1427
+ error,
1428
+ ...scalarActors(origin),
1429
+ metadata: metadataFor(origin)
1430
+ });
1431
+ });
1432
+ }
1433
+ function scalarActors(origin) {
1434
+ return {
1435
+ guild: origin.guild ? {
1436
+ id: origin.guild.id,
1437
+ name: origin.guild.name
1438
+ } : void 0,
1439
+ user: origin.user ? {
1440
+ id: origin.user.id,
1441
+ username: origin.user.username
1442
+ } : void 0
1443
+ };
1444
+ }
1445
+ function metadataFor(origin) {
1446
+ if (origin.event) return {
1447
+ eventName: origin.event.name,
1448
+ handler: origin.event.handler,
1449
+ args: origin.event.args
1450
+ };
1451
+ return origin.metadata;
1452
+ }
1453
+ function buildEventSource(event, origin) {
1454
+ return {
1455
+ kind: "event",
1456
+ eventName: event.name,
1457
+ handler: event.handler,
1458
+ userId: origin.user?.id ?? null,
1459
+ guildId: origin.guild?.id ?? null,
1460
+ channelId: event.channelId,
1461
+ raw: event.args
1462
+ };
1463
+ }
1464
+ function faultKey(origin, error) {
1465
+ const name = error instanceof Notice ? error.name : error instanceof DiscordAPIError ? String(error.code) : error.constructor.name;
1466
+ if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1467
+ if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1468
+ if (origin.route) return `autocomplete:${origin.route}:${name}`;
1469
+ return `autocomplete:${name}`;
1470
+ }
1471
+ function interactionRoute(interaction) {
1472
+ if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1473
+ if (interaction.isContextMenuCommand()) return interaction.commandName;
1474
+ if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return prefixOf(interaction.customId) || interaction.customId;
1475
+ return "interaction";
1476
+ }
1477
+ function buildInteractionSource(interaction) {
1478
+ const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1479
+ const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1480
+ return {
1481
+ kind: "interaction",
1482
+ interactionKind: interactionKind(interaction),
1483
+ command,
1484
+ customId,
1485
+ userId: interaction.user.id,
1486
+ guildId: interaction.guildId,
1487
+ channelId: interaction.channelId,
1488
+ interactionId: interaction.id,
1489
+ raw: interaction
1490
+ };
1491
+ }
1492
+ function interactionKind(interaction) {
1493
+ if (interaction.isChatInputCommand()) return "slash";
1494
+ if (interaction.isContextMenuCommand()) return "context-menu";
1495
+ if (interaction.isButton()) return "button";
1496
+ if (interaction.isAnySelectMenu()) return "select";
1497
+ return "modal";
1498
+ }
1499
+
2178
1500
  //#endregion
2179
1501
  //#region src/bot/handleEventFault.ts
2180
1502
  const logger$1 = new Logger("EventBoundary", { channel: "errors" });
@@ -2608,21 +1930,9 @@ var InteractionDispatcher = class {
2608
1930
  logger: this.logger
2609
1931
  });
2610
1932
  }
2611
- /**
2612
- * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
2613
- * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
2614
- *
2615
- * @internal
2616
- */
2617
1933
  warnUnhandledRoutes(commandLeaves) {
2618
1934
  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.`);
2619
1935
  }
2620
- /**
2621
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
2622
- * at runtime. Checked per kind since a user and a message command can share a name.
2623
- *
2624
- * @internal
2625
- */
2626
1936
  warnUnhandledContextMenuRoutes(leaves) {
2627
1937
  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.`);
2628
1938
  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.`);
@@ -2963,29 +2273,39 @@ var Bot = class {
2963
2273
  interactions;
2964
2274
  events;
2965
2275
  emojiInjector;
2966
- commands;
2967
- emojis = Emojis;
2968
- mentions = CommandMentions;
2276
+ commandRegistry;
2969
2277
  /** @internal */
2970
2278
  async onHmr(event) {
2971
2279
  if (this.interactions) await this.interactions.onHmr(event);
2972
2280
  if (this.events) await this.events.onHmr(event);
2973
- if (this.commands) await this.commands.onHmr(event);
2281
+ if (this.commandRegistry) await this.commandRegistry.onHmr(event);
2974
2282
  }
2975
2283
  /** @internal */
2976
2284
  constructor(core) {
2977
2285
  this._client = new Client(core.config.bot.clientOptions);
2978
2286
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
2979
2287
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
2980
- if (core.config.bot.commands.path) {
2981
- const mentionInjector = new CommandMentionInjector(core);
2982
- this.commands = new CommandRegistry(core, (result) => {
2983
- mentionInjector.inject(result);
2288
+ const commandsDir = core.config.bot.commands.path;
2289
+ if (commandsDir) {
2290
+ const injector = new CommandInjector();
2291
+ const registry = new CommandRegistry({
2292
+ dir: commandsDir,
2293
+ rest: this._client.rest,
2294
+ applicationId: () => this.applicationId(),
2295
+ onDeployed: (result) => {
2296
+ injector.inject(result, registry.allCommands());
2297
+ }
2984
2298
  });
2299
+ this.commandRegistry = registry;
2985
2300
  }
2986
2301
  this.emojiInjector = new EmojiInjector(core);
2987
2302
  this.registerShutdownTasks(core);
2988
2303
  }
2304
+ applicationId() {
2305
+ const { application } = this._client;
2306
+ if (!application) throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable);
2307
+ return application.id;
2308
+ }
2989
2309
  registerShutdownTasks(core) {
2990
2310
  core.shutdown.addTask(ShutdownPhase.Unbind, "stop-dispatch", () => {
2991
2311
  this.stopAccepting();
@@ -3010,11 +2330,11 @@ var Bot = class {
3010
2330
  if (this.events) await this.events.init();
3011
2331
  await this.login(token);
3012
2332
  await this.emojiInjector.init();
3013
- if (this.commands) {
3014
- await this.commands.init();
3015
- await this.commands.setCommands();
3016
- this.interactions?.warnUnhandledRoutes(this.commands.routeLeaves());
3017
- this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
2333
+ if (this.commandRegistry) {
2334
+ await this.commandRegistry.init();
2335
+ await this.commandRegistry.setCommands();
2336
+ this.interactions?.warnUnhandledRoutes(this.commandRegistry.routeLeaves());
2337
+ this.interactions?.warnUnhandledContextMenuRoutes(this.commandRegistry.contextMenuLeaves());
3018
2338
  }
3019
2339
  }
3020
2340
  /** @internal */
@@ -3043,7 +2363,7 @@ __decorate([Envapt("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordTok
3043
2363
  //#endregion
3044
2364
  //#region src/version.ts
3045
2365
  /** Package version */
3046
- const version = "0.1.0-next.5";
2366
+ const version = "0.1.0";
3047
2367
 
3048
2368
  //#endregion
3049
2369
  //#region src/Seedcord.ts
@@ -3066,6 +2386,8 @@ var Seedcord = class Seedcord extends Pluggable {
3066
2386
  subscribers;
3067
2387
  /** @see {@link Bot} */
3068
2388
  bot;
2389
+ /** The discord.js client's REST client. Its token is set during the Login phase. */
2390
+ rest;
3069
2391
  /** @see {@link IRateLimiter} */
3070
2392
  rateLimiter;
3071
2393
  /**
@@ -3082,6 +2404,7 @@ var Seedcord = class Seedcord extends Pluggable {
3082
2404
  this.bus = new Bus(this);
3083
2405
  this.subscribers = new SubscriberLoader(this.bus, config.subscribers.path);
3084
2406
  this.bot = new Bot(this);
2407
+ this.rest = this.bot.client.rest;
3085
2408
  this.rateLimiter = config.store ?? new MemoryRateLimiter();
3086
2409
  this.healthCheck = HealthCheck.fromOption(this.shutdown, config.healthCheck);
3087
2410
  this.registerStartupTasks();
@@ -3090,13 +2413,6 @@ var Seedcord = class Seedcord extends Pluggable {
3090
2413
  get username() {
3091
2414
  return this.bot.client.user?.username;
3092
2415
  }
3093
- pluginCapabilities() {
3094
- return {
3095
- client: this.bot.client,
3096
- token: this.bot.botToken,
3097
- rest: this.bot.client.rest
3098
- };
3099
- }
3100
2416
  static reset() {
3101
2417
  super.reset();
3102
2418
  LoggerChannelRegistry.instance.reset();
@@ -3146,5 +2462,5 @@ var Seedcord = class Seedcord extends Pluggable {
3146
2462
  };
3147
2463
 
3148
2464
  //#endregion
3149
- export { ArraySource, AutocompleteHandler, ButtonHandler, CommandMentions, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, fetchGuildMember, fetchManyGuildMembers, fetchManyUsers, fetchRole, fetchText, fetchUser, getBotRole, getConfirmation, hasPermsToAssign, updateMemberRoles, version };
2465
+ export { ArraySource, AutocompleteHandler, ButtonHandler, ContextMenuHandler, CursorSource, Emojis, EventHandler, EventMiddleware, Gated, IgnoreBots, InteractionHandler, InteractionMiddleware, Middleware, MiddlewareType, ModalHandler, Nsfw, Paginator, Plugin, RegisterEvent, RepliableHandler, Seedcord, SelectMenuHandler, SlashHandler, Subscriber, WebhookLog, checkBotPermissions, checkPermissions, getConfirmation, hasPermsToAssign, version };
3150
2466
  //# sourceMappingURL=index.mjs.map