@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.cjs CHANGED
@@ -33,11 +33,9 @@ let _seedcord_errors = require("@seedcord/errors");
33
33
  let _seedcord_errors_internal = require("@seedcord/errors/internal");
34
34
  let _seedcord_core = require("@seedcord/core");
35
35
  let _seedcord_logger = require("@seedcord/logger");
36
+ let discord_js = require("discord.js");
36
37
  let _discordjs_builders = require("@discordjs/builders");
37
38
  let _seedcord_utils_internal = require("@seedcord/utils/internal");
38
- let discord_js = require("discord.js");
39
- let node_crypto = require("node:crypto");
40
- node_crypto = __toESM(node_crypto, 1);
41
39
  let _seedcord_core_plugin = require("@seedcord/core/plugin");
42
40
  let _seedcord_core_node_internal = require("@seedcord/core/node/internal");
43
41
  let _seedcord_rate_limiter = require("@seedcord/rate-limiter");
@@ -46,10 +44,11 @@ let envapt = require("envapt");
46
44
  let chalk = require("chalk");
47
45
  chalk = __toESM(chalk, 1);
48
46
  let envapt_legacy = require("envapt/legacy");
49
- let node_path = require("node:path");
50
47
  let _seedcord_core_hmr = require("@seedcord/core/hmr");
51
48
  let _seedcord_utils = require("@seedcord/utils");
52
49
  let _seedcord_utils_node = require("@seedcord/utils/node");
50
+ let node_crypto = require("node:crypto");
51
+ node_crypto = __toESM(node_crypto, 1);
53
52
 
54
53
  //#region src/logger-bootstrap.ts
55
54
  (0, _seedcord_logger_node.installNodeDefaults)();
@@ -242,59 +241,6 @@ var CannotAssignBotRole = class extends _seedcord_core.Notice {
242
241
  return { components: [new _seedcord_core_internal.NoticeCard("I cannot assign a managed role.").component] };
243
242
  }
244
243
  };
245
- var HasDangerousPermissions = class extends _seedcord_core.Notice {
246
- subject;
247
- dangerousPerms;
248
- customLead;
249
- constructor(message, subject, dangerousPerms) {
250
- super(message ?? "A dangerous permission is enabled.");
251
- this.subject = subject;
252
- this.dangerousPerms = dangerousPerms;
253
- this.customLead = message;
254
- }
255
- render() {
256
- const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
257
- return { components: [new _seedcord_core_internal.NoticeCard(`${this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`}\n\n${bullets}`).component] };
258
- }
259
- };
260
- var UserNotFound = class extends _seedcord_core.Notice {
261
- userArg;
262
- constructor(userArg) {
263
- super(`User not found: ${userArg}`);
264
- this.userArg = userArg;
265
- }
266
- render() {
267
- return { components: [new _seedcord_core_internal.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] };
268
- }
269
- };
270
- var UserNotInGuild = class extends _seedcord_core.Notice {
271
- constructor(message = "User is not in the guild.") {
272
- super(message);
273
- }
274
- render() {
275
- return { components: [new _seedcord_core_internal.NoticeCard(this.message).component] };
276
- }
277
- };
278
- var RoleDoesNotExist = class extends _seedcord_core.Notice {
279
- roleId;
280
- constructor(message, roleId) {
281
- super(message);
282
- this.roleId = roleId;
283
- }
284
- render() {
285
- return { components: [new _seedcord_core_internal.NoticeCard(`The role with ID \`${this.roleId}\` does not exist.`).component] };
286
- }
287
- };
288
- var CouldNotFindChannel = class extends _seedcord_core.Notice {
289
- channelId;
290
- constructor(message, channelId) {
291
- super(message);
292
- this.channelId = channelId;
293
- }
294
- render() {
295
- return { components: [new _seedcord_core_internal.NoticeCard(`Could not find channel with ID \`${this.channelId}\`. It could also be that the channel is not a text channel.`).component] };
296
- }
297
- };
298
244
 
299
245
  //#endregion
300
246
  //#region src/bot/gates/catalog/Nsfw.ts
@@ -332,20 +278,15 @@ function Nsfw(options) {
332
278
 
333
279
  //#endregion
334
280
  //#region src/bot/injectors/EmojiInjector.ts
335
- const emojiStorage = {};
336
- function isEmojiTuple(v) {
337
- return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "string";
338
- }
281
+ const emojiStorage = (0, _seedcord_core_internal.accessorStore)();
339
282
  /**
340
- * Global emoji mappings object
341
- *
342
- * @see {@link EmojiMap}
283
+ * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
284
+ * A read before that throws.
343
285
  */
344
- const Emojis = emojiStorage;
286
+ const Emojis = (0, _seedcord_core_internal.guardedAccessor)("Emojis", emojiStorage);
345
287
  /**
346
- * Loads and injects emojis based on bot configuration.
347
- *
348
- * For emojis injected from specific guilds, ensure that the Guilds intent is provided in client options.
288
+ * Loads and injects emojis based on bot configuration. A guild emoji requires the Guilds intent in the client
289
+ * options.
349
290
  *
350
291
  * @internal
351
292
  */
@@ -365,7 +306,7 @@ var EmojiInjector = class {
365
306
  await this.core.bot.client.application?.emojis.fetch();
366
307
  const failures = [];
367
308
  const fetchedGuilds = /* @__PURE__ */ new Set();
368
- for (const [key, value] of Object.entries(configEmojis)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, failures, fetchedGuilds);
309
+ for (const [key, value] of Object.entries(configEmojis)) if ((0, _seedcord_core_internal.isEmojiTuple)(value)) await this.resolveTuple(key, value, failures, fetchedGuilds);
369
310
  else if (typeof value === "string") this.resolveString(key, value, failures);
370
311
  else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
371
312
  if (failures.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
@@ -398,126 +339,10 @@ var EmojiInjector = class {
398
339
  emojiStorage[key] = appEmoji;
399
340
  }
400
341
  clearEmojis() {
401
- for (const key of Object.keys(emojiStorage)) Reflect.deleteProperty(emojiStorage, key);
402
- }
403
- };
404
-
405
- //#endregion
406
- //#region src/bot/injectors/CommandMentionInjector.ts
407
- const mentionStorage = {};
408
- /**
409
- * Ready-to-send command mention strings, keyed by {@link SlashOptionRegistry}.
410
- *
411
- * A value is a clickable `</name:id>` when the command's id resolved from the deploy, otherwise the plain
412
- * `/name` text. Populated by {@link CommandMentionInjector} after each deploy.
413
- */
414
- const CommandMentions = mentionStorage;
415
- const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
416
- /**
417
- * Turns each deployed slash route into a clickable command mention exposed through {@link CommandMentions}.
418
- *
419
- * Consumes the ids {@link DeployResult} carries, so it runs after a deploy rather than resolving from a name
420
- * cache (discord.js offers none for command ids). A global command is clickable everywhere by its app-wide id.
421
- * A guild command deployed to one guild is clickable by that guild's id. A command deployed to two or more
422
- * guilds mints a different id per guild, so one accessor key cannot resolve it, and it falls back to plain
423
- * `/name` text with one warn. On a non-owning shard a guild command also falls back to plain text.
424
- *
425
- * @internal
426
- */
427
- var CommandMentionInjector = class {
428
- core;
429
- logger = new _seedcord_logger.Logger("CommandMentions", { channel: "bot" });
430
- constructor(core) {
431
- this.core = core;
432
- }
433
- inject(deploy) {
434
- this.clear();
435
- const globalIds = this.indexByName(deploy.global);
436
- const guildIds = this.indexGuilds(deploy.guilds);
437
- const warned = /* @__PURE__ */ new Set();
438
- let clickable = 0;
439
- for (const command of this.allSlashBuilders()) {
440
- const id = this.resolveId(command.name, globalIds, guildIds, warned);
441
- for (const leaf of (0, _seedcord_utils_internal.routeLeavesOf)(command.toJSON())) {
442
- mentionStorage[leaf.route] = id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route);
443
- if (id) clickable++;
444
- }
445
- }
446
- this.logger.utils.summary("Linked mentions", {
447
- clickable,
448
- plain: Object.keys(mentionStorage).length - clickable
449
- });
450
- }
451
- allSlashBuilders() {
452
- const registry = this.core.bot.commands;
453
- if (!registry) return [];
454
- const slash = [...registry.globalCommands, ...registry.guildCommands.values()].flat().filter((command) => command instanceof _discordjs_builders.SlashCommandBuilder);
455
- return [...new Set(slash)];
456
- }
457
- indexByName(collection) {
458
- const map = /* @__PURE__ */ new Map();
459
- for (const command of collection.values()) map.set(command.name, command.id);
460
- return map;
461
- }
462
- indexGuilds(guilds) {
463
- const map = /* @__PURE__ */ new Map();
464
- for (const collection of guilds.values()) for (const command of collection.values()) {
465
- const existing = map.get(command.name);
466
- if (existing === void 0) map.set(command.name, command.id);
467
- else if (existing !== command.id) map.set(command.name, MULTI_GUILD);
468
- }
469
- return map;
470
- }
471
- resolveId(name, globalIds, guildIds, warned) {
472
- const global = globalIds.get(name);
473
- if (global) return global;
474
- const guild = guildIds.get(name);
475
- if (guild === void 0) return void 0;
476
- if (guild === MULTI_GUILD) {
477
- if (!warned.has(name)) {
478
- this.logger.warn(`${name} is deployed to multiple guilds, falling back to plain text (not clickable).`);
479
- warned.add(name);
480
- }
481
- return;
482
- }
483
- return guild;
484
- }
485
- toMention(route, id) {
486
- const [name, middle, sub] = route.split("/");
487
- if (name && middle && sub) return (0, discord_js.chatInputApplicationCommandMention)(name, middle, sub, id);
488
- if (name && middle) return (0, discord_js.chatInputApplicationCommandMention)(name, middle, id);
489
- return (0, discord_js.chatInputApplicationCommandMention)(name ?? route, id);
490
- }
491
- toPlain(route) {
492
- return `/${route.replaceAll("/", " ")}`;
493
- }
494
- clear() {
495
- for (const key of Object.keys(mentionStorage)) Reflect.deleteProperty(mentionStorage, key);
342
+ (0, _seedcord_core_internal.clearStore)(emojiStorage);
496
343
  }
497
344
  };
498
345
 
499
- //#endregion
500
- //#region src/bot/utilities/channels/fetchText.ts
501
- /**
502
- * Fetches and validates a text channel. Refuses when the channel doesn't exist or isn't a text channel.
503
- *
504
- * @param client - The Discord client instance
505
- * @param channelId - Channel ID or TextChannel instance
506
- * @returns Promise resolving to the text channel
507
- */
508
- async function fetchText(client, channelId) {
509
- if (channelId instanceof discord_js.TextChannel) return channelId;
510
- let channel = client.channels.cache.get(channelId);
511
- if (!channel) try {
512
- channel = await client.channels.fetch(channelId);
513
- } catch (err) {
514
- if (err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownChannel) throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
515
- throw err;
516
- }
517
- if (channel?.isTextBased()) return channel;
518
- throw new CouldNotFindChannel("Channel not found or not a text channel", channelId);
519
- }
520
-
521
346
  //#endregion
522
347
  //#region src/bot/notices/utils.ts
523
348
  function mentionFor(subject) {
@@ -528,48 +353,37 @@ function mentionFor(subject) {
528
353
  //#endregion
529
354
  //#region src/bot/utilities/permissions/checkPermissions.ts
530
355
  function checkPermissions(a, b, c, d, e) {
531
- const { for: pFor, in: pIn, scope, inverse = false, missingNotice: missingCtor, dangerousNotice: dangerousCtor } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
356
+ const { for: pFor, in: pIn, scope, inverse = false, missingNotice, dangerousNotice } = a instanceof discord_js.Role || a instanceof discord_js.GuildMember ? {
532
357
  for: a,
533
358
  in: b,
534
359
  scope: c,
535
360
  inverse: d ?? false,
536
361
  ...e
537
362
  } : a;
538
- const Missing = missingCtor ?? _seedcord_core_internal.MissingPermissions;
539
- const Dangerous = dangerousCtor ?? HasDangerousPermissions;
540
- const subject = mentionFor(pFor);
541
363
  const perms = pIn instanceof discord_js.Guild ? pFor.permissions : pIn.permissionsFor(pFor, true);
542
- const names = (bits) => bits.map((bit) => _seedcord_core_internal.PermissionNames.get(bit) ?? String(bit));
543
- if (inverse) {
544
- const present = scope.filter((bit) => perms.has(bit, true));
545
- if (present.length > 0) throw new Dangerous(void 0, subject, names(present));
546
- return;
547
- }
548
- const missingBits = scope.filter((bit) => !perms.has(bit, true));
549
- if (missingBits.length > 0) throw new Missing(void 0, subject, names(missingBits));
364
+ (0, _seedcord_core.assertPermissions)({
365
+ subject: mentionFor(pFor),
366
+ permissions: perms.bitfield,
367
+ scope,
368
+ inverse,
369
+ ...missingNotice && { missingNotice },
370
+ ...dangerousNotice && { dangerousNotice }
371
+ });
550
372
  }
551
373
 
552
374
  //#endregion
553
375
  //#region src/bot/utilities/permissions/checkBotPermissions.ts
554
376
  /**
555
- * Checks if the bot has required permissions in a {@link Guild} or {@link TextChannel}. Refuses when a
556
- * required permission is missing.
557
- *
558
- * @param target - Guild or text channel to check in
559
- * @param scope - Permission bits to validate
560
- * @param inverse - Whether to check for absence of the given permissions
561
- * @param errors - Optional custom error constructors
377
+ * Checks the bot's own permissions in a {@link Guild} or {@link TextChannel}. Refuses when a permission in
378
+ * `scope` is missing, or with `inverse` when one is present. When the bot member is uncached, the refusal
379
+ * reports every permission in `scope` as missing.
562
380
  *
563
381
  * @example
564
382
  * ```ts
565
383
  * import { PermissionFlagsBits } from 'discord.js';
566
384
  * import { checkBotPermissions } from '@seedcord/gateway';
567
385
  *
568
- * // Check if the bot has SendMessages and ViewChannel permissions in a text channel
569
- * checkBotPermissions(textChannel, [
570
- * PermissionFlagsBits.SendMessages,
571
- * PermissionFlagsBits.ViewChannel
572
- * ]);
386
+ * checkBotPermissions(textChannel, [PermissionFlagsBits.SendMessages, PermissionFlagsBits.ViewChannel]);
573
387
  * ```
574
388
  */
575
389
  function checkBotPermissions(target, scope, inverse = false, errors) {
@@ -592,26 +406,17 @@ function checkBotPermissions(target, scope, inverse = false, errors) {
592
406
  }
593
407
 
594
408
  //#endregion
595
- //#region src/bot/utilities/roles/getBotRole.ts
596
- /**
597
- * Gets the bot's managed role in a guild.
598
- *
599
- * @param guild - The guild to get the bot role from
600
- * @returns The bot's managed role in the guild
601
- * @throws A **SeedcordError** if the client user is unavailable or if the bot role is missing
602
- */
603
- function getBotRole(guild) {
409
+ //#region src/bot/utilities/permissions/hasPermsToAssign.ts
410
+ function botRoleOf(guild) {
604
411
  const botRole = guild.roles.botRoleFor(guild.client.user);
605
412
  if (!botRole) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreBotRoleMissing, [guild.id]);
606
413
  return botRole;
607
414
  }
608
-
609
- //#endregion
610
- //#region src/bot/utilities/permissions/hasPermsToAssign.ts
611
415
  /**
612
- * 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.
416
+ * 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.
613
417
  *
614
418
  * @param roleOrOptions - Target role or complete options for the check
419
+ * @throws A `Notice` on refusal, or a `SeedcordError` when the bot has no role in the guild's role cache.
615
420
  *
616
421
  * @example
617
422
  * ```ts
@@ -639,138 +444,123 @@ function hasPermsToAssign(roleOrOptions) {
639
444
  const HigherNotice = errors?.higherNotice ?? RoleHigherThanMe;
640
445
  const ManagedNotice = errors?.managedNotice ?? CannotAssignBotRole;
641
446
  const MissingNotice = errors?.missingNotice ?? _seedcord_core_internal.MissingPermissions;
642
- const botRole = getBotRole(targetRole.guild);
447
+ const botRole = botRoleOf(targetRole.guild);
643
448
  if (targetRole.comparePositionTo(botRole) >= 0) throw new HigherNotice("Role is higher than me", targetRole, botRole);
644
449
  if (targetRole.managed) throw new ManagedNotice(`Cannot assign managed role ${targetRole.name}`);
645
450
  checkBotPermissions(targetRole.guild, [discord_js.PermissionFlagsBits.ManageRoles], false, { missingNotice: MissingNotice });
646
451
  }
647
452
 
648
453
  //#endregion
649
- //#region src/bot/utilities/roles/fetchRole.ts
650
- function isUnknownRole(err) {
651
- return err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownRole;
652
- }
653
- async function fetchRoleInGuild(guild, roleId) {
654
- const cached = guild.roles.cache.get(roleId);
655
- if (cached) return cached;
656
- try {
657
- return await guild.roles.fetch(roleId);
658
- } catch (err) {
659
- if (isUnknownRole(err)) throw new RoleDoesNotExist("Role not found in specified guild", roleId);
660
- throw err;
661
- }
662
- }
663
- async function scanGuildsForRole(client, roleId) {
664
- const cached = client.guilds.cache.map((guild) => guild.roles.cache.get(roleId)).find(Boolean);
665
- if (cached) return cached;
666
- for (const guild of client.guilds.cache.values()) try {
667
- const role = await guild.roles.fetch(roleId);
668
- if (role) return role;
669
- } catch (err) {
670
- if (isUnknownRole(err)) continue;
671
- throw err;
672
- }
673
- return null;
674
- }
454
+ //#region src/bot/confirm/reserved.ts
455
+ const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
675
456
  /**
676
- * Fetches a role by ID from a client or guild. Refuses when the role doesn't exist.
457
+ * The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
458
+ * in-process collector reads them. Do not mint a real component from it.
677
459
  *
678
- * @param clientOrGuild - Discord client or guild instance
679
- * @param roleId - The role ID to fetch
680
- * @returns Promise resolving to the role
460
+ * @internal
681
461
  */
682
- async function fetchRole(clientOrGuild, roleId) {
683
- if (!roleId) throw new RoleDoesNotExist("Role ID is null or undefined", roleId);
684
- const role = clientOrGuild instanceof discord_js.Guild ? await fetchRoleInGuild(clientOrGuild, roleId) : await scanGuildsForRole(clientOrGuild, roleId);
685
- if (!role) throw new RoleDoesNotExist("Role not found", roleId);
686
- return role;
687
- }
462
+ const CONFIRM_DEF = new _seedcord_core.CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
688
463
 
689
464
  //#endregion
690
- //#region src/bot/utilities/users/fetchGuildMember.ts
691
- /**
692
- * Fetches a guild member by user ID. Refuses when the user is not in the guild.
693
- *
694
- * @param guild - The guild to fetch the member from
695
- * @param userId - The Discord user ID
696
- * @returns Promise resolving to the guild member
697
- */
698
- async function fetchGuildMember(guild, userId) {
699
- let user = guild.members.cache.get(userId);
700
- user ??= await guild.members.fetch(userId).catch((err) => {
701
- if (err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownMember) throw new UserNotInGuild(`User with ID ${userId} not found in guild`);
702
- throw err;
703
- });
704
- return user;
465
+ //#region src/bot/confirm/getConfirmation.ts
466
+ const DEFAULT_TIMEOUT_MS = 3e4;
467
+ const CONFIRM_IDS = {
468
+ confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
469
+ cancel: CONFIRM_DEF.encode({ choice: "cancel" })
470
+ };
471
+ var ConfirmButtons = class extends _seedcord_core.RowComponent {
472
+ constructor(options) {
473
+ super("button");
474
+ this.instance.addComponents(new _discordjs_builders.ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? discord_js.ButtonStyle.Danger), new _discordjs_builders.ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(discord_js.ButtonStyle.Secondary));
475
+ }
476
+ };
477
+ var ConfirmPromptCard = class extends _seedcord_core.BuilderComponent {
478
+ constructor(text, options) {
479
+ super("container");
480
+ this.instance.addTextDisplayComponents(new _discordjs_builders.TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
481
+ }
482
+ };
483
+ function defaultPrompt(text, options) {
484
+ return { components: [new ConfirmPromptCard(text, options).component] };
705
485
  }
706
-
707
- //#endregion
708
- //#region src/bot/utilities/users/fetchManyGuildMembers.ts
709
- /**
710
- * Fetches multiple guild members by user IDs.
711
- *
712
- * @param guild - The guild to fetch members from
713
- * @param userIds - Array of Discord user IDs
714
- * @returns Promise resolving to array of successfully fetched members
715
- */
716
- async function fetchManyGuildMembers(guild, userIds) {
717
- return (await Promise.allSettled(userIds.map((userId) => fetchGuildMember(guild, userId)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
486
+ async function collectChoice(message, userId, timeoutMs) {
487
+ try {
488
+ return await message.awaitMessageComponent({
489
+ componentType: discord_js.ComponentType.Button,
490
+ time: timeoutMs,
491
+ filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
492
+ });
493
+ } catch {
494
+ return null;
495
+ }
718
496
  }
719
-
720
- //#endregion
721
- //#region src/bot/utilities/users/fetchUser.ts
722
- /**
723
- * Fetches a Discord user by ID. Refuses when the user doesn't exist, with `options.throwAs` or the default.
724
- *
725
- * @param client - The Discord client instance
726
- * @param userId - The Discord user ID
727
- * @param options - Optional overrides, including the {@link FetchUserOptions.throwAs} notice
728
- * @returns Promise resolving to the user
729
- */
730
- async function fetchUser(client, userId, options) {
731
- const Throw = options?.throwAs ?? UserNotFound;
732
- let user = client.users.cache.get(userId);
733
- user ??= await client.users.fetch(userId).catch((err) => {
734
- if (err instanceof discord_js.DiscordAPIError && err.code === discord_js.RESTJSONErrorCodes.UnknownUser) throw new Throw(userId);
735
- throw err;
736
- });
737
- return user;
497
+ async function settle(sender, message, outcome) {
498
+ if (outcome === void 0) {
499
+ await sender.delete(message).catch(() => void 0);
500
+ return;
501
+ }
502
+ await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
503
+ }
504
+ async function getConfirmation(handler, prompt, options) {
505
+ const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
506
+ const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
507
+ const interaction = handler.getEvent();
508
+ const sender = handler.getSender();
509
+ const message = await sender.send(response, { ephemeral });
510
+ const winner = await collectChoice(message, interaction.user.id, timeoutMs);
511
+ if (!winner) {
512
+ await settle(sender, message, options?.onTimeout);
513
+ return false;
514
+ }
515
+ await winner.deferUpdate().catch(() => void 0);
516
+ const confirmed = winner.customId === CONFIRM_IDS.confirm;
517
+ await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
518
+ return confirmed;
738
519
  }
739
520
 
740
521
  //#endregion
741
- //#region src/bot/utilities/users/fetchManyUsers.ts
522
+ //#region src/handlers/BaseHandler.ts
742
523
  /**
743
- * Fetches multiple Discord users by IDs.
744
- *
745
- * @param client - The Discord client instance
746
- * @param userIds - Array of Discord user IDs
747
- * @param options - Optional overrides threaded to each {@link fetchUser} call
748
- * @returns Promise resolving to array of successfully fetched users
524
+ * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
525
+ * gate hook.
749
526
  */
750
- async function fetchManyUsers(client, userIds, options) {
751
- return (await Promise.allSettled(userIds.map((userId) => fetchUser(client, userId, options)))).filter((result) => result.status === "fulfilled").map((result) => result.value);
752
- }
527
+ var BaseHandler = class extends _seedcord_core.BaseHandler {
528
+ /** @internal */
529
+ getEvent() {
530
+ return this.event;
531
+ }
532
+ };
753
533
 
754
534
  //#endregion
755
- //#region src/bot/utilities/users/updateMemberRoles.ts
535
+ //#region src/handlers/interaction/InteractionMiddleware.ts
756
536
  /**
757
- * Updates a guild member's roles by adding and removing specified roles.
537
+ * Base class for interaction middleware
538
+ *
539
+ * Middleware runs before interaction handlers and can modify behavior or block execution.
540
+ * Unlike handlers, middleware should not send responses directly.
758
541
  *
759
- * @param rolesToAdd - Array of role IDs to add to the member
760
- * @param rolesToRemove - Array of role IDs to remove from the member
761
- * @param member - The guild member to update
762
- * @returns Promise that resolves when roles are updated
542
+ * @typeParam Repliable - The interaction type this middleware processes
763
543
  */
764
- async function updateMemberRoles(rolesToAdd, rolesToRemove, member) {
765
- const current = new Set(member.roles.cache.map((r) => r.id));
766
- const toAdd = new Set(rolesToAdd);
767
- const toRemove = new Set(rolesToRemove);
768
- const updated = current.union(toAdd).difference(toRemove);
769
- await member.roles.set([...updated]);
770
- }
544
+ var InteractionMiddleware = class extends BaseHandler {
545
+ constructor(event, core) {
546
+ super(event, core, void 0, "interactions");
547
+ }
548
+ };
771
549
 
772
550
  //#endregion
773
551
  //#region src/bot/ReplySender.ts
552
+ function isReplyFile(file) {
553
+ return typeof file === "object" && "data" in file && ArrayBuffer.isView(file.data);
554
+ }
555
+ function toDjsFile(file) {
556
+ if (!isReplyFile(file)) return file;
557
+ return {
558
+ attachment: Buffer.from(file.data),
559
+ name: file.name,
560
+ ...file.description !== void 0 && { description: file.description },
561
+ ...file.title !== void 0 && { title: file.title }
562
+ };
563
+ }
774
564
  function seedState(interaction) {
775
565
  if (interaction.deferred && interaction.ephemeral === null) return "deferred-update";
776
566
  if (interaction.replied) return "replied";
@@ -853,7 +643,7 @@ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
853
643
  components: reply.components,
854
644
  flags,
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
  editOptions(response) {
@@ -862,7 +652,7 @@ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
862
652
  components: reply.components,
863
653
  flags: discord_js.MessageFlags.IsComponentsV2,
864
654
  ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
865
- ...reply.files && { files: [...reply.files] }
655
+ ...reply.files && { files: reply.files.map(toDjsFile) }
866
656
  };
867
657
  }
868
658
  updateOptions(response) {
@@ -871,7 +661,7 @@ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
871
661
  components: reply.components,
872
662
  flags: discord_js.MessageFlags.IsComponentsV2,
873
663
  ...reply.allowedMentions && { allowedMentions: reply.allowedMentions },
874
- ...reply.files && { files: [...reply.files] }
664
+ ...reply.files && { files: reply.files.map(toDjsFile) }
875
665
  };
876
666
  }
877
667
  createdMessage(result) {
@@ -879,266 +669,6 @@ var ReplySender = class extends _seedcord_core_internal.BaseReplySender {
879
669
  }
880
670
  };
881
671
 
882
- //#endregion
883
- //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
884
- /**
885
- * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
886
- * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
887
- * interaction and a generated registry key always match.
888
- *
889
- * @internal
890
- */
891
- function slashRouteOf(interaction) {
892
- const group = interaction.options.getSubcommandGroup(false) ?? void 0;
893
- const subcommand = interaction.options.getSubcommand(false) ?? void 0;
894
- return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
895
- }
896
-
897
- //#endregion
898
- //#region src/miscellaneous/extractErrorResponse.ts
899
- const logger$2 = new _seedcord_logger.Logger("ErrorsHandling", { channel: "errors" });
900
- /**
901
- * Processes an error into a user-facing reply and routes its report.
902
- *
903
- * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
904
- * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
905
- * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
906
- * threaded into both the reply and the bus payload.
907
- */
908
- function extractErrorResponse(error, core, origin) {
909
- const uuid = node_crypto.randomUUID();
910
- const developerUsername = core.config.notifications?.developerUsername;
911
- const ctx = developerUsername === void 0 ? { uuid } : {
912
- uuid,
913
- developerUsername
914
- };
915
- if (error instanceof _seedcord_core.Notice) {
916
- if (error.report) reportFault(error, core, origin, uuid);
917
- return {
918
- uuid,
919
- response: error.render(ctx)
920
- };
921
- }
922
- reportRawFault(error, core, origin, uuid);
923
- const override = core.config.errors?.defaultError;
924
- return {
925
- uuid,
926
- response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
927
- };
928
- }
929
- function withThrottle(core, origin, error, publish) {
930
- const throttle = _seedcord_core_internal.FaultThrottle.for(core);
931
- const key = faultKey(origin, error);
932
- if (!throttle.shouldReport(key)) {
933
- logger$2.debug(`throttled duplicate fault ${key}`);
934
- return;
935
- }
936
- publish();
937
- throttle.markReported(key);
938
- }
939
- function reportFault(denial, core, origin, uuid) {
940
- logger$2.error(`${denial.name}: ${uuid}`, denial);
941
- withThrottle(core, origin, denial, () => {
942
- if (origin.interaction) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
943
- denial,
944
- uuid,
945
- source: buildInteractionSource(origin.interaction)
946
- });
947
- else if (origin.event) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
948
- denial,
949
- uuid,
950
- source: buildEventSource(origin.event, origin)
951
- });
952
- else core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
953
- uuid,
954
- error: denial,
955
- ...scalarActors(origin),
956
- metadata: metadataFor(origin)
957
- });
958
- });
959
- }
960
- function causeLine(error) {
961
- const { cause } = error;
962
- if (!Error.isError(cause)) return "";
963
- const [first] = cause.message.split("\n");
964
- return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
965
- }
966
- function reportRawFault(error, core, origin, uuid) {
967
- if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
968
- else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
969
- withThrottle(core, origin, error, () => {
970
- core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
971
- uuid,
972
- error,
973
- ...scalarActors(origin),
974
- metadata: metadataFor(origin)
975
- });
976
- });
977
- }
978
- function scalarActors(origin) {
979
- return {
980
- guild: origin.guild ? {
981
- id: origin.guild.id,
982
- name: origin.guild.name
983
- } : void 0,
984
- user: origin.user ? {
985
- id: origin.user.id,
986
- username: origin.user.username
987
- } : void 0
988
- };
989
- }
990
- function metadataFor(origin) {
991
- if (origin.event) return {
992
- eventName: origin.event.name,
993
- handler: origin.event.handler,
994
- args: origin.event.args
995
- };
996
- return origin.metadata;
997
- }
998
- function buildEventSource(event, origin) {
999
- return {
1000
- kind: "event",
1001
- eventName: event.name,
1002
- handler: event.handler,
1003
- userId: origin.user?.id ?? null,
1004
- guildId: origin.guild?.id ?? null,
1005
- channelId: event.channelId,
1006
- raw: event.args
1007
- };
1008
- }
1009
- function faultKey(origin, error) {
1010
- const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
1011
- if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1012
- if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1013
- if (origin.route) return `autocomplete:${origin.route}:${name}`;
1014
- return `autocomplete:${name}`;
1015
- }
1016
- /** The stable route id of a repliable interaction, its slash route path, command name, or customId prefix. */
1017
- function interactionRoute(interaction) {
1018
- if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1019
- if (interaction.isContextMenuCommand()) return interaction.commandName;
1020
- if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
1021
- return "interaction";
1022
- }
1023
- function buildInteractionSource(interaction) {
1024
- const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1025
- const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1026
- return {
1027
- kind: "interaction",
1028
- interactionKind: interactionKind(interaction),
1029
- command,
1030
- customId,
1031
- userId: interaction.user.id,
1032
- guildId: interaction.guildId,
1033
- channelId: interaction.channelId,
1034
- interactionId: interaction.id,
1035
- raw: interaction
1036
- };
1037
- }
1038
- function interactionKind(interaction) {
1039
- if (interaction.isChatInputCommand()) return "slash";
1040
- if (interaction.isContextMenuCommand()) return "context-menu";
1041
- if (interaction.isButton()) return "button";
1042
- if (interaction.isAnySelectMenu()) return "select";
1043
- return "modal";
1044
- }
1045
-
1046
- //#endregion
1047
- //#region src/bot/confirm/reserved.ts
1048
- const RESERVED_CONFIRM_PREFIX = "__seedcord_confirm";
1049
- /**
1050
- * The reserved confirm/cancel codec. It has no route, so the dispatcher ignores its clicks and only the
1051
- * in-process collector reads them. Do not mint a real component from it.
1052
- *
1053
- * @internal
1054
- */
1055
- const CONFIRM_DEF = new _seedcord_core.CustomId(RESERVED_CONFIRM_PREFIX).oneOf("choice", ["confirm", "cancel"]);
1056
-
1057
- //#endregion
1058
- //#region src/bot/confirm/getConfirmation.ts
1059
- const DEFAULT_TIMEOUT_MS = 3e4;
1060
- const CONFIRM_IDS = {
1061
- confirm: CONFIRM_DEF.encode({ choice: "confirm" }),
1062
- cancel: CONFIRM_DEF.encode({ choice: "cancel" })
1063
- };
1064
- var ConfirmButtons = class extends _seedcord_core.RowComponent {
1065
- constructor(options) {
1066
- super("button");
1067
- this.instance.addComponents(new _discordjs_builders.ButtonBuilder().setCustomId(CONFIRM_IDS.confirm).setLabel(options?.confirmLabel ?? "Confirm").setStyle(options?.confirmStyle ?? discord_js.ButtonStyle.Danger), new _discordjs_builders.ButtonBuilder().setCustomId(CONFIRM_IDS.cancel).setLabel(options?.cancelLabel ?? "Cancel").setStyle(discord_js.ButtonStyle.Secondary));
1068
- }
1069
- };
1070
- var ConfirmPromptCard = class extends _seedcord_core.BuilderComponent {
1071
- constructor(text, options) {
1072
- super("container");
1073
- this.instance.addTextDisplayComponents(new _discordjs_builders.TextDisplayBuilder().setContent(text)).addActionRowComponents(new ConfirmButtons(options).component);
1074
- }
1075
- };
1076
- function defaultPrompt(text, options) {
1077
- return { components: [new ConfirmPromptCard(text, options).component] };
1078
- }
1079
- async function collectChoice(message, userId, timeoutMs) {
1080
- try {
1081
- return await message.awaitMessageComponent({
1082
- componentType: discord_js.ComponentType.Button,
1083
- time: timeoutMs,
1084
- filter: (click) => click.user.id === userId && CONFIRM_DEF.owns(click.customId)
1085
- });
1086
- } catch {
1087
- return null;
1088
- }
1089
- }
1090
- async function settle(sender, message, outcome) {
1091
- if (outcome === void 0) {
1092
- await sender.delete(message).catch(() => void 0);
1093
- return;
1094
- }
1095
- await sender.edit(message, typeof outcome === "function" ? await outcome() : outcome);
1096
- }
1097
- async function getConfirmation(interaction, prompt, options) {
1098
- const { ephemeral = true, timeoutMs = DEFAULT_TIMEOUT_MS } = options ?? {};
1099
- const response = typeof prompt === "string" ? defaultPrompt(prompt, options) : await prompt(CONFIRM_IDS);
1100
- const sender = new ReplySender(interaction, interactionRoute(interaction));
1101
- const message = await sender.send(response, { ephemeral });
1102
- const winner = await collectChoice(message, interaction.user.id, timeoutMs);
1103
- if (!winner) {
1104
- await settle(sender, message, options?.onTimeout);
1105
- return false;
1106
- }
1107
- await winner.deferUpdate().catch(() => void 0);
1108
- const confirmed = winner.customId === CONFIRM_IDS.confirm;
1109
- await settle(sender, message, confirmed ? options?.onConfirm : options?.onCancel);
1110
- return confirmed;
1111
- }
1112
-
1113
- //#endregion
1114
- //#region src/handlers/BaseHandler.ts
1115
- /**
1116
- * The gateway base, fixes the core base's `TCore` to the gateway {@link Core} and adds the `getEvent`
1117
- * gate hook.
1118
- */
1119
- var BaseHandler = class extends _seedcord_core.BaseHandler {
1120
- /** @internal */
1121
- getEvent() {
1122
- return this.event;
1123
- }
1124
- };
1125
-
1126
- //#endregion
1127
- //#region src/handlers/interaction/InteractionMiddleware.ts
1128
- /**
1129
- * Base class for interaction middleware
1130
- *
1131
- * Middleware runs before interaction handlers and can modify behavior or block execution.
1132
- * Unlike handlers, middleware should not send responses directly.
1133
- *
1134
- * @typeParam Repliable - The interaction type this middleware processes
1135
- */
1136
- var InteractionMiddleware = class extends BaseHandler {
1137
- constructor(event, core) {
1138
- super(event, core, void 0, "interactions");
1139
- }
1140
- };
1141
-
1142
672
  //#endregion
1143
673
  //#region src/handlers/RepliableHandler.ts
1144
674
  /**
@@ -1184,6 +714,21 @@ var InteractionHandler = class extends RepliableHandler {
1184
714
  }
1185
715
  };
1186
716
 
717
+ //#endregion
718
+ //#region src/bot/utilities/miscellaneous/slashRouteOf.ts
719
+ /**
720
+ * Computes the dispatch route for a chat-input or autocomplete interaction. The controller and
721
+ * `seedcord codegen` both assemble route keys through the same {@link buildSlashRoute}, so a dispatched
722
+ * interaction and a generated registry key always match.
723
+ *
724
+ * @internal
725
+ */
726
+ function slashRouteOf(interaction) {
727
+ const group = interaction.options.getSubcommandGroup(false) ?? void 0;
728
+ const subcommand = interaction.options.getSubcommand(false) ?? void 0;
729
+ return (0, _seedcord_utils_internal.buildSlashRoute)(interaction.commandName, subcommand, group);
730
+ }
731
+
1187
732
  //#endregion
1188
733
  //#region src/handlers/interaction/SlashHandler.ts
1189
734
  /**
@@ -1503,7 +1048,22 @@ var ButtonHandler = class extends ComponentHandler {};
1503
1048
  * }
1504
1049
  * ```
1505
1050
  */
1506
- var ModalHandler = class extends ComponentHandler {};
1051
+ var ModalHandler = class extends ComponentHandler {
1052
+ /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1053
+ update(response) {
1054
+ this.ensureSourceMessage("update");
1055
+ return super.update(response);
1056
+ }
1057
+ /** Acknowledge the submit without changing the source message. Throws when a command opened the modal. */
1058
+ deferUpdate() {
1059
+ this.ensureSourceMessage("deferUpdate");
1060
+ return super.deferUpdate();
1061
+ }
1062
+ ensureSourceMessage(method) {
1063
+ if (this.event.isFromMessage()) return;
1064
+ throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ReplyUpdateWithoutSource, [method, this.routeId]);
1065
+ }
1066
+ };
1507
1067
 
1508
1068
  //#endregion
1509
1069
  //#region src/handlers/interaction/components/SelectMenuHandler.ts
@@ -1617,138 +1177,14 @@ var EventMiddleware = class extends BaseHandler {
1617
1177
  }
1618
1178
  /**
1619
1179
  * The event that fired this middleware. Read it on a catchall or multi-event middleware, where `this.event`
1620
- * is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
1621
- * `this.event` directly and does not need this.
1622
- */
1623
- get eventName() {
1624
- if (this.firedEvent === void 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.EventMiddlewareNameUnavailable);
1625
- return this.firedEvent;
1626
- }
1627
- };
1628
-
1629
- //#endregion
1630
- //#region src/pagination/controls.ts
1631
- const DEFAULT_LABEL = {
1632
- first: "First",
1633
- prev: "Prev",
1634
- indicator: "",
1635
- next: "Next",
1636
- last: "Last"
1637
- };
1638
- const CONTROL_SLOT = {
1639
- first: 0,
1640
- prev: 1,
1641
- indicator: 2,
1642
- next: 3,
1643
- last: 4
1644
- };
1645
- const MAX_BUTTONS_PER_ROW = 5;
1646
- var Controls = class {
1647
- cursor;
1648
- view;
1649
- constructor(cursor, view) {
1650
- this.cursor = cursor;
1651
- this.view = view;
1652
- }
1653
- button(key, cosmetics) {
1654
- const { target, disabled } = this.navState(key);
1655
- const defaultLabel = key === "indicator" ? this.indicatorText() : DEFAULT_LABEL[key];
1656
- const label = cosmetics?.label ?? (key === "indicator" || !cosmetics?.emoji ? defaultLabel : void 0);
1657
- const button = new _discordjs_builders.ButtonBuilder().setCustomId(this.cursor.encode({
1658
- page: Math.min(Math.max(0, target), _seedcord_core_internal.PAGE_MAX),
1659
- slot: CONTROL_SLOT[key]
1660
- })).setStyle(cosmetics?.style ?? discord_js.ButtonStyle.Secondary).setDisabled(disabled);
1661
- if (label !== void 0) button.setLabel(label);
1662
- if (cosmetics?.emoji) button.setEmoji(cosmetics.emoji);
1663
- return button;
1664
- }
1665
- navState(key) {
1666
- const { page, totalPages, hasPrev, hasNext } = this.view;
1667
- const knownTotal = totalPages !== void 0;
1668
- return {
1669
- target: {
1670
- first: 0,
1671
- prev: page - 1,
1672
- indicator: page,
1673
- next: page + 1,
1674
- last: knownTotal ? totalPages - 1 : page
1675
- }[key],
1676
- disabled: {
1677
- first: !hasPrev,
1678
- prev: !hasPrev,
1679
- indicator: true,
1680
- next: !hasNext || page + 1 > _seedcord_core_internal.PAGE_MAX,
1681
- last: !hasNext || !knownTotal || totalPages - 1 > _seedcord_core_internal.PAGE_MAX
1682
- }[key]
1683
- };
1684
- }
1685
- row(...keys) {
1686
- if (keys.length === 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationEmptyControls);
1687
- if (keys.length > MAX_BUTTONS_PER_ROW) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationTooManyControls, [keys.length]);
1688
- const seen = /* @__PURE__ */ new Set();
1689
- for (const key of keys) {
1690
- if (seen.has(key)) throw new _seedcord_errors_internal.SeedcordTypeError(_seedcord_errors.SeedcordErrorCode.PaginationDuplicateControls, [key]);
1691
- seen.add(key);
1692
- }
1693
- return new _discordjs_builders.ActionRowBuilder().addComponents(keys.map((key) => this.button(key)));
1694
- }
1695
- indicatorText() {
1696
- const { page, totalPages } = this.view;
1697
- return totalPages === void 0 ? `Page ${page + 1}` : `Page ${page + 1} of ${totalPages}`;
1698
- }
1699
- };
1700
-
1701
- //#endregion
1702
- //#region src/pagination/render.ts
1703
- var PageContainer = class extends _seedcord_core.BuilderComponent {
1704
- constructor(view, renderItem, controlRow) {
1705
- super("container");
1706
- const base = view.page * view.perPage;
1707
- let lines = [];
1708
- const flush = () => {
1709
- if (lines.length === 0) return;
1710
- this.instance.addTextDisplayComponents(new _discordjs_builders.TextDisplayBuilder().setContent(lines.join("\n")));
1711
- lines = [];
1712
- };
1713
- view.items.forEach((item, offset) => {
1714
- const rendered = renderItem(item, base + offset);
1715
- if (typeof rendered === "string") {
1716
- lines.push(rendered);
1717
- return;
1718
- }
1719
- flush();
1720
- this.instance.addSectionComponents(rendered.component);
1721
- });
1722
- flush();
1723
- this.instance.addActionRowComponents(controlRow);
1180
+ * is `never`, to do work that does not depend on the payload shape. A single-event middleware reads
1181
+ * `this.event` directly and does not need this.
1182
+ */
1183
+ get eventName() {
1184
+ if (this.firedEvent === void 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.EventMiddlewareNameUnavailable);
1185
+ return this.firedEvent;
1724
1186
  }
1725
1187
  };
1726
- function toReplyResponse(renderable) {
1727
- if (typeof renderable === "string") return { components: [new _discordjs_builders.TextDisplayBuilder().setContent(renderable)] };
1728
- if (Array.isArray(renderable)) return { components: renderable };
1729
- return renderable;
1730
- }
1731
- const ARRAY_KEYS = [
1732
- "first",
1733
- "prev",
1734
- "indicator",
1735
- "next",
1736
- "last"
1737
- ];
1738
- const CURSOR_KEYS = [
1739
- "prev",
1740
- "indicator",
1741
- "next"
1742
- ];
1743
- /**
1744
- * Build a page's V2 reply. A `render` override builds the whole tree, otherwise the default container lists
1745
- * the items and appends the controls (all five for a known total, prev/indicator/next for a cursor source).
1746
- */
1747
- function renderPage(view, cursor, config) {
1748
- const controls = new Controls(cursor, view);
1749
- if (config.render) return toReplyResponse(config.render(view, controls));
1750
- return { components: [new PageContainer(view, config.renderItem ?? String, controls.row(...view.totalPages === void 0 ? CURSOR_KEYS : ARRAY_KEYS)).component] };
1751
- }
1752
1188
 
1753
1189
  //#endregion
1754
1190
  //#region src/pagination/Paginator.ts
@@ -1757,7 +1193,7 @@ function contextOf(interaction, core) {
1757
1193
  interaction,
1758
1194
  user: interaction.user,
1759
1195
  guild: interaction.guild,
1760
- ...core && { core }
1196
+ core
1761
1197
  };
1762
1198
  }
1763
1199
  /**
@@ -1779,15 +1215,11 @@ function contextOf(interaction, core) {
1779
1215
  * export class BansNav extends Bans.Handler {}
1780
1216
  * ```
1781
1217
  */
1782
- var Paginator = class {
1783
- /** The page cursor, pass it to your `@ButtonRoute`. */
1784
- cursor;
1218
+ var Paginator = class extends _seedcord_core.PaginatorBase {
1785
1219
  /** The nav handler base. Extend it with an empty body and decorate it, `@ButtonRoute(p.cursor)`. */
1786
1220
  Handler;
1787
- config;
1788
1221
  constructor(config) {
1789
- this.cursor = (0, _seedcord_core_internal.pageCursor)(config.prefix);
1790
- this.config = config;
1222
+ super(config);
1791
1223
  const loadPage = (ctx, n) => this.page(ctx, n);
1792
1224
  this.Handler = class Nav extends ButtonHandler {
1793
1225
  async execute() {
@@ -1798,7 +1230,8 @@ var Paginator = class {
1798
1230
  };
1799
1231
  }
1800
1232
  /**
1801
- * Render page 0 and send it through the handler's sender, picking reply or followUp from its ack state.
1233
+ * Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
1234
+ * ack state.
1802
1235
  *
1803
1236
  * @param handler - The handler starting the paginator, normally `this`.
1804
1237
  */
@@ -1807,34 +1240,18 @@ var Paginator = class {
1807
1240
  const response = await this.page(contextOf(interaction, handler.core), 0);
1808
1241
  return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1809
1242
  }
1810
- /** Render a page as a {@link ReplyResponse}. To post it elsewhere, add `flags: MessageFlags.IsComponentsV2`. */
1811
- async page(ctx, n) {
1812
- return renderPage(await this.config.source.page(ctx, n), this.cursor, this.config);
1813
- }
1814
1243
  };
1815
1244
 
1816
1245
  //#endregion
1817
1246
  //#region src/pagination/sources.ts
1818
- const DEFAULT_PER_PAGE = 10;
1819
1247
  /**
1820
1248
  * A source for a bounded list you can load whole. It loads the full list on every click, then slices the
1821
- * page via {@link paginate}, so the real total is known (a real last button, "Page X of Y"). Cache inside
1822
- * your loader if the load is expensive.
1249
+ * page, so the real total is known (a real last button, "Page X of Y"). Cache inside your loader if the
1250
+ * load is expensive.
1823
1251
  *
1824
1252
  * @typeParam Item - The item type, inferred from the loader's return.
1825
1253
  */
1826
- var ArraySource = class {
1827
- load;
1828
- perPage;
1829
- constructor(load, opts) {
1830
- this.load = load;
1831
- this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
1832
- if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
1833
- }
1834
- async page(ctx, n) {
1835
- return (0, _seedcord_core.paginate)(await this.load(ctx), n, this.perPage);
1836
- }
1837
- };
1254
+ var ArraySource = class extends _seedcord_core.ArraySource {};
1838
1255
  /**
1839
1256
  * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1840
1257
  * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
@@ -1843,26 +1260,7 @@ var ArraySource = class {
1843
1260
  *
1844
1261
  * @typeParam Item - The item type, inferred from the fetcher's slice.
1845
1262
  */
1846
- var CursorSource = class {
1847
- fetch;
1848
- perPage;
1849
- constructor(fetch, opts) {
1850
- this.fetch = fetch;
1851
- this.perPage = opts?.perPage ?? DEFAULT_PER_PAGE;
1852
- if (!Number.isSafeInteger(this.perPage) || this.perPage <= 0) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.PaginationInvalidPerPage, [this.perPage]);
1853
- }
1854
- async page(ctx, n) {
1855
- const page = Math.max(0, Math.trunc(n));
1856
- const { items, hasNext } = await this.fetch(ctx, page, this.perPage);
1857
- return {
1858
- items: [...items],
1859
- page,
1860
- perPage: this.perPage,
1861
- hasPrev: page > 0,
1862
- hasNext
1863
- };
1864
- }
1865
- };
1263
+ var CursorSource = class extends _seedcord_core.CursorSource {};
1866
1264
 
1867
1265
  //#endregion
1868
1266
  //#region src/subscribers/index.ts
@@ -1886,236 +1284,12 @@ var WebhookLog = class extends _seedcord_core.WebhookLog {};
1886
1284
  * Base class for a gateway plugin, binding `this.core` to the gateway {@link Core}.
1887
1285
  *
1888
1286
  * A plugin that runs on either transport extends the base from `@seedcord/core/plugin`, whose
1889
- * `this.core` carries the shared members only.
1287
+ * `this.core` is typed to the shared members only.
1890
1288
  *
1891
- * @typeParam Opts - The transport, runtime, and capabilities the plugin declares.
1289
+ * @typeParam Opts - The transport and runtime the plugin declares.
1892
1290
  */
1893
1291
  var Plugin = class extends _seedcord_core_plugin.Plugin {};
1894
1292
 
1895
- //#endregion
1896
- //#region src/bot/utilities/miscellaneous/contextMenuLeaves.ts
1897
- /**
1898
- * Collects the names of every registered context-menu command across a set of command builders, split into a
1899
- * user set and a message set. The same builder may appear more than once (a guild command is pushed once per
1900
- * guild), and slash commands carry no context-menu name, so both collapse here. The split mirrors the
1901
- * controller's two name-keyed maps, so the boot guard checks each name against its own kind.
1902
- *
1903
- * @internal
1904
- */
1905
- function contextMenuLeaves(commands) {
1906
- const user = /* @__PURE__ */ new Set();
1907
- const message = /* @__PURE__ */ new Set();
1908
- for (const command of commands) {
1909
- if (!(command instanceof _discordjs_builders.ContextMenuCommandBuilder)) continue;
1910
- if (command.type === discord_js.ApplicationCommandType.User) user.add(command.name);
1911
- else message.add(command.name);
1912
- }
1913
- return {
1914
- user,
1915
- message
1916
- };
1917
- }
1918
-
1919
- //#endregion
1920
- //#region src/bot/utilities/miscellaneous/slashRouteLeaves.ts
1921
- /**
1922
- * Collects the route keys of every executable slash leaf across a set of command builders, deduplicated. The
1923
- * same builder may appear more than once (a guild command is pushed once per guild), and context-menu
1924
- * commands carry no slash route, so both collapse here. Route keys come from the same {@link routeLeavesOf}
1925
- * that `seedcord codegen` reads, so the boot-time check compares against the generated registry's keys.
1926
- *
1927
- * @internal
1928
- */
1929
- function slashRouteLeaves(commands) {
1930
- const leaves = /* @__PURE__ */ new Set();
1931
- for (const command of commands) {
1932
- if (!(command instanceof _discordjs_builders.SlashCommandBuilder)) continue;
1933
- for (const leaf of (0, _seedcord_utils_internal.routeLeavesOf)(command.toJSON())) leaves.add(leaf.route);
1934
- }
1935
- return leaves;
1936
- }
1937
-
1938
- //#endregion
1939
- //#region src/bot/controllers/CommandRegistry.ts
1940
- /**
1941
- * Manages Discord application command registration and deployment.
1942
- *
1943
- * Scans command directories, builds command structures, and registers both global and guild-scoped commands
1944
- * to Discord's API. Accessed via `core.bot.commands`. Do not construct it directly.
1945
- */
1946
- var CommandRegistry = class {
1947
- core;
1948
- onDeployed;
1949
- logger = new _seedcord_logger.Logger("Commands", { channel: "commands" });
1950
- isInitialised = false;
1951
- globalCommands = [];
1952
- guildCommands = new discord_js.Collection();
1953
- ctorToCommand = /* @__PURE__ */ new Map();
1954
- loading = false;
1955
- loadedCommands = [];
1956
- hmrHandler;
1957
- pendingEvents = /* @__PURE__ */ new Map();
1958
- constructor(core, onDeployed) {
1959
- this.core = core;
1960
- this.onDeployed = onDeployed;
1961
- const commandsDir = this.core.config.bot.commands.path;
1962
- if (!commandsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
1963
- if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
1964
- this.hmrHandler = new _seedcord_core_hmr.HmrModuleHandler({
1965
- handlersDir: commandsDir,
1966
- isHandler: this.isCommandClass.bind(this),
1967
- registerHandler: this.registerCommand.bind(this),
1968
- unregisterHandler: this.unregisterCommand.bind(this),
1969
- getArtifacts: this.getArtifacts.bind(this),
1970
- logger: this.logger
1971
- });
1972
- }
1973
- getArtifacts(ctor) {
1974
- return this.ctorToCommand.get(ctor);
1975
- }
1976
- /** @internal */
1977
- async init() {
1978
- if (this.isInitialised) return;
1979
- this.isInitialised = true;
1980
- const commandsDir = this.core.config.bot.commands.path;
1981
- if (!commandsDir) return;
1982
- this.loading = true;
1983
- this.loadedCommands.length = 0;
1984
- try {
1985
- await this.loadCommands(commandsDir);
1986
- } finally {
1987
- this.loading = false;
1988
- }
1989
- this.reportLoad();
1990
- (0, _seedcord_core_internal.getDevChannel)()?.on("seedcord:refresh-commands", (data) => {
1991
- this.refresh(data.shouldRefresh);
1992
- });
1993
- }
1994
- reportLoad() {
1995
- const { utils } = this.logger;
1996
- utils.summary("Loaded commands", {
1997
- global: this.globalCommands.length,
1998
- "guild groups": this.guildCommands.size
1999
- });
2000
- let slash = 0;
2001
- let menu = 0;
2002
- for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
2003
- else menu++;
2004
- utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
2005
- "slash commands": slash,
2006
- "context menus": menu
2007
- })]);
2008
- }
2009
- /** @internal */
2010
- async refresh(shouldRefresh = true) {
2011
- if (!shouldRefresh) {
2012
- this.logger.info(chalk.default.italic("Command refresh cancelled."));
2013
- this.pendingEvents.clear();
2014
- return;
2015
- }
2016
- this.logger.info(chalk.default.italic("Refreshing commands..."));
2017
- for (const event of this.pendingEvents.values()) await this.hmrHandler?.handle(event);
2018
- this.pendingEvents.clear();
2019
- await this.setCommands();
2020
- }
2021
- /** @internal */
2022
- async onHmr(event) {
2023
- const commandsDir = this.core.config.bot.commands.path;
2024
- if (commandsDir && event.file.startsWith((0, node_path.resolve)(process.cwd(), commandsDir))) {
2025
- this.pendingEvents.delete(event.file);
2026
- this.pendingEvents.set(event.file, event);
2027
- (0, _seedcord_core_internal.getDevChannel)()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => (0, _seedcord_utils.formatFilePath)(f)) });
2028
- } else await this.hmrHandler?.handle(event);
2029
- }
2030
- async loadCommands(dir) {
2031
- await (0, _seedcord_utils_node.traverseDirectory)(dir, (fullPath, rel, mod) => {
2032
- for (const exported of Object.values(mod)) if (this.isCommandClass(exported)) {
2033
- this.registerCommand(exported, rel);
2034
- this.hmrHandler?.trackHandler(fullPath, exported);
2035
- }
2036
- });
2037
- }
2038
- isCommandClass(obj) {
2039
- if (typeof obj !== "function") return false;
2040
- return obj.prototype instanceof _seedcord_core.BuilderComponent && Reflect.hasMetadata(_seedcord_core_internal.CommandMetadataKey, obj);
2041
- }
2042
- registerCommand(ctor, rel) {
2043
- const meta = Reflect.getMetadata(_seedcord_core_internal.CommandMetadataKey, ctor);
2044
- if (!meta) return;
2045
- const comp = new ctor().component;
2046
- const kind = comp instanceof _discordjs_builders.SlashCommandBuilder ? "slash command" : "context menu";
2047
- if (meta.scope === "global") this.globalCommands.push(comp);
2048
- else for (const g of meta.guilds) {
2049
- const arr = this.guildCommands.get(g) ?? [];
2050
- arr.push(comp);
2051
- this.guildCommands.set(g, arr);
2052
- }
2053
- this.ctorToCommand.set(ctor, {
2054
- name: comp.name,
2055
- scope: meta.scope,
2056
- ...meta.scope === "guild" && { guilds: meta.guilds }
2057
- });
2058
- if (this.loading) {
2059
- this.loadedCommands.push({
2060
- name: comp.name,
2061
- from: (0, _seedcord_utils.formatFilePath)(rel),
2062
- kind
2063
- });
2064
- return;
2065
- }
2066
- this.logger.utils.registration(comp.name, rel, `${meta.scope} ${kind}`);
2067
- }
2068
- unregisterCommand(ctor, artifacts) {
2069
- const info = artifacts ?? this.ctorToCommand.get(ctor);
2070
- if (!info) return;
2071
- if (info.scope === "global") {
2072
- const idx = this.globalCommands.findIndex((c) => c.name === info.name);
2073
- if (idx !== -1) this.globalCommands.splice(idx, 1);
2074
- } else for (const g of info.guilds ?? []) {
2075
- const arr = this.guildCommands.get(g);
2076
- if (arr) {
2077
- const idx = arr.findIndex((c) => c.name === info.name);
2078
- if (idx !== -1) arr.splice(idx, 1);
2079
- }
2080
- }
2081
- this.ctorToCommand.delete(ctor);
2082
- }
2083
- /** @internal */
2084
- async setCommands() {
2085
- const result = {
2086
- global: new discord_js.Collection(),
2087
- guilds: new discord_js.Collection()
2088
- };
2089
- if (this.globalCommands.length > 0) {
2090
- const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
2091
- if (deployed) result.global = deployed;
2092
- const tag = this.globalCommands.length === 1 ? "command" : "commands";
2093
- this.logger.utils.block(`Deployed ${this.globalCommands.length} global ${tag}`, this.logger.utils.wrap(this.globalCommands.map((command) => command.name)), "info");
2094
- }
2095
- for (const [guildId, commands] of this.guildCommands.entries()) {
2096
- const guild = this.core.bot.client.guilds.cache.get(guildId);
2097
- if (!guild) {
2098
- this.logger.warn(`Guild with ID ${guildId} not found, skipping command registration.`);
2099
- continue;
2100
- }
2101
- const deployed = await guild.commands.set(commands);
2102
- result.guilds.set(guildId, deployed);
2103
- const tag = commands.length === 1 ? "command" : "commands";
2104
- this.logger.utils.block(`Deployed ${commands.length} ${tag} to ${_seedcord_logger.paint.amber.bold(guild.name)}`, this.logger.utils.wrap(commands.map((command) => command.name)), "info");
2105
- }
2106
- this.onDeployed?.(result);
2107
- return result;
2108
- }
2109
- /** The deduplicated slash route keys across every global and guild command. @internal */
2110
- routeLeaves() {
2111
- return slashRouteLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2112
- }
2113
- /** The registered context-menu command names, split by kind, across every global and guild command. @internal */
2114
- contextMenuLeaves() {
2115
- return contextMenuLeaves([...this.globalCommands, ...this.guildCommands.values()].flat());
2116
- }
2117
- };
2118
-
2119
1293
  //#endregion
2120
1294
  //#region src/miscellaneous/deriveEventActor.ts
2121
1295
  function deriveEventActor(args) {
@@ -2193,6 +1367,154 @@ function eventGateContext(eventName, args, core) {
2193
1367
  };
2194
1368
  }
2195
1369
 
1370
+ //#endregion
1371
+ //#region src/miscellaneous/extractErrorResponse.ts
1372
+ const logger$2 = new _seedcord_logger.Logger("ErrorsHandling", { channel: "errors" });
1373
+ /**
1374
+ * Processes an error into a user-facing reply and routes its report.
1375
+ *
1376
+ * A {@link Notice} always renders its own reply. A reporting denial (`report: true`) additionally
1377
+ * publishes `handledException` on the interaction path. A raw, non-denial throw publishes
1378
+ * `unknownException` and renders the configured `defaultError` (or a generic {@link Fault}). One uuid is
1379
+ * threaded into both the reply and the bus payload.
1380
+ */
1381
+ function extractErrorResponse(error, core, origin) {
1382
+ const uuid = node_crypto.randomUUID();
1383
+ const developerUsername = core.config.notifications?.developerUsername;
1384
+ const ctx = developerUsername === void 0 ? { uuid } : {
1385
+ uuid,
1386
+ developerUsername
1387
+ };
1388
+ if (error instanceof _seedcord_core.Notice) {
1389
+ if (error.report) reportFault(error, core, origin, uuid);
1390
+ return {
1391
+ uuid,
1392
+ response: error.render(ctx)
1393
+ };
1394
+ }
1395
+ reportRawFault(error, core, origin, uuid);
1396
+ const override = core.config.errors?.defaultError;
1397
+ return {
1398
+ uuid,
1399
+ response: override ? new override(uuid).render(ctx) : new _seedcord_core.Fault().render(ctx)
1400
+ };
1401
+ }
1402
+ function withThrottle(core, origin, error, publish) {
1403
+ const throttle = _seedcord_core_internal.FaultThrottle.for(core);
1404
+ const key = faultKey(origin, error);
1405
+ if (!throttle.shouldReport(key)) {
1406
+ logger$2.debug(`throttled duplicate fault ${key}`);
1407
+ return;
1408
+ }
1409
+ publish();
1410
+ throttle.markReported(key);
1411
+ }
1412
+ function reportFault(denial, core, origin, uuid) {
1413
+ logger$2.error(`${denial.name}: ${uuid}`, denial);
1414
+ withThrottle(core, origin, denial, () => {
1415
+ if (origin.interaction) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
1416
+ denial,
1417
+ uuid,
1418
+ source: buildInteractionSource(origin.interaction)
1419
+ });
1420
+ else if (origin.event) core.bus[_seedcord_core_internal.PublishDefault]("handledException", {
1421
+ denial,
1422
+ uuid,
1423
+ source: buildEventSource(origin.event, origin)
1424
+ });
1425
+ else core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
1426
+ uuid,
1427
+ error: denial,
1428
+ ...scalarActors(origin),
1429
+ metadata: metadataFor(origin)
1430
+ });
1431
+ });
1432
+ }
1433
+ function causeLine(error) {
1434
+ const { cause } = error;
1435
+ if (!Error.isError(cause)) return "";
1436
+ const [first] = cause.message.split("\n");
1437
+ return `\ncaused by ${cause.name}: ${first ?? cause.message}`;
1438
+ }
1439
+ function reportRawFault(error, core, origin, uuid) {
1440
+ if (core.config.errors?.errorStack ?? false) logger$2.error(uuid, error);
1441
+ else logger$2.error(`${uuid} | ${error.message}${causeLine(error)}`);
1442
+ withThrottle(core, origin, error, () => {
1443
+ core.bus[_seedcord_core_internal.PublishDefault]("unknownException", {
1444
+ uuid,
1445
+ error,
1446
+ ...scalarActors(origin),
1447
+ metadata: metadataFor(origin)
1448
+ });
1449
+ });
1450
+ }
1451
+ function scalarActors(origin) {
1452
+ return {
1453
+ guild: origin.guild ? {
1454
+ id: origin.guild.id,
1455
+ name: origin.guild.name
1456
+ } : void 0,
1457
+ user: origin.user ? {
1458
+ id: origin.user.id,
1459
+ username: origin.user.username
1460
+ } : void 0
1461
+ };
1462
+ }
1463
+ function metadataFor(origin) {
1464
+ if (origin.event) return {
1465
+ eventName: origin.event.name,
1466
+ handler: origin.event.handler,
1467
+ args: origin.event.args
1468
+ };
1469
+ return origin.metadata;
1470
+ }
1471
+ function buildEventSource(event, origin) {
1472
+ return {
1473
+ kind: "event",
1474
+ eventName: event.name,
1475
+ handler: event.handler,
1476
+ userId: origin.user?.id ?? null,
1477
+ guildId: origin.guild?.id ?? null,
1478
+ channelId: event.channelId,
1479
+ raw: event.args
1480
+ };
1481
+ }
1482
+ function faultKey(origin, error) {
1483
+ const name = error instanceof _seedcord_core.Notice ? error.name : error instanceof discord_js.DiscordAPIError ? String(error.code) : error.constructor.name;
1484
+ if (origin.event) return `${origin.event.name}:${origin.event.handler}:${name}`;
1485
+ if (origin.interaction) return `${interactionRoute(origin.interaction)}:${name}`;
1486
+ if (origin.route) return `autocomplete:${origin.route}:${name}`;
1487
+ return `autocomplete:${name}`;
1488
+ }
1489
+ function interactionRoute(interaction) {
1490
+ if (interaction.isChatInputCommand()) return slashRouteOf(interaction);
1491
+ if (interaction.isContextMenuCommand()) return interaction.commandName;
1492
+ if (interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit()) return (0, _seedcord_core_internal.prefixOf)(interaction.customId) || interaction.customId;
1493
+ return "interaction";
1494
+ }
1495
+ function buildInteractionSource(interaction) {
1496
+ const command = interaction.isChatInputCommand() ? slashRouteOf(interaction) : interaction.isContextMenuCommand() ? interaction.commandName : null;
1497
+ const customId = interaction.isButton() || interaction.isAnySelectMenu() || interaction.isModalSubmit() ? interaction.customId : null;
1498
+ return {
1499
+ kind: "interaction",
1500
+ interactionKind: interactionKind(interaction),
1501
+ command,
1502
+ customId,
1503
+ userId: interaction.user.id,
1504
+ guildId: interaction.guildId,
1505
+ channelId: interaction.channelId,
1506
+ interactionId: interaction.id,
1507
+ raw: interaction
1508
+ };
1509
+ }
1510
+ function interactionKind(interaction) {
1511
+ if (interaction.isChatInputCommand()) return "slash";
1512
+ if (interaction.isContextMenuCommand()) return "context-menu";
1513
+ if (interaction.isButton()) return "button";
1514
+ if (interaction.isAnySelectMenu()) return "select";
1515
+ return "modal";
1516
+ }
1517
+
2196
1518
  //#endregion
2197
1519
  //#region src/bot/handleEventFault.ts
2198
1520
  const logger$1 = new _seedcord_logger.Logger("EventBoundary", { channel: "errors" });
@@ -2626,21 +1948,9 @@ var InteractionDispatcher = class {
2626
1948
  logger: this.logger
2627
1949
  });
2628
1950
  }
2629
- /**
2630
- * Warns for each command route with no registered `@SlashRoute` handler, which would fall through to
2631
- * UnhandledRepliable at runtime. A bot may route commands outside the registry, so this does not throw.
2632
- *
2633
- * @internal
2634
- */
2635
1951
  warnUnhandledRoutes(commandLeaves) {
2636
1952
  for (const route of commandLeaves) if (!this.slashMap.has(route)) this.logger.warn(`Slash route ${_seedcord_logger.paint.sky.bold(route)} has no registered ${chalk.default.bold("@SlashRoute")} handler.`);
2637
1953
  }
2638
- /**
2639
- * Warns for each context-menu command with no matching handler, which would fall through to UnhandledRepliable
2640
- * at runtime. Checked per kind since a user and a message command can share a name.
2641
- *
2642
- * @internal
2643
- */
2644
1954
  warnUnhandledContextMenuRoutes(leaves) {
2645
1955
  for (const name of leaves.user) if (!this.userContextMenuMap.has(name)) this.logger.warn(`User context menu ${_seedcord_logger.paint.sky.bold(name)} has no registered ${chalk.default.bold("@ContextMenuRoute")} handler.`);
2646
1956
  for (const name of leaves.message) if (!this.messageContextMenuMap.has(name)) this.logger.warn(`Message context menu ${_seedcord_logger.paint.sky.bold(name)} has no registered ${chalk.default.bold("@ContextMenuRoute")} handler.`);
@@ -2981,29 +2291,39 @@ var Bot = class {
2981
2291
  interactions;
2982
2292
  events;
2983
2293
  emojiInjector;
2984
- commands;
2985
- emojis = Emojis;
2986
- mentions = CommandMentions;
2294
+ commandRegistry;
2987
2295
  /** @internal */
2988
2296
  async onHmr(event) {
2989
2297
  if (this.interactions) await this.interactions.onHmr(event);
2990
2298
  if (this.events) await this.events.onHmr(event);
2991
- if (this.commands) await this.commands.onHmr(event);
2299
+ if (this.commandRegistry) await this.commandRegistry.onHmr(event);
2992
2300
  }
2993
2301
  /** @internal */
2994
2302
  constructor(core) {
2995
2303
  this._client = new discord_js.Client(core.config.bot.clientOptions);
2996
2304
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
2997
2305
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
2998
- if (core.config.bot.commands.path) {
2999
- const mentionInjector = new CommandMentionInjector(core);
3000
- this.commands = new CommandRegistry(core, (result) => {
3001
- mentionInjector.inject(result);
2306
+ const commandsDir = core.config.bot.commands.path;
2307
+ if (commandsDir) {
2308
+ const injector = new _seedcord_core_internal.CommandInjector();
2309
+ const registry = new _seedcord_core_node_internal.CommandRegistry({
2310
+ dir: commandsDir,
2311
+ rest: this._client.rest,
2312
+ applicationId: () => this.applicationId(),
2313
+ onDeployed: (result) => {
2314
+ injector.inject(result, registry.allCommands());
2315
+ }
3002
2316
  });
2317
+ this.commandRegistry = registry;
3003
2318
  }
3004
2319
  this.emojiInjector = new EmojiInjector(core);
3005
2320
  this.registerShutdownTasks(core);
3006
2321
  }
2322
+ applicationId() {
2323
+ const { application } = this._client;
2324
+ if (!application) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreApplicationUnavailable);
2325
+ return application.id;
2326
+ }
3007
2327
  registerShutdownTasks(core) {
3008
2328
  core.shutdown.addTask(_seedcord_core_node_internal.ShutdownPhase.Unbind, "stop-dispatch", () => {
3009
2329
  this.stopAccepting();
@@ -3028,11 +2348,11 @@ var Bot = class {
3028
2348
  if (this.events) await this.events.init();
3029
2349
  await this.login(token);
3030
2350
  await this.emojiInjector.init();
3031
- if (this.commands) {
3032
- await this.commands.init();
3033
- await this.commands.setCommands();
3034
- this.interactions?.warnUnhandledRoutes(this.commands.routeLeaves());
3035
- this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
2351
+ if (this.commandRegistry) {
2352
+ await this.commandRegistry.init();
2353
+ await this.commandRegistry.setCommands();
2354
+ this.interactions?.warnUnhandledRoutes(this.commandRegistry.routeLeaves());
2355
+ this.interactions?.warnUnhandledContextMenuRoutes(this.commandRegistry.contextMenuLeaves());
3036
2356
  }
3037
2357
  }
3038
2358
  /** @internal */
@@ -3061,7 +2381,7 @@ __decorate([(0, envapt_legacy.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) =>
3061
2381
  //#endregion
3062
2382
  //#region src/version.ts
3063
2383
  /** Package version */
3064
- const version = "0.1.0-next.5";
2384
+ const version = "0.1.0";
3065
2385
 
3066
2386
  //#endregion
3067
2387
  //#region src/Seedcord.ts
@@ -3084,6 +2404,8 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3084
2404
  subscribers;
3085
2405
  /** @see {@link Bot} */
3086
2406
  bot;
2407
+ /** The discord.js client's REST client. Its token is set during the Login phase. */
2408
+ rest;
3087
2409
  /** @see {@link IRateLimiter} */
3088
2410
  rateLimiter;
3089
2411
  /**
@@ -3100,6 +2422,7 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3100
2422
  this.bus = new _seedcord_core.Bus(this);
3101
2423
  this.subscribers = new _seedcord_core_node_internal.SubscriberLoader(this.bus, config.subscribers.path);
3102
2424
  this.bot = new Bot(this);
2425
+ this.rest = this.bot.client.rest;
3103
2426
  this.rateLimiter = config.store ?? new _seedcord_rate_limiter.MemoryRateLimiter();
3104
2427
  this.healthCheck = _seedcord_core_node_internal.HealthCheck.fromOption(this.shutdown, config.healthCheck);
3105
2428
  this.registerStartupTasks();
@@ -3108,13 +2431,6 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3108
2431
  get username() {
3109
2432
  return this.bot.client.user?.username;
3110
2433
  }
3111
- pluginCapabilities() {
3112
- return {
3113
- client: this.bot.client,
3114
- token: this.bot.botToken,
3115
- rest: this.bot.client.rest
3116
- };
3117
- }
3118
2434
  static reset() {
3119
2435
  super.reset();
3120
2436
  _seedcord_logger.LoggerChannelRegistry.instance.reset();
@@ -3167,7 +2483,6 @@ var Seedcord = class Seedcord extends _seedcord_core_node_internal.Pluggable {
3167
2483
  exports.ArraySource = ArraySource;
3168
2484
  exports.AutocompleteHandler = AutocompleteHandler;
3169
2485
  exports.ButtonHandler = ButtonHandler;
3170
- exports.CommandMentions = CommandMentions;
3171
2486
  exports.ContextMenuHandler = ContextMenuHandler;
3172
2487
  exports.CursorSource = CursorSource;
3173
2488
  exports.Emojis = Emojis;
@@ -3192,16 +2507,8 @@ exports.Subscriber = Subscriber;
3192
2507
  exports.WebhookLog = WebhookLog;
3193
2508
  exports.checkBotPermissions = checkBotPermissions;
3194
2509
  exports.checkPermissions = checkPermissions;
3195
- exports.fetchGuildMember = fetchGuildMember;
3196
- exports.fetchManyGuildMembers = fetchManyGuildMembers;
3197
- exports.fetchManyUsers = fetchManyUsers;
3198
- exports.fetchRole = fetchRole;
3199
- exports.fetchText = fetchText;
3200
- exports.fetchUser = fetchUser;
3201
- exports.getBotRole = getBotRole;
3202
2510
  exports.getConfirmation = getConfirmation;
3203
2511
  exports.hasPermsToAssign = hasPermsToAssign;
3204
- exports.updateMemberRoles = updateMemberRoles;
3205
2512
  exports.version = version;
3206
2513
  Object.keys(_seedcord_core).forEach(function (k) {
3207
2514
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {