@seedcord/core 0.3.0 → 0.4.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.
Files changed (39) hide show
  1. package/dist/{Bus-DcuUtOEL.mjs → Bus-BVwmKc-A.mjs} +51 -17
  2. package/dist/Bus-BVwmKc-A.mjs.map +1 -0
  3. package/dist/Pluggable-Cf5-WWmW.mjs +280 -0
  4. package/dist/Pluggable-Cf5-WWmW.mjs.map +1 -0
  5. package/dist/Pluggable-DFNUIlJM.d.mts +148 -0
  6. package/dist/{Plugin-Bkeqcjy6.mjs → Plugin-C6oMqWf8.mjs} +32 -7
  7. package/dist/Plugin-C6oMqWf8.mjs.map +1 -0
  8. package/dist/Plugin-CG9pWbPF.d.mts +193 -0
  9. package/dist/{cursor-DKKKUa_7.d.mts → cursor-BcRfmTSo.d.mts} +27 -140
  10. package/dist/{cursor-Cv1Ui6gZ.mjs → cursor-GBUYz3jk.mjs} +377 -176
  11. package/dist/cursor-GBUYz3jk.mjs.map +1 -0
  12. package/dist/{index-uBwh8r7x.d.mts → index-ZzdQafFv.d.mts} +2 -2
  13. package/dist/index.d.mts +132 -19
  14. package/dist/index.mjs +186 -19
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/internal.index.d.mts +11 -4
  17. package/dist/internal.index.mjs +8 -455
  18. package/dist/internal.index.mjs.map +1 -1
  19. package/dist/node-internal.index.d.mts +10 -146
  20. package/dist/node-internal.index.mjs +43 -470
  21. package/dist/node-internal.index.mjs.map +1 -1
  22. package/dist/node.index.d.mts +2 -0
  23. package/dist/node.index.mjs +256 -0
  24. package/dist/node.index.mjs.map +1 -0
  25. package/dist/{Plugin-B6iRzm3g.d.mts → phases-F8yvPd60.d.mts} +111 -207
  26. package/dist/plugin.index.d.mts +2 -2
  27. package/dist/plugin.index.mjs +1 -1
  28. package/dist/publishDefault-D826XXDQ.mjs +14 -0
  29. package/dist/publishDefault-D826XXDQ.mjs.map +1 -0
  30. package/dist/{slashRouteLeaves-De1SxqnX.mjs → slashRouteLeaves-CbYRwqMY.mjs} +10 -2
  31. package/dist/slashRouteLeaves-CbYRwqMY.mjs.map +1 -0
  32. package/dist/subscribers-CGGQkUfM.mjs +139 -0
  33. package/dist/subscribers-CGGQkUfM.mjs.map +1 -0
  34. package/package.json +14 -8
  35. package/dist/Bus-DcuUtOEL.mjs.map +0 -1
  36. package/dist/Plugin-Bkeqcjy6.mjs.map +0 -1
  37. package/dist/cursor-Cv1Ui6gZ.mjs.map +0 -1
  38. package/dist/slashRouteLeaves-De1SxqnX.mjs.map +0 -1
  39. package/dist/subscribers-DuHg-bGT.mjs +0 -3
@@ -1,158 +1,13 @@
1
- import { b as InteractionRoutes, f as BuilderComponent, v as InteractionMetadataKey, w as contextMenuLeaves, y as InteractionRouteKeys } from "./Bus-DcuUtOEL.mjs";
1
+ import { C as InteractionMetadataKey, T as InteractionRoutes, g as BuilderComponent, w as InteractionRouteKeys } from "./Bus-BVwmKc-A.mjs";
2
+ import { n as asError, t as PublishDefault } from "./publishDefault-D826XXDQ.mjs";
2
3
  import "reflect-metadata";
3
- import { SlashCommandBuilder, TextDisplayBuilder } from "@discordjs/builders";
4
+ import { TextDisplayBuilder } from "@discordjs/builders";
4
5
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
5
6
  import { Logger } from "@seedcord/logger";
6
- import { routeLeavesOf } from "@seedcord/utils/internal";
7
7
  import { ApplicationCommandType, PermissionFlagsBits } from "discord-api-types/v10";
8
- import { SeedcordError, SeedcordRangeError } from "@seedcord/errors/internal";
8
+ import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
9
9
  import { prettify, toEpochSeconds } from "@seedcord/utils";
10
10
 
11
- //#region src/miscellaneous/guarded.ts
12
- function accessorStore() {
13
- return Object.create(null);
14
- }
15
- function clearStore(store) {
16
- for (const key of Object.keys(store)) Reflect.deleteProperty(store, key);
17
- }
18
- function guardedAccessor(accessor, storage) {
19
- return new Proxy(storage, { get(target, key, receiver) {
20
- if (typeof key !== "string" || Object.hasOwn(target, key)) return Reflect.get(target, key, receiver);
21
- throw new SeedcordError(SeedcordErrorCode.CoreAccessorUnresolved, [accessor, key]);
22
- } });
23
- }
24
-
25
- //#endregion
26
- //#region src/commands/CommandInjector.ts
27
- const commandStorage = accessorStore();
28
- const userMenuStorage = accessorStore();
29
- const messageMenuStorage = accessorStore();
30
- /**
31
- * The bot's deployed slash commands, keyed by {@link SlashRegistry}. Filled by
32
- * {@link CommandInjector} after each deploy, and a read before that throws.
33
- *
34
- * @example
35
- * ```ts
36
- * import { Commands } from '@seedcord/gateway';
37
- *
38
- * await this.reply(`Try ${Commands['config/set'].mention}`);
39
- *
40
- * // a help listing
41
- * const lines = Object.values(Commands).map((c) => `${c.mention} ${c.description}`);
42
- * ```
43
- */
44
- const Commands = guardedAccessor("Commands", commandStorage);
45
- /**
46
- * The bot's deployed context-menu commands. Filled by {@link CommandInjector} after each deploy, and a read
47
- * before that throws.
48
- *
49
- * @example
50
- * ```ts
51
- * import { ContextMenus } from '@seedcord/gateway';
52
- *
53
- * const id = ContextMenus.user['View Profile'].id;
54
- * ```
55
- */
56
- const ContextMenus = {
57
- user: guardedAccessor("ContextMenus.user", userMenuStorage),
58
- message: guardedAccessor("ContextMenus.message", messageMenuStorage)
59
- };
60
- const MULTI_GUILD = Symbol("seedcord:commands:multi-guild");
61
- function commandKey(type, name) {
62
- return `${type}:${name}`;
63
- }
64
- function slashBuildersOf(all) {
65
- const slash = all.filter((command) => command instanceof SlashCommandBuilder);
66
- return [...new Set(slash)];
67
- }
68
- /** @internal */
69
- var CommandInjector = class {
70
- logger = new Logger("Commands", { channel: "bot" });
71
- inject(deploy, all) {
72
- this.clear();
73
- const ids = {
74
- global: this.indexByKey(deploy.global),
75
- guild: this.indexGuilds(deploy.guilds),
76
- warned: /* @__PURE__ */ new Set()
77
- };
78
- let clickable = 0;
79
- for (const command of slashBuildersOf(all)) {
80
- const id = this.resolveId(ApplicationCommandType.ChatInput, command.name, ids, "falling back to plain text (not clickable)");
81
- for (const leaf of routeLeavesOf(command.toJSON())) {
82
- commandStorage[leaf.route] = {
83
- mention: id ? this.toMention(leaf.route, id) : this.toPlain(leaf.route),
84
- ...id && { id },
85
- route: leaf.route,
86
- description: leaf.description
87
- };
88
- if (id) clickable++;
89
- }
90
- }
91
- const menus = contextMenuLeaves(all);
92
- this.injectMenus(menus.user, ApplicationCommandType.User, userMenuStorage, ids);
93
- this.injectMenus(menus.message, ApplicationCommandType.Message, messageMenuStorage, ids);
94
- this.logger.utils.summary("Linked commands", {
95
- clickable,
96
- plain: Object.keys(commandStorage).length - clickable,
97
- "context menus": menus.user.size + menus.message.size
98
- });
99
- }
100
- injectMenus(names, kind, storage, ids) {
101
- for (const name of names) {
102
- const id = this.resolveId(kind, name, ids, "leaving its id unset");
103
- storage[name] = {
104
- ...id && { id },
105
- name,
106
- kind
107
- };
108
- }
109
- }
110
- indexByKey(collection) {
111
- const map = /* @__PURE__ */ new Map();
112
- for (const command of collection.values()) map.set(commandKey(command.type, command.name), command.id);
113
- return map;
114
- }
115
- indexGuilds(guilds) {
116
- const map = /* @__PURE__ */ new Map();
117
- for (const collection of guilds.values()) for (const command of collection.values()) {
118
- const key = commandKey(command.type, command.name);
119
- const existing = map.get(key);
120
- if (existing === void 0) map.set(key, command.id);
121
- else if (existing !== command.id) map.set(key, MULTI_GUILD);
122
- }
123
- return map;
124
- }
125
- resolveId(type, name, ids, consequence) {
126
- const key = commandKey(type, name);
127
- const global = ids.global.get(key);
128
- if (global) return global;
129
- const guild = ids.guild.get(key);
130
- if (guild === void 0) return void 0;
131
- if (guild === MULTI_GUILD) {
132
- if (!ids.warned.has(key)) {
133
- this.logger.warn(`${paint.sky.bold(name)} is deployed to multiple guilds, ${consequence}.`);
134
- ids.warned.add(key);
135
- }
136
- return;
137
- }
138
- return guild;
139
- }
140
- toMention(route, id) {
141
- return `</${route.replaceAll("/", " ")}:${id}>`;
142
- }
143
- toPlain(route) {
144
- return `/${route.replaceAll("/", " ")}`;
145
- }
146
- clear() {
147
- for (const store of [
148
- commandStorage,
149
- userMenuStorage,
150
- messageMenuStorage
151
- ]) clearStore(store);
152
- }
153
- };
154
-
155
- //#endregion
156
11
  //#region src/stops/Notice.ts
157
12
  /**
158
13
  * Base class for a user-facing refusal or a reported fault.
@@ -207,8 +62,9 @@ var Notice = class extends Error {
207
62
  */
208
63
  ephemeral = true;
209
64
  /**
210
- * A short one-line reason. When every arm of an `or` gate refuses and each refusal sets this, `or`
211
- * lists them as the reply.
65
+ * A short one-line reason. When every arm of an {@link or} gate refuses and each one sets this, `or`
66
+ * throws a refusal listing them under `You need to meet at least one of these:`. Write a verb phrase that
67
+ * finishes that line, for example, "be the bot owner".
212
68
  */
213
69
  summary;
214
70
  constructor(message, options) {
@@ -328,6 +184,9 @@ function isBounded(field) {
328
184
  return field.kind === "snowflake" || field.kind === "uuid" || field.kind === "bool" || field.kind === "oneOf";
329
185
  }
330
186
  function radixOf(field) {
187
+ return kindRadix(field) + (field.nullable === true ? 1n : 0n);
188
+ }
189
+ function kindRadix(field) {
331
190
  switch (field.kind) {
332
191
  case "snowflake": return 1n << 64n;
333
192
  case "uuid": return 1n << 128n;
@@ -342,6 +201,13 @@ function radixOf(field) {
342
201
  }
343
202
  }
344
203
  function boundedToBigint(field, name, value) {
204
+ if (field.nullable === true) {
205
+ if (value === null) return 0n;
206
+ return boundedToBigint({
207
+ ...field,
208
+ nullable: false
209
+ }, name, value) + 1n;
210
+ }
345
211
  const slot = boundedSlot(field, name, value);
346
212
  if (slot < 0n || slot >= radixOf(field)) outOfRange(name, value);
347
213
  return slot;
@@ -372,6 +238,13 @@ function outOfRange(name, value) {
372
238
  throw new SeedcordRangeError(SeedcordErrorCode.CustomIdValueOutOfRange, [name, String(value)]);
373
239
  }
374
240
  function bigintToBoundedValue(field, slot) {
241
+ if (field.nullable === true) return slot === 0n ? null : bigintToBoundedValue({
242
+ ...field,
243
+ nullable: false
244
+ }, slot - 1n);
245
+ return kindValue(field, slot);
246
+ }
247
+ function kindValue(field, slot) {
375
248
  switch (field.kind) {
376
249
  case "snowflake": return slot.toString();
377
250
  case "uuid": return bigintToUuid(slot);
@@ -385,7 +258,16 @@ function bigintToUuid(value) {
385
258
  const hex = value.toString(16).padStart(32, "0");
386
259
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
387
260
  }
261
+ const ABSENT = "0";
262
+ const PRESENT = "1";
388
263
  function encodeUnboundedToken(field, name, value) {
264
+ if (field.nullable === true) {
265
+ if (value === null) return ABSENT;
266
+ return PRESENT + encodeUnboundedToken({
267
+ ...field,
268
+ nullable: false
269
+ }, name, value);
270
+ }
389
271
  if (field.kind === "int") {
390
272
  if (!Number.isSafeInteger(value)) outOfRange(name, value);
391
273
  return bigintToBase64(zigzagEncode(value));
@@ -393,6 +275,15 @@ function encodeUnboundedToken(field, name, value) {
393
275
  return escapeToken(value);
394
276
  }
395
277
  function decodeUnboundedToken(field, piece) {
278
+ if (field.nullable === true) {
279
+ const marker = piece.charAt(0);
280
+ if (marker === ABSENT && piece.length === 1) return null;
281
+ if (marker !== PRESENT) throw new InvalidCustomId(`bad presence marker ${JSON.stringify(marker)}`);
282
+ return decodeUnboundedToken({
283
+ ...field,
284
+ nullable: false
285
+ }, piece.slice(1));
286
+ }
396
287
  if (field.kind !== "int") return unescapeToken(piece);
397
288
  if (piece === "") throw new InvalidCustomId("empty integer token");
398
289
  const decoded = zigzagDecode(base64ToBigint(piece));
@@ -405,6 +296,7 @@ function computeLayoutHash(shape) {
405
296
  name,
406
297
  field.kind,
407
298
  isBounded(field),
299
+ field.nullable === true,
408
300
  field.kind === "oneOf" ? field.choices ?? [] : null,
409
301
  field.kind === "int" ? [field.min ?? null, field.max ?? null] : null
410
302
  ]));
@@ -510,14 +402,14 @@ var CustomId = class CustomId {
510
402
  /** The prefix plus a short hash of the shape, the part of the wire before the colon. */
511
403
  routeKey;
512
404
  constructor(prefix, shape = {}) {
513
- if (!prefix || /[:\x1B\x1F]/.test(prefix)) throw new SeedcordError(SeedcordErrorCode.CustomIdInvalidPrefix, [prefix]);
405
+ if (!prefix || /[:\x1B\x1F]/.test(prefix)) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdInvalidPrefix, [prefix]);
514
406
  this.prefix = prefix;
515
407
  this.shape = shape;
516
408
  this.routeKey = prefix + computeLayoutHash(shape);
517
409
  }
518
410
  add(name, field) {
519
- if (/^(?:0|[1-9]\d*)$/.test(name)) throw new SeedcordError(SeedcordErrorCode.CustomIdReservedFieldName, [name]);
520
- if (name in this.shape) throw new SeedcordError(SeedcordErrorCode.CustomIdDuplicateFieldName, [name]);
411
+ if (/^(?:0|[1-9]\d*)$/.test(name)) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdReservedFieldName, [name]);
412
+ if (name in this.shape) throw new SeedcordTypeError(SeedcordErrorCode.CustomIdDuplicateFieldName, [name]);
521
413
  const shape = {
522
414
  ...this.shape,
523
415
  [name]: field
@@ -532,8 +424,11 @@ var CustomId = class CustomId {
532
424
  * new CustomId('ban').snowflake('userId');
533
425
  * ```
534
426
  */
535
- snowflake(name) {
536
- return this.add(name, { kind: "snowflake" });
427
+ snowflake(name, opts) {
428
+ return this.add(name, {
429
+ ...opts,
430
+ kind: "snowflake"
431
+ });
537
432
  }
538
433
  /**
539
434
  * Add a UUID field, decoded as a lowercase uuid string.
@@ -543,21 +438,29 @@ var CustomId = class CustomId {
543
438
  * new CustomId('ticket').uuid('ticketId');
544
439
  * ```
545
440
  */
546
- uuid(name) {
547
- return this.add(name, { kind: "uuid" });
441
+ uuid(name, opts) {
442
+ return this.add(name, {
443
+ ...opts,
444
+ kind: "uuid"
445
+ });
548
446
  }
549
- int(name, min, max) {
550
- if (min !== void 0 && max !== void 0 && min > max) throw new SeedcordError(SeedcordErrorCode.CustomIdInvalidBounds, [
447
+ int(name, minOrOpts, max, opts) {
448
+ const min = typeof minOrOpts === "number" ? minOrOpts : void 0;
449
+ if (min !== void 0 && max !== void 0 && min > max) throw new SeedcordRangeError(SeedcordErrorCode.CustomIdInvalidBounds, [
551
450
  name,
552
451
  min,
553
452
  max
554
453
  ]);
555
- const field = min === void 0 || max === void 0 ? { kind: "int" } : {
556
- kind: "int",
454
+ const options = typeof minOrOpts === "number" ? opts : minOrOpts;
455
+ const bounds = min === void 0 || max === void 0 ? {} : {
557
456
  min,
558
457
  max
559
458
  };
560
- return this.add(name, field);
459
+ return this.add(name, {
460
+ ...options,
461
+ kind: "int",
462
+ ...bounds
463
+ });
561
464
  }
562
465
  /**
563
466
  * Add a boolean flag.
@@ -567,8 +470,11 @@ var CustomId = class CustomId {
567
470
  * new CustomId('settings').bool('silent');
568
471
  * ```
569
472
  */
570
- bool(name) {
571
- return this.add(name, { kind: "bool" });
473
+ bool(name, opts) {
474
+ return this.add(name, {
475
+ ...opts,
476
+ kind: "bool"
477
+ });
572
478
  }
573
479
  /**
574
480
  * Add a field that is one value from a fixed list, decoded as the literal union. No `as const` needed.
@@ -578,9 +484,10 @@ var CustomId = class CustomId {
578
484
  * new CustomId('poll').oneOf('choice', ['yes', 'no', 'abstain']);
579
485
  * ```
580
486
  */
581
- oneOf(name, choices) {
582
- if (choices.length === 0) throw new SeedcordError(SeedcordErrorCode.CustomIdEmptyChoices, [name]);
487
+ oneOf(name, choices, opts) {
488
+ if (choices.length === 0) throw new SeedcordRangeError(SeedcordErrorCode.CustomIdEmptyChoices, [name]);
583
489
  return this.add(name, {
490
+ ...opts,
584
491
  kind: "oneOf",
585
492
  choices
586
493
  });
@@ -594,8 +501,11 @@ var CustomId = class CustomId {
594
501
  * new CustomId('note').str('message');
595
502
  * ```
596
503
  */
597
- str(name) {
598
- return this.add(name, { kind: "string" });
504
+ str(name, opts) {
505
+ return this.add(name, {
506
+ ...opts,
507
+ kind: "string"
508
+ });
599
509
  }
600
510
  /**
601
511
  * Mint a wire string from values. Throws if a value is out of its field's range or the wire is over 100 chars.
@@ -713,6 +623,285 @@ function contextMenuRouteOf(kind) {
713
623
  return kind === ApplicationCommandType.User ? "userContextMenu" : "messageContextMenu";
714
624
  }
715
625
 
626
+ //#endregion
627
+ //#region src/reply/ackLegality.ts
628
+ const showModalReason = {
629
+ reason: "this interaction was already acknowledged",
630
+ alternative: "showModal() must be the initial response."
631
+ };
632
+ function deferReason(what) {
633
+ return {
634
+ reason: `this interaction was already acknowledged with ${what}`,
635
+ alternative: "A deferral can only be the initial response."
636
+ };
637
+ }
638
+ const ILLEGAL = {
639
+ reply: {
640
+ "deferred-reply": {
641
+ reason: "this interaction was already deferred",
642
+ alternative: "Use edit() to fill the deferred reply or followUp() for a new message."
643
+ },
644
+ "deferred-update": {
645
+ reason: "this interaction was already acknowledged with a deferred update",
646
+ alternative: "Use followUp() for a new message."
647
+ },
648
+ replied: {
649
+ reason: "this interaction was already replied to",
650
+ alternative: "Use followUp() for a new message or edit() to rewrite the reply."
651
+ }
652
+ },
653
+ defer: {
654
+ "deferred-reply": deferReason("a deferred reply"),
655
+ "deferred-update": deferReason("a deferred update"),
656
+ replied: deferReason("a reply")
657
+ },
658
+ deferUpdate: {
659
+ "deferred-reply": deferReason("a deferred reply"),
660
+ "deferred-update": deferReason("a deferred update"),
661
+ replied: deferReason("a reply")
662
+ },
663
+ update: {
664
+ "deferred-reply": {
665
+ reason: "this interaction was deferred as a reply",
666
+ alternative: "The placeholder is yours via edit(). update() rewrites a source message."
667
+ },
668
+ replied: {
669
+ reason: "this interaction was already replied to",
670
+ alternative: "update() rewrites a source message. Use edit() or followUp() instead."
671
+ }
672
+ },
673
+ followUp: { unacked: {
674
+ reason: "this interaction has not been acknowledged yet",
675
+ alternative: "Ack first with reply() or defer()."
676
+ } },
677
+ edit: { unacked: {
678
+ reason: "nothing has been sent yet",
679
+ alternative: "Send with reply() or defer() first, then edit() rewrites it."
680
+ } },
681
+ delete: { unacked: {
682
+ reason: "nothing has been sent yet",
683
+ alternative: "Acknowledge first with reply() or defer(), then delete() removes it."
684
+ } },
685
+ showModal: {
686
+ "deferred-reply": showModalReason,
687
+ "deferred-update": showModalReason,
688
+ replied: showModalReason
689
+ }
690
+ };
691
+ const SEND_TARGET = {
692
+ unacked: "reply",
693
+ "deferred-reply": "edit",
694
+ "deferred-update": "followUp",
695
+ replied: "followUp"
696
+ };
697
+ function checkAckLegality(method, state, routeId, ackedBy) {
698
+ if (method === "send") return;
699
+ const illegal = ILLEGAL[method][state];
700
+ if (!illegal) return;
701
+ throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
702
+ method,
703
+ illegal.reason,
704
+ illegal.alternative,
705
+ routeId
706
+ ], { cause: ackedBy });
707
+ }
708
+ function sendTarget(state) {
709
+ return SEND_TARGET[state];
710
+ }
711
+
712
+ //#endregion
713
+ //#region src/reply/AckTrace.ts
714
+ var AckTrace = class extends Error {
715
+ constructor(method) {
716
+ super(`${method}() acknowledged this interaction`);
717
+ this.name = "AckTrace";
718
+ }
719
+ };
720
+
721
+ //#endregion
722
+ //#region src/reply/responseReport.ts
723
+ let replyLogger;
724
+ function logger() {
725
+ replyLogger ??= new Logger("Reply", { channel: "interactions" });
726
+ return replyLogger;
727
+ }
728
+ /** @internal */
729
+ function publishResponse(telemetry, report) {
730
+ const durationMs = performance.now() - report.startedAt;
731
+ const write = {
732
+ routeId: report.routeId,
733
+ interactionId: telemetry.interactionId,
734
+ method: report.method,
735
+ durationMs
736
+ };
737
+ telemetry.bus[PublishDefault]("responseAttempted", report.outcome === "failed" ? {
738
+ ...write,
739
+ outcome: "failed",
740
+ messageId: null,
741
+ error: report.error
742
+ } : {
743
+ ...write,
744
+ outcome: "sent",
745
+ messageId: report.messageId
746
+ });
747
+ logger().trace(`${paint.sky.bold(report.routeId)} ${report.method} ${report.outcome} ${paint.mute("in")} ${Math.round(durationMs)}ms`);
748
+ }
749
+ async function reportedWrite(telemetry, routeId, method, write) {
750
+ const startedAt = performance.now();
751
+ const result = await attemptWrite(telemetry, routeId, method, startedAt, write);
752
+ publishResponse(telemetry, {
753
+ routeId,
754
+ method,
755
+ startedAt,
756
+ outcome: "sent",
757
+ messageId: null
758
+ });
759
+ return result;
760
+ }
761
+ async function attemptWrite(telemetry, routeId, method, startedAt, write) {
762
+ try {
763
+ return await write();
764
+ } catch (caught) {
765
+ publishResponse(telemetry, {
766
+ routeId,
767
+ method,
768
+ startedAt,
769
+ outcome: "failed",
770
+ error: asError(caught)
771
+ });
772
+ throw caught;
773
+ }
774
+ }
775
+
776
+ //#endregion
777
+ //#region src/reply/translateSerialization.ts
778
+ const MAX_DEPTH = 8;
779
+ const OPERATOR_WORD = {
780
+ "<=": "at most",
781
+ ">=": "at least",
782
+ "<": "fewer than",
783
+ ">": "more than"
784
+ };
785
+ const KIND_WORD = {
786
+ string: "a string",
787
+ number: "a number",
788
+ boolean: "a boolean",
789
+ bigint: "a bigint",
790
+ array: "a list",
791
+ tuple: "a list",
792
+ date: "a date",
793
+ object: "an object"
794
+ };
795
+ function asRecord(value) {
796
+ return typeof value === "object" && value !== null ? value : null;
797
+ }
798
+ function firstLine(message) {
799
+ if (typeof message !== "string") return "unknown validation failure";
800
+ return message.split("\n")[0] ?? message;
801
+ }
802
+ const GIVEN_PREVIEW_LIMIT = 40;
803
+ function gotPhrase(given) {
804
+ if (given === void 0) return "nothing";
805
+ if (given === null) return "null";
806
+ if (typeof given === "string") {
807
+ if (given.length === 0) return "an empty string";
808
+ if (given.length > GIVEN_PREVIEW_LIMIT) return `${given.length} characters`;
809
+ return JSON.stringify(given);
810
+ }
811
+ if (Array.isArray(given)) return given.length === 0 ? "an empty list" : `${given.length} items`;
812
+ if (typeof given === "number" || typeof given === "bigint" || typeof given === "boolean") return String(given);
813
+ if (typeof given === "object") return given.constructor?.name ?? "an object";
814
+ return typeof given;
815
+ }
816
+ function needPhrase(expected, given) {
817
+ const bounds = [...expected.matchAll(/expected(\.length)?\s*(<=|>=|<|>)\s*(\d+)/g)];
818
+ if (bounds.length === 0) return null;
819
+ return bounds.map(([, lengthPart, operator, limit]) => {
820
+ const word = OPERATOR_WORD[operator ?? ""] ?? operator ?? "";
821
+ if (!lengthPart) return `${word} ${limit}`;
822
+ const unit = typeof given === "string" ? "character" : "item";
823
+ return `${word} ${limit} ${Number(limit) === 1 ? unit : `${unit}s`}`;
824
+ }).join(" and ");
825
+ }
826
+ function instanceName(expected) {
827
+ if (typeof expected === "function" && expected.name) return expected.name;
828
+ return "specific class instance";
829
+ }
830
+ function describeLeaf(record) {
831
+ if (typeof record.constraint === "string") {
832
+ const need = typeof record.expected === "string" ? needPhrase(record.expected, record.given) : null;
833
+ if (need) return `need ${need}, got ${gotPhrase(record.given)}`;
834
+ return `${firstLine(record.message)}${"given" in record ? ` (got ${gotPhrase(record.given)})` : ""}`;
835
+ }
836
+ if (typeof record.expected === "function") return `need a ${instanceName(record.expected)}, got ${gotPhrase(record.given)}`;
837
+ if (typeof record.validator === "string") {
838
+ const kind = KIND_WORD[record.validator.replace(/^s\./, "").replace(/\(.*$/, "")];
839
+ if (kind) return `need ${kind}, got ${gotPhrase(record.given)}`;
840
+ return `${firstLine(record.message)}, got ${gotPhrase(record.given)}`;
841
+ }
842
+ return firstLine(record.message);
843
+ }
844
+ function describeUnion(branches, depth) {
845
+ const records = [];
846
+ for (const branch of branches) {
847
+ const record = asRecord(branch);
848
+ if (record) records.push(record);
849
+ }
850
+ const bounded = records.find((record) => typeof record.constraint === "string");
851
+ if (bounded) return describeLeaf(bounded);
852
+ const names = [];
853
+ let allInstances = records.length > 0;
854
+ for (const record of records) if (typeof record.expected === "function") names.push(instanceName(record.expected));
855
+ else allInstances = false;
856
+ if (allInstances) return `need a ${[...new Set(names)].join(" or ")}, got ${gotPhrase(records[0]?.given)}`;
857
+ return detailOf(branches[0], depth + 1);
858
+ }
859
+ function detailOf(value, depth) {
860
+ const record = asRecord(value);
861
+ if (!record || depth >= MAX_DEPTH) return firstLine(record?.message);
862
+ if (Array.isArray(record.errors) && record.errors.length > 0) {
863
+ const errors = record.errors;
864
+ const [first] = errors;
865
+ if (Array.isArray(first)) {
866
+ const [key, inner] = first;
867
+ return `${String(key)}: ${detailOf(inner, depth + 1)}`;
868
+ }
869
+ return describeUnion(errors, depth);
870
+ }
871
+ return describeLeaf(record);
872
+ }
873
+ function translateSerializationError(error, componentClass, index, routeId) {
874
+ const detail = detailOf(error, 0);
875
+ return new SeedcordError(SeedcordErrorCode.ReplyComponentSerialization, [
876
+ componentClass,
877
+ index,
878
+ detail,
879
+ routeId
880
+ ], { cause: error });
881
+ }
882
+
883
+ //#endregion
884
+ //#region src/reply/serializeReply.ts
885
+ function serializeReply(response, routeId) {
886
+ if (typeof response === "string") try {
887
+ return { components: [new TextDisplayBuilder().setContent(response).toJSON()] };
888
+ } catch (error) {
889
+ throw translateSerializationError(error, "TextDisplayBuilder", 0, routeId);
890
+ }
891
+ return {
892
+ components: response.components.map((component, index) => {
893
+ try {
894
+ return component.toJSON();
895
+ } catch (error) {
896
+ const name = component.constructor?.name;
897
+ throw translateSerializationError(error, !name || name === "Object" ? "component" : name, index, routeId);
898
+ }
899
+ }),
900
+ ...response.allowedMentions && { allowedMentions: response.allowedMentions },
901
+ ...response.files && { files: response.files }
902
+ };
903
+ }
904
+
716
905
  //#endregion
717
906
  //#region src/gates/Gate.ts
718
907
  /**
@@ -807,23 +996,30 @@ var GateNotice = class extends Notice {
807
996
  var NotOwner = class extends GateNotice {
808
997
  constructor(message = "Only the bot owner can use this.") {
809
998
  super(message);
999
+ this.summary = "be the bot owner";
810
1000
  }
811
1001
  };
812
1002
  var NotInGuild = class extends GateNotice {
813
1003
  constructor(message = "This can only be used in a server.") {
814
1004
  super(message);
1005
+ this.summary = "be in a server";
815
1006
  }
816
1007
  };
817
1008
  var NotInDm = class extends GateNotice {
818
1009
  constructor(message = "This can only be used in a direct message.") {
819
1010
  super(message);
1011
+ this.summary = "be in a direct message";
820
1012
  }
821
1013
  };
1014
+ function retryAt(resetAt) {
1015
+ return `<t:${toEpochSeconds(resetAt)}:R>`;
1016
+ }
822
1017
  var OnCooldown = class extends GateNotice {
823
1018
  resetAt;
824
1019
  constructor(resetAt, message) {
825
- super(message ?? `You are doing that too fast. Try again <t:${toEpochSeconds(resetAt)}:R>.`);
1020
+ super(message ?? `You are doing that too fast. Try again ${retryAt(resetAt)}.`);
826
1021
  this.resetAt = resetAt;
1022
+ this.summary = `try again ${retryAt(resetAt)}`;
827
1023
  }
828
1024
  };
829
1025
  var NotAllowed = class extends Notice {
@@ -838,16 +1034,17 @@ var NeedsAny = class extends Notice {
838
1034
  summaries;
839
1035
  constructor(summaries) {
840
1036
  super("not allowed");
841
- this.summaries = summaries;
1037
+ this.summaries = [...new Set(summaries)];
842
1038
  }
843
1039
  render() {
844
1040
  const bullets = this.summaries.map((summary) => `• ${summary}`).join("\n");
845
- return { components: [new NoticeCard(`You need any of:\n${bullets}`).component] };
1041
+ return { components: [new NoticeCard(`You need to meet at least one of these:\n${bullets}`).component] };
846
1042
  }
847
1043
  };
848
1044
  var MissingRole = class extends GateNotice {
849
1045
  constructor(message, roleId) {
850
1046
  super(message ?? (roleId ? `You need the <@&${roleId}> role to use this.` : "You do not have the required role."));
1047
+ this.summary = roleId ? `hold the <@&${roleId}> role` : "hold the required role";
851
1048
  }
852
1049
  };
853
1050
  var MissingPermissions = class extends Notice {
@@ -859,6 +1056,8 @@ var MissingPermissions = class extends Notice {
859
1056
  this.subject = subject;
860
1057
  this.missingPerms = missingPerms;
861
1058
  this.customLead = message;
1059
+ const names = missingPerms.join(", ");
1060
+ this.summary = subject === null ? `hold ${names}` : `${subject} needs ${names}`;
862
1061
  }
863
1062
  render() {
864
1063
  const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
@@ -875,6 +1074,7 @@ var HasDangerousPermissions = class extends Notice {
875
1074
  this.subject = subject;
876
1075
  this.dangerousPerms = dangerousPerms;
877
1076
  this.customLead = message;
1077
+ this.summary = `${subject} must not hold ${dangerousPerms.join(", ")}`;
878
1078
  }
879
1079
  render() {
880
1080
  const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
@@ -1022,8 +1222,9 @@ function RequireRole(roleId, options) {
1022
1222
  * ```ts
1023
1223
  * import { Silence } from '@seedcord/gateway';
1024
1224
  *
1025
- * // before any reply or defer, drop the interaction with no reply and no report
1026
- * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');
1225
+ * // a helper under a guildMemberAdd handler, stopping when this guild configured nothing to send
1226
+ * const template = await settings.welcome(member.guild.id);
1227
+ * if (!template) throw new Silence('no welcome message set');
1027
1228
  * ```
1028
1229
  */
1029
1230
  var Silence = class extends Error {
@@ -1048,5 +1249,5 @@ function pageCursor(prefix) {
1048
1249
  }
1049
1250
 
1050
1251
  //#endregion
1051
- export { areRoutes as A, prefixOf as B, NotInDm as C, defineEffectGate as D, OnCooldown as E, storeInteractionRoute as F, ContextMenus as G, Notice as H, ComponentDefsKey as I, guardedAccessor as J, accessorStore as K, decodeComponentRoute as L, interactionRoutesOf as M, selectMenuRouteOf as N, defineGate as O, storeComponentRoute as P, CustomId as R, NotAllowed as S, NotOwner as T, CommandInjector as U, NoticeCard as V, Commands as W, GateNotice as _, RequirePermissions as a, MissingRole as b, hasAll as c, pickNotice as d, discardCommits as f, runCommits as g, runCheck as h, RequireBotPermissions as i, contextMenuRouteOf as j, SelectMenuKind as k, missingNames as l, rollbackCommits as m, pageCursor as n, RequireRole as o, markCommits as p, clearStore as q, Silence as r, PermissionNames as s, PAGE_MAX as t, presentNames as u, HasDangerousPermissions as v, NotInGuild as w, NeedsAny as x, MissingPermissions as y, decodeFor as z };
1052
- //# sourceMappingURL=cursor-Cv1Ui6gZ.mjs.map
1252
+ export { translateSerializationError as A, interactionRoutesOf as B, NotInDm as C, defineEffectGate as D, OnCooldown as E, checkAckLegality as F, decodeComponentRoute as G, storeComponentRoute as H, sendTarget as I, prefixOf as J, CustomId as K, SelectMenuKind as L, publishResponse as M, reportedWrite as N, defineGate as O, AckTrace as P, areRoutes as R, NotAllowed as S, NotOwner as T, storeInteractionRoute as U, selectMenuRouteOf as V, ComponentDefsKey as W, Notice as X, NoticeCard as Y, GateNotice as _, RequirePermissions as a, MissingRole as b, hasAll as c, pickNotice as d, discardCommits as f, runCommits as g, runCheck as h, RequireBotPermissions as i, attemptWrite as j, serializeReply as k, missingNames as l, rollbackCommits as m, pageCursor as n, RequireRole as o, markCommits as p, decodeFor as q, Silence as r, PermissionNames as s, PAGE_MAX as t, presentNames as u, HasDangerousPermissions as v, NotInGuild as w, NeedsAny as x, MissingPermissions as y, contextMenuRouteOf as z };
1253
+ //# sourceMappingURL=cursor-GBUYz3jk.mjs.map