@manybot/manybot 5.9.0 → 5.9.2

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.
@@ -178,6 +178,37 @@ function getMsgSenderPn(msg) {
178
178
  return normalizeJid(msg.chatId);
179
179
  return null;
180
180
  }
181
+ /**
182
+ * Split a quoted message's `contextInfo.participant` into `fromLid`/`fromPn`
183
+ * the same way the adapter's `toBotMessage()` splits `key.participant` /
184
+ * `key.participantAlt` via `splitLidPn()` — see the comment there. Unlike
185
+ * the key, `contextInfo` carries only a single `participant` field (no
186
+ * companion "Alt"), and it holds whichever form matches the chat's current
187
+ * addressing mode: under the modern default "lid" mode that's the @lid
188
+ * form, under legacy "pn" mode it's the phone-number form. Blindly
189
+ * assigning it to `fromPn` (as the quoted-message synthetic used to)
190
+ * leaves `fromLid` unset — so `getMsgSender()` (which only reads
191
+ * `participantAlt`/`fromLid`) falls through to `null` — while
192
+ * `getMsgSenderPn()` happily normalizes and returns the @lid value
193
+ * verbatim, since it never checks the suffix. Route by suffix here, and
194
+ * fill in the other side from the store's learned LID<->PN mapping when
195
+ * one is known.
196
+ */
197
+ function splitQuotedParticipant(store, participant) {
198
+ if (!participant)
199
+ return {};
200
+ if (participant.endsWith("@lid")) {
201
+ const resolved = store.resolveJid(participant);
202
+ return {
203
+ fromLid: participant,
204
+ fromPn: resolved !== participant ? resolved : undefined,
205
+ };
206
+ }
207
+ return {
208
+ fromLid: store.resolvePn(participant) ?? undefined,
209
+ fromPn: participant,
210
+ };
211
+ }
181
212
  /** Quoted-message metadata as the rest of the api uses it. */
182
213
  function getQuotedContext(msg) {
183
214
  if (!msg.quotedKey)
@@ -935,16 +966,33 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
935
966
  const quotedRaw = contextInfo?.quotedMessage
936
967
  ? (() => {
937
968
  const decoded = decodeContent(contextInfo.quotedMessage);
969
+ const { fromLid: quotedFromLid, fromPn: quotedFromPn } = splitQuotedParticipant(store, contextInfo.participant);
970
+ // contextInfo itself never carries the quoted message's original
971
+ // send time (WhatsApp's ContextInfo proto has no timestamp field —
972
+ // only stanzaId/participant/quotedMessage). Best-effort recover it
973
+ // from the message store, keyed by the same chatId+stanzaId, when
974
+ // the original is still cached (it usually is, since a reply
975
+ // almost always quotes a recent message in the same chat). Stays
976
+ // 0 — same as before — only when the original has aged out of the
977
+ // store's per-chat cap or the chat history hasn't been seen yet.
978
+ const quotedOriginal = contextInfo.stanzaId
979
+ ? store.messages.get(msg.chatId)?.get(contextInfo.stanzaId)
980
+ : undefined;
981
+ const quotedTimestamp = quotedOriginal
982
+ ? Number(quotedOriginal.messageTimestamp ?? 0) * 1000
983
+ : 0;
938
984
  return {
939
985
  id: contextInfo.stanzaId ?? "",
940
986
  chatId: msg.chatId,
941
987
  fromMe: false,
942
988
  type: decoded.type,
943
989
  contentHash: "",
944
- timestamp: 0,
990
+ timestamp: quotedTimestamp,
945
991
  body: decoded.body,
946
992
  mimetype: decoded.mimetype,
947
- fromPn: contextInfo.participant ?? undefined,
993
+ fromLid: quotedFromLid,
994
+ fromPn: quotedFromPn,
995
+ participantAlt: quotedFromLid,
948
996
  pushName: lookupPushName(contextInfo.participant),
949
997
  _raw: {
950
998
  contextInfo: {
@@ -962,16 +1010,27 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
962
1010
  // key-only synthetic so hasReply()/getReply() still work, but
963
1011
  // hasMedia/downloadMedia on the result will degrade gracefully
964
1012
  // (type=other, mimetype=undefined).
965
- ? {
966
- id: msg.quotedKey.id ?? "",
967
- chatId: msg.chatId,
968
- fromMe: false,
969
- type: "other",
970
- contentHash: "",
971
- timestamp: 0,
972
- fromPn: msg.quotedKey.participant ?? undefined,
973
- pushName: lookupPushName(msg.quotedKey.participant),
974
- }
1013
+ ? (() => {
1014
+ const { fromLid: quotedFromLid, fromPn: quotedFromPn } = splitQuotedParticipant(store, msg.quotedKey.participant);
1015
+ const quotedOriginal = msg.quotedKey.id
1016
+ ? store.messages.get(msg.chatId)?.get(msg.quotedKey.id)
1017
+ : undefined;
1018
+ const quotedTimestamp = quotedOriginal
1019
+ ? Number(quotedOriginal.messageTimestamp ?? 0) * 1000
1020
+ : 0;
1021
+ return {
1022
+ id: msg.quotedKey.id ?? "",
1023
+ chatId: msg.chatId,
1024
+ fromMe: false,
1025
+ type: "other",
1026
+ contentHash: "",
1027
+ timestamp: quotedTimestamp,
1028
+ fromLid: quotedFromLid,
1029
+ fromPn: quotedFromPn,
1030
+ participantAlt: quotedFromLid,
1031
+ pushName: lookupPushName(msg.quotedKey.participant),
1032
+ };
1033
+ })()
975
1034
  : null;
976
1035
  return {
977
1036
  id: msg.id,
@@ -1680,15 +1739,26 @@ function buildAdminApi(contract, store, chatJid) {
1680
1739
  const botJid = me.id ? normalizeJid(jidNormalizedUser(me.id)) : null;
1681
1740
  const botLid = me.lid ? normalizeJid(me.lid) : null;
1682
1741
  /**
1683
- * Resolve admin-supplied identifiers (bare phone number, @c.us,
1684
- * @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on file for
1685
- * that participant *in this group*. A naive toWireJid() guess is only
1686
- * correct for pn-addressed groups — lid-addressed groups (increasingly
1687
- * common, e.g. when a member hides their phone number) only recognize
1688
- * that member by @lid, which a phone number typed by the admin can't
1689
- * produce on its own. Baileys' raw groupMetadata() participants carry
1690
- * `id` (whatever WA addresses them as here), `jid` (its best
1691
- * phone-number guess) and `lid` — check all three.
1742
+ * Resolve admin-supplied identifiers (bare phone number, bare LID,
1743
+ * @c.us, @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on
1744
+ * file for that participant *in this group*. A naive toWireJid() guess
1745
+ * is only correct for pn-addressed groups — lid-addressed groups
1746
+ * (increasingly common, e.g. when a member hides their phone number)
1747
+ * only recognize that member by @lid, which a phone number typed by
1748
+ * the admin can't produce on its own. Baileys' raw groupMetadata()
1749
+ * participants carry `id` (whatever WA addresses them as here), `jid`
1750
+ * (its best phone-number guess) and `lid` — check all three.
1751
+ *
1752
+ * When the identifier has no explicit domain (the admin typed a bare
1753
+ * number), we don't know if it's a PN or a LID — toWireJid() would
1754
+ * silently guess PN, which never matches a participant that's only
1755
+ * addressable by @lid even when the digits are identical (e.g. typed
1756
+ * "118077710708981", the actual member is "118077710708981@lid"). In
1757
+ * that case, compare by digits alone against id/jid/lid instead of
1758
+ * forcing a domain. When the identifier *does* carry an explicit
1759
+ * domain (@c.us, @s.whatsapp.net, @lid), the domain is meaningful
1760
+ * (disambiguates a PN from a same-digit LID) and the original
1761
+ * wire/resolved comparison is used as before.
1692
1762
  *
1693
1763
  * Throws per-identifier if it doesn't match a current member, instead
1694
1764
  * of silently sending a guessed jid that WhatsApp rejects deep inside
@@ -1698,14 +1768,23 @@ function buildAdminApi(contract, store, chatJid) {
1698
1768
  const meta = await getGroupMetadataCached(contract, groupJid);
1699
1769
  const out = [];
1700
1770
  for (const id of identifiers) {
1771
+ const hasExplicitDomain = /@(s\.whatsapp\.net|lid|g\.us|c\.us)$/.test(id.trim());
1701
1772
  const wire = toWireJid(id);
1702
1773
  const resolved = normalizeJid(store.resolveJid(id));
1774
+ const idDigits = id.replace(/\D/g, "");
1703
1775
  const match = meta.participants.find((p) => {
1704
1776
  const pAny = p;
1705
- return (toWireJid(pAny.id) === wire ||
1706
- normalizeJid(store.resolveJid(pAny.id)) === resolved ||
1707
- (pAny.jid ? toWireJid(pAny.jid) === wire : false) ||
1708
- (pAny.lid ? toWireJid(pAny.lid) === wire : false));
1777
+ if (hasExplicitDomain) {
1778
+ return (toWireJid(pAny.id) === wire ||
1779
+ normalizeJid(store.resolveJid(pAny.id)) === resolved ||
1780
+ (pAny.jid ? toWireJid(pAny.jid) === wire : false) ||
1781
+ (pAny.lid ? toWireJid(pAny.lid) === wire : false));
1782
+ }
1783
+ // Bare number, no domain hint: match by digits against id/jid/lid,
1784
+ // instead of assuming a PN domain that may not be the one WhatsApp
1785
+ // actually uses for this participant (see PN-vs-LID note above).
1786
+ const cands = [pAny.id, pAny.jid, pAny.lid].filter((v) => !!v);
1787
+ return idDigits.length > 0 && cands.some((c) => c.replace(/\D/g, "") === idDigits);
1709
1788
  });
1710
1789
  if (!match) {
1711
1790
  throw new Error(t("driver.groupParticipantNotFound", { id, group: groupJid }));
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.9.0",
8
+ "version": "5.9.2",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {