whalibmob 5.5.89 → 5.5.93

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/README.md CHANGED
@@ -1323,8 +1323,10 @@ const [def, ...rest] = await client.queryStatusPrivacy()
1323
1323
  except `list`, `whitelist` to `list` only. Your own JID is always included so
1324
1324
  the post reaches your other devices.
1325
1325
 
1326
- If history has not synced yet the contact list is unknown, and `sendStatus`
1327
- will say so rather than post to nobody — pass `recipients` in that case.
1326
+ Recipients are taken from the named contacts history sync brought; when there
1327
+ are none, the people you have chats with stand in. If neither is known,
1328
+ `sendStatus` says so rather than posting a message no one holds the key for —
1329
+ pass `recipients` in that case.
1328
1330
 
1329
1331
  ## Send States in Chat
1330
1332
 
@@ -1495,6 +1497,9 @@ await client.changeGroupPicture('120363000000000000@g.us', fs.readFileSync('./gr
1495
1497
  const blocked = await client.blockContact('919634847671@s.whatsapp.net')
1496
1498
  await client.unblockContact('919634847671@s.whatsapp.net')
1497
1499
 
1500
+ // the block list is addressed by LID; a phone number is resolved to one first,
1501
+ // looking it up if it is not already known
1502
+
1498
1503
  // blocking done from the phone arrives as an event
1499
1504
  client.on('blocklist', ({ changes }) => console.log(changes))
1500
1505
  ```
package/lib/Client.js CHANGED
@@ -1099,6 +1099,69 @@ class WhalibmobClient extends EventEmitter {
1099
1099
  return base;
1100
1100
  }
1101
1101
 
1102
+ // The key a privacy token is stored and looked up under.
1103
+ //
1104
+ // Both references resolve it to the contact's LID and fall back to the plain
1105
+ // JID only when no LID is known — whatsmeow's resolveTCTokenStorageLID and
1106
+ // Baileys' resolveTcTokenJid are the same three lines:
1107
+ //
1108
+ // if (isLidUser(jid)) return jid
1109
+ // const lid = await getLIDForPN(jid)
1110
+ // return lid ?? jid
1111
+ //
1112
+ // whalibmob resolved it on the receiving side but not on the sending side,
1113
+ // which put the two halves out of step: a token a contact issued us arrived
1114
+ // on a privacy_token notification and was filed under their LID, while every
1115
+ // send looked it up under their phone JID and found nothing. The token was
1116
+ // therefore never attached to anything, and the server kept counting our
1117
+ // messages as anonymous reach-outs — which is what error 463 is.
1118
+ _resolveTcTokenJid(jid) {
1119
+ const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
1120
+ const norm = normalizeJidForTcToken(jid);
1121
+ if (norm.endsWith('@lid')) return norm;
1122
+ const lidUser = this._pnToLid && this._pnToLid.get(norm.split('@')[0]);
1123
+ return lidUser ? lidUser + '@lid' : norm;
1124
+ }
1125
+
1126
+ // The JID a token is *issued* to, which is not the JID it is *stored* under.
1127
+ //
1128
+ // Storage is always the LID. Issuance follows AB prop 14303
1129
+ // (lid_trusted_token_issue_to_lid), whose WA Web default is off — so the IQ
1130
+ // names the phone JID. Baileys implements exactly this in resolveIssuanceJid
1131
+ // and defaults the prop to false; whatsmeow issues to the LID instead. We
1132
+ // follow WA Web, and more importantly we now follow it in one place: the
1133
+ // send path, the 463 recovery and the post-identity-change re-issue used to
1134
+ // each pick a different address family for the same contact.
1135
+ _resolveTcTokenIssuanceJid(jid) {
1136
+ const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
1137
+ const norm = normalizeJidForTcToken(jid);
1138
+ if (!norm.endsWith('@lid')) return norm;
1139
+ const pnUser = this._lidToPn && this._lidToPn.get(norm.split('@')[0]);
1140
+ return pnUser ? pnUser + '@s.whatsapp.net' : norm;
1141
+ }
1142
+
1143
+ // The one way to read a token that is safe to put on the wire.
1144
+ //
1145
+ // whatsmeow funnels every attach site through ensureTCToken, which returns
1146
+ // nothing for an expired token. whalibmob had the expiry check on the message
1147
+ // path only; presence subscribe, group create, participant add and the
1148
+ // profile-picture query each tested `token.length` and nothing else, so once
1149
+ // a token aged past the 28-day window those four kept sending it. Same rule
1150
+ // everywhere now, and the dead token gets cleared on the way out.
1151
+ _tcTokenFor(jid) {
1152
+ if (!this._tcTokenStore) return null;
1153
+ const { tcTokenExpired } = require('./messages/TcTokenStore');
1154
+ const tcJid = this._resolveTcTokenJid(jid);
1155
+ const entry = this._tcTokenStore.get(tcJid);
1156
+ if (!entry || !entry.token || !entry.token.length) return null;
1157
+ if (tcTokenExpired(entry.timestamp)) {
1158
+ this._tcTokenStore.clearToken(tcJid);
1159
+ _whaDbg('[DBG] TCTOKEN_EXPIRED jid=' + tcJid + ' — cleared');
1160
+ return null;
1161
+ }
1162
+ return entry.token;
1163
+ }
1164
+
1102
1165
  // True when a JID names this account, in either address family.
1103
1166
  _isOwnUser(jid) {
1104
1167
  if (!jid) return false;
@@ -1953,6 +2016,7 @@ class WhalibmobClient extends EventEmitter {
1953
2016
  _storeTcTokenFromIqResult(result, fallbackJid, issueTs, opts) {
1954
2017
  const saveSenderTs = !opts || opts.saveSenderTs !== false; // default true
1955
2018
  if (!this._tcTokenStore) return;
2019
+ const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
1956
2020
 
1957
2021
  // ── Debug: dump result structure so we can trace server response ─────────
1958
2022
  if (!result) {
@@ -2002,10 +2066,21 @@ class WhalibmobClient extends EventEmitter {
2002
2066
  // → always prefer fallbackJid; only fall back to a.jid when fallbackJid is absent
2003
2067
  const jid = fallbackJid || String(a.jid);
2004
2068
  const t = a.t ? parseInt(String(a.t), 10) : issueTs;
2069
+ // Baileys drops anything that is not a regular user here — the guard
2070
+ // is against a malformed or spoofed notification filing a token under
2071
+ // the PSA account or a bot.
2072
+ if (!isRegularTcTokenUser(jid)) {
2073
+ _whaDbg('[DBG] STORE_TCTOKEN skipped non-user jid=' + jid);
2074
+ continue;
2075
+ }
2005
2076
  if (bytes && bytes.length) {
2006
- this._tcTokenStore.setToken(jid, bytes, t);
2007
- tokenStoredCount++;
2008
- _whaDbg('[DBG] TCTOKEN_STORED jid=' + jid + ' t=' + t + ' len=' + bytes.length);
2077
+ // setToken refuses a timestamp-less or older-than-stored token.
2078
+ if (this._tcTokenStore.setToken(jid, bytes, t)) {
2079
+ tokenStoredCount++;
2080
+ _whaDbg('[DBG] TCTOKEN_STORED jid=' + jid + ' t=' + t + ' len=' + bytes.length);
2081
+ } else {
2082
+ _whaDbg('[DBG] STORE_TCTOKEN rejected (no/older timestamp) jid=' + jid + ' t=' + t);
2083
+ }
2009
2084
  } else {
2010
2085
  _whaDbg('[DBG] STORE_TCTOKEN token node has no bytes jid=' + jid);
2011
2086
  }
@@ -2021,7 +2096,9 @@ class WhalibmobClient extends EventEmitter {
2021
2096
  // ── we flood the server with privacy IQs. ─────
2022
2097
  // ── For incoming privacy_token notifications saveSenderTs=false so ─────
2023
2098
  // ── we don't overwrite our own send-dedupe timestamp with the peer's ─────
2024
- if (saveSenderTs && issueTs != null) {
2099
+ // Same user gate as the token itself — no dedupe row for the PSA account
2100
+ // or a bot, since we never issue to them in the first place.
2101
+ if (saveSenderTs && issueTs != null && isRegularTcTokenUser(fallbackJid)) {
2025
2102
  try {
2026
2103
  this._tcTokenStore.setSenderTimestamp(fallbackJid, issueTs);
2027
2104
  _whaDbg('[DBG] SENDER_TS_SAVED jid=' + fallbackJid + ' ts=' + issueTs +
@@ -2081,11 +2158,9 @@ class WhalibmobClient extends EventEmitter {
2081
2158
  // from = LID_USER@s.whatsapp.net — WA sends LID notifications this way.
2082
2159
  // The user part is already the LID user; convert domain to @lid.
2083
2160
  storageJid = fromUser + '@lid';
2084
- } else if (this._pnToLid && this._pnToLid.has(fromUser)) {
2085
- // from is a PN → convert to LID for storage (Baileys always stores under LID)
2086
- storageJid = this._pnToLid.get(fromUser) + '@lid';
2087
2161
  } else {
2088
- storageJid = normalizeJidForTcToken(rawFrom);
2162
+ // Same resolver the send side uses, so both halves agree on the key.
2163
+ storageJid = this._resolveTcTokenJid(rawFrom);
2089
2164
  }
2090
2165
 
2091
2166
  _whaDbg('[DBG] PRIVACY_TOKEN_NOTIF storageJid=' + storageJid);
@@ -2112,11 +2187,7 @@ class WhalibmobClient extends EventEmitter {
2112
2187
 
2113
2188
  void (async () => {
2114
2189
  try {
2115
- // Resolve storage JID: PN→LID if available, otherwise use from as-is
2116
- const fromNorm = normalizeJidForTcToken(from);
2117
- const fromUser = fromNorm.split('@')[0];
2118
- const lidUser = this._pnToLid && this._pnToLid.get(fromUser);
2119
- const tcJid = lidUser ? (lidUser + '@lid') : fromNorm;
2190
+ const tcJid = this._resolveTcTokenJid(from);
2120
2191
 
2121
2192
  const entry = this._tcTokenStore.get(tcJid);
2122
2193
  const senderTs = entry && entry.senderTimestamp;
@@ -2128,9 +2199,15 @@ class WhalibmobClient extends EventEmitter {
2128
2199
  _whaDbg('[DBG] REISSUE_TC_TOKEN_AFTER_IDENTITY_CHANGE jid=' + tcJid +
2129
2200
  ' prevSenderTs=' + senderTs);
2130
2201
 
2131
- const issueTs = Math.floor(Date.now() / 1000);
2132
- const result = await this._issuePrivacyTokens(tcJid, issueTs);
2133
- this._storeTcTokenFromIqResult(result, tcJid, issueTs);
2202
+ // Baileys re-issues with the *previous* senderTimestamp, not a fresh
2203
+ // one, and stores the result without touching the dedupe state. That
2204
+ // matters: this is a repair of an existing grant after the peer's
2205
+ // device changed, not a new grant, so it must not move the 7-day
2206
+ // bucket that governs regular issuance. We were stamping it with
2207
+ // Date.now(), which silently skipped the next scheduled issuance.
2208
+ const issueJid = this._resolveTcTokenIssuanceJid(from);
2209
+ const result = await this._issuePrivacyTokens(issueJid, senderTs);
2210
+ this._storeTcTokenFromIqResult(result, tcJid, senderTs, { saveSenderTs: false });
2134
2211
  } catch (e) {
2135
2212
  _whaDbg('[DBG] REISSUE_TC_TOKEN_ERR from=' + from +
2136
2213
  ' err=' + (e && e.message));
@@ -2539,15 +2616,13 @@ class WhalibmobClient extends EventEmitter {
2539
2616
  subscribeToPresence(jids) {
2540
2617
  if (!this._socket || !this._connected) return;
2541
2618
  const arr = Array.isArray(jids) ? jids : [jids];
2542
- const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
2619
+ const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
2543
2620
  for (const raw of arr) {
2544
2621
  const jid = makeJid(raw);
2545
- const entry = this._tcTokenStore && this._tcTokenStore.get(
2546
- normalizeJidForTcToken(jid)
2547
- );
2548
- const children = (entry && entry.token && entry.token.length)
2549
- ? [new BinaryNode('tctoken', {}, entry.token)]
2550
- : null;
2622
+ // Groups and newsletters have no privacy token; Baileys guards the same
2623
+ // way before building the node.
2624
+ const token = isRegularTcTokenUser(jid) ? this._tcTokenFor(jid) : null;
2625
+ const children = token ? [new BinaryNode('tctoken', {}, token)] : null;
2551
2626
  this._socket.sendNode(new BinaryNode('presence', {
2552
2627
  to: jidStrToObj(jid),
2553
2628
  type: 'subscribe'
@@ -2682,12 +2757,8 @@ class WhalibmobClient extends EventEmitter {
2682
2757
  iqAttrs = { id: this._genMsgId(), xmlns: 'w:g2', to: jidStrToObj(jidFull), type: 'get' };
2683
2758
  content = [new BinaryNode('pictures', {}, [new BinaryNode('picture', attrs, null)])];
2684
2759
  } else {
2685
- const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
2686
- const entry = this._tcTokenStore &&
2687
- this._tcTokenStore.get(normalizeJidForTcToken(jidFull));
2688
- const picChildren = (entry && entry.token && entry.token.length)
2689
- ? [new BinaryNode('tctoken', {}, entry.token)]
2690
- : null;
2760
+ const picToken = this._tcTokenFor(jidFull);
2761
+ const picChildren = picToken ? [new BinaryNode('tctoken', {}, picToken)] : null;
2691
2762
  iqAttrs = {
2692
2763
  id: this._genMsgId(),
2693
2764
  xmlns: 'w:profile:picture',
@@ -3007,26 +3078,39 @@ class WhalibmobClient extends EventEmitter {
3007
3078
  }
3008
3079
 
3009
3080
  const ownJid = String(this._store.phoneNumber) + '@s.whatsapp.net';
3010
- let recipients;
3081
+ let recipients, source;
3011
3082
 
3012
3083
  if (privacy.type === 'whitelist') {
3013
3084
  recipients = privacy.list.slice();
3085
+ source = 'whitelist';
3014
3086
  } else {
3015
- const hist = this.getHistoryStore();
3016
- const contacts = (hist && hist.contacts) ? Object.keys(hist.contacts) : [];
3017
- // Only real contacts — an entry the history sync created purely to hold a
3018
- // push name is not somebody you have in your address book.
3019
- const named = contacts.filter(jid => {
3087
+ const hist = this.getHistoryStore() || {};
3088
+ const isPerson = (jid) => !jid.endsWith('@g.us') && !jid.endsWith('@broadcast') &&
3089
+ !jid.endsWith('@newsletter') && !this._isOwnUser(jid);
3090
+
3091
+ // Contacts proper: entries the history sync gave a name to.
3092
+ const contacts = Object.keys(hist.contacts || {}).filter(jid => {
3020
3093
  const c = hist.contacts[jid];
3021
- return c && (c.name || c.displayName) && !jid.endsWith('@g.us') &&
3022
- !jid.endsWith('@broadcast');
3094
+ return c && (c.name || c.displayName) && isPerson(jid);
3023
3095
  });
3096
+
3097
+ // A named contact list is what WhatsApp itself posts to. When history has
3098
+ // not brought one — a fresh session, or a sync that carried chats without
3099
+ // names — the people you actually have conversations with are the next
3100
+ // best thing, and far better than posting to nobody.
3101
+ let pool = contacts;
3102
+ source = 'contacts';
3103
+ if (pool.length === 0) {
3104
+ pool = Object.keys(hist.chats || {}).filter(isPerson);
3105
+ source = 'chats';
3106
+ }
3107
+
3024
3108
  const blocked = new Set(privacy.type === 'blacklist' ? privacy.list : []);
3025
- recipients = named.filter(jid => !blocked.has(jid));
3109
+ recipients = pool.filter(jid => !blocked.has(jid));
3026
3110
  }
3027
3111
 
3028
3112
  if (!recipients.some(j => this._isOwnUser(j))) recipients.push(ownJid);
3029
- _whaDbg('[DBG] STATUS_RECIPIENTS type=' + privacy.type +
3113
+ _whaDbg('[DBG] STATUS_RECIPIENTS type=' + privacy.type + ' source=' + source +
3030
3114
  ' count=' + recipients.length);
3031
3115
  return recipients;
3032
3116
  }
@@ -3509,27 +3593,71 @@ class WhalibmobClient extends EventEmitter {
3509
3593
  return this._updateBlocklist(jid, 'unblock');
3510
3594
  }
3511
3595
 
3512
- // The one IQ behind both, as whatsmeow's UpdateBlocklist sends it:
3596
+ // Work out the LID a block list item has to be addressed with, and the phone
3597
+ // JID that goes beside it.
3598
+ //
3599
+ // The server only accepts LID-addressed items on a LID account: a phone JID
3600
+ // comes back as <error code="400" text="bad-request" addressing_mode="lid"/>,
3601
+ // the attribute being the server saying which addressing it wanted. When the
3602
+ // mapping is not known yet a contact usync fetches it.
3603
+ async _resolveBlockTarget(jid) {
3604
+ const norm = toNonAdJid(makeJid(jid));
3605
+ const user = norm.split('@')[0];
3606
+
3607
+ if (norm.endsWith('@lid')) {
3608
+ const pn = this._lidToPn.get(user);
3609
+ return { lid: norm, pn: pn ? pn + '@s.whatsapp.net' : null };
3610
+ }
3611
+
3612
+ let lidUser = this._pnToLid.get(user);
3613
+ if (!lidUser && this._devMgr) {
3614
+ _whaDbg('[DBG] BLOCK_LID_LOOKUP ' + user);
3615
+ try { await this._devMgr._doContactUsync([user]); } catch (_) {}
3616
+ lidUser = this._pnToLid.get(user);
3617
+ }
3618
+ return { lid: lidUser ? lidUser + '@lid' : null, pn: norm };
3619
+ }
3620
+
3621
+ // The one IQ behind both:
3513
3622
  //
3514
3623
  // <iq xmlns="blocklist" type="set" to="s.whatsapp.net">
3515
- // <item jid="<JID>" action="block|unblock"/>
3624
+ // <item action="block" jid="<LID>" pn_jid="<phone JID>"/>
3516
3625
  // </iq>
3517
3626
  //
3518
- // Three things were wrong with the pair this replaces. The jid went out as a
3519
- // raw string rather than a binary JID. It was not reduced to its bare user
3520
- // first, so blocking a contact you happened to name with a device suffix sent
3521
- // the server a device JID it has nothing to block. And the reply was thrown
3522
- // away without being looked at — including a type="error" reply — so a block
3523
- // the server refused came back looking exactly like one that worked.
3627
+ // The item is addressed by LID, and a block carries the phone number beside
3628
+ // it; an unblock carries only the LID. That is the shape Baileys'
3629
+ // updateBlockStatus builds, and the one this server accepts — whatsmeow's
3630
+ // UpdateBlocklist still sends the plain JID it was handed, which is what this
3631
+ // used to do and what the server answered with 400 bad-request.
3632
+ //
3633
+ // Three other things were wrong with the pair this replaces. The jid went out
3634
+ // as a raw string rather than a binary JID. It was not reduced to its bare
3635
+ // user first, so naming a contact with a device suffix sent a device JID
3636
+ // there is nothing to block. And the reply was thrown away without being
3637
+ // looked at, so a refusal came back looking exactly like a success.
3524
3638
  async _updateBlocklist(jid, action) {
3525
- const target = toNonAdJid(makeJid(jid));
3639
+ const { lid, pn } = await this._resolveBlockTarget(jid);
3640
+ if (!lid) {
3641
+ throw new Error(action + 'Contact: could not resolve a LID for ' + jid +
3642
+ ' — the server only accepts LID-addressed block list items. ' +
3643
+ 'Check the number is on WhatsApp.');
3644
+ }
3645
+ const attrs = { action, jid: jidStrToObj(lid) };
3646
+ if (action === 'block') {
3647
+ if (!pn) {
3648
+ throw new Error('blockContact: no phone number known for ' + lid +
3649
+ ' — a block has to carry pn_jid beside the LID.');
3650
+ }
3651
+ attrs.pn_jid = jidStrToObj(pn);
3652
+ }
3653
+
3526
3654
  const node = new BinaryNode('iq', {
3527
3655
  id: this._genMsgId(),
3528
3656
  xmlns: 'blocklist',
3529
3657
  to: 's.whatsapp.net',
3530
3658
  type: 'set'
3531
3659
  }, [
3532
- new BinaryNode('item', { jid: jidStrToObj(target), action }, null)
3660
+ new BinaryNode('item', attrs, null)
3533
3661
  ]);
3534
3662
 
3535
3663
  const resp = await this._sendIq(node);
@@ -3712,7 +3840,6 @@ class WhalibmobClient extends EventEmitter {
3712
3840
  async createGroup(subject, participants, opts) {
3713
3841
  opts = opts || {};
3714
3842
  const id = this._genMsgId();
3715
- const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
3716
3843
  // participant nodes MUST use attr `jid`, not `value`
3717
3844
  const partNodes = (participants || []).map(p => {
3718
3845
  const jid = makeJid(p);
@@ -3721,11 +3848,8 @@ class WhalibmobClient extends EventEmitter {
3721
3848
  const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
3722
3849
  if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
3723
3850
  }
3724
- const entry = this._tcTokenStore &&
3725
- this._tcTokenStore.get(normalizeJidForTcToken(jid));
3726
- const children = (entry && entry.token && entry.token.length)
3727
- ? [new BinaryNode('privacy', {}, entry.token)]
3728
- : null;
3851
+ const token = this._tcTokenFor(jid);
3852
+ const children = token ? [new BinaryNode('privacy', {}, token)] : null;
3729
3853
  return new BinaryNode('participant', attrs, children);
3730
3854
  });
3731
3855
 
@@ -3832,7 +3956,6 @@ class WhalibmobClient extends EventEmitter {
3832
3956
  async _groupParticipantAction(groupJid, action, jids) {
3833
3957
  const gJid = makeJid(groupJid);
3834
3958
  const arr = Array.isArray(jids) ? jids : [jids];
3835
- const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
3836
3959
  // Same shape whatsmeow's UpdateGroupParticipants builds: binary JIDs, and on
3837
3960
  // an add, the phone number behind a LID plus our privacy token for them.
3838
3961
  const participantNodes = arr.map(j => {
@@ -3844,11 +3967,8 @@ class WhalibmobClient extends EventEmitter {
3844
3967
  const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
3845
3968
  if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
3846
3969
  }
3847
- const entry = this._tcTokenStore &&
3848
- this._tcTokenStore.get(normalizeJidForTcToken(jid));
3849
- if (entry && entry.token && entry.token.length) {
3850
- children = [new BinaryNode('privacy', {}, entry.token)];
3851
- }
3970
+ const token = this._tcTokenFor(jid);
3971
+ if (token) children = [new BinaryNode('privacy', {}, token)];
3852
3972
  }
3853
3973
  return new BinaryNode('participant', attrs, children);
3854
3974
  });
@@ -23,7 +23,8 @@ const {
23
23
  } = require('../proto/MessageProto');
24
24
  const { uploadMedia } = require('../MediaService');
25
25
  const { imageThumbnail, videoThumbnail, probeImageSize } = require('../MediaThumbnail');
26
- const { tcTokenExpired, shouldSendNewTcToken, normalizeJidForTcToken } = require('./TcTokenStore');
26
+ const { tcTokenExpired, shouldSendNewTcToken, normalizeJidForTcToken,
27
+ isRegularTcTokenUser } = require('./TcTokenStore');
27
28
 
28
29
  // ─── Helpers ──────────────────────────────────────────────────────────────────
29
30
 
@@ -1014,7 +1015,10 @@ class MessageSender {
1014
1015
  // event. Once enough accumulate the server returns error 463 (timelock).
1015
1016
  // We attach the stored trusted-contact token when one exists and is valid.
1016
1017
  const tcStore = this._client._tcTokenStore;
1017
- const tcJid = normalizeJidForTcToken(routingToJid);
1018
+ // Keyed by the contact's LID, which is where a token they issued us was
1019
+ // filed when it arrived. Looking it up by the phone JID — which is what
1020
+ // this did — never found one, so nothing was ever attached.
1021
+ const tcJid = this._client._resolveTcTokenJid(routingToJid);
1018
1022
  if (tcStore) {
1019
1023
  const tcEntry = tcStore.get(tcJid);
1020
1024
  if (tcEntry && tcEntry.token && tcEntry.token.length) {
@@ -1078,11 +1082,23 @@ class MessageSender {
1078
1082
  // for this JID (one issuance per 7-day bucket is enough). The token is
1079
1083
  // stored and attached on future sends so the server does not count them
1080
1084
  // as anonymous "reaching out" events (which would trigger error 463).
1081
- if (tcStore && tcStore.shouldIssue(tcJid) &&
1085
+ //
1086
+ // Two targets never get one. WhatsApp's own announcement account
1087
+ // (0@s.whatsapp.net) and the bot numbers, including Meta AI, are not
1088
+ // contacts you can be "timelocked" against, and no real client issues to
1089
+ // them — whatsmeow gates on shouldSendTCTokenInChatAction, Baileys on
1090
+ // isBotOrPSA. Protocol messages (a revoke, an ephemeral-timer change) are
1091
+ // out too: Baileys skips issuance for them because they are bookkeeping,
1092
+ // not a reach-out. Attaching an existing token to them is still correct
1093
+ // and still happens above — this only governs asking for a new one.
1094
+ const isProtocolMsg = !!(options && options._protocol);
1095
+ if (tcStore && isRegularTcTokenUser(tcJid) && !isProtocolMsg &&
1096
+ tcStore.shouldIssue(tcJid) &&
1082
1097
  !this._client._inFlightTcTokenIssuance.has(tcJid)) {
1083
1098
  this._client._inFlightTcTokenIssuance.add(tcJid);
1084
- const issueTs = Math.floor(Date.now() / 1000);
1085
- this._client._issuePrivacyTokens(routingToJid, issueTs)
1099
+ const issueTs = Math.floor(Date.now() / 1000);
1100
+ const issueJid = this._client._resolveTcTokenIssuanceJid(routingToJid);
1101
+ this._client._issuePrivacyTokens(issueJid, issueTs)
1086
1102
  .then(result => this._client._storeTcTokenFromIqResult(result, tcJid, issueTs))
1087
1103
  .catch(() => {})
1088
1104
  .finally(() => this._client._inFlightTcTokenIssuance.delete(tcJid));
@@ -1110,10 +1126,17 @@ class MessageSender {
1110
1126
  (typeof this._client._getStatusRecipients === 'function'
1111
1127
  ? await this._client._getStatusRecipients()
1112
1128
  : []);
1113
- if (!members.length) {
1129
+ // Our own JID is always in the list so the post reaches our own devices,
1130
+ // so a list of length one means there is nobody else on it. Counting the
1131
+ // raw list let that through: the walk below then dropped us as the
1132
+ // sender, left no targets, and the stanza went out as a bare skmsg with
1133
+ // no <participants> — a SenderKey message nobody holds the key for, which
1134
+ // the server answers with ack 479.
1135
+ const others = members.filter(jid => !this._client._isOwnUser(jid));
1136
+ if (others.length === 0) {
1114
1137
  throw new Error(
1115
- 'Cannot post a Status: no recipients. Sync history so the contact ' +
1116
- 'list is known, or pass { recipients: [...] }.'
1138
+ 'Cannot post a Status: it would reach nobody. The contact list is ' +
1139
+ 'empty — sync history first, or pass { recipients: [...] }.'
1117
1140
  );
1118
1141
  }
1119
1142
  }
@@ -1258,6 +1281,16 @@ class MessageSender {
1258
1281
  if (myLidUser) selfAddrs.add(adString(`${myLidUser}@lid`));
1259
1282
  const allTargets = enumeratedDevices.filter(jid => !selfAddrs.has(adString(jid)));
1260
1283
 
1284
+ // Whatever the member list said, if nothing came back with a session there
1285
+ // is no one to hand the SenderKey to and the skmsg below is undeliverable.
1286
+ // Better to say so than to emit it and read the 479 back.
1287
+ if (isStatus && allTargets.length === 0) {
1288
+ throw new Error(
1289
+ 'Cannot post a Status: no Signal session could be opened with any ' +
1290
+ 'recipient, so the post would reach nobody.'
1291
+ );
1292
+ }
1293
+
1261
1294
  const skStore = this._signal.senderKeyStore;
1262
1295
  const existingSkdmMap = skStore.getSKDMMap(groupJid);
1263
1296
  // Same exclusions Baileys applies when choosing SenderKey recipients:
@@ -1411,7 +1444,7 @@ class MessageSender {
1411
1444
  // concurrent 463 recoveries and proactive issuances don't clash.
1412
1445
  if (error === '463' && !_isRetry) {
1413
1446
  const ackFrom = ackAttrs.from ? String(ackAttrs.from) : '';
1414
- const tcJid = normalizeJidForTcToken(
1447
+ const tcJid = client._resolveTcTokenJid(
1415
1448
  ackFrom || String(node.attrs && node.attrs.to || '')
1416
1449
  );
1417
1450
  _whaDbg('[DBG] ACK_463 from=' + ackFrom + ' id=' + id +
@@ -1439,7 +1472,13 @@ class MessageSender {
1439
1472
  client._inFlight463Recoveries.add(tcJid);
1440
1473
  const issueTs = Math.floor(Date.now() / 1000);
1441
1474
 
1442
- client._issuePrivacyTokens(ackFrom || tcJid, issueTs)
1475
+ // Same address family as every other issuance path — see
1476
+ // Client._resolveTcTokenIssuanceJid. This used to hand the server
1477
+ // whatever the ack happened to carry.
1478
+ const issueJid = client._resolveTcTokenIssuanceJid(
1479
+ ackFrom || String(node.attrs && node.attrs.to || '')
1480
+ );
1481
+ client._issuePrivacyTokens(issueJid, issueTs)
1443
1482
  .then(result => client._storeTcTokenFromIqResult(result, tcJid, issueTs))
1444
1483
  .then(() => {
1445
1484
  // Verify that real token bytes were received from the server.
@@ -1540,7 +1579,9 @@ class MessageSender {
1540
1579
  const msgBuf = encodeMessage('protocol', revokePayload);
1541
1580
  // ProtocolMessage is one of the explicit "text" cases in whatsmeow's
1542
1581
  // getTypeFromMessage; there is no "protocol" stanza type.
1543
- return this._sendMessage(toJid, msgId, msgBuf, 'text', { ...opts, edit: editBit });
1582
+ // _protocol marks it for the tcToken issuance gate — see _sendDMMessage.
1583
+ return this._sendMessage(toJid, msgId, msgBuf, 'text',
1584
+ { ...opts, edit: editBit, _protocol: true });
1544
1585
  }
1545
1586
 
1546
1587
  // ─── Ephemeral timer DM ───────────────────────────────────────────────────
@@ -1555,7 +1596,7 @@ class MessageSender {
1555
1596
  ephemeralExpiration: seconds
1556
1597
  });
1557
1598
  const msgBuf = encodeMessage('protocol', payload);
1558
- return this._sendMessage(toJid, msgId, msgBuf, 'text', opts);
1599
+ return this._sendMessage(toJid, msgId, msgBuf, 'text', { ...opts, _protocol: true });
1559
1600
  }
1560
1601
 
1561
1602
  // ─── Utility: mark read, presence, receipt, ping ─────────────────────────
@@ -6,6 +6,14 @@ const path = require('path');
6
6
  // 7-day buckets, 4 buckets = ~28-day rolling window (matches Baileys tc-token-utils)
7
7
  const TC_BUCKET_DURATION = 604800;
8
8
  const TC_NUM_BUCKETS = 4;
9
+ // whatsmeow prunes expired rows out of the token table at most once a day
10
+ // (tcTokenDBPruneInterval). Ours is a JSON file, so the same cadence applies.
11
+ const TC_PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
12
+
13
+ // The two phone-number ranges WhatsApp uses for bots, including Meta AI.
14
+ // Byte-for-byte the same expression in whatsmeow (botUserRegex) and in
15
+ // Baileys (BOT_PHONE_REGEX).
16
+ const BOT_USER_RE = /^1313555\d{4}$|^131655500\d{2}$/;
9
17
 
10
18
  function tcTokenExpired(timestamp) {
11
19
  if (timestamp == null) return true;
@@ -25,6 +33,26 @@ function shouldSendNewTcToken(senderTimestamp) {
25
33
  return curBucket > senderBucket;
26
34
  }
27
35
 
36
+ // Whether a JID is a real person we may hold a trusted-contact token for.
37
+ //
38
+ // whatsmeow gates issuance with shouldSendTCTokenInChatAction and Baileys gates
39
+ // both issuance and storage with isRegularUser; the two agree on the rule:
40
+ // a user address (phone or LID), not the PSA pseudo-contact 0@…, not a bot.
41
+ // Without it we issue a privacy IQ to WhatsApp's own announcement account and
42
+ // to Meta AI, which no real client ever does.
43
+ function isRegularTcTokenUser(jid) {
44
+ if (!jid) return false;
45
+ const norm = normalizeJidForTcToken(jid);
46
+ const atIdx = norm.indexOf('@');
47
+ if (atIdx < 0) return false;
48
+ const user = norm.slice(0, atIdx);
49
+ const server = norm.slice(atIdx + 1);
50
+ if (server !== 's.whatsapp.net' && server !== 'c.us' && server !== 'lid') return false;
51
+ if (user === '0') return false; // PSA
52
+ if (BOT_USER_RE.test(user)) return false; // bots, Meta AI
53
+ return true;
54
+ }
55
+
28
56
  // Strip device part and normalize to bare JID
29
57
  function normalizeJidForTcToken(jid) {
30
58
  if (!jid) return String(jid || '');
@@ -43,7 +71,36 @@ class TcTokenStore {
43
71
  this._path = filePath;
44
72
  // jid → { token: Buffer|null, timestamp: number|null, senderTimestamp: number|null }
45
73
  this._map = new Map();
74
+ this._lastPrune = 0;
46
75
  this._load();
76
+ this.pruneExpired();
77
+ }
78
+
79
+ // Drop entries that are past the 28-day window on both counts.
80
+ //
81
+ // whatsmeow runs DeleteExpiredPrivacyTokens against the same cutoff and lets
82
+ // its in-memory sender-timestamp map age out on the same boundary. An entry
83
+ // whose token has expired but whose senderTimestamp is still inside the
84
+ // window has to stay: that timestamp is what keeps us from re-issuing on
85
+ // every single send.
86
+ pruneExpired() {
87
+ let removed = 0;
88
+ for (const [jid, entry] of this._map) {
89
+ const tokenDead = !entry.token || !entry.token.length || tcTokenExpired(entry.timestamp);
90
+ const senderDead = tcTokenExpired(entry.senderTimestamp);
91
+ if (tokenDead && senderDead) {
92
+ this._map.delete(jid);
93
+ removed++;
94
+ }
95
+ }
96
+ this._lastPrune = Date.now();
97
+ if (removed) this._save();
98
+ return removed;
99
+ }
100
+
101
+ _maybePrune() {
102
+ if (Date.now() - this._lastPrune < TC_PRUNE_INTERVAL_MS) return;
103
+ this.pruneExpired();
47
104
  }
48
105
 
49
106
  _load() {
@@ -77,14 +134,36 @@ class TcTokenStore {
77
134
  }
78
135
 
79
136
  get(jid) {
137
+ // whatsmeow prunes from ensureTCToken, i.e. on the read path, rate-limited
138
+ // to once a day. Same here.
139
+ this._maybePrune();
80
140
  return this._map.get(normalizeJidForTcToken(jid)) || null;
81
141
  }
82
142
 
143
+ // Returns true when the token was accepted.
144
+ //
145
+ // Two rejections, both taken from Baileys' storeTcTokensFromIqResult:
146
+ // - a token with no timestamp is already expired by definition, so storing
147
+ // it only wastes a slot and guarantees a miss on the next send;
148
+ // - a token older than the one on file must never overwrite it. Token
149
+ // deliveries are not ordered — an issuance result and a privacy_token
150
+ // notification for the same contact can land in either order — so without
151
+ // this a stale copy can replace a fresh one.
83
152
  setToken(jid, tokenBuf, timestamp) {
84
153
  const key = normalizeJidForTcToken(jid);
85
- const existing = this._map.get(key) || {};
86
- this._map.set(key, { ...existing, token: tokenBuf, timestamp });
154
+ const incoming = timestamp != null ? Number(timestamp) : 0;
155
+ if (!incoming || isNaN(incoming)) return false;
156
+
157
+ const existing = this._map.get(key) || {};
158
+ const existingTs = existing.timestamp != null ? Number(existing.timestamp) : 0;
159
+ if (existingTs > 0 && existingTs > incoming &&
160
+ existing.token && existing.token.length) {
161
+ return false;
162
+ }
163
+
164
+ this._map.set(key, { ...existing, token: tokenBuf, timestamp: incoming });
87
165
  this._save();
166
+ return true;
88
167
  }
89
168
 
90
169
  clearToken(jid) {
@@ -113,4 +192,12 @@ class TcTokenStore {
113
192
  }
114
193
  }
115
194
 
116
- module.exports = { TcTokenStore, tcTokenExpired, shouldSendNewTcToken, normalizeJidForTcToken };
195
+ module.exports = {
196
+ TcTokenStore,
197
+ tcTokenExpired,
198
+ shouldSendNewTcToken,
199
+ normalizeJidForTcToken,
200
+ isRegularTcTokenUser,
201
+ TC_BUCKET_DURATION,
202
+ TC_NUM_BUCKETS
203
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.5.89",
3
+ "version": "5.5.93",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto50",
6
6
  "main": "index.js",