@seedcord/http 0.4.0 → 0.5.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.
@@ -1,13 +1,13 @@
1
1
  import "reflect-metadata";
2
- import { SeedcordError, validateDiscordToken } from "@seedcord/errors/internal";
2
+ import { GatedMetadataKey, PublishDefault, RegisterDefaults, RegisterSubscriber, accessorStore, asError, clearStore, decodeComponentRoute, deferFlags, guardedAccessor, isEmojiTuple, outcomeFor, prefixOf, queuedMsFor, reportDispatch, reportedWrite, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
3
+ import { SeedcordError, SeedcordTypeError, applicationIdFromToken, validateDiscordToken } from "@seedcord/errors/internal";
4
+ import { Logger } from "@seedcord/logger";
3
5
  import { Converters, Envapter } from "envapt";
4
6
  import { DiscordAPIError, REST } from "@discordjs/rest";
5
- import { ArraySource, BaseHandler, Bus, CursorSource, DispatchContext, Fault, Notice, PaginatorBase, RepliableHandler, ResolvedEmoji, Silence, Subscriber, WebhookLog } from "@seedcord/core";
6
- import { BaseReplySender, GatedMetadataKey, PublishDefault, accessorStore, asError, clearStore, decodeComponentRoute, deferFlags, guardedAccessor, isEmojiTuple, outcomeFor, prefixOf, queuedMsFor, reportDispatch, reportedWrite, runHandlerGates, sendFlags, slowGateMonitor } from "@seedcord/core/internal";
7
+ import { ArraySourceBase, BaseHandler, BaseReplySender, Bus, CursorSourceBase, DispatchContext, Fault, Notice, PaginatorBase, RepliableHandler, ResolvedEmoji, Silence, Subscriber, WebhookLog } from "@seedcord/core";
7
8
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
8
- import { Logger } from "@seedcord/logger";
9
9
  import { MemoryRateLimiter } from "@seedcord/rate-limiter";
10
- import { ApplicationCommandOptionType, ApplicationCommandType, ComponentType, InteractionResponseType, InteractionType, MessageFlags, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
10
+ import { ApplicationCommandOptionType, ApplicationCommandType, ChannelType, ComponentType, InteractionResponseType, InteractionType, MessageFlags, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
11
11
  import { API } from "@discordjs/core/http-only";
12
12
  import { buildSlashRoute } from "@seedcord/utils/internal";
13
13
 
@@ -270,11 +270,21 @@ function logger() {
270
270
  dispatchLogger ??= new Logger("Dispatcher", { channel: "interactions" });
271
271
  return dispatchLogger;
272
272
  }
273
+ function noLifecycle(accessor) {
274
+ throw new SeedcordError(SeedcordErrorCode.CoreLifecycleUnavailable, [accessor]);
275
+ }
276
+ const edgeShutdown = { addTask: () => noLifecycle("shutdown") };
277
+ const edgeStartup = { addTask: () => noLifecycle("startup") };
273
278
  function createCore(config, token) {
274
279
  const draft = {
275
280
  config,
276
281
  rateLimiter: config.store ?? new MemoryRateLimiter(),
277
- rest: new REST().setToken(token)
282
+ rest: new REST().setToken(token),
283
+ shutdown: edgeShutdown,
284
+ startup: edgeStartup,
285
+ get applicationId() {
286
+ return applicationIdFromToken(token);
287
+ }
278
288
  };
279
289
  draft.bus = new Bus(draft);
280
290
  return draft;
@@ -434,7 +444,7 @@ async function dispatchInteraction(args) {
434
444
  core,
435
445
  payload,
436
446
  routeId,
437
- sender: handler instanceof RepliableHandler$1 ? handler.getSender() : null
447
+ sender: handler instanceof RepliableHandler$1 ? handler.sender : null
438
448
  };
439
449
  const refusal = await gateRefusal(ctor, match, payload, core);
440
450
  if (refusal) {
@@ -466,7 +476,7 @@ async function gateRefusal(ctor, match, payload, core) {
466
476
  //#endregion
467
477
  //#region src/dispatch/registerSubscribers.ts
468
478
  function registerSubscribers(bus, manifest) {
469
- for (const row of manifest.subscriberRoutes) bus.register({
479
+ for (const row of manifest.subscriberRoutes) bus[RegisterSubscriber]({
470
480
  keys: row.keys,
471
481
  frequency: row.frequency,
472
482
  resolve: async () => {
@@ -483,7 +493,7 @@ function registerSubscribers(bus, manifest) {
483
493
  row.from
484
494
  ]);
485
495
  const exported = module[row.exportName];
486
- if (typeof exported !== "function" || !(exported.prototype instanceof Subscriber)) throw new SeedcordError(SeedcordErrorCode.SubscriberRouteNotASubscriber, [
496
+ if (typeof exported !== "function" || !(exported.prototype instanceof Subscriber)) throw new SeedcordTypeError(SeedcordErrorCode.SubscriberRouteNotASubscriber, [
487
497
  keys,
488
498
  row.exportName,
489
499
  row.from
@@ -620,7 +630,7 @@ var AutocompleteHandler = class extends BaseHandler {
620
630
  const { name, value } = this.focused;
621
631
  const respond = (choices) => this.respond(choices);
622
632
  const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
623
- if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
633
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
624
634
  return await arm(value, respond);
625
635
  }
626
636
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
@@ -842,7 +852,7 @@ var Ed25519Verifier = class {
842
852
  key;
843
853
  constructor(publicKeyHex) {
844
854
  const publicKey = hexToBytes(publicKeyHex, PUBLIC_KEY_BYTES);
845
- if (!publicKey) throw new SeedcordError(SeedcordErrorCode.ConfigInvalidEnv, ["DISCORD_PUBLIC_KEY", "64 hex characters"]);
855
+ if (!publicKey) throw new SeedcordTypeError(SeedcordErrorCode.ConfigInvalidEnv, ["DISCORD_PUBLIC_KEY", "64 hex characters"]);
846
856
  this.key = crypto.subtle.importKey("raw", publicKey, "Ed25519", false, ["verify"]);
847
857
  }
848
858
  async verify(signatureHex, timestamp, body) {
@@ -979,24 +989,191 @@ function buildEngine(core, maps) {
979
989
  *
980
990
  * Reads `DISCORD_PUBLIC_KEY` and `DISCORD_BOT_TOKEN` from the environment through envapt and throws a
981
991
  * `SeedcordError` when either is missing or malformed.
992
+ *
993
+ * Applies `config.logger`. An omitted `logger` resets the level and the sinks to their defaults, the
994
+ * way `installNodeDefaults` does on the node hosts.
982
995
  */
983
996
  function createSeedcord(config, manifest) {
997
+ Logger.configure(config.logger ?? {});
984
998
  const token = validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
985
999
  const core = createCore(config, token);
986
- core.bus.registerDefaults();
1000
+ core.bus[RegisterDefaults]();
987
1001
  registerSubscribers(core.bus, manifest);
988
1002
  return buildEngine(core, buildRouteMaps(manifest)).handle;
989
1003
  }
990
1004
 
991
1005
  //#endregion
992
- //#region src/applicationId.ts
993
- async function fetchApplicationId(rest) {
994
- try {
995
- return (await rest.get(Routes.currentApplication())).id;
996
- } catch (error) {
997
- throw new SeedcordError(SeedcordErrorCode.CoreApplicationUnavailable, { cause: error });
1006
+ //#region src/inputs/pick.ts
1007
+ function pick(ids, bucket) {
1008
+ const found = /* @__PURE__ */ new Map();
1009
+ if (!ids || !bucket) return found;
1010
+ for (const id of ids) if (Object.hasOwn(bucket, id)) found.set(id, bucket[id]);
1011
+ return found;
1012
+ }
1013
+
1014
+ //#endregion
1015
+ //#region src/inputs/ModalFields.ts
1016
+ const FIELD_KINDS = {
1017
+ [ComponentType.TextInput]: {
1018
+ label: "a text input",
1019
+ getter: "getTextInputValue"
1020
+ },
1021
+ [ComponentType.StringSelect]: {
1022
+ label: "a string select",
1023
+ getter: "getStringSelectValues"
1024
+ },
1025
+ [ComponentType.UserSelect]: {
1026
+ label: "a user select",
1027
+ getter: "getSelectedUsers"
1028
+ },
1029
+ [ComponentType.RoleSelect]: {
1030
+ label: "a role select",
1031
+ getter: "getSelectedRoles"
1032
+ },
1033
+ [ComponentType.MentionableSelect]: {
1034
+ label: "a mentionable select",
1035
+ getter: "getSelectedMentionables"
1036
+ },
1037
+ [ComponentType.ChannelSelect]: {
1038
+ label: "a channel select",
1039
+ getter: "getSelectedChannels"
1040
+ },
1041
+ [ComponentType.FileUpload]: {
1042
+ label: "a file upload",
1043
+ getter: "getUploadedFiles"
1044
+ },
1045
+ [ComponentType.RadioGroup]: {
1046
+ label: "a radio group",
1047
+ getter: "getRadioGroup"
1048
+ },
1049
+ [ComponentType.CheckboxGroup]: {
1050
+ label: "a checkbox group",
1051
+ getter: "getCheckboxGroup"
1052
+ },
1053
+ [ComponentType.Checkbox]: {
1054
+ label: "a checkbox",
1055
+ getter: "getCheckbox"
998
1056
  }
1057
+ };
1058
+ function nameOfChannelType(type) {
1059
+ return ChannelType[type] ?? String(type);
999
1060
  }
1061
+ function listOf(values) {
1062
+ return values ?? [];
1063
+ }
1064
+ function isKind(entry, kinds) {
1065
+ return kinds.includes(entry.type);
1066
+ }
1067
+ /**
1068
+ * The fields a modal carries, read by custom id.
1069
+ *
1070
+ * Every getter throws when no field in the modal has that custom id. It also throws when the field holds a
1071
+ * kind that getter does not read. The message names the getter that does.
1072
+ *
1073
+ * A select getter resolves the ids its field picked against the interaction's `resolved` data and returns a
1074
+ * `Map` keyed by id. A field that picked nothing returns null. Pass `required` to throw there instead.
1075
+ */
1076
+ var ModalFields = class {
1077
+ entries = /* @__PURE__ */ new Map();
1078
+ resolved;
1079
+ constructor(data) {
1080
+ this.resolved = data.resolved;
1081
+ for (const component of data.components ?? []) {
1082
+ if ("components" in component) for (const child of component.components) this.entries.set(child.custom_id, child);
1083
+ if ("component" in component) this.entries.set(component.component.custom_id, component.component);
1084
+ }
1085
+ }
1086
+ entry(customId) {
1087
+ const found = this.entries.get(customId);
1088
+ if (!found) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldNotFound, [customId]);
1089
+ return found;
1090
+ }
1091
+ field(customId, kinds) {
1092
+ const entry = this.entry(customId);
1093
+ if (!isKind(entry, kinds)) {
1094
+ const known = FIELD_KINDS[entry.type];
1095
+ const args = known ? [
1096
+ customId,
1097
+ known.label,
1098
+ known.getter
1099
+ ] : [customId, `component type ${entry.type}`];
1100
+ throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldWrongKind, [...args]);
1101
+ }
1102
+ return entry;
1103
+ }
1104
+ selected(customId, kinds, bucket, required) {
1105
+ const { values } = this.field(customId, kinds);
1106
+ const found = pick(values, bucket);
1107
+ if (found.size > 0) return found;
1108
+ if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
1109
+ return null;
1110
+ }
1111
+ getField(customId, kind) {
1112
+ return kind === void 0 ? this.entry(customId) : this.field(customId, [kind]);
1113
+ }
1114
+ getTextInputValue(customId) {
1115
+ return this.field(customId, [ComponentType.TextInput]).value;
1116
+ }
1117
+ getStringSelectValues(customId) {
1118
+ return listOf(this.field(customId, [ComponentType.StringSelect]).values);
1119
+ }
1120
+ getSelectedUsers(customId, required = false) {
1121
+ const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
1122
+ return this.selected(customId, kinds, this.resolved?.users, required);
1123
+ }
1124
+ /** The guild members behind the picked users. Discord resolves these only inside a guild. */
1125
+ getSelectedMembers(customId) {
1126
+ const kinds = [ComponentType.UserSelect, ComponentType.MentionableSelect];
1127
+ return this.selected(customId, kinds, this.resolved?.members, false);
1128
+ }
1129
+ getSelectedRoles(customId, required = false) {
1130
+ const kinds = [ComponentType.RoleSelect, ComponentType.MentionableSelect];
1131
+ return this.selected(customId, kinds, this.resolved?.roles, required);
1132
+ }
1133
+ getSelectedChannels(customId, required = false, channelTypes = []) {
1134
+ const kinds = [ComponentType.ChannelSelect];
1135
+ const picked = this.selected(customId, kinds, this.resolved?.channels, required);
1136
+ if (!picked || channelTypes.length === 0) return picked;
1137
+ for (const found of picked.values()) {
1138
+ if (channelTypes.includes(found.type)) continue;
1139
+ const allowed = channelTypes.map(nameOfChannelType).join(", ");
1140
+ throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldChannelType, [
1141
+ customId,
1142
+ nameOfChannelType(found.type),
1143
+ allowed
1144
+ ]);
1145
+ }
1146
+ return picked;
1147
+ }
1148
+ getSelectedMentionables(customId, required = false) {
1149
+ const { values } = this.field(customId, [ComponentType.MentionableSelect]);
1150
+ const resolved = this.resolved;
1151
+ const picked = {
1152
+ users: pick(values, resolved?.users),
1153
+ members: pick(values, resolved?.members),
1154
+ roles: pick(values, resolved?.roles)
1155
+ };
1156
+ if (picked.users.size + picked.members.size + picked.roles.size > 0) return picked;
1157
+ if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
1158
+ return null;
1159
+ }
1160
+ getUploadedFiles(customId, required = false) {
1161
+ const kinds = [ComponentType.FileUpload];
1162
+ return this.selected(customId, kinds, this.resolved?.attachments, required);
1163
+ }
1164
+ getRadioGroup(customId, required = false) {
1165
+ const value = this.field(customId, [ComponentType.RadioGroup]).value ?? null;
1166
+ if (value !== null) return value;
1167
+ if (required) throw new SeedcordTypeError(SeedcordErrorCode.ModalFieldEmpty, [customId]);
1168
+ return null;
1169
+ }
1170
+ getCheckboxGroup(customId) {
1171
+ return listOf(this.field(customId, [ComponentType.CheckboxGroup]).values);
1172
+ }
1173
+ getCheckbox(customId) {
1174
+ return this.field(customId, [ComponentType.Checkbox]).value;
1175
+ }
1176
+ };
1000
1177
 
1001
1178
  //#endregion
1002
1179
  //#region src/emojis/EmojiInjector.ts
@@ -1011,16 +1188,14 @@ function byName(list) {
1011
1188
  }
1012
1189
  /**
1013
1190
  * The bot's resolved emojis, keyed by {@link EmojiMap}. Filled by {@link EmojiInjector} during startup.
1014
- * A read before that throws.
1191
+ * Throws if you read a key the injector did not fill.
1015
1192
  */
1016
1193
  const Emojis = guardedAccessor("Emojis", emojiStorage);
1017
1194
  var EmojiInjector = class {
1018
1195
  core;
1019
- applicationId;
1020
1196
  logger = new Logger("Emojis", { channel: "bot" });
1021
- constructor(core, applicationId = () => fetchApplicationId(core.rest)) {
1197
+ constructor(core) {
1022
1198
  this.core = core;
1023
- this.applicationId = applicationId;
1024
1199
  }
1025
1200
  async init() {
1026
1201
  clearStore(emojiStorage);
@@ -1042,7 +1217,7 @@ var EmojiInjector = class {
1042
1217
  }
1043
1218
  async applicationEmojis(failures) {
1044
1219
  try {
1045
- const appId = await this.applicationId();
1220
+ const appId = this.core.applicationId;
1046
1221
  return byName((await this.core.rest.get(Routes.applicationEmojis(appId))).items);
1047
1222
  } catch (error) {
1048
1223
  failures.push(` - the application emojis could not be read (${reasonOf(error)})`);
@@ -1086,9 +1261,14 @@ var EmojiInjector = class {
1086
1261
  var SlashHandler = class extends InteractionHandler {
1087
1262
  resolver;
1088
1263
  /**
1089
- * The typed options for this command's route. Required options drop the null, choices narrow to their
1090
- * literal union, and only the getters for kinds this command actually uses appear. Rich kinds resolve
1091
- * from the interaction's `resolved` data.
1264
+ * The typed options for this command's route. Only the getters for kinds this command declares appear.
1265
+ * Each getter's name argument is restricted to that command's options of its kind.
1266
+ *
1267
+ * - A required option drops the null.
1268
+ * - Choices narrow to their literal union.
1269
+ * - A channel option declared with `addChannelTypes` narrows to those types.
1270
+ *
1271
+ * Rich kinds resolve from the interaction's `resolved` data.
1092
1272
  */
1093
1273
  get options() {
1094
1274
  this.resolver ??= new HttpSlashOptions(this.event.data);
@@ -1108,13 +1288,18 @@ var SlashHandler = class extends InteractionHandler {
1108
1288
  async match(arms) {
1109
1289
  const route = slashRouteOf(this.event.data);
1110
1290
  const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1111
- if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1291
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1112
1292
  return await arm(this.options);
1113
1293
  }
1114
1294
  };
1115
1295
 
1116
1296
  //#endregion
1117
1297
  //#region src/handlers/interaction/ContextMenuHandler.ts
1298
+ function armFor(arms, name) {
1299
+ const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
1300
+ if (typeof arm !== "function") throw new SeedcordTypeError(SeedcordErrorCode.ContextMenuMatchArmMissing, [name]);
1301
+ return arm;
1302
+ }
1118
1303
  /**
1119
1304
  * Base class for a user context-menu command handler on the HTTP transport (right-click a user).
1120
1305
  *
@@ -1133,6 +1318,37 @@ var UserContextMenuHandler = class extends InteractionHandler {
1133
1318
  const { target_id: targetId, resolved } = this.event.data;
1134
1319
  return resolved.members?.[targetId] ?? null;
1135
1320
  }
1321
+ /** Which of the registered commands fired. */
1322
+ get commandName() {
1323
+ return this.event.data.name;
1324
+ }
1325
+ /**
1326
+ * Run the arm for whichever command fired. Use this only when the handler is registered for several
1327
+ * names.
1328
+ *
1329
+ * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
1330
+ * arm receives the clicked user and that user's guild member. The arms must cover every name in the
1331
+ * generic. A missing name or an unknown key is a compile error.
1332
+ *
1333
+ * @param arms - One handler per registered command name.
1334
+ * @returns The result of the arm that ran.
1335
+ *
1336
+ * @example
1337
+ * ```ts
1338
+ * \@UserContextMenuRoute('View Profile', 'Warn')
1339
+ * class Moderation extends UserContextMenuHandler<'View Profile' | 'Warn'> {
1340
+ * async execute() {
1341
+ * await this.match({
1342
+ * 'View Profile': (user) => this.reply(`Profile for ${user.username}.`),
1343
+ * Warn: (user, member) => this.reply(`Warned ${member?.nick ?? user.username}.`)
1344
+ * });
1345
+ * }
1346
+ * }
1347
+ * ```
1348
+ */
1349
+ async match(arms) {
1350
+ return await armFor(arms, this.event.data.name)(this.target, this.targetMember);
1351
+ }
1136
1352
  };
1137
1353
  /**
1138
1354
  * Base class for a message context-menu command handler on the HTTP transport (right-click a message).
@@ -1147,6 +1363,37 @@ var MessageContextMenuHandler = class extends InteractionHandler {
1147
1363
  const { target_id: targetId, resolved } = this.event.data;
1148
1364
  return resolved.messages[targetId];
1149
1365
  }
1366
+ /** Which of the registered commands fired. */
1367
+ get commandName() {
1368
+ return this.event.data.name;
1369
+ }
1370
+ /**
1371
+ * Run the arm for whichever command fired. Use this only when the handler is registered for several
1372
+ * names.
1373
+ *
1374
+ * Provide one arm per registered name, keyed by the name Discord shows in the right-click menu. Each
1375
+ * arm receives the clicked message. The arms must cover every name in the generic. A missing name or
1376
+ * an unknown key is a compile error.
1377
+ *
1378
+ * @param arms - One handler per registered command name.
1379
+ * @returns The result of the arm that ran.
1380
+ *
1381
+ * @example
1382
+ * ```ts
1383
+ * \@MessageContextMenuRoute('Report Message', 'Quote Message')
1384
+ * class MessageTools extends MessageContextMenuHandler<'Report Message' | 'Quote Message'> {
1385
+ * async execute() {
1386
+ * await this.match({
1387
+ * 'Report Message': (message) => this.report(message),
1388
+ * 'Quote Message': (message) => this.reply(`> ${message.content}`)
1389
+ * });
1390
+ * }
1391
+ * }
1392
+ * ```
1393
+ */
1394
+ async match(arms) {
1395
+ return await armFor(arms, this.event.data.name)(this.target);
1396
+ }
1150
1397
  };
1151
1398
 
1152
1399
  //#endregion
@@ -1215,7 +1462,7 @@ var ComponentHandler = class extends InteractionHandler {
1215
1462
  async match(arms) {
1216
1463
  const { prefix, params } = this.route;
1217
1464
  const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1218
- if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1465
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1219
1466
  return await arm(params);
1220
1467
  }
1221
1468
  };
@@ -1240,12 +1487,30 @@ var ButtonHandler = class extends ComponentHandler {};
1240
1487
  * Base class for a modal submit handler on the HTTP transport.
1241
1488
  *
1242
1489
  * Register the customId definitions this handler decodes with `@ModalRoute` and list the same ones in the
1243
- * generic. Read the submitted inputs off `this.event.data.components`. Reply through the handler members.
1244
- * Discord forbids opening a modal in response to a modal, so `showModal` fails compilation on this kind.
1490
+ * generic. Read the submitted inputs from `this.fields`. Reply through the handler members. `showModal`
1491
+ * fails compilation on this kind, because Discord forbids opening a modal in response to a modal.
1245
1492
  *
1246
1493
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof ConfigId]`.
1494
+ *
1495
+ * @example
1496
+ * ```ts
1497
+ * \@ModalRoute(ConfigId)
1498
+ * class ConfigModal extends ModalHandler<[typeof ConfigId]> {
1499
+ * async execute() {
1500
+ * const { guildId } = this.params;
1501
+ * const name = this.fields.getTextInputValue('name');
1502
+ * await this.reply(`saved ${name} for ${guildId}`);
1503
+ * }
1504
+ * }
1505
+ * ```
1247
1506
  */
1248
1507
  var ModalHandler = class extends ComponentHandler {
1508
+ reader;
1509
+ /** The fields this modal submitted, read by custom id. */
1510
+ get fields() {
1511
+ this.reader ??= new ModalFields(this.event.data);
1512
+ return this.reader;
1513
+ }
1249
1514
  /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1250
1515
  update(response) {
1251
1516
  this.ensureSourceMessage("update");
@@ -1267,15 +1532,55 @@ var ModalHandler = class extends ComponentHandler {
1267
1532
  /**
1268
1533
  * Base class for a select menu handler on the HTTP transport.
1269
1534
  *
1270
- * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1271
- * so `this.event.data` and its `values`/`resolved` are narrowed to that kind. Read `this.params` for a
1272
- * single route or `this.match` for several, and reply through the handler members or rewrite the source
1273
- * message with `this.update`.
1535
+ * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`.
1536
+ * Read the picked ids from `this.values`. The kind decides which of `this.users`, `this.members`,
1537
+ * `this.roles`, and `this.channels` this handler carries. The rest are `never`.
1538
+ *
1539
+ * Read `this.params` for a single route or `this.match` for several. Reply through the handler members, or
1540
+ * rewrite the source message with `this.update`.
1274
1541
  *
1275
1542
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1276
1543
  * @typeParam Defs - The customId definitions this handler decodes, e.g. `[typeof AssignId]`.
1544
+ *
1545
+ * @example
1546
+ * ```ts
1547
+ * \@SelectMenuRoute(SelectMenuKind.User, AssignId)
1548
+ * class AssignSelect extends SelectMenuHandler<SelectMenuKind.User, [typeof AssignId]> {
1549
+ * async execute() {
1550
+ * const { roleId } = this.params;
1551
+ * await this.reply(`assigning ${this.users.size} member(s) to <@&${roleId}>`);
1552
+ * }
1553
+ * }
1554
+ * ```
1277
1555
  */
1278
- var SelectMenuHandler = class extends ComponentHandler {};
1556
+ var SelectMenuHandler = class extends ComponentHandler {
1557
+ get selection() {
1558
+ return this.event.data;
1559
+ }
1560
+ resolvedMap(bucket) {
1561
+ return pick(this.selection.values, bucket);
1562
+ }
1563
+ /** The ids this select picked. */
1564
+ get values() {
1565
+ return this.selection.values ?? [];
1566
+ }
1567
+ /** The users this select picked. */
1568
+ get users() {
1569
+ return this.resolvedMap(this.selection.resolved?.users);
1570
+ }
1571
+ /** The guild members behind the picked users. Discord resolves these only inside a guild. */
1572
+ get members() {
1573
+ return this.resolvedMap(this.selection.resolved?.members);
1574
+ }
1575
+ /** The roles this select picked. */
1576
+ get roles() {
1577
+ return this.resolvedMap(this.selection.resolved?.roles);
1578
+ }
1579
+ /** The channels this select picked. */
1580
+ get channels() {
1581
+ return this.resolvedMap(this.selection.resolved?.channels);
1582
+ }
1583
+ };
1279
1584
 
1280
1585
  //#endregion
1281
1586
  //#region src/subscribers/index.ts
@@ -1327,25 +1632,33 @@ var Paginator = class extends PaginatorBase {
1327
1632
  Handler;
1328
1633
  constructor(config) {
1329
1634
  super(config);
1330
- const loadPage = (ctx, n) => this.page(ctx, n);
1635
+ const pageFor = (handler, n) => this.page(handler, n);
1331
1636
  this.Handler = class Nav extends ButtonHandler {
1332
1637
  async execute() {
1333
1638
  await this.deferUpdate();
1334
- const response = await loadPage(contextOf(this.event, this.core), this.params.page);
1639
+ const response = await pageFor(this, this.params.page);
1335
1640
  await this.update(response);
1336
1641
  }
1337
1642
  };
1338
1643
  }
1339
1644
  /**
1340
- * Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
1341
- * ack state.
1645
+ * Render a page as a {@link ReplyResponse} without sending anything.
1646
+ *
1647
+ * @param handler - The handler rendering the page, normally `this`.
1648
+ * @param n - The zero-based page.
1649
+ */
1650
+ async page(handler, n) {
1651
+ return this.buildPage(contextOf(handler.getEvent(), handler.core), n);
1652
+ }
1653
+ /**
1654
+ * Send a page through the handler's sender. The sender picks reply, edit, or followUp from its ack state.
1342
1655
  *
1343
1656
  * @param handler - The handler starting the paginator, normally `this`.
1657
+ * @param n - The zero-based page to open on.
1344
1658
  */
1345
- async start(handler) {
1346
- const interaction = handler.getEvent();
1347
- const response = await this.page(contextOf(interaction, handler.core), 0);
1348
- return handler.getSender().send(response, { ephemeral: this.config.ephemeral ?? false });
1659
+ async start(handler, n = 0) {
1660
+ const response = await this.page(handler, n);
1661
+ return handler.sender.send(response, { ephemeral: this.config.ephemeral ?? false });
1349
1662
  }
1350
1663
  };
1351
1664
 
@@ -1358,7 +1671,7 @@ var Paginator = class extends PaginatorBase {
1358
1671
  *
1359
1672
  * @typeParam Item - The item type, inferred from the loader's return.
1360
1673
  */
1361
- var ArraySource$1 = class extends ArraySource {};
1674
+ var ArraySource = class extends ArraySourceBase {};
1362
1675
  /**
1363
1676
  * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1364
1677
  * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
@@ -1367,7 +1680,7 @@ var ArraySource$1 = class extends ArraySource {};
1367
1680
  *
1368
1681
  * @typeParam Item - The item type, inferred from the fetcher's slice.
1369
1682
  */
1370
- var CursorSource$1 = class extends CursorSource {};
1683
+ var CursorSource = class extends CursorSourceBase {};
1371
1684
 
1372
1685
  //#endregion
1373
1686
  //#region src/gates/Gated.ts
@@ -1412,8 +1725,8 @@ function Gated(...gates) {
1412
1725
 
1413
1726
  //#endregion
1414
1727
  //#region src/version.ts
1415
- const version = "0.4.0";
1728
+ const version = "0.5.0";
1416
1729
 
1417
1730
  //#endregion
1418
- export { RepliableHandler$1 as C, AutocompleteHandler as S, fetchApplicationId as _, Paginator as a, buildRouteMaps as b, SelectMenuHandler as c, ComponentHandler as d, MessageContextMenuHandler as f, Emojis as g, EmojiInjector as h, CursorSource$1 as i, ModalHandler as l, SlashHandler as m, Gated as n, Subscriber$1 as o, UserContextMenuHandler as p, ArraySource$1 as r, WebhookLog$1 as s, version as t, ButtonHandler as u, createSeedcord as v, InteractionHandler as x, buildEngine as y };
1419
- //# sourceMappingURL=edge.index-D9PlLJ9b.mjs.map
1731
+ export { RepliableHandler$1 as C, AutocompleteHandler as S, ModalFields as _, Paginator as a, buildRouteMaps as b, SelectMenuHandler as c, ComponentHandler as d, MessageContextMenuHandler as f, Emojis as g, EmojiInjector as h, CursorSource as i, ModalHandler as l, SlashHandler as m, Gated as n, Subscriber$1 as o, UserContextMenuHandler as p, ArraySource as r, WebhookLog$1 as s, version as t, ButtonHandler as u, createSeedcord as v, ReplySender as w, InteractionHandler as x, buildEngine as y };
1732
+ //# sourceMappingURL=edge.index-DwRPmkh2.mjs.map