@nexustechpro/baileys 2.0.5 → 2.0.6

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.
@@ -1,9 +1,11 @@
1
1
  import { proto } from '../../WAProto/index.js';
2
+ import { Boom } from '@hapi/boom'
2
3
  import { WAMessageStubType } from '../Types/index.js';
3
4
  import { getContentType, normalizeMessageContent } from '../Utils/messages.js';
4
5
  import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
5
6
  import { aesDecryptGCM, hmacSign } from './crypto.js';
6
- import { toNumber } from './generics.js';
7
+ import { resolveTcTokenJid, buildMergedTcTokenIndexWrite } from './tc-token-utils.js'
8
+ import { toNumber } from './generics.js'
7
9
  import { downloadAndProcessHistorySyncNotification } from './history.js';
8
10
  const REAL_MSG_STUB_TYPES = new Set([
9
11
  WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
@@ -73,10 +75,9 @@ export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !me
73
75
  * Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
74
76
  */
75
77
  export const getChatId = ({ remoteJid, participant, fromMe }) => {
76
- if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
77
- return participant;
78
- }
79
- return remoteJid;
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) return participant
80
+ return remoteJid
80
81
  };
81
82
  /**
82
83
  * Decrypt a poll vote
@@ -101,7 +102,34 @@ export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsg
101
102
  return Buffer.from(txt);
102
103
  }
103
104
  }
104
- const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options }) => {
105
+
106
+ async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
107
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
108
+ const candidates = []
109
+ for (const chat of chats) {
110
+ const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0
111
+ if (chat.tcToken?.length && ts > 0) { const jid = jidNormalizedUser(chat.id); const storageJid = await resolveTcTokenJid(jid, getLIDForPN); candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined }) }
112
+ }
113
+ if (!candidates.length) return
114
+ const jids = candidates.map(c => c.storageJid)
115
+ const existing = await keyStore.get('tctoken', jids)
116
+ const entries = {}
117
+ for (const c of candidates) {
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
+ }
128
+ }
129
+
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
+
132
+ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => {
105
133
  const meId = creds.me.id;
106
134
  const { accountSettings } = creds;
107
135
  const chat = { id: jidNormalizedUser(getChatId(message.key)) };
@@ -123,6 +151,7 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
123
151
  }
124
152
  const protocolMsg = content?.protocolMessage;
125
153
  if (protocolMsg) {
154
+ if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_TYPES.has(protocolMsg.type) && !message.key.fromMe) { 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'); return }
126
155
  switch (protocolMsg.type) {
127
156
  case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
128
157
  const histNotification = protocolMsg.historySyncNotification;
@@ -135,21 +164,13 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
135
164
  isLatest
136
165
  }, 'got history notification');
137
166
  if (process) {
138
- // TODO: investigate
139
167
  if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
140
- ev.emit('creds.update', {
141
- processedHistoryMessages: [
142
- ...(creds.processedHistoryMessages || []),
143
- { key: message.key, messageTimestamp: message.messageTimestamp }
144
- ]
145
- });
168
+ ev.emit('creds.update', { processedHistoryMessages: [...(creds.processedHistoryMessages || []), { key: message.key, messageTimestamp: message.messageTimestamp }] });
146
169
  }
147
170
  const data = await downloadAndProcessHistorySyncNotification(histNotification, options);
148
- ev.emit('messaging-history.set', {
149
- ...data,
150
- isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
151
- peerDataRequestSessionId: histNotification.peerDataRequestSessionId
152
- });
171
+ if (data.lidPnMappings?.length) { logger?.debug({ count: data.lidPnMappings.length }, 'processing LID-PN mappings from history sync'); await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync')) }
172
+ await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger)
173
+ ev.emit('messaging-history.set', { ...data, isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined, chunkOrder: histNotification.chunkOrder, peerDataRequestSessionId: histNotification.peerDataRequestSessionId });
153
174
  }
154
175
  break;
155
176
  case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
@@ -192,24 +213,21 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
192
213
  case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
193
214
  const response = protocolMsg.peerDataOperationRequestResponseMessage;
194
215
  if (response) {
195
- await placeholderResendCache?.del(response.stanzaId);
196
- // TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
197
- const { peerDataOperationResult } = response;
216
+ const peerDataOperationResult = response.peerDataOperationResult || []
198
217
  for (const result of peerDataOperationResult) {
199
- const { placeholderMessageResendResponse: retryResponse } = result;
200
- //eslint-disable-next-line max-depth
201
- if (retryResponse) {
202
- const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes);
203
- // wait till another upsert event is available, don't want it to be part of the PDO response message
204
- // TODO: parse through proper message handling utilities (to add relevant key fields)
205
- setTimeout(() => {
206
- ev.emit('messages.upsert', {
207
- messages: [webMessageInfo],
208
- type: 'notify',
209
- requestId: response.stanzaId
210
- });
211
- }, 500);
212
- }
218
+ const retryResponse = result?.placeholderMessageResendResponse
219
+ if (!retryResponse?.webMessageInfoBytes) continue
220
+ try {
221
+ const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes)
222
+ const msgId = webMessageInfo.key?.id
223
+ const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined
224
+ if (msgId) await placeholderResendCache?.del(msgId)
225
+ let finalMsg
226
+ if (cachedData && typeof cachedData === 'object') { cachedData.message = webMessageInfo.message; if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp; finalMsg = cachedData }
227
+ else finalMsg = webMessageInfo
228
+ logger?.debug({ msgId, requestId: response.stanzaId }, 'received placeholder resend')
229
+ ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId })
230
+ } catch (err) { logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response') }
213
231
  }
214
232
  }
215
233
  break;
@@ -0,0 +1,155 @@
1
+ import { createHmac } from 'crypto'
2
+ import { hkdf } from './crypto.js'
3
+
4
+ const reportingFields = [
5
+ { f: 1 },
6
+ { f: 3, s: [{ f: 2 }, { f: 3 }, { f: 8 }, { f: 11 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 25 }] },
7
+ { f: 4, s: [{ f: 1 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
8
+ { f: 5, s: [{ f: 3 }, { f: 4 }, { f: 5 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
9
+ { f: 6, s: [{ f: 1 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 30 }] },
10
+ { f: 7, s: [{ f: 2 }, { f: 7 }, { f: 10 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
11
+ { f: 8, s: [{ f: 2 }, { f: 7 }, { f: 9 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 21 }] },
12
+ { f: 9, s: [{ f: 2 }, { f: 6 }, { f: 7 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
13
+ { f: 12, s: [{ f: 1 }, { f: 2 }, { f: 14, m: true }, { f: 15 }] },
14
+ { f: 18, s: [{ f: 6 }, { f: 16 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
15
+ { f: 26, s: [{ f: 4 }, { f: 5 }, { f: 8 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }] },
16
+ { f: 28, s: [{ f: 1 }, { f: 2 }, { f: 4 }, { f: 5 }, { f: 6 }, { f: 7, s: [{ f: 21 }, { f: 22 }] }] },
17
+ { f: 37, s: [{ f: 1, m: true }] },
18
+ { f: 49, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] }, { f: 8, s: [{ f: 1 }, { f: 2 }] }] },
19
+ { f: 53, s: [{ f: 1, m: true }] },
20
+ { f: 55, s: [{ f: 1, m: true }] },
21
+ { f: 58, s: [{ f: 1, m: true }] },
22
+ { f: 59, s: [{ f: 1, m: true }] },
23
+ { f: 60, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] }, { f: 8, s: [{ f: 1 }, { f: 2 }] }] },
24
+ { f: 64, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] }, { f: 8, s: [{ f: 1 }, { f: 2 }] }] },
25
+ { f: 66, s: [{ f: 2 }, { f: 6 }, { f: 7 }, { f: 13 }, { f: 17, s: [{ f: 21 }, { f: 22 }] }, { f: 20 }] },
26
+ { f: 74, s: [{ f: 1, m: true }] },
27
+ { f: 87, s: [{ f: 1, m: true }] },
28
+ { f: 88, s: [{ f: 1 }, { f: 2, s: [{ f: 1 }] }, { f: 3, s: [{ f: 21 }, { f: 22 }] }] },
29
+ { f: 92, s: [{ f: 1, m: true }] },
30
+ { f: 93, s: [{ f: 1, m: true }] },
31
+ { f: 94, s: [{ f: 1, m: true }] }
32
+ ]
33
+
34
+ const EMPTY_MAP = new Map()
35
+ const ENC_SECRET_REPORT_TOKEN = 'Report Token'
36
+ const WIRE = { VARINT: 0, FIXED64: 1, BYTES: 2, FIXED32: 5 }
37
+
38
+ const compileReportingFields = (fields) => {
39
+ const map = new Map()
40
+ for (const f of fields) map.set(f.f, { m: f.m, children: f.s ? compileReportingFields(f.s) : undefined })
41
+ return map
42
+ }
43
+
44
+ const compiledReportingFields = compileReportingFields(reportingFields)
45
+
46
+ const decodeVarint = (buffer, offset) => {
47
+ let value = 0, bytes = 0, shift = 0
48
+ while (offset + bytes < buffer.length) {
49
+ const current = buffer[offset + bytes]
50
+ value |= (current & 0x7f) << shift
51
+ bytes++
52
+ if ((current & 0x80) === 0) return { value, bytes, ok: true }
53
+ shift += 7
54
+ if (shift > 35) return { value: 0, bytes: 0, ok: false }
55
+ }
56
+ return { value: 0, bytes: 0, ok: false }
57
+ }
58
+
59
+ const encodeVarint = (value) => {
60
+ const parts = []
61
+ let remaining = value >>> 0
62
+ while (remaining > 0x7f) { parts.push((remaining & 0x7f) | 0x80); remaining >>>= 7 }
63
+ parts.push(remaining)
64
+ return Buffer.from(parts)
65
+ }
66
+
67
+ const extractReportingTokenContent = (data, cfg) => {
68
+ const out = []
69
+ let i = 0
70
+ while (i < data.length) {
71
+ const tag = decodeVarint(data, i)
72
+ if (!tag.ok) return null
73
+ const fieldNum = tag.value >> 3
74
+ const wireType = tag.value & 0x7
75
+ const fieldStart = i
76
+ i += tag.bytes
77
+ const fieldCfg = cfg.get(fieldNum)
78
+ const pushSlice = (end) => { if (end > data.length) return false; out.push({ num: fieldNum, bytes: data.subarray(fieldStart, end) }); i = end; return true }
79
+ const skip = (end) => { if (end > data.length) return false; i = end; return true }
80
+ if (wireType === WIRE.VARINT) {
81
+ const v = decodeVarint(data, i)
82
+ if (!v.ok) return null
83
+ const end = i + v.bytes
84
+ if (!fieldCfg) { if (!skip(end)) return null; continue }
85
+ if (!pushSlice(end)) return null
86
+ continue
87
+ }
88
+ if (wireType === WIRE.FIXED64) {
89
+ const end = i + 8
90
+ if (!fieldCfg) { if (!skip(end)) return null; continue }
91
+ if (!pushSlice(end)) return null
92
+ continue
93
+ }
94
+ if (wireType === WIRE.FIXED32) {
95
+ const end = i + 4
96
+ if (!fieldCfg) { if (!skip(end)) return null; continue }
97
+ if (!pushSlice(end)) return null
98
+ continue
99
+ }
100
+ if (wireType === WIRE.BYTES) {
101
+ const len = decodeVarint(data, i)
102
+ if (!len.ok) return null
103
+ const valStart = i + len.bytes
104
+ const valEnd = valStart + len.value
105
+ if (valEnd > data.length) return null
106
+ if (!fieldCfg) { i = valEnd; continue }
107
+ if (fieldCfg.m || fieldCfg.children) {
108
+ const sub = extractReportingTokenContent(data.subarray(valStart, valEnd), fieldCfg.children ?? EMPTY_MAP)
109
+ if (sub === null) return null
110
+ if (sub.length > 0) {
111
+ const newTag = encodeVarint(tag.value)
112
+ const newLen = encodeVarint(sub.length)
113
+ out.push({ num: fieldNum, bytes: Buffer.concat([newTag, newLen, sub]) })
114
+ }
115
+ i = valEnd
116
+ continue
117
+ }
118
+ out.push({ num: fieldNum, bytes: data.subarray(fieldStart, valEnd) })
119
+ i = valEnd
120
+ continue
121
+ }
122
+ return null
123
+ }
124
+ if (out.length === 0) return Buffer.alloc(0)
125
+ out.sort((a, b) => a.num - b.num)
126
+ return Buffer.concat(out.map(f => f.bytes))
127
+ }
128
+
129
+ const generateMsgSecretKey = (modificationType, origMsgId, origMsgSender, modificationSender, origMsgSecret) => {
130
+ const useCaseSecret = Buffer.concat([
131
+ Buffer.from(origMsgId, 'utf8'), Buffer.from(origMsgSender, 'utf8'),
132
+ Buffer.from(modificationSender, 'utf8'), Buffer.from(modificationType, 'utf8')
133
+ ])
134
+ return hkdf(origMsgSecret, 32, { info: useCaseSecret.toString('latin1') })
135
+ }
136
+
137
+ export const shouldIncludeReportingToken = (message) =>
138
+ !message.reactionMessage && !message.encReactionMessage &&
139
+ !message.encEventResponseMessage && !message.pollUpdateMessage
140
+
141
+ export const getMessageReportingToken = async (msgProtobuf, message, key) => {
142
+ const msgSecret = message.messageContextInfo?.messageSecret
143
+ if (!msgSecret || !key.id) return null
144
+ const from = key.fromMe ? key.remoteJid : key.participant || key.remoteJid
145
+ const to = key.fromMe ? key.participant || key.remoteJid : key.remoteJid
146
+ const reportingSecret = generateMsgSecretKey(ENC_SECRET_REPORT_TOKEN, key.id, from, to, msgSecret)
147
+ const content = extractReportingTokenContent(msgProtobuf, compiledReportingFields)
148
+ if (!content || content.length === 0) return null
149
+ const reportingToken = createHmac('sha256', reportingSecret).update(content).digest().subarray(0, 16)
150
+ return {
151
+ tag: 'reporting',
152
+ attrs: {},
153
+ content: [{ tag: 'reporting_token', attrs: { v: '2' }, content: reportingToken }]
154
+ }
155
+ }
@@ -1,43 +1,36 @@
1
- import { KEY_BUNDLE_TYPE } from '../Defaults/index.js';
2
- import { assertNodeErrorFree, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, getServerFromDomainType, jidDecode, S_WHATSAPP_NET, WAJIDDomains } from '../WABinary/index.js';
3
- import { Curve, generateSignalPubKey } from './crypto.js';
4
- import { encodeBigEndian } from './generics.js';
5
- function chunk(array, size) {
6
- const chunks = [];
7
- for (let i = 0; i < array.length; i += size) {
8
- chunks.push(array.slice(i, i + size));
9
- }
10
- return chunks;
1
+ import { KEY_BUNDLE_TYPE } from '../Defaults/index.js'
2
+ import { assertNodeErrorFree, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, getServerFromDomainType, jidDecode, S_WHATSAPP_NET, WAJIDDomains } from '../WABinary/index.js'
3
+ import { Curve, generateSignalPubKey } from './crypto.js'
4
+ import { encodeBigEndian } from './generics.js'
5
+
6
+ const chunk = (array, size) => {
7
+ const chunks = []
8
+ for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size))
9
+ return chunks
11
10
  }
12
- export const createSignalIdentity = (wid, accountSignatureKey) => {
13
- return {
14
- identifier: { name: wid, deviceId: 0 },
15
- identifierKey: generateSignalPubKey(accountSignatureKey)
16
- };
17
- };
11
+
12
+ export const createSignalIdentity = (wid, accountSignatureKey) => ({
13
+ identifier: { name: wid, deviceId: 0 },
14
+ identifierKey: generateSignalPubKey(accountSignatureKey)
15
+ })
16
+
18
17
  export const getPreKeys = async ({ get }, min, limit) => {
19
- const idList = [];
20
- for (let id = min; id < limit; id++) {
21
- idList.push(id.toString());
22
- }
23
- return get('pre-key', idList);
24
- };
18
+ const idList = []
19
+ for (let id = min; id < limit; id++) idList.push(id.toString())
20
+ return get('pre-key', idList)
21
+ }
22
+
25
23
  export const generateOrGetPreKeys = (creds, range) => {
26
- const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId;
27
- const remaining = range - avaliable;
28
- const lastPreKeyId = creds.nextPreKeyId + remaining - 1;
29
- const newPreKeys = {};
24
+ const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
25
+ const remaining = range - avaliable
26
+ const lastPreKeyId = creds.nextPreKeyId + remaining - 1
27
+ const newPreKeys = {}
30
28
  if (remaining > 0) {
31
- for (let i = creds.nextPreKeyId; i <= lastPreKeyId; i++) {
32
- newPreKeys[i] = Curve.generateKeyPair();
33
- }
29
+ for (let i = creds.nextPreKeyId; i <= lastPreKeyId; i++) newPreKeys[i] = Curve.generateKeyPair()
34
30
  }
35
- return {
36
- newPreKeys,
37
- lastPreKeyId,
38
- preKeysRange: [creds.firstUnuploadedPreKeyId, range]
39
- };
40
- };
31
+ return { newPreKeys, lastPreKeyId, preKeysRange: [creds.firstUnuploadedPreKeyId, range] }
32
+ }
33
+
41
34
  export const xmppSignedPreKey = (key) => ({
42
35
  tag: 'skey',
43
36
  attrs: {},
@@ -46,7 +39,8 @@ export const xmppSignedPreKey = (key) => ({
46
39
  { tag: 'value', attrs: {}, content: key.keyPair.public },
47
40
  { tag: 'signature', attrs: {}, content: key.signature }
48
41
  ]
49
- });
42
+ })
43
+
50
44
  export const xmppPreKey = (pair, id) => ({
51
45
  tag: 'key',
52
46
  attrs: {},
@@ -54,100 +48,137 @@ export const xmppPreKey = (pair, id) => ({
54
48
  { tag: 'id', attrs: {}, content: encodeBigEndian(id, 3) },
55
49
  { tag: 'value', attrs: {}, content: pair.public }
56
50
  ]
57
- });
51
+ })
52
+
53
+ const isValidUInt = (n) => typeof n === 'number' && Number.isInteger(n) && n >= 0
54
+
55
+ /**
56
+ * Extract a full E2E session bundle from a retry receipt's <keys> node.
57
+ * Returns null if the bundle is missing, malformed, or fails any integrity check.
58
+ * Used in sendMessagesAgain to inject the sender's fresh session on retry.
59
+ */
60
+ export const extractE2ESessionFromRetryReceipt = (receipt) => {
61
+ const keysNode = getBinaryNodeChild(receipt, 'keys')
62
+ if (!keysNode) return null
63
+
64
+ const typeBuf = getBinaryNodeChildBuffer(keysNode, 'type')
65
+ if (!typeBuf || typeBuf.length !== 1 || typeBuf[0] !== KEY_BUNDLE_TYPE[0]) return null
66
+
67
+ const identity = getBinaryNodeChildBuffer(keysNode, 'identity')
68
+ const skey = getBinaryNodeChild(keysNode, 'skey')
69
+ if (!identity || identity.length !== 32 || !skey) return null
70
+
71
+ const registrationId = getBinaryNodeChildUInt(receipt, 'registration', 4)
72
+ if (!isValidUInt(registrationId)) return null
73
+
74
+ const signedPubKey = getBinaryNodeChildBuffer(skey, 'value')
75
+ const signedSig = getBinaryNodeChildBuffer(skey, 'signature')
76
+ const signedKeyId = getBinaryNodeChildUInt(skey, 'id', 3)
77
+ if (!signedPubKey || signedPubKey.length !== 32 || !signedSig || !isValidUInt(signedKeyId)) return null
78
+
79
+ const preKeyNode = getBinaryNodeChild(keysNode, 'key')
80
+ let preKey
81
+ if (preKeyNode) {
82
+ const preKeyPub = getBinaryNodeChildBuffer(preKeyNode, 'value')
83
+ const preKeyId = getBinaryNodeChildUInt(preKeyNode, 'id', 3)
84
+ if (!preKeyPub || preKeyPub.length !== 32 || !isValidUInt(preKeyId)) return null
85
+ preKey = { keyId: preKeyId, publicKey: generateSignalPubKey(preKeyPub) }
86
+ }
87
+
88
+ return {
89
+ registrationId,
90
+ identityKey: generateSignalPubKey(identity),
91
+ signedPreKey: { keyId: signedKeyId, publicKey: generateSignalPubKey(signedPubKey), signature: signedSig },
92
+ preKey
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Parse and inject E2E sessions from a server key response node.
98
+ * Unlike upstream, we filter error nodes individually so one bad user
99
+ * never aborts the entire batch — all valid sessions still get injected.
100
+ * CPU-heavy work is chunked in groups of 100 to yield to the event loop between batches.
101
+ */
58
102
  export const parseAndInjectE2ESessions = async (node, repository) => {
59
- const extractKey = (key) => key
60
- ? {
61
- keyId: getBinaryNodeChildUInt(key, 'id', 3),
62
- publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')),
63
- signature: getBinaryNodeChildBuffer(key, 'signature')
64
- }
65
- : undefined;
103
+ const extractKey = (key) => key ? {
104
+ keyId: getBinaryNodeChildUInt(key, 'id', 3),
105
+ publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')),
106
+ signature: getBinaryNodeChildBuffer(key, 'signature')
107
+ } : undefined
108
+
66
109
  const allNodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
67
- // Filter out error nodes individually instead of aborting everything
68
110
  const nodes = allNodes.filter(node => {
69
111
  try { assertNodeErrorFree(node); return true }
70
112
  catch { return false }
71
113
  })
72
- // Most of the work in repository.injectE2ESession is CPU intensive, not IO
73
- // So Promise.all doesn't really help here,
74
- // but blocks even loop if we're using it inside keys.transaction, and it makes it "sync" actually
75
- // This way we chunk it in smaller parts and between those parts we can yield to the event loop
76
- // It's rare case when you need to E2E sessions for so many users, but it's possible
77
- const chunkSize = 100;
78
- const chunks = chunk(nodes, chunkSize);
114
+
115
+ const chunks = chunk(nodes, 100)
79
116
  for (const nodesChunk of chunks) {
80
117
  for (const node of nodesChunk) {
81
- const signedKey = getBinaryNodeChild(node, 'skey');
82
- const key = getBinaryNodeChild(node, 'key');
83
- const identity = getBinaryNodeChildBuffer(node, 'identity');
84
- const jid = node.attrs.jid;
85
- const registrationId = getBinaryNodeChildUInt(node, 'registration', 4);
118
+ const signedKey = getBinaryNodeChild(node, 'skey')
119
+ const key = getBinaryNodeChild(node, 'key')
120
+ const identity = getBinaryNodeChildBuffer(node, 'identity')
121
+ const jid = node.attrs.jid
122
+ const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
86
123
  await repository.injectE2ESession({
87
124
  jid,
88
125
  session: {
89
- registrationId: registrationId,
126
+ registrationId,
90
127
  identityKey: generateSignalPubKey(identity),
91
128
  signedPreKey: extractKey(signedKey),
92
129
  preKey: extractKey(key)
93
130
  }
94
- });
131
+ })
95
132
  }
133
+ // Yield to event loop between chunks to avoid blocking on large batches
134
+ await new Promise(resolve => setImmediate(resolve))
96
135
  }
97
- };
136
+ }
137
+
138
+ /**
139
+ * Extract device JIDs from a USync result list.
140
+ * Skips your own current device, respects excludeZeroDevices flag,
141
+ * and correctly resolves hosted LID/PN domain types.
142
+ */
98
143
  export const extractDeviceJids = (result, myJid, myLid, excludeZeroDevices) => {
99
- const { user: myUser, device: myDevice } = jidDecode(myJid);
100
- const extracted = [];
144
+ const { user: myUser, device: myDevice } = jidDecode(myJid)
145
+ const extracted = []
101
146
  for (const userResult of result) {
102
- const { devices, id } = userResult;
103
- const decoded = jidDecode(id), { user, server } = decoded;
104
- let { domainType } = decoded;
105
- const deviceList = devices?.deviceList;
106
- if (!Array.isArray(deviceList))
107
- continue;
147
+ const { devices, id } = userResult
148
+ const decoded = jidDecode(id)
149
+ const { user, server } = decoded
150
+ let { domainType } = decoded
151
+ const deviceList = devices?.deviceList
152
+ if (!Array.isArray(deviceList)) continue
108
153
  for (const { id: device, keyIndex, isHosted } of deviceList) {
109
- if ((!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
110
- ((myUser !== user && myLid !== user) || myDevice !== device) && // either different user or if me user, not this device
111
- (device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
112
- ) {
113
- if (isHosted) {
114
- domainType = domainType === WAJIDDomains.LID ? WAJIDDomains.HOSTED_LID : WAJIDDomains.HOSTED;
115
- }
116
- extracted.push({
117
- user,
118
- device,
119
- domainType,
120
- server: getServerFromDomainType(server, domainType)
121
- });
154
+ if ((!excludeZeroDevices || device !== 0) &&
155
+ ((myUser !== user && myLid !== user) || myDevice !== device) &&
156
+ (device === 0 || !!keyIndex)) {
157
+ if (isHosted) domainType = domainType === WAJIDDomains.LID ? WAJIDDomains.HOSTED_LID : WAJIDDomains.HOSTED
158
+ extracted.push({ user, device, domainType, server: getServerFromDomainType(server, domainType) })
122
159
  }
123
160
  }
124
161
  }
125
- return extracted;
126
- };
127
- /**
128
- * get the next N keys for upload or processing
129
- * @param count number of pre-keys to get or generate
130
- */
162
+ return extracted
163
+ }
164
+
131
165
  export const getNextPreKeys = async ({ creds, keys }, count) => {
132
- const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count);
166
+ const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count)
133
167
  const update = {
134
168
  nextPreKeyId: Math.max(lastPreKeyId + 1, creds.nextPreKeyId),
135
169
  firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId + 1)
136
- };
137
- await keys.set({ 'pre-key': newPreKeys });
138
- const preKeys = await getPreKeys(keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]);
139
- return { update, preKeys };
140
- };
170
+ }
171
+ await keys.set({ 'pre-key': newPreKeys })
172
+ const preKeys = await getPreKeys(keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1])
173
+ return { update, preKeys }
174
+ }
175
+
141
176
  export const getNextPreKeysNode = async (state, count) => {
142
- const { creds } = state;
143
- const { update, preKeys } = await getNextPreKeys(state, count);
177
+ const { creds } = state
178
+ const { update, preKeys } = await getNextPreKeys(state, count)
144
179
  const node = {
145
180
  tag: 'iq',
146
- attrs: {
147
- xmlns: 'encrypt',
148
- type: 'set',
149
- to: S_WHATSAPP_NET
150
- },
181
+ attrs: { xmlns: 'encrypt', type: 'set', to: S_WHATSAPP_NET },
151
182
  content: [
152
183
  { tag: 'registration', attrs: {}, content: encodeBigEndian(creds.registrationId) },
153
184
  { tag: 'type', attrs: {}, content: KEY_BUNDLE_TYPE },
@@ -155,7 +186,6 @@ export const getNextPreKeysNode = async (state, count) => {
155
186
  { tag: 'list', attrs: {}, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
156
187
  xmppSignedPreKey(creds.signedPreKey)
157
188
  ]
158
- };
159
- return { update, node };
160
- };
161
- //# sourceMappingURL=signal.js.map
189
+ }
190
+ return { update, node }
191
+ }
@@ -0,0 +1,33 @@
1
+ import { isLidUser, isPnUser } from '../WABinary/index.js'
2
+
3
+ export const processContactAction = (action, id, logger) => {
4
+ const results = []
5
+ if (!id) {
6
+ logger?.warn({ hasFullName: !!action.fullName, hasLidJid: !!action.lidJid, hasPnJid: !!action.pnJid }, 'contactAction sync: missing id in index')
7
+ return results
8
+ }
9
+ const lidJid = action.lidJid
10
+ const idIsPn = isPnUser(id)
11
+ const phoneNumber = idIsPn ? id : action.pnJid || undefined
12
+ results.push({
13
+ event: 'contacts.upsert',
14
+ data: [{
15
+ id,
16
+ name: action.fullName || action.firstName || action.username || undefined,
17
+ username: action.username || undefined,
18
+ lid: lidJid || undefined,
19
+ phoneNumber
20
+ }]
21
+ })
22
+ if (lidJid && isLidUser(lidJid) && idIsPn) {
23
+ results.push({ event: 'lid-mapping.update', data: { lid: lidJid, pn: id } })
24
+ }
25
+ return results
26
+ }
27
+
28
+ export const emitSyncActionResults = (ev, results) => {
29
+ for (const result of results) {
30
+ if (result.event === 'contacts.upsert') ev.emit('contacts.upsert', result.data)
31
+ else ev.emit('lid-mapping.update', result.data)
32
+ }
33
+ }