@whanext/core 0.18.0 → 0.19.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.js CHANGED
@@ -420,7 +420,7 @@ var CommandContextImplementation = class {
420
420
  this.chat = { id: options.message.chatId, isGroup: options.message.isGroup };
421
421
  this.command = options.command;
422
422
  this.commands = options.commands;
423
- this.prefix = options.commands.prefix;
423
+ this.prefix = options.prefix ?? options.commands.prefix;
424
424
  this.options = options.options;
425
425
  this.args = options.args;
426
426
  this.locale = options.locale;
@@ -742,8 +742,11 @@ var UserService = class {
742
742
  // src/commands/router.ts
743
743
  var CommandRouter = class {
744
744
  #roots = /* @__PURE__ */ new Map();
745
+ #prefixlessRoots = /* @__PURE__ */ new Map();
745
746
  #definitions = /* @__PURE__ */ new Set();
746
- #prefix;
747
+ #prefix = "!";
748
+ #prefixes = ["!"];
749
+ #matchingPrefixes = ["!"];
747
750
  #services;
748
751
  #legacyOnError;
749
752
  #globalMiddleware = [];
@@ -755,22 +758,25 @@ var CommandRouter = class {
755
758
  #cooldownOperations = 0;
756
759
  constructor(servicesOrGroup, options = {}) {
757
760
  this.#services = isRuntimeServices(servicesOrGroup) ? servicesOrGroup : createLegacyServices(servicesOrGroup);
758
- this.#prefix = options.prefix ?? "!";
761
+ this.setPrefixes(options.prefix ?? "!");
759
762
  this.#legacyOnError = options.onError;
760
763
  this.#beforeExecute = options.beforeExecute;
761
764
  this.#afterExecute = options.afterExecute;
762
765
  if (options.onCommandError) this.#errorHandlers.push(options.onCommandError);
763
- if (this.#prefix.length === 0 || /\s/.test(this.#prefix)) {
764
- throw new WhaNextError(
765
- "ARGUMENT_INVALID",
766
- "The command prefix cannot be empty or contain whitespace.",
767
- { context: { prefix: this.#prefix } }
768
- );
769
- }
770
766
  }
771
767
  get prefix() {
772
768
  return this.#prefix;
773
769
  }
770
+ get prefixes() {
771
+ return this.#prefixes;
772
+ }
773
+ setPrefixes(prefixes) {
774
+ const normalized = normalizePrefixes(prefixes);
775
+ this.#prefixes = normalized;
776
+ this.#matchingPrefixes = [...normalized].sort((left, right) => right.length - left.length);
777
+ this.#prefix = normalized[0];
778
+ return this;
779
+ }
774
780
  get size() {
775
781
  return this.catalog({ includeHidden: true }).length;
776
782
  }
@@ -789,6 +795,19 @@ var CommandRouter = class {
789
795
  ...name.locale ? { locale: name.locale } : {}
790
796
  });
791
797
  }
798
+ for (const name of prefixlessCommandNames(definition)) {
799
+ const normalized = name.value.toLowerCase();
800
+ if (this.#prefixlessRoots.has(normalized)) {
801
+ throw new WhaNextError(
802
+ "ARGUMENT_INVALID",
803
+ `The prefixless command trigger "${normalized}" is already registered.`
804
+ );
805
+ }
806
+ this.#prefixlessRoots.set(normalized, {
807
+ definition,
808
+ ...name.locale ? { locale: name.locale } : {}
809
+ });
810
+ }
792
811
  this.#definitions.add(definition);
793
812
  return this;
794
813
  }
@@ -840,13 +859,15 @@ _Nenhum comando dispon\xEDvel._`;
840
859
  return context.reply(text);
841
860
  }
842
861
  async dispatch(message) {
843
- const text = message.text?.trim();
844
- if (!text?.startsWith(this.#prefix)) return false;
845
- const tokens = tokenize(text.slice(this.#prefix.length));
862
+ const text = (message.interactive?.id ?? message.text)?.trim();
863
+ if (!text) return false;
864
+ const matchedPrefix = this.#matchPrefix(text);
865
+ const tokens = tokenize(matchedPrefix === void 0 ? text : text.slice(matchedPrefix.length));
846
866
  const rootName = tokens.shift()?.toLowerCase();
847
867
  if (!rootName) return false;
848
- const root = this.#roots.get(rootName);
868
+ const root = matchedPrefix === void 0 ? this.#prefixlessRoots.get(rootName) : this.#roots.get(rootName);
849
869
  if (!root) return false;
870
+ const invocationPrefix = matchedPrefix ?? "";
850
871
  let resolved;
851
872
  try {
852
873
  resolved = this.#resolve(root, tokens);
@@ -869,6 +890,7 @@ _Nenhum comando dispon\xEDvel._`;
869
890
  args: new ArgsParser(tokens),
870
891
  services: this.#services,
871
892
  commands: this,
893
+ prefix: invocationPrefix,
872
894
  signal: new AbortController().signal,
873
895
  ...root.locale ? { locale: root.locale } : {}
874
896
  });
@@ -912,6 +934,7 @@ _Nenhum comando dispon\xEDvel._`;
912
934
  args: legacyArgs,
913
935
  services: this.#services,
914
936
  commands: this,
937
+ prefix: invocationPrefix,
915
938
  signal,
916
939
  ...resolved.locale ? { locale: resolved.locale } : {}
917
940
  });
@@ -933,6 +956,7 @@ _Nenhum comando dispon\xEDvel._`;
933
956
  args: legacyArgs,
934
957
  services: this.#services,
935
958
  commands: this,
959
+ prefix: invocationPrefix,
936
960
  signal: new AbortController().signal,
937
961
  ...resolved.locale ? { locale: resolved.locale } : {}
938
962
  });
@@ -940,6 +964,9 @@ _Nenhum comando dispon\xEDvel._`;
940
964
  throw normalized;
941
965
  }
942
966
  }
967
+ #matchPrefix(text) {
968
+ return this.#matchingPrefixes.find((prefix) => text.startsWith(prefix));
969
+ }
943
970
  #resolve(root, inputTokens) {
944
971
  const tokens = [...inputTokens];
945
972
  const layers = [root.definition];
@@ -1076,6 +1103,26 @@ _Nenhum comando dispon\xEDvel._`;
1076
1103
  );
1077
1104
  }
1078
1105
  }
1106
+ if (definition.prefixless && parents.length > 0) {
1107
+ throw new WhaNextError(
1108
+ "ARGUMENT_INVALID",
1109
+ "Prefixless triggers are only supported on root commands and command groups.",
1110
+ { context: { command: definition.name } }
1111
+ );
1112
+ }
1113
+ if (Array.isArray(definition.prefixless)) {
1114
+ const available = new Set(commandNames(definition).map((name) => name.value.toLowerCase()));
1115
+ for (const trigger of definition.prefixless) {
1116
+ const normalized = trigger.trim().toLowerCase();
1117
+ if (!normalized || /\s/.test(trigger) || !available.has(normalized)) {
1118
+ throw new WhaNextError(
1119
+ "ARGUMENT_INVALID",
1120
+ "Every prefixless trigger must match the command name or one of its aliases.",
1121
+ { context: { command: definition.name, trigger } }
1122
+ );
1123
+ }
1124
+ }
1125
+ }
1079
1126
  if (!isCommandGroup(definition)) return;
1080
1127
  if (definition.subcommands.length === 0) {
1081
1128
  throw new WhaNextError("ARGUMENT_INVALID", `The command group "${definition.name}" is empty.`);
@@ -1116,6 +1163,30 @@ async function composeMiddleware(middleware, context, execute) {
1116
1163
  };
1117
1164
  await dispatch(0);
1118
1165
  }
1166
+ function normalizePrefixes(input) {
1167
+ const values = typeof input === "string" ? [input] : [...input];
1168
+ const prefixes = [...new Set(values)];
1169
+ if (prefixes.length === 0) {
1170
+ throw new WhaNextError("ARGUMENT_INVALID", "At least one command prefix is required.");
1171
+ }
1172
+ for (const prefix of prefixes) {
1173
+ if (prefix.length === 0 || /\s/.test(prefix)) {
1174
+ throw new WhaNextError(
1175
+ "ARGUMENT_INVALID",
1176
+ "Command prefixes cannot be empty or contain whitespace.",
1177
+ { context: { prefix } }
1178
+ );
1179
+ }
1180
+ }
1181
+ return prefixes;
1182
+ }
1183
+ function prefixlessCommandNames(definition) {
1184
+ if (!definition.prefixless) return [];
1185
+ const names = commandNames(definition);
1186
+ if (definition.prefixless === true) return names;
1187
+ const enabled = new Set(definition.prefixless.map((name) => name.toLowerCase()));
1188
+ return names.filter((name) => enabled.has(name.value.toLowerCase()));
1189
+ }
1119
1190
  function commandNames(definition) {
1120
1191
  const names = [
1121
1192
  definition.name,
@@ -2039,6 +2110,12 @@ var MessageService = class {
2039
2110
  buttons(chatId, content) {
2040
2111
  return this.send(chatId, content);
2041
2112
  }
2113
+ list(chatId, content) {
2114
+ return this.send(chatId, content);
2115
+ }
2116
+ poll(chatId, content) {
2117
+ return this.send(chatId, content);
2118
+ }
2042
2119
  };
2043
2120
 
2044
2121
  // src/app/whanext-app.ts
@@ -2311,12 +2388,16 @@ import {
2311
2388
  // src/provider/zapo/normalize-message.ts
2312
2389
  function unwrapZapoMessageContent(input) {
2313
2390
  if (!input) return void 0;
2314
- const nested = input.ephemeralMessage?.message ?? input.viewOnceMessage?.message ?? input.viewOnceMessageV2?.message ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message;
2391
+ const nested = input.ephemeralMessage?.message ?? input.viewOnceMessage?.message ?? input.viewOnceMessageV2?.message ?? viewOnceV2ExtensionMessage(input) ?? input.deviceSentMessage?.message ?? input.documentWithCaptionMessage?.message;
2315
2392
  return nested ? unwrapZapoMessageContent(nested) : input;
2316
2393
  }
2394
+ function viewOnceV2ExtensionMessage(input) {
2395
+ const extension = input.viewOnceMessageV2Extension;
2396
+ return extension?.message ?? void 0;
2397
+ }
2317
2398
  function isZapoViewOnceContent(input) {
2318
2399
  if (!input) return false;
2319
- if (input.viewOnceMessage?.message || input.viewOnceMessageV2?.message) {
2400
+ if (input.viewOnceMessage?.message || input.viewOnceMessageV2?.message || viewOnceV2ExtensionMessage(input)) {
2320
2401
  return true;
2321
2402
  }
2322
2403
  return isZapoViewOnceContent(input.ephemeralMessage?.message);
@@ -2326,7 +2407,7 @@ function extractQuotedZapoMessage(input) {
2326
2407
  const chatId = event.key.remoteJid;
2327
2408
  const content = unwrapZapoMessageContent(event.message);
2328
2409
  if (!chatId || !content) return void 0;
2329
- const node = contentNode(content);
2410
+ const { node } = contentNode(content);
2330
2411
  const context = getContextInfo(node);
2331
2412
  if (!context?.stanzaId || !context.quotedMessage) return void 0;
2332
2413
  return {
@@ -2376,6 +2457,7 @@ function normalizeZapoMessage(input) {
2376
2457
  const text = getText(content);
2377
2458
  const caption = getCaption(content);
2378
2459
  const quoted = getQuoted(context, chatId);
2460
+ const interactive = getInteractiveResponse(content);
2379
2461
  const message = {
2380
2462
  id,
2381
2463
  jid: chatId,
@@ -2402,6 +2484,7 @@ function normalizeZapoMessage(input) {
2402
2484
  if (caption !== void 0) message.caption = caption;
2403
2485
  if (media !== void 0) message.media = media;
2404
2486
  if (quoted !== void 0) message.quoted = quoted;
2487
+ if (interactive !== void 0) message.interactive = interactive;
2405
2488
  return message;
2406
2489
  }
2407
2490
  function normalizeZapoKey(key) {
@@ -2481,7 +2564,67 @@ function getContentKind(type) {
2481
2564
  }
2482
2565
  }
2483
2566
  function getText(content) {
2484
- return content.conversation ?? content.extendedTextMessage?.text ?? content.buttonsResponseMessage?.selectedDisplayText ?? content.listResponseMessage?.title ?? content.templateButtonReplyMessage?.selectedDisplayText ?? void 0;
2567
+ return content.conversation ?? content.extendedTextMessage?.text ?? content.buttonsResponseMessage?.selectedDisplayText ?? content.listResponseMessage?.title ?? content.templateButtonReplyMessage?.selectedDisplayText ?? content.pollCreationMessage?.name ?? content.pollCreationMessageV2?.name ?? content.pollCreationMessageV3?.name ?? content.pollCreationMessageV5?.name ?? getNativeFlowDisplayText(content) ?? void 0;
2568
+ }
2569
+ function getInteractiveResponse(content) {
2570
+ const buttons = content.buttonsResponseMessage;
2571
+ if (buttons?.selectedButtonId) {
2572
+ return {
2573
+ kind: "button",
2574
+ id: buttons.selectedButtonId,
2575
+ ...buttons.selectedDisplayText ? { title: buttons.selectedDisplayText } : {}
2576
+ };
2577
+ }
2578
+ const template = content.templateButtonReplyMessage;
2579
+ if (template?.selectedId) {
2580
+ return {
2581
+ kind: "button",
2582
+ id: template.selectedId,
2583
+ ...template.selectedDisplayText ? { title: template.selectedDisplayText } : {}
2584
+ };
2585
+ }
2586
+ const list = content.listResponseMessage;
2587
+ const rowId = list?.singleSelectReply?.selectedRowId;
2588
+ if (rowId) {
2589
+ return {
2590
+ kind: "list",
2591
+ id: rowId,
2592
+ ...list?.title ? { title: list.title } : {}
2593
+ };
2594
+ }
2595
+ const native = nativeFlowParams(content);
2596
+ const nativeId = stringField(native, ["id", "selected_id", "row_id", "button_id"]);
2597
+ if (nativeId) {
2598
+ const title = stringField(native, ["display_text", "title"]);
2599
+ return {
2600
+ kind: native && ("row_id" in native || "selected_id" in native) ? "list" : "button",
2601
+ id: nativeId,
2602
+ ...title ? { title } : {}
2603
+ };
2604
+ }
2605
+ return void 0;
2606
+ }
2607
+ function getNativeFlowDisplayText(content) {
2608
+ return stringField(nativeFlowParams(content), ["display_text", "title"]);
2609
+ }
2610
+ function nativeFlowParams(content) {
2611
+ const response = content.interactiveResponseMessage;
2612
+ const json = response?.nativeFlowResponseMessage?.paramsJson;
2613
+ if (!json) return void 0;
2614
+ try {
2615
+ const parsed = JSON.parse(json);
2616
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
2617
+ } catch {
2618
+ return void 0;
2619
+ }
2620
+ }
2621
+ function stringField(value, keys) {
2622
+ if (!value) return void 0;
2623
+ for (const key of keys) {
2624
+ const field = value[key];
2625
+ if (typeof field === "string" && field.length > 0) return field;
2626
+ }
2627
+ return void 0;
2485
2628
  }
2486
2629
  function getCaption(content) {
2487
2630
  return content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? void 0;
@@ -2559,6 +2702,7 @@ var ZapoProvider = class {
2559
2702
  #events = new TypedEventEmitter();
2560
2703
  #logger;
2561
2704
  #messageStore = /* @__PURE__ */ new Map();
2705
+ #messageKeyStore = /* @__PURE__ */ new WeakMap();
2562
2706
  #messageCacheSize;
2563
2707
  #client;
2564
2708
  #voip;
@@ -2651,6 +2795,9 @@ var ZapoProvider = class {
2651
2795
  if ("buttons" in content) {
2652
2796
  return this.#sendButtons(chatId, content, replyTo);
2653
2797
  }
2798
+ if ("list" in content) {
2799
+ return this.#sendList(chatId, content, replyTo);
2800
+ }
2654
2801
  const client = this.#requireClient();
2655
2802
  const { value, mentions, viewOnce } = await this.#toContent(content);
2656
2803
  const result = await client.message.send(chatId, value, {
@@ -2691,7 +2838,7 @@ var ZapoProvider = class {
2691
2838
  return this.#sent(result, key.chatId);
2692
2839
  }
2693
2840
  async downloadMedia(key) {
2694
- const message = this.#findStoredMessage(this.#toZapoKey(key));
2841
+ const message = this.#messageKeyStore.get(key) ?? this.#findStoredMessage(this.#toZapoKey(key));
2695
2842
  if (!message?.message) {
2696
2843
  throw new WhaNextError(
2697
2844
  "MEDIA_NOT_AVAILABLE",
@@ -2881,9 +3028,10 @@ var ZapoProvider = class {
2881
3028
  }
2882
3029
  #bind(client) {
2883
3030
  client.on("auth_pairing_required", () => {
2884
- this.#pairingRequired = true;
2885
- this.#resolvePairingReady?.();
2886
- this.#resolvePairingReady = void 0;
3031
+ this.#markPairingReady();
3032
+ });
3033
+ client.on("auth_qr", () => {
3034
+ this.#markPairingReady();
2887
3035
  });
2888
3036
  client.on("auth_paired", () => {
2889
3037
  this.#pairingRequired = false;
@@ -2944,12 +3092,19 @@ var ZapoProvider = class {
2944
3092
  }
2945
3093
  if (stored.key?.id && stored.message) this.#remember(stored);
2946
3094
  const quoted = extractQuotedZapoMessage(event);
2947
- if (quoted) {
2948
- const quotedStored = quoted;
2949
- if (quotedStored.key?.id && quotedStored.message) this.#remember(quotedStored);
3095
+ if (quoted?.key.id && quoted.message) {
3096
+ this.#remember(quoted);
2950
3097
  }
2951
3098
  const message = normalizeZapoMessage(event);
2952
- if (message) void this.#events.emit("message", message);
3099
+ if (!message) return;
3100
+ this.#messageKeyStore.set(message.keys, stored);
3101
+ if (message.quoted && quoted?.message) {
3102
+ this.#messageKeyStore.set(
3103
+ message.quoted.key,
3104
+ quoted
3105
+ );
3106
+ }
3107
+ void this.#events.emit("message", message);
2953
3108
  }
2954
3109
  #handleProtocolEvent(event) {
2955
3110
  const protocol = event.protocolMessage ?? event.message?.protocolMessage;
@@ -3087,6 +3242,7 @@ var ZapoProvider = class {
3087
3242
  }, delay + Math.floor(Math.random() * 250));
3088
3243
  }
3089
3244
  async #sendButtons(chatId, content, replyTo) {
3245
+ this.#validateButtons(content);
3090
3246
  const mentions = content.mentions ? this.#mentions(content.mentions) : [];
3091
3247
  const raw = {
3092
3248
  interactiveMessage: {
@@ -3100,19 +3256,33 @@ var ZapoProvider = class {
3100
3256
  ...content.footer !== void 0 ? { footer: { text: content.footer } } : {},
3101
3257
  ...mentions.length > 0 ? { contextInfo: { mentionedJid: mentions } } : {},
3102
3258
  nativeFlowMessage: {
3103
- buttons: content.buttons.map((button) => button.type === "copy" ? {
3104
- name: "cta_copy",
3105
- buttonParamsJson: JSON.stringify({
3106
- display_text: button.label,
3107
- copy_code: button.code
3108
- })
3109
- } : {
3110
- name: "cta_url",
3111
- buttonParamsJson: JSON.stringify({
3112
- display_text: button.label,
3113
- url: button.url,
3114
- merchant_url: button.url
3115
- })
3259
+ buttons: content.buttons.map((button) => {
3260
+ if (button.type === "copy") {
3261
+ return {
3262
+ name: "cta_copy",
3263
+ buttonParamsJson: JSON.stringify({
3264
+ display_text: button.label,
3265
+ copy_code: button.code
3266
+ })
3267
+ };
3268
+ }
3269
+ if (button.type === "reply") {
3270
+ return {
3271
+ name: "quick_reply",
3272
+ buttonParamsJson: JSON.stringify({
3273
+ display_text: button.label,
3274
+ id: button.id
3275
+ })
3276
+ };
3277
+ }
3278
+ return {
3279
+ name: "cta_url",
3280
+ buttonParamsJson: JSON.stringify({
3281
+ display_text: button.label,
3282
+ url: button.url,
3283
+ merchant_url: button.url
3284
+ })
3285
+ };
3116
3286
  }),
3117
3287
  messageParamsJson: "{}",
3118
3288
  messageVersion: 1
@@ -3125,6 +3295,33 @@ var ZapoProvider = class {
3125
3295
  });
3126
3296
  return this.#sent(result, chatId);
3127
3297
  }
3298
+ async #sendList(chatId, content, replyTo) {
3299
+ this.#validateList(content);
3300
+ const mentions = content.mentions ? this.#mentions(content.mentions) : [];
3301
+ const raw = {
3302
+ listMessage: {
3303
+ ...content.title !== void 0 ? { title: content.title } : {},
3304
+ description: content.text,
3305
+ buttonText: content.buttonText,
3306
+ ...content.footer !== void 0 ? { footerText: content.footer } : {},
3307
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
3308
+ sections: content.list.map((section) => ({
3309
+ ...section.title !== void 0 ? { title: section.title } : {},
3310
+ rows: section.rows.map((row) => ({
3311
+ rowId: row.id,
3312
+ title: row.title,
3313
+ ...row.description !== void 0 ? { description: row.description } : {}
3314
+ }))
3315
+ })),
3316
+ ...mentions.length > 0 ? { contextInfo: { mentionedJid: mentions } } : {}
3317
+ }
3318
+ };
3319
+ const result = await this.#requireClient().message.send(chatId, raw, {
3320
+ ...replyTo ? { quote: this.#toZapoKey(replyTo) } : {},
3321
+ ...mentions.length > 0 ? { mentions } : {}
3322
+ });
3323
+ return this.#sent(result, chatId);
3324
+ }
3128
3325
  async #toContent(content) {
3129
3326
  if ("text" in content) {
3130
3327
  return {
@@ -3132,6 +3329,12 @@ var ZapoProvider = class {
3132
3329
  mentions: content.mentions ? this.#mentions(content.mentions) : []
3133
3330
  };
3134
3331
  }
3332
+ if ("poll" in content) {
3333
+ return {
3334
+ value: this.#pollContent(content),
3335
+ mentions: []
3336
+ };
3337
+ }
3135
3338
  if ("image" in content) {
3136
3339
  return {
3137
3340
  value: {
@@ -3175,6 +3378,85 @@ var ZapoProvider = class {
3175
3378
  mentions: []
3176
3379
  };
3177
3380
  }
3381
+ #validateButtons(content) {
3382
+ if (!content.text.trim() || content.buttons.length === 0) {
3383
+ throw new WhaNextError(
3384
+ "ARGUMENT_INVALID",
3385
+ "Interactive buttons require body text and at least one button."
3386
+ );
3387
+ }
3388
+ for (const button of content.buttons) {
3389
+ if (!button.label.trim()) {
3390
+ throw new WhaNextError("ARGUMENT_INVALID", "Interactive button labels cannot be empty.");
3391
+ }
3392
+ if (button.type === "reply" && !button.id.trim()) {
3393
+ throw new WhaNextError("ARGUMENT_INVALID", "Reply button IDs cannot be empty.");
3394
+ }
3395
+ if (button.type === "copy" && !button.code) {
3396
+ throw new WhaNextError("ARGUMENT_INVALID", "Copy buttons require a non-empty code.");
3397
+ }
3398
+ if (button.type === "link" && !button.url.trim()) {
3399
+ throw new WhaNextError("ARGUMENT_INVALID", "Link buttons require a non-empty URL.");
3400
+ }
3401
+ }
3402
+ }
3403
+ #validateList(content) {
3404
+ if (!content.text.trim() || !content.buttonText.trim() || content.list.length === 0) {
3405
+ throw new WhaNextError(
3406
+ "ARGUMENT_INVALID",
3407
+ "List menus require body text, button text and at least one section."
3408
+ );
3409
+ }
3410
+ let rowCount = 0;
3411
+ const ids = /* @__PURE__ */ new Set();
3412
+ for (const section of content.list) {
3413
+ for (const row of section.rows) {
3414
+ rowCount += 1;
3415
+ const id = row.id.trim();
3416
+ if (!id || !row.title.trim()) {
3417
+ throw new WhaNextError(
3418
+ "ARGUMENT_INVALID",
3419
+ "Every list row requires a non-empty id and title."
3420
+ );
3421
+ }
3422
+ if (ids.has(id)) {
3423
+ throw new WhaNextError(
3424
+ "ARGUMENT_INVALID",
3425
+ `Duplicate list row id "${id}".`
3426
+ );
3427
+ }
3428
+ ids.add(id);
3429
+ }
3430
+ }
3431
+ if (rowCount === 0) {
3432
+ throw new WhaNextError("ARGUMENT_INVALID", "List menus require at least one row.");
3433
+ }
3434
+ }
3435
+ #pollContent(content) {
3436
+ const name = content.poll.trim();
3437
+ const options = content.options.map((option2) => option2.trim()).filter(Boolean);
3438
+ const selectableCount = content.selectableCount ?? 1;
3439
+ if (!name || options.length < 2) {
3440
+ throw new WhaNextError(
3441
+ "ARGUMENT_INVALID",
3442
+ "Polls require a question and at least two non-empty options."
3443
+ );
3444
+ }
3445
+ if (!Number.isInteger(selectableCount) || selectableCount < 1 || selectableCount > options.length) {
3446
+ throw new WhaNextError(
3447
+ "ARGUMENT_INVALID",
3448
+ "selectableCount must be an integer between 1 and the number of poll options.",
3449
+ { context: { selectableCount, options: options.length } }
3450
+ );
3451
+ }
3452
+ return {
3453
+ type: "poll",
3454
+ name,
3455
+ options,
3456
+ selectableCount,
3457
+ ...content.allowAddOption !== void 0 ? { allowAddOption: content.allowAddOption } : {}
3458
+ };
3459
+ }
3178
3460
  async #media(source) {
3179
3461
  if (source instanceof Uint8Array) return source;
3180
3462
  if ("path" in source) return source.path;
@@ -3280,11 +3562,21 @@ var ZapoProvider = class {
3280
3562
  if (direct) return direct;
3281
3563
  if (!key.id) return void 0;
3282
3564
  for (const message of this.#messageStore.values()) {
3283
- if (message.key.id === key.id) return message;
3565
+ if (message.key.id === key.id && message.message) return message;
3566
+ }
3567
+ for (const message of this.#messageStore.values()) {
3568
+ const embeddedQuoted = extractQuotedZapoMessage(message);
3569
+ if (!embeddedQuoted?.key.id || embeddedQuoted.key.id !== key.id || !embeddedQuoted.message) {
3570
+ continue;
3571
+ }
3572
+ const storedQuoted = embeddedQuoted;
3573
+ this.#remember(storedQuoted);
3574
+ return storedQuoted;
3284
3575
  }
3285
3576
  return void 0;
3286
3577
  }
3287
3578
  #remember(message) {
3579
+ if (!message.key.id || !message.message) return;
3288
3580
  const key = this.#messageStoreKey(message.key);
3289
3581
  this.#messageStore.delete(key);
3290
3582
  this.#messageStore.set(key, message);
@@ -3304,6 +3596,11 @@ var ZapoProvider = class {
3304
3596
  if (timestamp === void 0) return false;
3305
3597
  return timestamp < this.#connectedAtSeconds - 3;
3306
3598
  }
3599
+ #markPairingReady() {
3600
+ this.#pairingRequired = true;
3601
+ this.#resolvePairingReady?.();
3602
+ this.#resolvePairingReady = void 0;
3603
+ }
3307
3604
  #preparePairingGate() {
3308
3605
  this.#pairingRequired = false;
3309
3606
  this.#pairingReady = new Promise((resolve3) => {