@manybot/manybot 5.6.0 → 5.7.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.
@@ -22,15 +22,114 @@
22
22
  * Returns/dispatches are pure adapters — error semantics, retries, fallbacks
23
23
  * all live elsewhere (sendFallbackGuard, sendGuard, pluginGuard).
24
24
  */
25
- import { normalizeMessageContent, downloadMediaMessage, jidNormalizedUser, } from "@whiskeysockets/baileys";
25
+ import { normalizeMessageContent, downloadMediaMessage, jidNormalizedUser, decryptPollVote as baileysDecryptPollVote, getAggregateVotesInPollMessage, } from "@whiskeysockets/baileys";
26
26
  import { createHash } from "node:crypto";
27
27
  import { logger } from "#logger";
28
+ /**
29
+ * Classify a Baileys message-content payload (`WAMessageContent`) into
30
+ * the neutral `(type, body, mimetype)` triple used by `BotMessage`. Pure
31
+ * function — no side effects, no closures — so it's safe to call for
32
+ * both real incoming messages and the embedded `quotedMessage` content
33
+ * carried in `contextInfo` (which can be an `ephemeralMessage`/
34
+ * `viewOnceMessage` wrapper, hence the `normalizeMessageContent` call).
35
+ *
36
+ * Exported so `api/index.ts` can decode the quoted payload when
37
+ * synthesizing the `BotMessage` returned by `getReply()`.
38
+ */
39
+ export function decodeContent(content) {
40
+ const m = normalizeMessageContent(content) ?? undefined;
41
+ let type = "other";
42
+ let body = "";
43
+ let mimetype;
44
+ if (m?.conversation) {
45
+ type = "text";
46
+ body = m.conversation;
47
+ }
48
+ else if (m?.extendedTextMessage?.text) {
49
+ type = "text";
50
+ body = m.extendedTextMessage.text ?? "";
51
+ }
52
+ else if (m?.imageMessage) {
53
+ type = "image";
54
+ body = m.imageMessage.caption ?? "";
55
+ mimetype = m.imageMessage.mimetype ?? undefined;
56
+ }
57
+ else if (m?.videoMessage) {
58
+ type = "video";
59
+ body = m.videoMessage.caption ?? "";
60
+ mimetype = m.videoMessage.mimetype ?? undefined;
61
+ }
62
+ else if (m?.audioMessage) {
63
+ type = "audio";
64
+ mimetype = m.audioMessage.mimetype ?? undefined;
65
+ }
66
+ else if (m?.documentMessage) {
67
+ type = "document";
68
+ body = m.documentMessage.caption ?? "";
69
+ mimetype = m.documentMessage.mimetype ?? undefined;
70
+ }
71
+ else if (m?.stickerMessage) {
72
+ type = "sticker";
73
+ mimetype = m.stickerMessage.mimetype ?? undefined;
74
+ }
75
+ return { type, body, mimetype };
76
+ }
28
77
  export function createBaileysAdapter(initial) {
29
78
  // mutable so rebind() can swap it; closure-scoped so the contract below
30
79
  // always sees the latest sock.
31
80
  let sock = initial.sock;
32
81
  const store = initial.store;
33
82
  // ── Adapter-local helpers ────────────────────────────────────────────────
83
+ /**
84
+ * Build the `quoted` option for `sock.sendMessage(jid, content, opts)`.
85
+ *
86
+ * Baileys expects the `quoted` field to be a full message-shaped object —
87
+ * `{ key: {...}, message: {...} }` — so it can extract both the attribution
88
+ * (stanzaId/participant/fromMe from `key`) and the preview content (from
89
+ * `message`) when generating contextInfo. Passing only `key` makes
90
+ * `generateWAMessageFromContent` call `normalizeMessageContent(undefined)`
91
+ * → `getContentType(undefined)` → `undefined`, then index it with `[]`
92
+ * and throw `Cannot read properties of undefined (reading 'undefined')`.
93
+ *
94
+ * We resolve the full envelope from the in-memory store, indexed by the
95
+ * (remoteJid, id) pair carried in the neutral `BotQuotedRef`. If the
96
+ * envelope is no longer present (evicted past MAX_MSGS_PER_CHAT, lost on
97
+ * restart, etc.) we return `undefined` rather than emit a half-formed
98
+ * `quoted` — degrading to a plain unquoted reply is safer than crashing
99
+ * the calling plugin.
100
+ */
101
+ function buildQuotedOpts(quoted) {
102
+ if (!quoted?.id || !quoted?.remoteJid)
103
+ return undefined;
104
+ const raw = store.messages.get(quoted.remoteJid)?.get(quoted.id);
105
+ const inner = raw?.message;
106
+ if (!inner)
107
+ return undefined; // safe fallback: no quoted at all
108
+ // `quoted` (used by sendMessage for reply-citation) is a
109
+ // message-shaped object: `{ key: {...}, message: {...} }`. Baileys
110
+ // reads `quoted.key.*` to populate contextInfo (stanzaId, participant,
111
+ // fromMe) — passing the fields flat used to misattribute the reply to
112
+ // the bot itself. `quoted.message` is also required: Baileys' internal
113
+ // `generateWAMessageFromContent` calls `normalizeMessageContent(
114
+ // quoted.message)` and indexes the result with `getContentType(...)`,
115
+ // which throws if `.message` is missing.
116
+ return { quoted: { key: toFlatKey(quoted), message: inner } };
117
+ }
118
+ /**
119
+ * Translate a neutral `BotQuotedRef` into the FLAT key shape Baileys
120
+ * expects for `react`/`delete`/`edit`/`readMessages` (proto.IMessageKey
121
+ * — `{ id, remoteJid, fromMe, participant }` directly, NOT nested under
122
+ * a `.key` field). The nested `{key, message}` form is reserved for the
123
+ * `quoted` field on sendMessage — see `buildQuotedOpts` above.
124
+ */
125
+ function toFlatKey(ref) {
126
+ return {
127
+ id: ref.id ?? null,
128
+ remoteJid: ref.remoteJid ?? "",
129
+ fromMe: !!ref.fromMe,
130
+ participant: ref.participant ?? undefined,
131
+ };
132
+ }
34
133
  /** Compute sha1-hex of a normalized buffer or string. */
35
134
  function sha1(input) {
36
135
  const hash = createHash("sha1");
@@ -45,40 +144,7 @@ export function createBaileysAdapter(initial) {
45
144
  /** Translate a Baileys WAMessage into the neutral BotMessage envelope. */
46
145
  function toBotMessage(msg) {
47
146
  const m = normalizeMessageContent(msg.message) ?? undefined;
48
- let type = "other";
49
- let body = "";
50
- let mimetype;
51
- if (m?.conversation) {
52
- type = "text";
53
- body = m.conversation;
54
- }
55
- else if (m?.extendedTextMessage?.text) {
56
- type = "text";
57
- body = m.extendedTextMessage.text ?? "";
58
- }
59
- else if (m?.imageMessage) {
60
- type = "image";
61
- body = m.imageMessage.caption ?? "";
62
- mimetype = m.imageMessage.mimetype ?? undefined;
63
- }
64
- else if (m?.videoMessage) {
65
- type = "video";
66
- body = m.videoMessage.caption ?? "";
67
- mimetype = m.videoMessage.mimetype ?? undefined;
68
- }
69
- else if (m?.audioMessage) {
70
- type = "audio";
71
- mimetype = m.audioMessage.mimetype ?? undefined;
72
- }
73
- else if (m?.documentMessage) {
74
- type = "document";
75
- body = m.documentMessage.caption ?? "";
76
- mimetype = m.documentMessage.mimetype ?? undefined;
77
- }
78
- else if (m?.stickerMessage) {
79
- type = "sticker";
80
- mimetype = m.stickerMessage.mimetype ?? undefined;
81
- }
147
+ const { type, body, mimetype } = decodeContent(msg.message);
82
148
  const key = msg.key;
83
149
  const contextInfo = m?.extendedTextMessage?.contextInfo ??
84
150
  m?.imageMessage?.contextInfo ??
@@ -86,6 +152,7 @@ export function createBaileysAdapter(initial) {
86
152
  m?.audioMessage?.contextInfo ??
87
153
  m?.documentMessage?.contextInfo ??
88
154
  undefined;
155
+ const ciTyped = contextInfo;
89
156
  return {
90
157
  id: msg.key.id ?? "",
91
158
  chatId: msg.key.remoteJid ?? "",
@@ -96,20 +163,27 @@ export function createBaileysAdapter(initial) {
96
163
  body,
97
164
  mimetype: mimetype ?? undefined,
98
165
  pushName: msg.pushName,
99
- mentionedJid: contextInfo?.mentionedJid ?? undefined,
100
- quotedKey: contextInfo?.stanzaId ? {
101
- id: contextInfo.stanzaId,
166
+ mentionedJid: ciTyped?.mentionedJid ?? undefined,
167
+ quotedKey: ciTyped?.stanzaId ? {
168
+ id: ciTyped.stanzaId,
102
169
  remoteJid: msg.key.remoteJid ?? undefined,
103
170
  fromMe: false,
104
- participant: contextInfo.participant ?? undefined,
171
+ participant: ciTyped.participant ?? undefined,
105
172
  } : undefined,
106
173
  fromLid: key.participantAlt,
107
174
  fromPn: key.participant,
108
175
  participantAlt: key.participantAlt,
109
176
  remoteJidAlt: key.remoteJidAlt,
110
- // Poll-decryption key lives here for poll vote decryption downstream.
177
+ // Driver-specific escape hatches:
178
+ // - pollEncKeyRaw: poll-decryption key for vote decryption
179
+ // - contextInfo: full IContextInfo (incl. embedded quotedMessage)
180
+ // so quoted-message consumers (api/index.ts
181
+ // buildMessageContext / downloadMedia fallback)
182
+ // can decode the quoted payload without going
183
+ // back through the store.
111
184
  _raw: {
112
185
  pollEncKeyRaw: m?.messageContextInfo?.messageSecret ?? undefined,
186
+ contextInfo: ciTyped ?? undefined,
113
187
  },
114
188
  };
115
189
  }
@@ -127,6 +201,44 @@ export function createBaileysAdapter(initial) {
127
201
  lid: c.lid,
128
202
  };
129
203
  }
204
+ /**
205
+ * Compute every plausible JID candidate for a side of a poll-vote
206
+ * decryption. WhatsApp doesn't consistently pick the same JID shape
207
+ * (LID vs PN) when deriving the poll-vote decryption key — it depends
208
+ * on addressingMode, 1:1 vs group, and which side sent last. Trying
209
+ * to compute "the" correct JID up front causes AES-GCM auth failures
210
+ * whenever WhatsApp actually used the LID; brute-forcing candidates
211
+ * is the only reliable approach (see
212
+ * https://github.com/WhiskeySockets/Baileys/issues/2342 and #1678).
213
+ *
214
+ * For the bot's own side (`self === true`), candidates are the bot's
215
+ * `user.id` and `user.lid`. For an external side, candidates are
216
+ * the participant/remoteJid, the `participantPn` if any, and any
217
+ * LID→PN mapping the store has learned.
218
+ */
219
+ function jidCandidatesFromKey(key, sock, store, self) {
220
+ const cands = [];
221
+ if (self) {
222
+ const selfLid = sock.user?.lid;
223
+ if (selfLid)
224
+ cands.push(jidNormalizedUser(selfLid));
225
+ if (sock.user?.id)
226
+ cands.push(jidNormalizedUser(sock.user.id));
227
+ }
228
+ else {
229
+ const rawParticipant = key.participant ?? key.remoteJid;
230
+ if (rawParticipant)
231
+ cands.push(jidNormalizedUser(rawParticipant));
232
+ if (key.participantPn)
233
+ cands.push(jidNormalizedUser(key.participantPn));
234
+ if (rawParticipant?.endsWith("@lid")) {
235
+ const resolved = store.resolveJid(rawParticipant);
236
+ if (resolved && resolved !== rawParticipant)
237
+ cands.push(jidNormalizedUser(resolved));
238
+ }
239
+ }
240
+ return Array.from(new Set(cands.filter(Boolean)));
241
+ }
130
242
  const subscribers = new Map();
131
243
  // Tracks the handlers each socket has registered so we can remove them on a
132
244
  // fresh socket after reconnect. Declared up here because the rebind helpers
@@ -237,17 +349,17 @@ export function createBaileysAdapter(initial) {
237
349
  return toSentRef(ref, jid);
238
350
  },
239
351
  async react(jid, target, emoji) {
240
- const key = toBaileysKey(target);
352
+ const key = toFlatKey(target);
241
353
  await sock.sendMessage(jid, { react: { text: emoji, key } });
242
354
  },
243
355
  async deleteMessage(jid, target, forEveryone) {
244
356
  if (!forEveryone)
245
357
  return;
246
- const key = toBaileysKey(target);
358
+ const key = toFlatKey(target);
247
359
  await sock.sendMessage(jid, { delete: key });
248
360
  },
249
361
  async editMessage(jid, target, text) {
250
- const key = toBaileysKey(target);
362
+ const key = toFlatKey(target);
251
363
  await sock.sendMessage(jid, { text, edit: key });
252
364
  },
253
365
  // ── presence + read ───────────────────────────────────────────────────
@@ -258,7 +370,7 @@ export function createBaileysAdapter(initial) {
258
370
  await sock.sendPresenceUpdate(baileyState, jid);
259
371
  },
260
372
  async readMessages(keys) {
261
- const baileyKeys = keys.map(toBaileysKey);
373
+ const baileyKeys = keys.map((k) => toFlatKey(k));
262
374
  await sock.readMessages(baileyKeys);
263
375
  },
264
376
  // ── contacts ──────────────────────────────────────────────────────────
@@ -385,10 +497,30 @@ export function createBaileysAdapter(initial) {
385
497
  },
386
498
  // ── media (download) ───────────────────────────────────────────────────
387
499
  async downloadMedia(msg, opts) {
388
- // The poll-decryption raw envelope left only what's needed for that,
389
- // so to downloadMediaMessage we still need the original WAMessage.
390
- // Get it back from the store by id.
391
- const raw = store.messages.get(msg.chatId)?.get(msg.id);
500
+ // Resolve the Baileys message envelope needed by `downloadMediaMessage`.
501
+ // Preferred path: if the caller already carries the embedded message
502
+ // payload (synthetic `BotMessage` for a quoted message, whose
503
+ // `_raw.contextInfo.quotedMessage` is the full WAMessageContent),
504
+ // build the envelope directly from that. This avoids the silent
505
+ // failure mode where the quoted message's original envelope has
506
+ // aged out of the store's per-chat ring buffer.
507
+ //
508
+ // Fallback: the regular case (downloading media for the incoming
509
+ // message itself, or any other BotMessage that has a real envelope
510
+ // in the store) — look it up by (chatId, id).
511
+ const embedded = msg._raw;
512
+ const embeddedContent = embedded?.contextInfo?.quotedMessage;
513
+ const raw = embeddedContent
514
+ ? {
515
+ key: {
516
+ id: embedded.contextInfo?.stanzaId ?? msg.id,
517
+ remoteJid: msg.chatId,
518
+ fromMe: false,
519
+ participant: embedded.contextInfo?.participant ?? undefined,
520
+ },
521
+ message: embeddedContent,
522
+ }
523
+ : store.messages.get(msg.chatId)?.get(msg.id);
392
524
  if (!raw)
393
525
  return null;
394
526
  try {
@@ -411,6 +543,72 @@ export function createBaileysAdapter(initial) {
411
543
  return null;
412
544
  }
413
545
  },
546
+ // ── poll decryption (Baileys-only) ───────────────────────────────────
547
+ //
548
+ // These are the only two methods on the contract that are explicitly
549
+ // Baileys-specific. whatsmeow (and any future driver) may leave them
550
+ // undefined; the only consumer today is `buildPollApi` in
551
+ // drivers/baileys/api/index.ts, which already tolerates the absence.
552
+ //
553
+ // Both live on the contract (not as a separate file-level helper)
554
+ // because the Baileys-side knowledge they encode — picking the
555
+ // correct LID-vs-PN JID on each side, knowing the bot's own
556
+ // `sock.user.id/lid`, knowing the WAMessage shape for the encrypted
557
+ // payload — would otherwise leak out of the adapter.
558
+ async decryptPollVote(opts) {
559
+ const voteRaw = store.messages.get(opts.voteKey.remoteJid ?? "")?.get(opts.voteKey.id ?? "");
560
+ const pum = voteRaw?.message?.pollUpdateMessage;
561
+ const vote = pum?.vote;
562
+ if (!vote)
563
+ return null;
564
+ const encKey = Buffer.isBuffer(opts.pollEncKey)
565
+ ? opts.pollEncKey
566
+ : Buffer.from(opts.pollEncKey, "base64");
567
+ // WhatsApp doesn't consistently use the same JID shape (LID vs PN) for
568
+ // pollCreatorJid/voterJid — it depends on addressingMode, 1:1 vs group,
569
+ // and which side sent last. Compute every plausible candidate and
570
+ // brute-force combinations until one decrypts successfully (see
571
+ // https://github.com/WhiskeySockets/Baileys/issues/2342 and #1678).
572
+ const creatorCandidates = jidCandidatesFromKey(opts.pollKey, sock, store, /*self*/ false);
573
+ const voterCandidates = jidCandidatesFromKey(opts.voteKey, sock, store, !!opts.voteKey.fromMe);
574
+ for (const pollCreatorJid of creatorCandidates) {
575
+ for (const voterJid of voterCandidates) {
576
+ try {
577
+ const decrypted = baileysDecryptPollVote(vote, {
578
+ pollCreatorJid,
579
+ pollMsgId: opts.pollKey.id ?? "",
580
+ pollEncKey: encKey,
581
+ voterJid,
582
+ });
583
+ // PollVoteMessage.selectedOptions is a list of { optionHash: Buffer | null }.
584
+ // Map to plain hex strings for the contract surface.
585
+ const selectedOptions = (decrypted.selectedOptions ?? [])
586
+ .map((o) => {
587
+ const h = o?.optionHash;
588
+ if (!h)
589
+ return null;
590
+ return Buffer.isBuffer(h) ? h.toString("hex") : Buffer.from(h).toString("hex");
591
+ })
592
+ .filter((x) => !!x);
593
+ return { selectedOptions, raw: decrypted };
594
+ }
595
+ catch {
596
+ // try next JID combination
597
+ }
598
+ }
599
+ }
600
+ return null;
601
+ },
602
+ aggregatePollVotes(opts) {
603
+ const pollRaw = store.messages.get(opts.pollKey.remoteJid ?? "")?.get(opts.pollKey.id ?? "");
604
+ if (!pollRaw?.message)
605
+ return [];
606
+ const meId = opts.selfJid ?? (sock.user?.id ? jidNormalizedUser(sock.user.id) : undefined);
607
+ const aggregated = getAggregateVotesInPollMessage(
608
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
609
+ { message: pollRaw.message, pollUpdates: opts.votes }, meId);
610
+ return aggregated;
611
+ },
414
612
  };
415
613
  // ── Rebind helpers (post-reconnect in drivers/baileys/index.ts) ────────
416
614
  //
@@ -434,6 +632,26 @@ export function createBaileysAdapter(initial) {
434
632
  const updates = arg;
435
633
  emit("messages.update", { updates });
436
634
  });
635
+ // Baileys emits one of two shapes for `messages.delete`: a `keys`
636
+ // array (per-message revoke) or `{ jid, all: true }` (chat clear).
637
+ // Normalize to a single payload here so the contract's consumer
638
+ // never has to special-case the variant.
639
+ register("messages.delete", (arg) => {
640
+ const a = arg;
641
+ if ("all" in a) {
642
+ emit("messages.delete", { keys: [], all: { jid: a.jid } });
643
+ }
644
+ else {
645
+ emit("messages.delete", {
646
+ keys: a.keys.map((k) => ({
647
+ id: k.id ?? null,
648
+ remoteJid: k.remoteJid ?? null,
649
+ fromMe: k.fromMe ?? null,
650
+ participant: k.participant ?? null,
651
+ })),
652
+ });
653
+ }
654
+ });
437
655
  register("messaging-history.set", (arg) => {
438
656
  const { chats, contacts, messages } = arg;
439
657
  emit("messaging-history.set", {
@@ -448,6 +666,9 @@ export function createBaileysAdapter(initial) {
448
666
  register("chats.update", (arg) => {
449
667
  emit("chats.update", { updates: arg });
450
668
  });
669
+ register("chats.delete", (arg) => {
670
+ emit("chats.delete", { ids: arg });
671
+ });
451
672
  register("contacts.upsert", (arg) => {
452
673
  emit("contacts.upsert", { contacts: arg.map(contactSummary) });
453
674
  });
@@ -458,9 +679,30 @@ export function createBaileysAdapter(initial) {
458
679
  const { id, participants } = arg;
459
680
  emit("group-participants.update", { id, participants });
460
681
  });
682
+ register("groups.upsert", (arg) => {
683
+ const groups = arg;
684
+ emit("groups.upsert", { groups: groups.map((g) => ({ id: g.id, subject: g.subject })) });
685
+ });
461
686
  register("groups.update", (arg) => {
462
687
  emit("groups.update", { updates: arg });
463
688
  });
689
+ register("group.join-request", (arg) => {
690
+ const a = arg;
691
+ emit("group.join-request", {
692
+ id: a.id,
693
+ author: a.author,
694
+ participant: a.participant,
695
+ action: a.action,
696
+ method: a.method ?? "unknown",
697
+ });
698
+ });
699
+ register("blocklist.set", (arg) => {
700
+ emit("blocklist.set", { blocklist: arg.blocklist });
701
+ });
702
+ register("blocklist.update", (arg) => {
703
+ const a = arg;
704
+ emit("blocklist.update", { blocklist: a.blocklist, type: a.type });
705
+ });
464
706
  register("connection.update", (arg) => {
465
707
  const { connection, lastDisconnect } = arg;
466
708
  emit("connection.update", {
@@ -508,31 +750,10 @@ export function createBaileysAdapter(initial) {
508
750
  };
509
751
  }
510
752
  // ── Helpers used inside the adapter above ────────────────────────────────────
511
- function buildQuotedOpts(quoted) {
512
- if (!quoted)
513
- return undefined;
514
- return { quoted: toBaileysKey(quoted) };
515
- }
516
- function toBaileysKey(ref) {
517
- // Baileys expects `quoted` to be a message-shaped object with these
518
- // fields nested under `.key` (quoted.key.id / .remoteJid / .fromMe /
519
- // .participant) — it reads quoted.key.* to build contextInfo
520
- // (stanzaId, participant, fromMe). Passing them flat (as this used to)
521
- // means quoted.key is undefined inside Baileys, so participant/fromMe
522
- // resolve to undefined and the quoted reply gets misattributed to the
523
- // bot itself instead of the original sender. The quoted text still
524
- // rendered before this fix because WhatsApp's client resolves the
525
- // preview content locally via stanzaId — only the author attribution
526
- // was broken.
527
- return {
528
- key: {
529
- id: ref.id ?? null,
530
- remoteJid: ref.remoteJid ?? "",
531
- fromMe: !!ref.fromMe,
532
- participant: ref.participant ?? undefined,
533
- },
534
- };
535
- }
753
+ //
754
+ // `buildQuotedOpts` and `toFlatKey` are defined inside the
755
+ // `createBaileysAdapter` closure (so they can see the `store`) — only
756
+ // `toSentRef` and `silentBaileysLogger` remain module-scoped helpers.
536
757
  function toSentRef(raw, fallbackChatId) {
537
758
  const r = raw;
538
759
  return {