@nexustechpro/baileys 2.1.2 → 2.1.3
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/lib/Socket/messages-recv.js +4 -3
- package/lib/Socket/messages-send.js +14 -19
- package/lib/Utils/chat-utils.js +2 -2
- package/lib/Utils/key-store.js +10 -5
- package/lib/Utils/lt-hash.js +2 -43
- package/lib/Utils/process-message.js +234 -215
- package/lib/Utils/tc-token-utils.js +38 -73
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -527,7 +527,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
527
527
|
|
|
528
528
|
const tcTokenKnownJids = new Set()
|
|
529
529
|
const tcTokenIndexLoaded = (async () => {
|
|
530
|
-
try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
|
|
530
|
+
try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); if (tcTokenKnownJids.size > 0) logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
|
|
531
531
|
catch (err) { logger.warn({ err: err?.message }, 'failed to load tctoken index') }
|
|
532
532
|
})()
|
|
533
533
|
|
|
@@ -942,7 +942,8 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
942
942
|
else survivors.add(jid)
|
|
943
943
|
}
|
|
944
944
|
if (mutated === 0) return
|
|
945
|
-
await authState.keys
|
|
945
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [...survivors])
|
|
946
|
+
await authState.keys.set({ tctoken: { ...writes, ...indexWrite } })
|
|
946
947
|
tcTokenKnownJids.clear()
|
|
947
948
|
for (const jid of survivors) tcTokenKnownJids.add(jid)
|
|
948
949
|
logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
|
|
@@ -962,5 +963,5 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
962
963
|
sendActiveReceipts = false
|
|
963
964
|
})
|
|
964
965
|
|
|
965
|
-
return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager }
|
|
966
|
+
return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, messageRetryManager, tcTokenIndexLoaded }
|
|
966
967
|
}
|
|
@@ -367,13 +367,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
367
367
|
const isStatus = jid === 'status@broadcast'
|
|
368
368
|
let isLid = server === 'lid'
|
|
369
369
|
const isNewsletter = server === 'newsletter'
|
|
370
|
-
if (isLid && !isGroup && !isStatus) {
|
|
371
|
-
const pn = await signalRepository.lidMapping.getPNForLID(jid)
|
|
372
|
-
if (pn) {
|
|
373
|
-
const resolvedJid = pn.replace(/:\d+(?=@)/, ''); logger.debug({ from: jid, to: resolvedJid }, 'relayMessage: resolved 1:1 LID to PN for wire protocol')
|
|
374
|
-
jid = resolvedJid; const decoded = jidDecode(jid); user = decoded.user; server = decoded.server; isLid = false
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
370
|
let activeSender = meId
|
|
378
371
|
let groupAddressingMode = 'pn'
|
|
379
372
|
if (isGroup && !isStatus) {
|
|
@@ -542,6 +535,14 @@ export const makeMessagesSocket = (config) => {
|
|
|
542
535
|
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
543
536
|
|
|
544
537
|
const { user: ownUser } = jidDecode(ownId)
|
|
538
|
+
const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
539
|
+
devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
|
|
540
|
+
|
|
541
|
+
if (user !== ownUser) {
|
|
542
|
+
const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
543
|
+
const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
|
|
544
|
+
devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
|
|
545
|
+
}
|
|
545
546
|
|
|
546
547
|
if (!participant) {
|
|
547
548
|
const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
@@ -570,22 +571,16 @@ export const makeMessagesSocket = (config) => {
|
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
573
|
}
|
|
573
|
-
|
|
574
574
|
const allRecipients = [], meRecipients = [], otherRecipients = []
|
|
575
575
|
const { user: mePnUser } = jidDecode(meId)
|
|
576
576
|
const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (isMe) meRecipients.push(jid)
|
|
583
|
-
else otherRecipients.push(jid)
|
|
584
|
-
allRecipients.push(jid)
|
|
577
|
+
for (const { user: devUser, jid: devJid } of devices) {
|
|
578
|
+
const isOwnUser = devUser === mePnUser || devUser === meLidUser
|
|
579
|
+
if (isOwnUser) meRecipients.push(devJid)
|
|
580
|
+
else otherRecipients.push(devJid)
|
|
581
|
+
allRecipients.push(devJid)
|
|
585
582
|
}
|
|
586
|
-
|
|
587
583
|
await assertSessions(allRecipients)
|
|
588
|
-
|
|
589
584
|
const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
|
|
590
585
|
createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
|
|
591
586
|
createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
|
|
@@ -613,7 +608,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
613
608
|
id: finalMsgId,
|
|
614
609
|
to: destinationJid,
|
|
615
610
|
type: getMessageType(message),
|
|
616
|
-
...(
|
|
611
|
+
...((isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
|
|
617
612
|
...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}),
|
|
618
613
|
...(additionalAttributes || {})
|
|
619
614
|
},
|
package/lib/Utils/chat-utils.js
CHANGED
|
@@ -53,7 +53,7 @@ export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
|
|
|
53
53
|
}
|
|
54
54
|
if (prevOp) subBuffs.push(prevOp.valueMac)
|
|
55
55
|
},
|
|
56
|
-
finish: () => {
|
|
56
|
+
finish: async () => {
|
|
57
57
|
const result = LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)
|
|
58
58
|
return { hash: Buffer.from(result), indexValueMap }
|
|
59
59
|
}
|
|
@@ -99,7 +99,7 @@ export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, op
|
|
|
99
99
|
const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
|
|
100
100
|
const generator = makeLtHashGenerator(state)
|
|
101
101
|
generator.mix({ indexMac, valueMac, operation })
|
|
102
|
-
Object.assign(state, generator.finish())
|
|
102
|
+
Object.assign(state, await generator.finish())
|
|
103
103
|
state.version += 1
|
|
104
104
|
const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
|
|
105
105
|
const patch = {
|
package/lib/Utils/key-store.js
CHANGED
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
// Single source of truth for blob key names — change here, changes everywhere
|
|
3
3
|
export const SESSION_INDEX_KEY = 'index'
|
|
4
4
|
export const DEVICE_LIST_INDEX_KEY = 'index'
|
|
5
|
+
export const TC_TOKEN_INDEX_KEY = 'index'
|
|
5
6
|
|
|
6
7
|
// Migrates old _index blob to new index key in-place, returns the batch data
|
|
7
8
|
export const migrateIndexKey = async (keys, type) => {
|
|
8
|
-
const
|
|
9
|
+
const oldKeys = ['_index', '__index']
|
|
9
10
|
const newKey = 'index'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
await keys.
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
for (const oldKey of oldKeys) {
|
|
13
|
+
const oldData = await keys.get(type, [oldKey])
|
|
14
|
+
if (oldData?.[oldKey] !== null && oldData?.[oldKey] !== undefined && typeof oldData[oldKey] === 'object') {
|
|
15
|
+
await keys.set({ [type]: { [newKey]: oldData[oldKey], [oldKey]: null } })
|
|
16
|
+
return oldData[oldKey]
|
|
17
|
+
}
|
|
14
18
|
}
|
|
19
|
+
|
|
15
20
|
const newData = await keys.get(type, [newKey])
|
|
16
21
|
return newData?.[newKey] || {}
|
|
17
22
|
}
|
package/lib/Utils/lt-hash.js
CHANGED
|
@@ -1,48 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LTHashAntiTampering } from 'whatsapp-rust-bridge';
|
|
2
2
|
/**
|
|
3
3
|
* LT Hash is a summation based hash algorithm that maintains the integrity of a piece of data
|
|
4
4
|
* over a series of mutations. You can add/remove mutations and it'll return a hash equal to
|
|
5
5
|
* if the same series of mutations was made sequentially.
|
|
6
6
|
*/
|
|
7
|
-
const
|
|
8
|
-
class LTHash {
|
|
9
|
-
constructor(e) {
|
|
10
|
-
this.salt = e;
|
|
11
|
-
}
|
|
12
|
-
async add(e, t) {
|
|
13
|
-
for (const item of t) {
|
|
14
|
-
e = await this._addSingle(e, item);
|
|
15
|
-
}
|
|
16
|
-
return e;
|
|
17
|
-
}
|
|
18
|
-
async subtract(e, t) {
|
|
19
|
-
for (const item of t) {
|
|
20
|
-
e = await this._subtractSingle(e, item);
|
|
21
|
-
}
|
|
22
|
-
return e;
|
|
23
|
-
}
|
|
24
|
-
async subtractThenAdd(e, addList, subtractList) {
|
|
25
|
-
const subtracted = await this.subtract(e, subtractList);
|
|
26
|
-
return this.add(subtracted, addList);
|
|
27
|
-
}
|
|
28
|
-
async _addSingle(e, t) {
|
|
29
|
-
const derived = new Uint8Array(hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
|
|
30
|
-
return this.performPointwiseWithOverflow(e, derived, (a, b) => a + b);
|
|
31
|
-
}
|
|
32
|
-
async _subtractSingle(e, t) {
|
|
33
|
-
const derived = new Uint8Array(hkdf(Buffer.from(t), o, { info: this.salt })).buffer;
|
|
34
|
-
return this.performPointwiseWithOverflow(e, derived, (a, b) => a - b);
|
|
35
|
-
}
|
|
36
|
-
performPointwiseWithOverflow(e, t, op) {
|
|
37
|
-
const n = new DataView(e);
|
|
38
|
-
const i = new DataView(t);
|
|
39
|
-
const out = new ArrayBuffer(n.byteLength);
|
|
40
|
-
const s = new DataView(out);
|
|
41
|
-
for (let offset = 0; offset < n.byteLength; offset += 2) {
|
|
42
|
-
s.setUint16(offset, op(n.getUint16(offset, true), i.getUint16(offset, true)), true);
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
export const LT_HASH_ANTI_TAMPERING = new LTHash('WhatsApp Patch Integrity');
|
|
48
|
-
//# sourceMappingURL=lt-hash.js.map
|
|
7
|
+
export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering();
|
|
@@ -1,179 +1,200 @@
|
|
|
1
1
|
import { proto } from '../../WAProto/index.js';
|
|
2
|
-
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { Boom } from '@hapi/boom';
|
|
3
3
|
import { WAMessageStubType } from '../Types/index.js';
|
|
4
4
|
import { getContentType, normalizeMessageContent } from '../Utils/messages.js';
|
|
5
|
-
import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
|
|
5
|
+
import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, isLidUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
|
|
6
6
|
import { aesDecryptGCM, hmacSign } from './crypto.js';
|
|
7
|
-
import {
|
|
8
|
-
import { toNumber } from './generics.js'
|
|
7
|
+
import { getKeyAuthor, toNumber } from './generics.js';
|
|
9
8
|
import { downloadAndProcessHistorySyncNotification } from './history.js';
|
|
9
|
+
import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js';
|
|
10
|
+
|
|
10
11
|
const REAL_MSG_STUB_TYPES = new Set([
|
|
11
12
|
WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
|
|
12
13
|
WAMessageStubType.CALL_MISSED_GROUP_VOICE,
|
|
13
14
|
WAMessageStubType.CALL_MISSED_VIDEO,
|
|
14
|
-
WAMessageStubType.CALL_MISSED_VOICE
|
|
15
|
+
WAMessageStubType.CALL_MISSED_VOICE,
|
|
15
16
|
]);
|
|
17
|
+
|
|
16
18
|
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
|
|
17
|
-
|
|
19
|
+
|
|
20
|
+
// Protocol message types that can only legitimately arrive from our own device.
|
|
21
|
+
// If they arrive from any other sender we drop them to prevent local-state spoofing.
|
|
22
|
+
// Cross-user types (REVOKE, MESSAGE_EDIT, EPHEMERAL_SETTING, GROUP_MEMBER_LABEL_CHANGE)
|
|
23
|
+
// must NOT be listed here — they legitimately arrive from other participants.
|
|
24
|
+
// Reference: https://github.com/tulir/whatsmeow/blob/8d3700152a/message.go#L842-L845
|
|
25
|
+
const SELF_ONLY_PROTOCOL_TYPES = new Set([
|
|
26
|
+
proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
|
|
27
|
+
proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
|
|
28
|
+
proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC,
|
|
29
|
+
proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** Persist tc-tokens discovered inside a history-sync payload, skipping stale entries. */
|
|
33
|
+
async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
|
|
34
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
35
|
+
const candidates = [];
|
|
36
|
+
for (const chat of chats) {
|
|
37
|
+
const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0;
|
|
38
|
+
if (!chat.tcToken?.length || ts <= 0) continue;
|
|
39
|
+
const jid = jidNormalizedUser(chat.id);
|
|
40
|
+
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
|
|
41
|
+
candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined });
|
|
42
|
+
}
|
|
43
|
+
if (!candidates.length) return;
|
|
44
|
+
|
|
45
|
+
const existing = await keyStore.get('tctoken', candidates.map(c => c.storageJid));
|
|
46
|
+
const entries = {};
|
|
47
|
+
for (const c of candidates) {
|
|
48
|
+
const existingTs = existing[c.storageJid]?.timestamp ? Number(existing[c.storageJid].timestamp) : 0;
|
|
49
|
+
if (existingTs > 0 && existingTs >= c.ts) continue;
|
|
50
|
+
entries[c.storageJid] = { ...existing[c.storageJid], token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) };
|
|
51
|
+
}
|
|
52
|
+
if (!Object.keys(entries).length) return;
|
|
53
|
+
|
|
54
|
+
logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync');
|
|
55
|
+
try {
|
|
56
|
+
// Include updated __index so cross-session pruning picks these JIDs up.
|
|
57
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries));
|
|
58
|
+
await keyStore.set({ tctoken: { ...entries, ...indexWrite } });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logger?.warn({ err }, 'failed to store tctokens from history sync');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Cleans a received message for further processing — strips device/agent from JIDs and corrects key perspective for reactions/polls. */
|
|
18
65
|
export const cleanMessage = (message, meId, meLid) => {
|
|
19
|
-
// ensure remoteJid and participant doesn't have device or agent in it
|
|
20
66
|
if (isHostedPnUser(message.key.remoteJid) || isHostedLidUser(message.key.remoteJid)) {
|
|
21
67
|
message.key.remoteJid = jidEncode(jidDecode(message.key?.remoteJid)?.user, isHostedPnUser(message.key.remoteJid) ? 's.whatsapp.net' : 'lid');
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
68
|
+
} else {
|
|
24
69
|
message.key.remoteJid = jidNormalizedUser(message.key.remoteJid);
|
|
25
70
|
}
|
|
26
71
|
if (isHostedPnUser(message.key.participant) || isHostedLidUser(message.key.participant)) {
|
|
27
72
|
message.key.participant = jidEncode(jidDecode(message.key.participant)?.user, isHostedPnUser(message.key.participant) ? 's.whatsapp.net' : 'lid');
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
73
|
+
} else {
|
|
30
74
|
message.key.participant = jidNormalizedUser(message.key.participant);
|
|
31
75
|
}
|
|
76
|
+
|
|
32
77
|
const content = normalizeMessageContent(message.message);
|
|
33
|
-
|
|
34
|
-
if (content?.
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
if (content?.pollUpdateMessage) {
|
|
38
|
-
normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
|
|
39
|
-
}
|
|
78
|
+
if (content?.reactionMessage) normaliseKey(content.reactionMessage.key);
|
|
79
|
+
if (content?.pollUpdateMessage) normaliseKey(content.pollUpdateMessage.pollCreationMessageKey);
|
|
80
|
+
|
|
40
81
|
function normaliseKey(msgKey) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) ||
|
|
48
|
-
areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
|
|
49
|
-
: // if the message being reacted to, was from them
|
|
50
|
-
// fromMe automatically becomes false
|
|
51
|
-
false;
|
|
52
|
-
// set the remoteJid to being the same as the chat the message came from
|
|
53
|
-
// TODO: investigate inconsistencies
|
|
54
|
-
msgKey.remoteJid = message.key.remoteJid;
|
|
55
|
-
// set participant of the message
|
|
56
|
-
msgKey.participant = msgKey.participant || message.key.participant;
|
|
57
|
-
}
|
|
82
|
+
if (message.key.fromMe) return;
|
|
83
|
+
msgKey.fromMe = !msgKey.fromMe
|
|
84
|
+
? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) || areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
|
|
85
|
+
: false; // message was from them — fromMe is definitively false from our perspective
|
|
86
|
+
msgKey.remoteJid = message.key.remoteJid; // TODO: investigate inconsistencies
|
|
87
|
+
msgKey.participant = msgKey.participant || message.key.participant;
|
|
58
88
|
}
|
|
59
89
|
};
|
|
90
|
+
|
|
60
91
|
// TODO: target:audit AUDIT THIS FUNCTION AGAIN
|
|
61
92
|
export const isRealMessage = (message) => {
|
|
62
93
|
const normalizedContent = normalizeMessageContent(message.message);
|
|
63
94
|
const hasSomeContent = !!getContentType(normalizedContent);
|
|
64
|
-
return (
|
|
65
|
-
REAL_MSG_STUB_TYPES.has(message.messageStubType) ||
|
|
66
|
-
REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
|
|
95
|
+
return (
|
|
96
|
+
(!!normalizedContent || REAL_MSG_STUB_TYPES.has(message.messageStubType) || REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
|
|
67
97
|
hasSomeContent &&
|
|
68
98
|
!normalizedContent?.protocolMessage &&
|
|
69
99
|
!normalizedContent?.reactionMessage &&
|
|
70
|
-
!normalizedContent?.pollUpdateMessage
|
|
100
|
+
!normalizedContent?.pollUpdateMessage
|
|
101
|
+
);
|
|
71
102
|
};
|
|
103
|
+
|
|
72
104
|
export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
|
|
105
|
+
|
|
73
106
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
107
|
+
* Derive the chat ID from a message key.
|
|
108
|
+
* For non-status broadcasts the chat is the participant (DM from that sender),
|
|
109
|
+
* for everything else it's the remoteJid.
|
|
76
110
|
*/
|
|
77
111
|
export const getChatId = ({ remoteJid, participant, fromMe }) => {
|
|
78
|
-
if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } })
|
|
79
|
-
if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe)
|
|
80
|
-
|
|
112
|
+
if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } });
|
|
113
|
+
if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
|
|
114
|
+
if (!participant) throw new Boom('Cannot derive chat id: broadcast message key is missing participant', { data: { remoteJid, fromMe } });
|
|
115
|
+
return participant;
|
|
116
|
+
}
|
|
117
|
+
return remoteJid;
|
|
81
118
|
};
|
|
119
|
+
|
|
82
120
|
/**
|
|
83
|
-
* Decrypt a poll vote
|
|
84
|
-
* @
|
|
85
|
-
* @param ctx additional info about the poll required for decryption
|
|
86
|
-
* @returns list of SHA256 options
|
|
121
|
+
* Decrypt a poll vote.
|
|
122
|
+
* @returns Decoded PollVoteMessage containing the SHA256 option hashes the voter selected.
|
|
87
123
|
*/
|
|
88
124
|
export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsgId, pollEncKey, voterJid }) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
toBinary(pollCreatorJid),
|
|
92
|
-
toBinary(voterJid),
|
|
93
|
-
toBinary('Poll Vote'),
|
|
94
|
-
new Uint8Array([1])
|
|
95
|
-
]);
|
|
125
|
+
const toBinary = (txt) => Buffer.from(txt);
|
|
126
|
+
const sign = Buffer.concat([toBinary(pollMsgId), toBinary(pollCreatorJid), toBinary(voterJid), toBinary('Poll Vote'), new Uint8Array([1])]);
|
|
96
127
|
const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256');
|
|
97
128
|
const decKey = hmacSign(sign, key0, 'sha256');
|
|
98
129
|
const aad = toBinary(`${pollMsgId}\u0000${voterJid}`);
|
|
99
|
-
|
|
100
|
-
return proto.Message.PollVoteMessage.decode(decrypted);
|
|
101
|
-
function toBinary(txt) {
|
|
102
|
-
return Buffer.from(txt);
|
|
103
|
-
}
|
|
130
|
+
return proto.Message.PollVoteMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, aad));
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const existingEntry = existing[c.storageJid]
|
|
119
|
-
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0
|
|
120
|
-
if (existingTs > 0 && existingTs >= c.ts) continue
|
|
121
|
-
entries[c.storageJid] = { ...existingEntry, token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) }
|
|
122
|
-
}
|
|
123
|
-
if (Object.keys(entries).length) {
|
|
124
|
-
logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync')
|
|
125
|
-
try { const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries)); await keyStore.set({ tctoken: { ...entries, ...indexWrite } }) }
|
|
126
|
-
catch (err) { logger?.warn({ err }, 'failed to store tctokens from history sync') }
|
|
127
|
-
}
|
|
133
|
+
/**
|
|
134
|
+
* Decrypt an event RSVP response.
|
|
135
|
+
* Mirrors decryptPollVote but for EventResponseMessage — uses 'Event Response' as the domain separator.
|
|
136
|
+
* @returns Decoded EventResponseMessage with the responder's RSVP choice.
|
|
137
|
+
*/
|
|
138
|
+
export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, eventMsgId, eventEncKey, responderJid }) {
|
|
139
|
+
const toBinary = (txt) => Buffer.from(txt);
|
|
140
|
+
const sign = Buffer.concat([toBinary(eventMsgId), toBinary(eventCreatorJid), toBinary(responderJid), toBinary('Event Response'), new Uint8Array([1])]);
|
|
141
|
+
const key0 = hmacSign(eventEncKey, new Uint8Array(32), 'sha256');
|
|
142
|
+
const decKey = hmacSign(sign, key0, 'sha256');
|
|
143
|
+
const aad = toBinary(`${eventMsgId}\u0000${responderJid}`);
|
|
144
|
+
return proto.Message.EventResponseMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, aad));
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
const SELF_ONLY_TYPES = new Set([proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION, proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE, proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC, proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE])
|
|
131
147
|
|
|
132
148
|
const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => {
|
|
133
149
|
const meId = creds.me.id;
|
|
134
150
|
const { accountSettings } = creds;
|
|
135
151
|
const chat = { id: jidNormalizedUser(getChatId(message.key)) };
|
|
136
152
|
const isRealMsg = isRealMessage(message);
|
|
153
|
+
|
|
137
154
|
if (isRealMsg) {
|
|
138
155
|
chat.messages = [{ message }];
|
|
139
156
|
chat.conversationTimestamp = toNumber(message.messageTimestamp);
|
|
140
|
-
|
|
141
|
-
if (shouldIncrementChatUnread(message)) {
|
|
142
|
-
chat.unreadCount = (chat.unreadCount || 0) + 1;
|
|
143
|
-
}
|
|
157
|
+
if (shouldIncrementChatUnread(message)) chat.unreadCount = (chat.unreadCount || 0) + 1;
|
|
144
158
|
}
|
|
159
|
+
|
|
145
160
|
const content = normalizeMessageContent(message.message);
|
|
146
|
-
|
|
147
|
-
//
|
|
161
|
+
|
|
162
|
+
// Unarchive if real message or someone reacted to our message and the setting is on
|
|
148
163
|
if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
|
|
149
164
|
chat.archived = false;
|
|
150
165
|
chat.readOnly = false;
|
|
151
166
|
}
|
|
167
|
+
|
|
152
168
|
const protocolMsg = content?.protocolMessage;
|
|
153
169
|
if (protocolMsg) {
|
|
154
|
-
|
|
170
|
+
// Drop self-only protocol messages that didn't come from our own device —
|
|
171
|
+
// an attacker could otherwise spoof history syncs, key shares, etc. to manipulate local state.
|
|
172
|
+
if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_PROTOCOL_TYPES.has(protocolMsg.type) && !message.key.fromMe) {
|
|
173
|
+
logger?.warn({ msgId: message.key.id, type: protocolMsg.type, from: message.key.participant || message.key.remoteJid }, 'dropping spoofed self-only protocolMessage from non-self origin');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
switch (protocolMsg.type) {
|
|
156
|
-
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
|
|
178
|
+
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION: {
|
|
157
179
|
const histNotification = protocolMsg.historySyncNotification;
|
|
158
|
-
const process = shouldProcessHistoryMsg;
|
|
159
180
|
const isLatest = !creds.processedHistoryMessages?.length;
|
|
160
|
-
logger?.info({
|
|
161
|
-
|
|
162
|
-
process,
|
|
163
|
-
id: message.key.id,
|
|
164
|
-
isLatest
|
|
165
|
-
}, 'got history notification');
|
|
166
|
-
if (process) {
|
|
181
|
+
logger?.info({ histNotification, process: shouldProcessHistoryMsg, id: message.key.id, isLatest }, 'got history notification');
|
|
182
|
+
if (shouldProcessHistoryMsg) {
|
|
167
183
|
if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
|
|
168
184
|
ev.emit('creds.update', { processedHistoryMessages: [...(creds.processedHistoryMessages || []), { key: message.key, messageTimestamp: message.messageTimestamp }] });
|
|
169
185
|
}
|
|
170
|
-
const data = await downloadAndProcessHistorySyncNotification(histNotification, options);
|
|
171
|
-
if (data.lidPnMappings?.length) {
|
|
172
|
-
|
|
186
|
+
const data = await downloadAndProcessHistorySyncNotification(histNotification, options, logger);
|
|
187
|
+
if (data.lidPnMappings?.length) {
|
|
188
|
+
logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync');
|
|
189
|
+
await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
|
|
190
|
+
}
|
|
191
|
+
await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger);
|
|
173
192
|
ev.emit('messaging-history.set', { ...data, isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined, chunkOrder: histNotification.chunkOrder, peerDataRequestSessionId: histNotification.peerDataRequestSessionId });
|
|
174
193
|
}
|
|
175
194
|
break;
|
|
176
|
-
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE: {
|
|
177
198
|
const keys = protocolMsg.appStateSyncKeyShare.keys;
|
|
178
199
|
if (keys?.length) {
|
|
179
200
|
let newAppStateSyncKeyId = '';
|
|
@@ -188,68 +209,75 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
188
209
|
logger?.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys');
|
|
189
210
|
}, meId);
|
|
190
211
|
ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId });
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
212
|
+
} else {
|
|
193
213
|
logger?.info({ protocolMsg }, 'recv app state sync with 0 keys');
|
|
194
214
|
}
|
|
195
215
|
break;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
...message.key,
|
|
201
|
-
id: protocolMsg.key.id
|
|
202
|
-
},
|
|
203
|
-
update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
|
|
204
|
-
}
|
|
205
|
-
]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case proto.Message.ProtocolMessage.Type.REVOKE: {
|
|
219
|
+
ev.emit('messages.update', [{ key: { ...message.key, id: protocolMsg.key.id }, update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key } }]);
|
|
206
220
|
break;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING: {
|
|
224
|
+
Object.assign(chat, { ephemeralSettingTimestamp: toNumber(message.messageTimestamp), ephemeralExpiration: protocolMsg.ephemeralExpiration || null });
|
|
212
225
|
break;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT: {
|
|
229
|
+
ev.emit('messages.update', [{
|
|
230
|
+
// Key is in sender's perspective — flip fromMe so it's in ours
|
|
231
|
+
key: { ...message.key, id: protocolMsg.key?.id },
|
|
232
|
+
update: {
|
|
233
|
+
message: { editedMessage: { message: protocolMsg.editedMessage } },
|
|
234
|
+
messageTimestamp: protocolMsg.timestampMs ? Math.floor(toNumber(protocolMsg.timestampMs) / 1000) : message.messageTimestamp,
|
|
235
|
+
},
|
|
236
|
+
}]);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE: {
|
|
241
|
+
const labelAssociationMsg = protocolMsg.memberLabel;
|
|
242
|
+
if (labelAssociationMsg?.label) {
|
|
243
|
+
ev.emit('group.member-tag.update', { groupId: chat.id, label: labelAssociationMsg.label, participant: message.key.participant, participantAlt: message.key.participantAlt, messageTimestamp: Number(message.messageTimestamp) });
|
|
232
244
|
}
|
|
233
245
|
break;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE: {
|
|
249
|
+
const response = protocolMsg.peerDataOperationRequestResponseMessage;
|
|
250
|
+
if (!response) break;
|
|
251
|
+
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.)
|
|
252
|
+
for (const result of (response.peerDataOperationResult || [])) {
|
|
253
|
+
const retryResponse = result?.placeholderMessageResendResponse;
|
|
254
|
+
if (!retryResponse?.webMessageInfoBytes) continue;
|
|
255
|
+
try {
|
|
256
|
+
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
|
|
257
|
+
const msgId = webMessageInfo.key?.id;
|
|
258
|
+
// Retrieve cached original message data — preserves LID details,
|
|
259
|
+
// timestamps, etc. that the phone may omit in its PDO response
|
|
260
|
+
const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined;
|
|
261
|
+
if (msgId) await placeholderResendCache?.del(msgId);
|
|
262
|
+
let finalMsg;
|
|
263
|
+
if (cachedData && typeof cachedData === 'object') {
|
|
264
|
+
// Apply decoded message content onto cached metadata (preserves LID etc.)
|
|
265
|
+
cachedData.message = webMessageInfo.message;
|
|
266
|
+
if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp;
|
|
267
|
+
finalMsg = cachedData;
|
|
268
|
+
} else {
|
|
269
|
+
finalMsg = webMessageInfo;
|
|
248
270
|
}
|
|
271
|
+
logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend');
|
|
272
|
+
ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response');
|
|
249
275
|
}
|
|
250
|
-
|
|
276
|
+
}
|
|
251
277
|
break;
|
|
252
|
-
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC: {
|
|
253
281
|
const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload;
|
|
254
282
|
const { pnToLidMappings, chatDbMigrationTimestamp } = proto.LIDMigrationMappingSyncPayload.decode(encodedPayload);
|
|
255
283
|
logger?.debug({ pnToLidMappings, chatDbMigrationTimestamp }, 'got lid mappings and chat db migration timestamp');
|
|
@@ -259,82 +287,73 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
259
287
|
pairs.push({ lid: `${lid}@lid`, pn: `${pn}@s.whatsapp.net` });
|
|
260
288
|
}
|
|
261
289
|
await signalRepository.lidMapping.storeLIDPNMappings(pairs);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
}
|
|
290
|
+
for (const { pn, lid } of pairs) await signalRepository.migrateSession(pn, lid);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
267
293
|
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
294
|
+
} else if (content?.reactionMessage) {
|
|
295
|
+
ev.emit('messages.reaction', [{ reaction: { ...content.reactionMessage, key: message.key }, key: content.reactionMessage?.key }]);
|
|
296
|
+
} else if (content?.encEventResponseMessage) {
|
|
297
|
+
// Decrypt and re-emit event RSVPs — mirrors the poll vote flow but for calendar events
|
|
298
|
+
const encEventResponse = content.encEventResponseMessage;
|
|
299
|
+
const creationMsgKey = encEventResponse.eventCreationMessageKey;
|
|
300
|
+
const eventMsg = await getMessage(creationMsgKey);
|
|
301
|
+
if (eventMsg) {
|
|
302
|
+
try {
|
|
303
|
+
const meIdNormalised = jidNormalizedUser(meId);
|
|
304
|
+
// Event creator JID must be a PN — resolve from LID if necessary
|
|
305
|
+
const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid;
|
|
306
|
+
const eventCreatorPn = isLidUser(eventCreatorKey) ? await signalRepository.lidMapping.getPNForLID(eventCreatorKey) : eventCreatorKey;
|
|
307
|
+
const eventCreatorJid = getKeyAuthor({ remoteJid: jidNormalizedUser(eventCreatorPn), fromMe: meIdNormalised === eventCreatorPn }, meIdNormalised);
|
|
308
|
+
const responderJid = getKeyAuthor(message.key, meIdNormalised);
|
|
309
|
+
const eventEncKey = eventMsg?.messageContextInfo?.messageSecret;
|
|
310
|
+
if (!eventEncKey) {
|
|
311
|
+
logger?.warn({ creationMsgKey }, 'event response: missing messageSecret for decryption');
|
|
312
|
+
} else {
|
|
313
|
+
const responseMsg = decryptEventResponse(encEventResponse, { eventEncKey, eventCreatorJid, eventMsgId: creationMsgKey.id, responderJid });
|
|
314
|
+
ev.emit('messages.update', [{ key: creationMsgKey, update: { eventResponses: [{ eventResponseMessageKey: message.key, senderTimestampMs: responseMsg.timestampMs, response: responseMsg }] } }]);
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
logger?.warn({ err, creationMsgKey }, 'failed to decrypt event response');
|
|
278
318
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
319
|
+
} else {
|
|
320
|
+
logger?.warn({ creationMsgKey }, 'event creation message not found, cannot decrypt response');
|
|
321
|
+
}
|
|
322
|
+
} else if (message.messageStubType) {
|
|
282
323
|
const jid = message.key?.remoteJid;
|
|
283
|
-
//let actor = whatsappID (message.participant)
|
|
284
324
|
let participants;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
ev.emit('groups.update', [
|
|
294
|
-
{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt }
|
|
295
|
-
]);
|
|
296
|
-
};
|
|
297
|
-
const emitGroupRequestJoin = (participant, action, method) => {
|
|
298
|
-
ev.emit('group.join-request', {
|
|
299
|
-
id: jid,
|
|
300
|
-
author: message.key.participant,
|
|
301
|
-
authorPn: message.key.participantAlt,
|
|
302
|
-
participant: participant.lid,
|
|
303
|
-
participantPn: participant.pn,
|
|
304
|
-
action,
|
|
305
|
-
method: method
|
|
306
|
-
});
|
|
307
|
-
};
|
|
308
|
-
const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid.phoneNumber)); // ADD SUPPORT FOR LID
|
|
325
|
+
|
|
326
|
+
const emitParticipantsUpdate = (action) => ev.emit('group-participants.update', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participants, action });
|
|
327
|
+
const emitGroupUpdate = (update) => ev.emit('groups.update', [{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername }]);
|
|
328
|
+
const emitGroupRequestJoin = (participant, action, method) => ev.emit('group.join-request', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participant: participant.lid, participantPn: participant.pn, action, method });
|
|
329
|
+
|
|
330
|
+
// TODO: ADD SUPPORT FOR LID in participantsIncludesMe
|
|
331
|
+
const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.phoneNumber));
|
|
332
|
+
|
|
309
333
|
switch (message.messageStubType) {
|
|
310
334
|
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
|
|
311
|
-
participants = message.messageStubParameters.map(a =>
|
|
312
|
-
emitParticipantsUpdate('modify')
|
|
313
|
-
break
|
|
335
|
+
participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
|
|
336
|
+
emitParticipantsUpdate('modify');
|
|
337
|
+
break;
|
|
314
338
|
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
|
|
315
339
|
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
|
|
316
|
-
participants = message.messageStubParameters.map(
|
|
340
|
+
participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
|
|
317
341
|
emitParticipantsUpdate('remove');
|
|
318
|
-
|
|
319
|
-
if (participantsIncludesMe()) {
|
|
320
|
-
chat.readOnly = true;
|
|
321
|
-
}
|
|
342
|
+
if (participantsIncludesMe()) chat.readOnly = true;
|
|
322
343
|
break;
|
|
323
344
|
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
|
|
324
345
|
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
|
|
325
346
|
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
|
|
326
|
-
participants = message.messageStubParameters.map(
|
|
327
|
-
if (participantsIncludesMe())
|
|
328
|
-
chat.readOnly = false;
|
|
329
|
-
}
|
|
347
|
+
participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
|
|
348
|
+
if (participantsIncludesMe()) chat.readOnly = false;
|
|
330
349
|
emitParticipantsUpdate('add');
|
|
331
350
|
break;
|
|
332
351
|
case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
|
|
333
|
-
participants = message.messageStubParameters.map(
|
|
352
|
+
participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
|
|
334
353
|
emitParticipantsUpdate('demote');
|
|
335
354
|
break;
|
|
336
355
|
case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
|
|
337
|
-
participants = message.messageStubParameters.map(
|
|
356
|
+
participants = message.messageStubParameters.map(a => JSON.parse(a)) || [];
|
|
338
357
|
emitParticipantsUpdate('promote');
|
|
339
358
|
break;
|
|
340
359
|
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
|
|
@@ -423,9 +442,9 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
423
442
|
)
|
|
424
443
|
}
|
|
425
444
|
} */
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
445
|
+
|
|
446
|
+
if (Object.keys(chat).length > 1) ev.emit('chats.update', [chat]);
|
|
429
447
|
};
|
|
448
|
+
|
|
430
449
|
export default processMessage;
|
|
431
450
|
//# sourceMappingURL=process-message.js.map
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidMetaAI, isLidUser, isPnUser, jidNormalizedUser } from '../WABinary/index.js';
|
|
2
|
+
import { migrateIndexKey, TC_TOKEN_INDEX_KEY } from '../Utils/index.js';
|
|
3
|
+
|
|
2
4
|
// Same phone-number pattern as WABinary's isJidBot, applied against the user
|
|
3
5
|
// part so the check is invariant to @c.us ↔ @s.whatsapp.net normalization.
|
|
4
6
|
const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Mirrors WA Web's `Wid.isRegularUser()` (user ∧ ¬PSA ∧ ¬Bot). Used to gate tctoken
|
|
7
10
|
* storage against malformed notifications — WA Web filters server-side but we
|
|
@@ -9,93 +12,73 @@ const BOT_PHONE_REGEX = /^1313555\d{4}$|^131655500\d{2}$/;
|
|
|
9
12
|
* Works for both pre- and post-normalized JIDs (`@c.us` vs `@s.whatsapp.net`).
|
|
10
13
|
*/
|
|
11
14
|
function isRegularUser(jid) {
|
|
12
|
-
if (!jid)
|
|
13
|
-
return false;
|
|
15
|
+
if (!jid) return false;
|
|
14
16
|
const user = jid.split('@')[0] ?? '';
|
|
15
|
-
if (user === '0')
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
return false; // Bot by phone pattern
|
|
19
|
-
if (isJidMetaAI(jid))
|
|
20
|
-
return false; // MetaAI (@bot server)
|
|
17
|
+
if (user === '0') return false; // PSA
|
|
18
|
+
if (BOT_PHONE_REGEX.test(user)) return false; // Bot by phone pattern
|
|
19
|
+
if (isJidMetaAI(jid)) return false; // MetaAI (@bot server)
|
|
21
20
|
return !!(isPnUser(jid) || isLidUser(jid) || isHostedPnUser(jid) || isHostedLidUser(jid) || jid.endsWith('@c.us'));
|
|
22
21
|
}
|
|
22
|
+
|
|
23
23
|
const TC_TOKEN_BUCKET_DURATION = 604800; // 7 days
|
|
24
24
|
const TC_TOKEN_NUM_BUCKETS = 4; // ~28-day rolling window
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** Read the persisted tctoken JID index and return its entries (never contains the sentinel key itself). */
|
|
25
|
+
|
|
26
|
+
/** Read the persisted tctoken JID index and return its entries. */
|
|
28
27
|
export async function readTcTokenIndex(keys) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
if (!entry?.token?.length)
|
|
32
|
-
return [];
|
|
33
|
-
try {
|
|
34
|
-
const parsed = JSON.parse(Buffer.from(entry.token).toString());
|
|
35
|
-
if (!Array.isArray(parsed))
|
|
36
|
-
return [];
|
|
37
|
-
return parsed.filter((j) => typeof j === 'string' && j.length > 0 && j !== TC_TOKEN_INDEX_KEY);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
28
|
+
const batch = await migrateIndexKey(keys, 'tctoken');
|
|
29
|
+
return Object.keys(batch).filter(j => j && j !== TC_TOKEN_INDEX_KEY);
|
|
42
30
|
}
|
|
43
|
-
|
|
31
|
+
|
|
32
|
+
/** Build a SignalDataSet fragment that writes the merged index (persisted ∪ added) under the index key. */
|
|
44
33
|
export async function buildMergedTcTokenIndexWrite(keys, addedJids) {
|
|
45
|
-
const
|
|
46
|
-
const merged =
|
|
47
|
-
for (const jid of addedJids) {
|
|
48
|
-
|
|
49
|
-
merged.add(jid);
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
[TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...merged])) }
|
|
53
|
-
};
|
|
34
|
+
const batch = await migrateIndexKey(keys, 'tctoken');
|
|
35
|
+
const merged = { ...batch };
|
|
36
|
+
for (const jid of addedJids) { if (jid && jid !== TC_TOKEN_INDEX_KEY) merged[jid] = merged[jid] || true; }
|
|
37
|
+
return { [TC_TOKEN_INDEX_KEY]: merged };
|
|
54
38
|
}
|
|
39
|
+
|
|
55
40
|
// WA Web has separate sender/receiver AB props for these but they're identical today
|
|
56
41
|
export function isTcTokenExpired(timestamp) {
|
|
57
|
-
if (timestamp === null || timestamp === undefined)
|
|
58
|
-
return true;
|
|
42
|
+
if (timestamp === null || timestamp === undefined) return true;
|
|
59
43
|
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
|
|
60
|
-
if (isNaN(ts))
|
|
61
|
-
return true;
|
|
44
|
+
if (isNaN(ts)) return true;
|
|
62
45
|
const now = Math.floor(Date.now() / 1000);
|
|
63
46
|
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
|
|
64
47
|
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1);
|
|
65
48
|
const cutoffTimestamp = cutoffBucket * TC_TOKEN_BUCKET_DURATION;
|
|
66
49
|
return ts < cutoffTimestamp;
|
|
67
50
|
}
|
|
51
|
+
|
|
68
52
|
export function shouldSendNewTcToken(senderTimestamp) {
|
|
69
|
-
if (senderTimestamp === undefined)
|
|
70
|
-
return true;
|
|
53
|
+
if (senderTimestamp === undefined) return true;
|
|
71
54
|
const now = Math.floor(Date.now() / 1000);
|
|
72
55
|
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION);
|
|
73
56
|
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION);
|
|
74
57
|
return currentBucket > senderBucket;
|
|
75
58
|
}
|
|
59
|
+
|
|
76
60
|
/** Resolve JID to LID for tctoken storage (WA Web stores under LID) */
|
|
77
61
|
export async function resolveTcTokenJid(jid, getLIDForPN) {
|
|
78
|
-
if (isLidUser(jid))
|
|
79
|
-
return jid;
|
|
62
|
+
if (isLidUser(jid)) return jid;
|
|
80
63
|
const lid = await getLIDForPN(jid);
|
|
81
64
|
return lid ?? jid;
|
|
82
65
|
}
|
|
66
|
+
|
|
83
67
|
/** Resolve target JID for issuing privacy token based on AB prop 14303 */
|
|
84
68
|
export async function resolveIssuanceJid(jid, issueToLid, getLIDForPN, getPNForLID) {
|
|
85
69
|
if (issueToLid) {
|
|
86
|
-
if (isLidUser(jid))
|
|
87
|
-
return jid;
|
|
70
|
+
if (isLidUser(jid)) return jid;
|
|
88
71
|
const lid = await getLIDForPN(jid);
|
|
89
72
|
return lid ?? jid;
|
|
90
73
|
}
|
|
91
|
-
if (!isLidUser(jid))
|
|
92
|
-
return jid;
|
|
74
|
+
if (!isLidUser(jid)) return jid;
|
|
93
75
|
if (getPNForLID) {
|
|
94
76
|
const pn = await getPNForLID(jid);
|
|
95
77
|
return pn ?? jid;
|
|
96
78
|
}
|
|
97
79
|
return jid;
|
|
98
80
|
}
|
|
81
|
+
|
|
99
82
|
export async function buildTcTokenFromJid({ authState, jid, baseContent = [], getLIDForPN }) {
|
|
100
83
|
try {
|
|
101
84
|
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
|
|
@@ -114,49 +97,31 @@ export async function buildTcTokenFromJid({ authState, jid, baseContent = [], ge
|
|
|
114
97
|
}
|
|
115
98
|
return baseContent.length > 0 ? baseContent : undefined;
|
|
116
99
|
}
|
|
117
|
-
baseContent.push({
|
|
118
|
-
tag: 'tctoken',
|
|
119
|
-
attrs: {},
|
|
120
|
-
content: tcTokenBuffer
|
|
121
|
-
});
|
|
100
|
+
baseContent.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer });
|
|
122
101
|
return baseContent;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
102
|
+
} catch (error) {
|
|
125
103
|
return baseContent.length > 0 ? baseContent : undefined;
|
|
126
104
|
}
|
|
127
105
|
}
|
|
106
|
+
|
|
128
107
|
export async function storeTcTokensFromIqResult({ result, fallbackJid, keys, getLIDForPN, onNewJidStored }) {
|
|
129
108
|
const tokensNode = getBinaryNodeChild(result, 'tokens');
|
|
130
|
-
if (!tokensNode)
|
|
131
|
-
return;
|
|
109
|
+
if (!tokensNode) return;
|
|
132
110
|
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
|
|
133
111
|
for (const tokenNode of tokenNodes) {
|
|
134
|
-
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array))
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
112
|
+
if (tokenNode.attrs.type !== 'trusted_contact' || !(tokenNode.content instanceof Uint8Array)) continue;
|
|
137
113
|
// In notifications tokenNode.attrs.jid is your own device JID, not the sender's
|
|
138
114
|
const rawJid = jidNormalizedUser(fallbackJid || tokenNode.attrs.jid);
|
|
139
|
-
if (!isRegularUser(rawJid))
|
|
140
|
-
continue;
|
|
115
|
+
if (!isRegularUser(rawJid)) continue;
|
|
141
116
|
const storageJid = await resolveTcTokenJid(rawJid, getLIDForPN);
|
|
142
117
|
const existingTcData = await keys.get('tctoken', [storageJid]);
|
|
143
118
|
const existingEntry = existingTcData[storageJid];
|
|
144
119
|
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
|
|
145
120
|
const incomingTs = tokenNode.attrs.t ? Number(tokenNode.attrs.t) : 0;
|
|
146
121
|
// timestamp-less tokens would be immediately expired
|
|
147
|
-
if (!incomingTs)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
continue;
|
|
151
|
-
await keys.set({
|
|
152
|
-
tctoken: {
|
|
153
|
-
[storageJid]: {
|
|
154
|
-
...existingEntry,
|
|
155
|
-
token: Buffer.from(tokenNode.content),
|
|
156
|
-
timestamp: tokenNode.attrs.t
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
122
|
+
if (!incomingTs) continue;
|
|
123
|
+
if (existingTs > 0 && existingTs > incomingTs) continue;
|
|
124
|
+
await keys.set({ tctoken: { [storageJid]: { ...existingEntry, token: Buffer.from(tokenNode.content), timestamp: tokenNode.attrs.t } } });
|
|
160
125
|
onNewJidStored?.(storageJid);
|
|
161
126
|
}
|
|
162
127
|
}
|
package/lib/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const banner = `
|
|
|
31
31
|
const info = `
|
|
32
32
|
┌───────────────────────────────────────────────────────────────────────┐
|
|
33
33
|
│ 📦 Package: @nexustechpro/baileys │
|
|
34
|
-
│ 🔖 Version: 2.1.
|
|
34
|
+
│ 🔖 Version: 2.1.3 │
|
|
35
35
|
│ ⚡ Status: Production Ready │
|
|
36
36
|
├───────────────────────────────────────────────────────────────────────┤
|
|
37
37
|
│ 🚀 Advanced WhatsApp Web API Client │
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@nexustechpro/baileys",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "2.1.
|
|
5
|
+
"version": "2.1.3",
|
|
6
6
|
"description": "Advanced WhatsApp Web API built on Baileys — interactive messages, buttons, carousels, products, events, payments, polls, albums, sticker packs and more.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"whatsapp",
|