@whanext/core 0.18.1 → 0.19.1

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,72 @@ 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
+ const nativeName = nativeFlowName(content);
2600
+ return {
2601
+ kind: nativeName === "single_select" || native && ("row_id" in native || "selected_id" in native) ? "list" : "button",
2602
+ id: nativeId,
2603
+ ...title ? { title } : {}
2604
+ };
2605
+ }
2606
+ return void 0;
2607
+ }
2608
+ function nativeFlowName(content) {
2609
+ const response = content.interactiveResponseMessage;
2610
+ return response?.nativeFlowResponseMessage?.name ?? void 0;
2611
+ }
2612
+ function getNativeFlowDisplayText(content) {
2613
+ return stringField(nativeFlowParams(content), ["display_text", "title"]);
2614
+ }
2615
+ function nativeFlowParams(content) {
2616
+ const response = content.interactiveResponseMessage;
2617
+ const json = response?.nativeFlowResponseMessage?.paramsJson;
2618
+ if (!json) return void 0;
2619
+ try {
2620
+ const parsed = JSON.parse(json);
2621
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
2622
+ } catch {
2623
+ return void 0;
2624
+ }
2625
+ }
2626
+ function stringField(value, keys) {
2627
+ if (!value) return void 0;
2628
+ for (const key of keys) {
2629
+ const field = value[key];
2630
+ if (typeof field === "string" && field.length > 0) return field;
2631
+ }
2632
+ return void 0;
2485
2633
  }
2486
2634
  function getCaption(content) {
2487
2635
  return content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? void 0;
@@ -2559,6 +2707,7 @@ var ZapoProvider = class {
2559
2707
  #events = new TypedEventEmitter();
2560
2708
  #logger;
2561
2709
  #messageStore = /* @__PURE__ */ new Map();
2710
+ #messageKeyStore = /* @__PURE__ */ new WeakMap();
2562
2711
  #messageCacheSize;
2563
2712
  #client;
2564
2713
  #voip;
@@ -2651,6 +2800,9 @@ var ZapoProvider = class {
2651
2800
  if ("buttons" in content) {
2652
2801
  return this.#sendButtons(chatId, content, replyTo);
2653
2802
  }
2803
+ if ("list" in content) {
2804
+ return this.#sendList(chatId, content, replyTo);
2805
+ }
2654
2806
  const client = this.#requireClient();
2655
2807
  const { value, mentions, viewOnce } = await this.#toContent(content);
2656
2808
  const result = await client.message.send(chatId, value, {
@@ -2691,7 +2843,7 @@ var ZapoProvider = class {
2691
2843
  return this.#sent(result, key.chatId);
2692
2844
  }
2693
2845
  async downloadMedia(key) {
2694
- const message = this.#findStoredMessage(this.#toZapoKey(key));
2846
+ const message = this.#messageKeyStore.get(key) ?? this.#findStoredMessage(this.#toZapoKey(key));
2695
2847
  if (!message?.message) {
2696
2848
  throw new WhaNextError(
2697
2849
  "MEDIA_NOT_AVAILABLE",
@@ -2945,12 +3097,19 @@ var ZapoProvider = class {
2945
3097
  }
2946
3098
  if (stored.key?.id && stored.message) this.#remember(stored);
2947
3099
  const quoted = extractQuotedZapoMessage(event);
2948
- if (quoted) {
2949
- const quotedStored = quoted;
2950
- if (quotedStored.key?.id && quotedStored.message) this.#remember(quotedStored);
3100
+ if (quoted?.key.id && quoted.message) {
3101
+ this.#remember(quoted);
2951
3102
  }
2952
3103
  const message = normalizeZapoMessage(event);
2953
- if (message) void this.#events.emit("message", message);
3104
+ if (!message) return;
3105
+ this.#messageKeyStore.set(message.keys, stored);
3106
+ if (message.quoted && quoted?.message) {
3107
+ this.#messageKeyStore.set(
3108
+ message.quoted.key,
3109
+ quoted
3110
+ );
3111
+ }
3112
+ void this.#events.emit("message", message);
2954
3113
  }
2955
3114
  #handleProtocolEvent(event) {
2956
3115
  const protocol = event.protocolMessage ?? event.message?.protocolMessage;
@@ -3088,6 +3247,7 @@ var ZapoProvider = class {
3088
3247
  }, delay + Math.floor(Math.random() * 250));
3089
3248
  }
3090
3249
  async #sendButtons(chatId, content, replyTo) {
3250
+ this.#validateButtons(content);
3091
3251
  const mentions = content.mentions ? this.#mentions(content.mentions) : [];
3092
3252
  const raw = {
3093
3253
  interactiveMessage: {
@@ -3101,19 +3261,33 @@ var ZapoProvider = class {
3101
3261
  ...content.footer !== void 0 ? { footer: { text: content.footer } } : {},
3102
3262
  ...mentions.length > 0 ? { contextInfo: { mentionedJid: mentions } } : {},
3103
3263
  nativeFlowMessage: {
3104
- buttons: content.buttons.map((button) => button.type === "copy" ? {
3105
- name: "cta_copy",
3106
- buttonParamsJson: JSON.stringify({
3107
- display_text: button.label,
3108
- copy_code: button.code
3109
- })
3110
- } : {
3111
- name: "cta_url",
3112
- buttonParamsJson: JSON.stringify({
3113
- display_text: button.label,
3114
- url: button.url,
3115
- merchant_url: button.url
3116
- })
3264
+ buttons: content.buttons.map((button) => {
3265
+ if (button.type === "copy") {
3266
+ return {
3267
+ name: "cta_copy",
3268
+ buttonParamsJson: JSON.stringify({
3269
+ display_text: button.label,
3270
+ copy_code: button.code
3271
+ })
3272
+ };
3273
+ }
3274
+ if (button.type === "reply") {
3275
+ return {
3276
+ name: "quick_reply",
3277
+ buttonParamsJson: JSON.stringify({
3278
+ display_text: button.label,
3279
+ id: button.id
3280
+ })
3281
+ };
3282
+ }
3283
+ return {
3284
+ name: "cta_url",
3285
+ buttonParamsJson: JSON.stringify({
3286
+ display_text: button.label,
3287
+ url: button.url,
3288
+ merchant_url: button.url
3289
+ })
3290
+ };
3117
3291
  }),
3118
3292
  messageParamsJson: "{}",
3119
3293
  messageVersion: 1
@@ -3126,6 +3300,48 @@ var ZapoProvider = class {
3126
3300
  });
3127
3301
  return this.#sent(result, chatId);
3128
3302
  }
3303
+ async #sendList(chatId, content, replyTo) {
3304
+ this.#validateList(content);
3305
+ const mentions = content.mentions ? this.#mentions(content.mentions) : [];
3306
+ const raw = {
3307
+ interactiveMessage: {
3308
+ ...content.title !== void 0 ? {
3309
+ header: {
3310
+ title: content.title,
3311
+ hasMediaAttachment: false
3312
+ }
3313
+ } : {},
3314
+ body: { text: content.text },
3315
+ ...content.footer !== void 0 ? { footer: { text: content.footer } } : {},
3316
+ ...mentions.length > 0 ? { contextInfo: { mentionedJid: mentions } } : {},
3317
+ nativeFlowMessage: {
3318
+ buttons: [
3319
+ {
3320
+ name: "single_select",
3321
+ buttonParamsJson: JSON.stringify({
3322
+ title: content.buttonText,
3323
+ sections: content.list.map((section) => ({
3324
+ ...section.title !== void 0 ? { title: section.title } : {},
3325
+ rows: section.rows.map((row) => ({
3326
+ id: row.id,
3327
+ title: row.title,
3328
+ ...row.description !== void 0 ? { description: row.description } : {}
3329
+ }))
3330
+ }))
3331
+ })
3332
+ }
3333
+ ],
3334
+ messageParamsJson: "{}",
3335
+ messageVersion: 1
3336
+ }
3337
+ }
3338
+ };
3339
+ const result = await this.#requireClient().message.send(chatId, raw, {
3340
+ ...replyTo ? { quote: this.#toZapoKey(replyTo) } : {},
3341
+ ...mentions.length > 0 ? { mentions } : {}
3342
+ });
3343
+ return this.#sent(result, chatId);
3344
+ }
3129
3345
  async #toContent(content) {
3130
3346
  if ("text" in content) {
3131
3347
  return {
@@ -3133,6 +3349,12 @@ var ZapoProvider = class {
3133
3349
  mentions: content.mentions ? this.#mentions(content.mentions) : []
3134
3350
  };
3135
3351
  }
3352
+ if ("poll" in content) {
3353
+ return {
3354
+ value: this.#pollContent(content),
3355
+ mentions: []
3356
+ };
3357
+ }
3136
3358
  if ("image" in content) {
3137
3359
  return {
3138
3360
  value: {
@@ -3176,6 +3398,85 @@ var ZapoProvider = class {
3176
3398
  mentions: []
3177
3399
  };
3178
3400
  }
3401
+ #validateButtons(content) {
3402
+ if (!content.text.trim() || content.buttons.length === 0) {
3403
+ throw new WhaNextError(
3404
+ "ARGUMENT_INVALID",
3405
+ "Interactive buttons require body text and at least one button."
3406
+ );
3407
+ }
3408
+ for (const button of content.buttons) {
3409
+ if (!button.label.trim()) {
3410
+ throw new WhaNextError("ARGUMENT_INVALID", "Interactive button labels cannot be empty.");
3411
+ }
3412
+ if (button.type === "reply" && !button.id.trim()) {
3413
+ throw new WhaNextError("ARGUMENT_INVALID", "Reply button IDs cannot be empty.");
3414
+ }
3415
+ if (button.type === "copy" && !button.code) {
3416
+ throw new WhaNextError("ARGUMENT_INVALID", "Copy buttons require a non-empty code.");
3417
+ }
3418
+ if (button.type === "link" && !button.url.trim()) {
3419
+ throw new WhaNextError("ARGUMENT_INVALID", "Link buttons require a non-empty URL.");
3420
+ }
3421
+ }
3422
+ }
3423
+ #validateList(content) {
3424
+ if (!content.text.trim() || !content.buttonText.trim() || content.list.length === 0) {
3425
+ throw new WhaNextError(
3426
+ "ARGUMENT_INVALID",
3427
+ "List menus require body text, button text and at least one section."
3428
+ );
3429
+ }
3430
+ let rowCount = 0;
3431
+ const ids = /* @__PURE__ */ new Set();
3432
+ for (const section of content.list) {
3433
+ for (const row of section.rows) {
3434
+ rowCount += 1;
3435
+ const id = row.id.trim();
3436
+ if (!id || !row.title.trim()) {
3437
+ throw new WhaNextError(
3438
+ "ARGUMENT_INVALID",
3439
+ "Every list row requires a non-empty id and title."
3440
+ );
3441
+ }
3442
+ if (ids.has(id)) {
3443
+ throw new WhaNextError(
3444
+ "ARGUMENT_INVALID",
3445
+ `Duplicate list row id "${id}".`
3446
+ );
3447
+ }
3448
+ ids.add(id);
3449
+ }
3450
+ }
3451
+ if (rowCount === 0) {
3452
+ throw new WhaNextError("ARGUMENT_INVALID", "List menus require at least one row.");
3453
+ }
3454
+ }
3455
+ #pollContent(content) {
3456
+ const name = content.poll.trim();
3457
+ const options = content.options.map((option2) => option2.trim()).filter(Boolean);
3458
+ const selectableCount = content.selectableCount ?? 1;
3459
+ if (!name || options.length < 2) {
3460
+ throw new WhaNextError(
3461
+ "ARGUMENT_INVALID",
3462
+ "Polls require a question and at least two non-empty options."
3463
+ );
3464
+ }
3465
+ if (!Number.isInteger(selectableCount) || selectableCount < 1 || selectableCount > options.length) {
3466
+ throw new WhaNextError(
3467
+ "ARGUMENT_INVALID",
3468
+ "selectableCount must be an integer between 1 and the number of poll options.",
3469
+ { context: { selectableCount, options: options.length } }
3470
+ );
3471
+ }
3472
+ return {
3473
+ type: "poll",
3474
+ name,
3475
+ options,
3476
+ selectableCount,
3477
+ ...content.allowAddOption !== void 0 ? { allowAddOption: content.allowAddOption } : {}
3478
+ };
3479
+ }
3179
3480
  async #media(source) {
3180
3481
  if (source instanceof Uint8Array) return source;
3181
3482
  if ("path" in source) return source.path;
@@ -3281,11 +3582,21 @@ var ZapoProvider = class {
3281
3582
  if (direct) return direct;
3282
3583
  if (!key.id) return void 0;
3283
3584
  for (const message of this.#messageStore.values()) {
3284
- if (message.key.id === key.id) return message;
3585
+ if (message.key.id === key.id && message.message) return message;
3586
+ }
3587
+ for (const message of this.#messageStore.values()) {
3588
+ const embeddedQuoted = extractQuotedZapoMessage(message);
3589
+ if (!embeddedQuoted?.key.id || embeddedQuoted.key.id !== key.id || !embeddedQuoted.message) {
3590
+ continue;
3591
+ }
3592
+ const storedQuoted = embeddedQuoted;
3593
+ this.#remember(storedQuoted);
3594
+ return storedQuoted;
3285
3595
  }
3286
3596
  return void 0;
3287
3597
  }
3288
3598
  #remember(message) {
3599
+ if (!message.key.id || !message.message) return;
3289
3600
  const key = this.#messageStoreKey(message.key);
3290
3601
  this.#messageStore.delete(key);
3291
3602
  this.#messageStore.set(key, message);