seedcord 0.13.0 → 0.14.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
@@ -1217,67 +1217,144 @@ var EmojiInjector = class {
1217
1217
  async init() {
1218
1218
  this.clearEmojis();
1219
1219
  if (!this.core.config.bot.emojis || Object.keys(this.core.config.bot.emojis).length === 0) {
1220
- this.logger.info(chalk.default.bold.yellow("No emojis configured, skipping emoji injection."));
1220
+ this.logger.debug(chalk.default.bold.yellow("No emojis configured, skipping emoji injection."));
1221
1221
  return;
1222
1222
  }
1223
1223
  const configEmojis = this.core.config.bot.emojis;
1224
1224
  await this.core.bot.client.application?.emojis.fetch();
1225
- let foundCount = 0;
1226
- const entries = Object.entries(configEmojis);
1227
- for (const [key, value] of entries) {
1228
- if (isEmojiTuple(value)) {
1229
- foundCount += this.handleTuple(key, value);
1230
- continue;
1231
- }
1232
- if (typeof value === "string") {
1233
- foundCount += this.handleString(key, value);
1234
- continue;
1235
- }
1236
- this.logger.warn(`${chalk.default.bold.yellow("Invalid")}: ${chalk.default.magenta.bold(String(key))} (expected string or [string, string])`);
1237
- }
1238
- this.logger.utils.summary("Loaded emojis", { emojis: foundCount });
1239
- }
1240
- /**
1241
- * Tuple `[emojiName, guildId]`: looks up the emoji in that guild. Returns 1 when found and stored as an emoji object, otherwise 0.
1242
- */
1243
- handleTuple(key, value) {
1225
+ const failures = [];
1226
+ const fetchedGuilds = /* @__PURE__ */ new Set();
1227
+ for (const [key, value] of Object.entries(configEmojis)) if (isEmojiTuple(value)) await this.resolveTuple(key, value, failures, fetchedGuilds);
1228
+ else if (typeof value === "string") this.resolveString(key, value, failures);
1229
+ else failures.push(` - "${key}" has an invalid value (expected a name or [name, guildId])`);
1230
+ if (failures.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigEmojiUnresolved, [failures.length, failures.join("\n")]);
1231
+ this.logger.utils.summary("Loaded emojis", { emojis: Object.keys(emojiStorage).length });
1232
+ }
1233
+ async resolveTuple(key, value, failures, fetchedGuilds) {
1244
1234
  const [emojiName, guildId] = value;
1245
1235
  const guild = this.core.bot.client.guilds.cache.get(guildId);
1246
1236
  if (!guild) {
1247
- emojiStorage[key] = emojiName;
1248
- this.logger.warn(`${chalk.default.bold.yellow("Missing")}: ${chalk.default.magenta.bold(emojiName)} in guild ${chalk.default.gray(guildId)} (guild not in cache or not found, using provided string)`);
1249
- return 0;
1237
+ failures.push(` - "${emojiName}" for "${key}" targets guild ${guildId}, which is not available (check the Guilds intent)`);
1238
+ return;
1239
+ }
1240
+ if (!fetchedGuilds.has(guildId)) {
1241
+ await guild.emojis.fetch();
1242
+ fetchedGuilds.add(guildId);
1250
1243
  }
1251
1244
  const guildEmoji = guild.emojis.cache.find((e) => e.name === emojiName);
1252
- if (guildEmoji) {
1253
- emojiStorage[key] = guildEmoji;
1254
- this.logger.debug(`${chalk.default.bold.green("Found")}: ${chalk.default.magenta.bold(emojiName)} (${guildEmoji.id}) in guild ${chalk.default.gray(guildId)}`);
1255
- return 1;
1245
+ if (!guildEmoji) {
1246
+ failures.push(` - "${emojiName}" for "${key}" was not found in guild ${guildId}`);
1247
+ return;
1256
1248
  }
1257
- emojiStorage[key] = emojiName;
1258
- this.logger.warn(`${chalk.default.bold.yellow("Missing")}: ${chalk.default.magenta.bold(emojiName)} in guild ${chalk.default.magenta.bold(guildId)} (using provided string)`);
1259
- return 0;
1249
+ emojiStorage[key] = guildEmoji;
1260
1250
  }
1261
- /**
1262
- * Handle emoji config values provided as a simple string (application emoji lookup).
1263
- * Returns 1 when the emoji was found and stored as an emoji object, otherwise 0.
1264
- */
1265
- handleString(key, emojiName) {
1251
+ resolveString(key, emojiName, failures) {
1266
1252
  const appEmoji = this.core.bot.client.application?.emojis.cache.find((e) => e.name === emojiName);
1267
- if (appEmoji) {
1268
- emojiStorage[key] = appEmoji;
1269
- this.logger.debug(`${chalk.default.bold.green("Found")}: ${chalk.default.magenta.bold(emojiName)} (${appEmoji.id})`);
1270
- return 1;
1253
+ if (!appEmoji) {
1254
+ failures.push(` - "${emojiName}" for "${key}" was not found among the application emojis`);
1255
+ return;
1271
1256
  }
1272
- emojiStorage[key] = emojiName;
1273
- this.logger.warn(`${chalk.default.bold.yellow("Missing")}: ${chalk.default.magenta.bold(emojiName)} (using provided string)`);
1274
- return 0;
1257
+ emojiStorage[key] = appEmoji;
1275
1258
  }
1276
1259
  clearEmojis() {
1277
1260
  for (const key of Object.keys(emojiStorage)) Reflect.deleteProperty(emojiStorage, key);
1278
1261
  }
1279
1262
  };
1280
1263
 
1264
+ //#endregion
1265
+ //#region src/bot/injectors/CommandMentionInjector.ts
1266
+ const mentionStorage = {};
1267
+ /**
1268
+ * Ready-to-send command mention strings, keyed by {@link SlashOptionRegistry}.
1269
+ *
1270
+ * A value is a clickable `</name:id>` when the command's id resolved from the deploy, otherwise the plain
1271
+ * `/name` text. Populated by {@link CommandMentionInjector} after each deploy.
1272
+ */
1273
+ const CommandMentions = mentionStorage;
1274
+ const MULTI_GUILD = Symbol("seedcord:commandmentions:multi-guild");
1275
+ /**
1276
+ * Turns each deployed slash route into a clickable command mention exposed through {@link CommandMentions}.
1277
+ *
1278
+ * Consumes the ids {@link DeployResult} carries, so it runs after a deploy rather than resolving from a name
1279
+ * cache (discord.js offers none for command ids). A global command is clickable everywhere by its app-wide id.
1280
+ * A guild command deployed to one guild is clickable by that guild's id. A command deployed to two or more
1281
+ * guilds mints a different id per guild, so one accessor key cannot resolve it, and it falls back to plain
1282
+ * `/name` text with one warn. On a non-owning shard a guild command also falls back to plain text.
1283
+ *
1284
+ * @internal
1285
+ */
1286
+ var CommandMentionInjector = class {
1287
+ core;
1288
+ logger = new _seedcord_services.Logger("CommandMentions");
1289
+ constructor(core) {
1290
+ this.core = core;
1291
+ }
1292
+ inject(deploy) {
1293
+ this.clear();
1294
+ const globalIds = this.indexByName(deploy.global);
1295
+ const guildIds = this.indexGuilds(deploy.guilds);
1296
+ const warned = /* @__PURE__ */ new Set();
1297
+ let clickable = 0;
1298
+ for (const command of this.allSlashBuilders()) {
1299
+ const id = this.resolveId(command.name, globalIds, guildIds, warned);
1300
+ for (const leaf of (0, _seedcord_utils_internal.routeLeavesOf)(command.toJSON())) {
1301
+ mentionStorage[leaf.route] = id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route);
1302
+ if (id) clickable++;
1303
+ }
1304
+ }
1305
+ this.logger.utils.summary("Linked mentions", {
1306
+ clickable,
1307
+ plain: Object.keys(mentionStorage).length - clickable
1308
+ });
1309
+ }
1310
+ allSlashBuilders() {
1311
+ const registry = this.core.bot.commands;
1312
+ if (!registry) return [];
1313
+ const slash = [...registry.globalCommands, ...registry.guildCommands.values()].flat().filter((command) => command instanceof discord_js.SlashCommandBuilder);
1314
+ return [...new Set(slash)];
1315
+ }
1316
+ indexByName(collection) {
1317
+ const map = /* @__PURE__ */ new Map();
1318
+ for (const command of collection.values()) map.set(command.name, command.id);
1319
+ return map;
1320
+ }
1321
+ indexGuilds(guilds) {
1322
+ const map = /* @__PURE__ */ new Map();
1323
+ for (const collection of guilds.values()) for (const command of collection.values()) {
1324
+ const existing = map.get(command.name);
1325
+ if (existing === void 0) map.set(command.name, command.id);
1326
+ else if (existing !== command.id) map.set(command.name, MULTI_GUILD);
1327
+ }
1328
+ return map;
1329
+ }
1330
+ resolveId(name, globalIds, guildIds, warned) {
1331
+ const global = globalIds.get(name);
1332
+ if (global) return global;
1333
+ const guild = guildIds.get(name);
1334
+ if (guild === void 0) return void 0;
1335
+ if (guild === MULTI_GUILD) {
1336
+ if (!warned.has(name)) {
1337
+ this.logger.warn(`${name} is deployed to multiple guilds, falling back to plain text (not clickable).`);
1338
+ warned.add(name);
1339
+ }
1340
+ return;
1341
+ }
1342
+ return guild;
1343
+ }
1344
+ toMention(route, id) {
1345
+ const [name, middle, sub] = route.split("/");
1346
+ if (name && middle && sub) return (0, discord_js.chatInputApplicationCommandMention)(name, middle, sub, id);
1347
+ if (name && middle) return (0, discord_js.chatInputApplicationCommandMention)(name, middle, id);
1348
+ return (0, discord_js.chatInputApplicationCommandMention)(name ?? route, id);
1349
+ }
1350
+ toPlain(route) {
1351
+ return `/${route.split("/").join(" ")}`;
1352
+ }
1353
+ clear() {
1354
+ for (const key of Object.keys(mentionStorage)) Reflect.deleteProperty(mentionStorage, key);
1355
+ }
1356
+ };
1357
+
1281
1358
  //#endregion
1282
1359
  //#region src/bot/utilities/channels/fetchText.ts
1283
1360
  /**
@@ -2597,6 +2674,7 @@ function slashRouteLeaves(commands) {
2597
2674
  */
2598
2675
  var CommandRegistry = class {
2599
2676
  core;
2677
+ onDeployed;
2600
2678
  name = "Commands";
2601
2679
  logger = new _seedcord_services.Logger("Commands");
2602
2680
  isInitialised = false;
@@ -2605,8 +2683,9 @@ var CommandRegistry = class {
2605
2683
  ctorToCommand = /* @__PURE__ */ new Map();
2606
2684
  hmrHandler;
2607
2685
  pendingEvents = /* @__PURE__ */ new Map();
2608
- constructor(core) {
2686
+ constructor(core, onDeployed) {
2609
2687
  this.core = core;
2688
+ this.onDeployed = onDeployed;
2610
2689
  const commandsDir = this.core.config.bot.commands.path;
2611
2690
  if (!commandsDir) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
2612
2691
  if (!envapt.Envapter.isDevelopment && !envapt.Envapter.isTest) return;
@@ -2711,8 +2790,13 @@ var CommandRegistry = class {
2711
2790
  this.ctorToCommand.delete(ctor);
2712
2791
  }
2713
2792
  async setCommands() {
2793
+ const result = {
2794
+ global: new discord_js.Collection(),
2795
+ guilds: new discord_js.Collection()
2796
+ };
2714
2797
  if (this.globalCommands.length > 0) {
2715
- await this.core.bot.client.application?.commands.set(this.globalCommands);
2798
+ const deployed = await this.core.bot.client.application?.commands.set(this.globalCommands);
2799
+ if (deployed) result.global = deployed;
2716
2800
  const tag = this.globalCommands.length === 1 ? "command" : "commands";
2717
2801
  this.logger.utils.summary("Configured global", { [tag]: this.globalCommands.length });
2718
2802
  this.logger.utils.item(`${this.globalCommands.map((command) => chalk.default.bold.cyan(command.name)).join(", ")}`);
@@ -2723,11 +2807,14 @@ var CommandRegistry = class {
2723
2807
  this.logger.warn(`Guild with ID ${guildId} not found, skipping command registration.`);
2724
2808
  continue;
2725
2809
  }
2726
- await guild.commands.set(commands);
2810
+ const deployed = await guild.commands.set(commands);
2811
+ result.guilds.set(guildId, deployed);
2727
2812
  const tag = commands.length === 1 ? "command" : "commands";
2728
2813
  this.logger.utils.summary(`Configured commands for ${chalk.default.bold.yellow(guild.name)}`, { [tag]: commands.length });
2729
2814
  this.logger.utils.item(`${commands.map((command) => chalk.default.bold.cyan(command.name)).join(", ")}`);
2730
2815
  }
2816
+ this.onDeployed?.(result);
2817
+ return result;
2731
2818
  }
2732
2819
  /** The deduplicated slash route keys across every global and guild command. @internal */
2733
2820
  routeLeaves() {
@@ -3505,22 +3592,6 @@ var InteractionDispatcher = class {
3505
3592
  }
3506
3593
  };
3507
3594
 
3508
- //#endregion
3509
- //#region src/miscellaneous/validateDiscordToken.ts
3510
- /**
3511
- * Checks if the token is present, is a string, and matches the general format of a Discord token.
3512
- *
3513
- * @internal
3514
- */
3515
- function validateDiscordToken(raw) {
3516
- if (raw === null || raw === void 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigMissingDiscordToken);
3517
- if (typeof raw !== "string") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigIncorrectDiscordToken);
3518
- const value = raw.trim();
3519
- if (value === "") throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigMissingDiscordToken);
3520
- if (!/^[A-Za-z\d_-]{24,}\.[A-Za-z\d_-]{6,}\.[A-Za-z\d_-]{27,}$/.test(value)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.ConfigIncorrectDiscordToken);
3521
- return value;
3522
- }
3523
-
3524
3595
  //#endregion
3525
3596
  //#region \0@oxc-project+runtime@0.135.0/helpers/esm/decorateMetadata.js
3526
3597
  function __decorateMetadata(k, v) {
@@ -3553,6 +3624,7 @@ var Bot = class extends Plugin {
3553
3624
  commands;
3554
3625
  emojiInjector;
3555
3626
  emojis = Emojis;
3627
+ mentions = CommandMentions;
3556
3628
  /** @internal For use in dev mode */
3557
3629
  async onHmr(event) {
3558
3630
  if (this.interactions) await this.interactions.onHmr(event);
@@ -3565,7 +3637,12 @@ var Bot = class extends Plugin {
3565
3637
  this._client = new discord_js.Client(core.config.bot.clientOptions);
3566
3638
  if (core.config.bot.interactions.path) this.interactions = new InteractionDispatcher(core);
3567
3639
  if (core.config.bot.events.path) this.events = new EventDispatcher(core);
3568
- if (core.config.bot.commands.path) this.commands = new CommandRegistry(core);
3640
+ if (core.config.bot.commands.path) {
3641
+ const mentionInjector = new CommandMentionInjector(core);
3642
+ this.commands = new CommandRegistry(core, (result) => {
3643
+ mentionInjector.inject(result);
3644
+ });
3645
+ }
3569
3646
  this.emojiInjector = new EmojiInjector(core);
3570
3647
  core.shutdown.addTask(_seedcord_services.ShutdownPhase.DiscordCleanup, "stop-bot", async () => await this.stop(), 2e3);
3571
3648
  }
@@ -3580,13 +3657,13 @@ var Bot = class extends Plugin {
3580
3657
  if (this.interactions) await this.interactions.init();
3581
3658
  if (this.events) await this.events.init();
3582
3659
  await this.login(token);
3660
+ await this.emojiInjector.init();
3583
3661
  if (this.commands) {
3584
3662
  await this.commands.init();
3585
3663
  await this.commands.setCommands();
3586
3664
  this.interactions?.warnUnhandledRoutes(this.commands.routeLeaves());
3587
3665
  this.interactions?.warnUnhandledContextMenuRoutes(this.commands.contextMenuLeaves());
3588
3666
  }
3589
- await this.emojiInjector.init();
3590
3667
  }
3591
3668
  /**
3592
3669
  * Stops the bot and cleans up connections
@@ -3618,7 +3695,7 @@ var Bot = class extends Plugin {
3618
3695
  return super.emit(event, ...args);
3619
3696
  }
3620
3697
  };
3621
- __decorate([(0, envapt.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => validateDiscordToken(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3698
+ __decorate([(0, envapt.Envapt)("DISCORD_BOT_TOKEN", { converter: (raw) => (0, _seedcord_errors_internal.validateDiscordToken)(raw) }), __decorateMetadata("design:type", String)], Bot.prototype, "botToken", void 0);
3622
3699
 
3623
3700
  //#endregion
3624
3701
  //#region src/hmr/HmrManager.ts
@@ -4048,13 +4125,14 @@ var Seedcord = class Seedcord extends Pluggable {
4048
4125
  //#endregion
4049
4126
  //#region src/index.ts
4050
4127
  /** Package version */
4051
- const version = "0.13.0";
4128
+ const version = "0.14.0";
4052
4129
 
4053
4130
  //#endregion
4054
4131
  exports.AutocompleteHandler = AutocompleteHandler;
4055
4132
  exports.AutocompleteRoute = AutocompleteRoute;
4056
4133
  exports.ButtonHandler = ButtonHandler;
4057
4134
  exports.ButtonRoute = ButtonRoute;
4135
+ exports.CommandMentions = CommandMentions;
4058
4136
  exports.ContextMenuHandler = ContextMenuHandler;
4059
4137
  exports.ContextMenuRoute = ContextMenuRoute;
4060
4138
  exports.Cooldown = Cooldown;