@whiskeysockets/baileys
Version:
A WebSockets library for interacting with WhatsApp Web
163 lines • 7.12 kB
JavaScript
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
// Same phone-number pattern as WABinary's isJidBot, applied against the user
// part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
/**
* Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
* storage against malformed notifications — WA Web filters server-side but we
* defend here for parity with `WAWebSetTcTokenChatAction.handleIncomingTcToken`.
* Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
*/
function isRegularUser(jid) {
if (!jid)
return false;
const user = jid.split('@')[0] ?? '';
if (user === '0')
return false; // PSA
if (BOT_PHONE_REGEX.test(user))
return false; // Bot by phone pattern
if (isJidMetaAI(jid))
return false; // MetaAI (@bot server)
return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
}
const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
/** Sentinel key under `tctoken` store holding a JSON array of tracked storage JIDs for cross-session pruning. */
export const TC_TOKEN_INDEX_KEY = '__index';
/** Read the persisted tctoken JID index and return its entries (never contains the sentinel key itself). */
export async function readTcTokenIndex(keys) {
const data = await keys.get('tctoken', [TC_TOKEN_INDEX_KEY]);
const entry = data[TC_TOKEN_INDEX_KEY];
if (!entry?.token?.length)
return [];
try {
const parsed = JSON.parse(Buffer.from(entry.token).toString());
if (!Array.isArray(parsed))
return [];
return parsed.filter((j) => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY);
}
catch {
return [];
}
}
/** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the sentinel key. */
export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
const persisted = await readTcTokenIndex(keys);
const merged = new Set(persisted);
for (const jid of addedJids) {
if (jid && jid !== TC_TOKEN_INDEX_KEY)
merged.add(jid);
}
return {
[TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
};
}
// WA Web has separate sender/receiver AB props for these but they're identical today
export function isTcTokenExpired(timestamp) {
if (timestamp === null || timestamp === undefined)
return true;
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
if (isNaN(ts))
return true;
const now = Math.floor(Date.now() / 1000);
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
return ts < cutoffTimestamp;
}
export function shouldSendNewTcToken(senderTimestamp) {
if (senderTimestamp === undefined)
return true;
const now = Math.floor(Date.now() / 1000);
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
return currentBucket > senderBucket;
}
/** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
export async function resolveTcTokenJid(jid, getLIDForPN) {
if (isLidUser(jid))
return jid;
const lid = await getLIDForPN(jid);
return lid ?? jid;
}
/** Resolve target JID for issuing privacy token based on AB prop 14303 */
export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
if (issueToLid) {
if (isLidUser(jid))
return jid;
const lid = await getLIDForPN(jid);
return lid ?? jid;
}
if (!isLidUser(jid))
return jid;
if (getPNForLID) {
const pn = await getPNForLID(jid);
return pn ?? jid;
}
return jid;
}
export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
try {
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
const tcTokenData = await authState.keys.get('tctoken', [storageJid]);
const entry = tcTokenData?.[storageJid];
const tcTokenBuffer = entry?.token;
if (!tcTokenBuffer?.length || isTcTokenExpired(entry?.timestamp)) {
if (tcTokenBuffer) {
// Preserve senderTimestamp so shouldSendNewTcToken() keeps its dedupe state
// after we drop the unusable peer token. Only wipe the record entirely when
// there's nothing worth keeping.
const cleared = entry?.senderTimestamp !== undefined
? { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }
: null;
await authState.keys.set({ tctoken: { [storageJid]: cleared } });
}
return baseContent.length > 0 ? baseContent : undefined;
}
baseContent.push({
tag: 'tctoken',
attrs: {},
content: tcTokenBuffer
});
return baseContent;
}
catch (error) {
return baseContent.length > 0 ? baseContent : undefined;
}
}
export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
const tokensNode = getBinaryNodeChild(result, 'tokens');
if (!tokensNode)
return;
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
for (const tokenNode of tokenNodes) {
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) {
continue;
}
// In notifications tokenNode.attrs.jid is your own device JID, not the sender's
const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
if (!isRegularUser(rawJid))
continue;
const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
const existingTcData = await keys.get('tctoken', [storageJid]);
const existingEntry = existingTcData[storageJid];
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
// timestamp-less tokens would be immediately expired
if (!incomingTs)
continue;
if (existingTs > 0 && existingTs > incomingTs)
continue;
await keys.set({
tctoken: {
[storageJid]: {
...existingEntry,
token: Buffer.from(tokenNode.content),
timestamp: tokenNode.attrs.t
}
}
});
onNewJidStored?.(storageJid);
}
}
//# sourceMappingURL=tc-token-utils.js.map