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 +4 -2
- package/lib/Client.js +127 -51
- package/lib/messages/MessageSender.js +53 -12
- package/lib/messages/TcTokenStore.js +90 -3
- package/package.json +1 -1
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
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2132
|
-
|
|
2133
|
-
this
|
|
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 {
|
|
2619
|
+
const { isRegularTcTokenUser } = require('./messages/TcTokenStore');
|
|
2543
2620
|
for (const raw of arr) {
|
|
2544
2621
|
const jid = makeJid(raw);
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
);
|
|
2548
|
-
const children = (
|
|
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
|
|
2686
|
-
const
|
|
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
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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) &&
|
|
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 =
|
|
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
|
|
3769
|
-
|
|
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
|
|
3892
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1085
|
-
this._client.
|
|
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
|
-
|
|
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:
|
|
1116
|
-
'
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
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 = {
|
|
195
|
+
module.exports = {
|
|
196
|
+
TcTokenStore,
|
|
197
|
+
tcTokenExpired,
|
|
198
|
+
shouldSendNewTcToken,
|
|
199
|
+
normalizeJidForTcToken,
|
|
200
|
+
isRegularTcTokenUser,
|
|
201
|
+
TC_BUCKET_DURATION,
|
|
202
|
+
TC_NUM_BUCKETS
|
|
203
|
+
};
|