@seedcord/http 0.4.1 → 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, applicationIdFromToken, 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,18 @@ 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
282
  rest: new REST().setToken(token),
283
+ shutdown: edgeShutdown,
284
+ startup: edgeStartup,
278
285
  get applicationId() {
279
286
  return applicationIdFromToken(token);
280
287
  }
@@ -437,7 +444,7 @@ async function dispatchInteraction(args) {
437
444
  core,
438
445
  payload,
439
446
  routeId,
440
- sender: handler instanceof RepliableHandler$1 ? handler.getSender() : null
447
+ sender: handler instanceof RepliableHandler$1 ? handler.sender : null
441
448
  };
442
449
  const refusal = await gateRefusal(ctor, match, payload, core);
443
450
  if (refusal) {
@@ -469,7 +476,7 @@ async function gateRefusal(ctor, match, payload, core) {
469
476
  //#endregion
470
477
  //#region src/dispatch/registerSubscribers.ts
471
478
  function registerSubscribers(bus, manifest) {
472
- for (const row of manifest.subscriberRoutes) bus.register({
479
+ for (const row of manifest.subscriberRoutes) bus[RegisterSubscriber]({
473
480
  keys: row.keys,
474
481
  frequency: row.frequency,
475
482
  resolve: async () => {
@@ -486,7 +493,7 @@ function registerSubscribers(bus, manifest) {
486
493
  row.from
487
494
  ]);
488
495
  const exported = module[row.exportName];
489
- 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, [
490
497
  keys,
491
498
  row.exportName,
492
499
  row.from
@@ -623,7 +630,7 @@ var AutocompleteHandler = class extends BaseHandler {
623
630
  const { name, value } = this.focused;
624
631
  const respond = (choices) => this.respond(choices);
625
632
  const arm = Object.hasOwn(arms, name) ? arms[name] : void 0;
626
- if (!arm) throw new SeedcordError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
633
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.AutocompleteMatchArmMissing, [name]);
627
634
  return await arm(value, respond);
628
635
  }
629
636
  /** Send autocomplete suggestions, callback type 8. Prefer {@link match}, which restricts each field's choices to its declared type. */
@@ -845,7 +852,7 @@ var Ed25519Verifier = class {
845
852
  key;
846
853
  constructor(publicKeyHex) {
847
854
  const publicKey = hexToBytes(publicKeyHex, PUBLIC_KEY_BYTES);
848
- 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"]);
849
856
  this.key = crypto.subtle.importKey("raw", publicKey, "Ed25519", false, ["verify"]);
850
857
  }
851
858
  async verify(signatureHex, timestamp, body) {
@@ -982,15 +989,192 @@ function buildEngine(core, maps) {
982
989
  *
983
990
  * Reads `DISCORD_PUBLIC_KEY` and `DISCORD_BOT_TOKEN` from the environment through envapt and throws a
984
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.
985
995
  */
986
996
  function createSeedcord(config, manifest) {
997
+ Logger.configure(config.logger ?? {});
987
998
  const token = validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
988
999
  const core = createCore(config, token);
989
- core.bus.registerDefaults();
1000
+ core.bus[RegisterDefaults]();
990
1001
  registerSubscribers(core.bus, manifest);
991
1002
  return buildEngine(core, buildRouteMaps(manifest)).handle;
992
1003
  }
993
1004
 
1005
+ //#endregion
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"
1056
+ }
1057
+ };
1058
+ function nameOfChannelType(type) {
1059
+ return ChannelType[type] ?? String(type);
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
+ };
1177
+
994
1178
  //#endregion
995
1179
  //#region src/emojis/EmojiInjector.ts
996
1180
  const emojiStorage = accessorStore();
@@ -1077,9 +1261,14 @@ var EmojiInjector = class {
1077
1261
  var SlashHandler = class extends InteractionHandler {
1078
1262
  resolver;
1079
1263
  /**
1080
- * The typed options for this command's route. Required options drop the null, choices narrow to their
1081
- * literal union, and only the getters for kinds this command actually uses appear. Rich kinds resolve
1082
- * 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.
1083
1272
  */
1084
1273
  get options() {
1085
1274
  this.resolver ??= new HttpSlashOptions(this.event.data);
@@ -1099,13 +1288,18 @@ var SlashHandler = class extends InteractionHandler {
1099
1288
  async match(arms) {
1100
1289
  const route = slashRouteOf(this.event.data);
1101
1290
  const arm = Object.hasOwn(arms, route) ? arms[route] : void 0;
1102
- if (!arm) throw new SeedcordError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1291
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.SlashMatchArmMissing, [route]);
1103
1292
  return await arm(this.options);
1104
1293
  }
1105
1294
  };
1106
1295
 
1107
1296
  //#endregion
1108
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
+ }
1109
1303
  /**
1110
1304
  * Base class for a user context-menu command handler on the HTTP transport (right-click a user).
1111
1305
  *
@@ -1124,6 +1318,37 @@ var UserContextMenuHandler = class extends InteractionHandler {
1124
1318
  const { target_id: targetId, resolved } = this.event.data;
1125
1319
  return resolved.members?.[targetId] ?? null;
1126
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
+ }
1127
1352
  };
1128
1353
  /**
1129
1354
  * Base class for a message context-menu command handler on the HTTP transport (right-click a message).
@@ -1138,6 +1363,37 @@ var MessageContextMenuHandler = class extends InteractionHandler {
1138
1363
  const { target_id: targetId, resolved } = this.event.data;
1139
1364
  return resolved.messages[targetId];
1140
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
+ }
1141
1397
  };
1142
1398
 
1143
1399
  //#endregion
@@ -1206,7 +1462,7 @@ var ComponentHandler = class extends InteractionHandler {
1206
1462
  async match(arms) {
1207
1463
  const { prefix, params } = this.route;
1208
1464
  const arm = Object.hasOwn(arms, prefix) ? arms[prefix] : void 0;
1209
- if (!arm) throw new SeedcordError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1465
+ if (!arm) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdMatchArmMissing, [prefix]);
1210
1466
  return await arm(params);
1211
1467
  }
1212
1468
  };
@@ -1231,12 +1487,30 @@ var ButtonHandler = class extends ComponentHandler {};
1231
1487
  * Base class for a modal submit handler on the HTTP transport.
1232
1488
  *
1233
1489
  * Register the customId definitions this handler decodes with `@ModalRoute` and list the same ones in the
1234
- * generic. Read the submitted inputs off `this.event.data.components`. Reply through the handler members.
1235
- * 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.
1236
1492
  *
1237
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
+ * ```
1238
1506
  */
1239
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
+ }
1240
1514
  /** Rewrite the message this modal was opened from. Throws when a command opened the modal. */
1241
1515
  update(response) {
1242
1516
  this.ensureSourceMessage("update");
@@ -1258,15 +1532,55 @@ var ModalHandler = class extends ComponentHandler {
1258
1532
  /**
1259
1533
  * Base class for a select menu handler on the HTTP transport.
1260
1534
  *
1261
- * Pass the select kind first and the customId definitions second, the same order as `@SelectMenuRoute`,
1262
- * so `this.event.data` and its `values`/`resolved` are narrowed to that kind. Read `this.params` for a
1263
- * single route or `this.match` for several, and reply through the handler members or rewrite the source
1264
- * 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`.
1265
1541
  *
1266
1542
  * @typeParam Kind - The select kind from {@link SelectMenuKind}, e.g. `SelectMenuKind.User`.
1267
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
+ * ```
1268
1555
  */
1269
- 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
+ };
1270
1584
 
1271
1585
  //#endregion
1272
1586
  //#region src/subscribers/index.ts
@@ -1318,25 +1632,33 @@ var Paginator = class extends PaginatorBase {
1318
1632
  Handler;
1319
1633
  constructor(config) {
1320
1634
  super(config);
1321
- const loadPage = (ctx, n) => this.page(ctx, n);
1635
+ const pageFor = (handler, n) => this.page(handler, n);
1322
1636
  this.Handler = class Nav extends ButtonHandler {
1323
1637
  async execute() {
1324
1638
  await this.deferUpdate();
1325
- const response = await loadPage(contextOf(this.event, this.core), this.params.page);
1639
+ const response = await pageFor(this, this.params.page);
1326
1640
  await this.update(response);
1327
1641
  }
1328
1642
  };
1329
1643
  }
1330
1644
  /**
1331
- * Render page 0 and send it through the handler's sender, which picks reply, edit, or followUp from its
1332
- * 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.
1333
1655
  *
1334
1656
  * @param handler - The handler starting the paginator, normally `this`.
1657
+ * @param n - The zero-based page to open on.
1335
1658
  */
1336
- async start(handler) {
1337
- const interaction = handler.getEvent();
1338
- const response = await this.page(contextOf(interaction, handler.core), 0);
1339
- 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 });
1340
1662
  }
1341
1663
  };
1342
1664
 
@@ -1349,7 +1671,7 @@ var Paginator = class extends PaginatorBase {
1349
1671
  *
1350
1672
  * @typeParam Item - The item type, inferred from the loader's return.
1351
1673
  */
1352
- var ArraySource$1 = class extends ArraySource {};
1674
+ var ArraySource = class extends ArraySourceBase {};
1353
1675
  /**
1354
1676
  * A source for a large or unknown-length set you fetch one page at a time (SQL LIMIT/OFFSET, a paged API).
1355
1677
  * The fetcher receives the page index and the page size and reports whether a next page exists. A cursor
@@ -1358,7 +1680,7 @@ var ArraySource$1 = class extends ArraySource {};
1358
1680
  *
1359
1681
  * @typeParam Item - The item type, inferred from the fetcher's slice.
1360
1682
  */
1361
- var CursorSource$1 = class extends CursorSource {};
1683
+ var CursorSource = class extends CursorSourceBase {};
1362
1684
 
1363
1685
  //#endregion
1364
1686
  //#region src/gates/Gated.ts
@@ -1403,8 +1725,8 @@ function Gated(...gates) {
1403
1725
 
1404
1726
  //#endregion
1405
1727
  //#region src/version.ts
1406
- const version = "0.4.1";
1728
+ const version = "0.5.0";
1407
1729
 
1408
1730
  //#endregion
1409
- export { RepliableHandler$1 as S, createSeedcord as _, Paginator as a, InteractionHandler 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, buildEngine as v, AutocompleteHandler as x, buildRouteMaps as y };
1410
- //# sourceMappingURL=edge.index-DK-Y14ge.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