whalibmob 5.5.90 → 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
 
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
  }
@@ -3756,7 +3840,6 @@ class WhalibmobClient extends EventEmitter {
3756
3840
  async createGroup(subject, participants, opts) {
3757
3841
  opts = opts || {};
3758
3842
  const id = this._genMsgId();
3759
- const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
3760
3843
  // participant nodes MUST use attr `jid`, not `value`
3761
3844
  const partNodes = (participants || []).map(p => {
3762
3845
  const jid = makeJid(p);
@@ -3765,11 +3848,8 @@ class WhalibmobClient extends EventEmitter {
3765
3848
  const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
3766
3849
  if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
3767
3850
  }
3768
- const entry = this._tcTokenStore &&
3769
- this._tcTokenStore.get(normalizeJidForTcToken(jid));
3770
- const children = (entry && entry.token && entry.token.length)
3771
- ? [new BinaryNode('privacy', {}, entry.token)]
3772
- : null;
3851
+ const token = this._tcTokenFor(jid);
3852
+ const children = token ? [new BinaryNode('privacy', {}, token)] : null;
3773
3853
  return new BinaryNode('participant', attrs, children);
3774
3854
  });
3775
3855
 
@@ -3876,7 +3956,6 @@ class WhalibmobClient extends EventEmitter {
3876
3956
  async _groupParticipantAction(groupJid, action, jids) {
3877
3957
  const gJid = makeJid(groupJid);
3878
3958
  const arr = Array.isArray(jids) ? jids : [jids];
3879
- const { normalizeJidForTcToken } = require('./messages/TcTokenStore');
3880
3959
  // Same shape whatsmeow's UpdateGroupParticipants builds: binary JIDs, and on
3881
3960
  // an add, the phone number behind a LID plus our privacy token for them.
3882
3961
  const participantNodes = arr.map(j => {
@@ -3888,11 +3967,8 @@ class WhalibmobClient extends EventEmitter {
3888
3967
  const pn = this._lidToPn.get(jid.split('@')[0].split(':')[0]);
3889
3968
  if (pn) attrs.phone_number = jidStrToObj(pn + '@s.whatsapp.net');
3890
3969
  }
3891
- const entry = this._tcTokenStore &&
3892
- this._tcTokenStore.get(normalizeJidForTcToken(jid));
3893
- if (entry && entry.token && entry.token.length) {
3894
- children = [new BinaryNode('privacy', {}, entry.token)];
3895
- }
3970
+ const token = this._tcTokenFor(jid);
3971
+ if (token) children = [new BinaryNode('privacy', {}, token)];
3896
3972
  }
3897
3973
  return new BinaryNode('participant', attrs, children);
3898
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.90",
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",