@zervonex/baileys 0.0.0-stage → 3.0.2

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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3206 -3
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +643 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +449 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +981 -0
  26. package/lib/Socket/messages-send.js +708 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +781 -0
  29. package/lib/Socket/registration.js +155 -0
  30. package/lib/Socket/socket.js +797 -0
  31. package/lib/Socket/spam-report.js +65 -0
  32. package/lib/Socket/username.js +82 -0
  33. package/lib/Socket/zervonex-handler.js +55 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +317 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +38 -0
  74. package/lib/Utils/link-preview.js +51 -0
  75. package/lib/Utils/logger.js +2 -0
  76. package/lib/Utils/lt-hash.js +3 -0
  77. package/lib/Utils/make-mutex.js +16 -0
  78. package/lib/Utils/message-composer.js +59 -0
  79. package/lib/Utils/message-retry-manager.js +48 -0
  80. package/lib/Utils/messages-media.js +520 -0
  81. package/lib/Utils/messages.js +752 -0
  82. package/lib/Utils/meta-ai-msmsg.js +122 -0
  83. package/lib/Utils/meta-compositing.js +75 -0
  84. package/lib/Utils/modern-messages.js +235 -0
  85. package/lib/Utils/native-flow.js +55 -0
  86. package/lib/Utils/noise-handler.js +161 -0
  87. package/lib/Utils/pre-key-manager.js +76 -0
  88. package/lib/Utils/process-message.js +399 -0
  89. package/lib/Utils/reporting-utils.js +171 -0
  90. package/lib/Utils/signal.js +167 -0
  91. package/lib/Utils/spam-report-utils.js +135 -0
  92. package/lib/Utils/sync-action-utils.js +33 -0
  93. package/lib/Utils/tc-token-utils.js +154 -0
  94. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  95. package/lib/Utils/validate-connection.js +181 -0
  96. package/lib/WABinary/constants.js +1321 -0
  97. package/lib/WABinary/decode.js +152 -0
  98. package/lib/WABinary/encode.js +140 -0
  99. package/lib/WABinary/generic-utils.js +179 -0
  100. package/lib/WABinary/index.js +6 -0
  101. package/lib/WABinary/jid-utils.js +235 -0
  102. package/lib/WABinary/types.js +4 -0
  103. package/lib/WAM/BinaryInfo.js +10 -0
  104. package/lib/WAM/constants.js +22853 -0
  105. package/lib/WAM/encode.js +150 -0
  106. package/lib/WAM/index.js +3 -0
  107. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  108. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  109. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  110. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  111. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  112. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  113. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  114. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  115. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  116. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  118. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  119. package/lib/WAUSync/Protocols/index.js +12 -0
  120. package/lib/WAUSync/USyncQuery.js +100 -0
  121. package/lib/WAUSync/USyncUser.js +20 -0
  122. package/lib/WAUSync/index.js +3 -0
  123. package/lib/index.js +67 -0
  124. package/package.json +220 -5
@@ -0,0 +1,317 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { expandAppStateKeys } from 'whatsapp-rust-bridge'
3
+ import { proto } from '../../WAProto/index.js'
4
+ import { LabelAssociationType } from '../Types/LabelAssociation.js'
5
+ import { processContactAction, emitSyncActionResults } from './sync-action-utils.js'
6
+ import { getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary/index.js'
7
+ import { aesDecrypt, aesEncrypt, hmacSign } from './crypto.js'
8
+ import { toNumber } from './generics.js'
9
+ import { LT_HASH_ANTI_TAMPERING } from './lt-hash.js'
10
+ import { downloadContentFromMessage } from './messages-media.js'
11
+
12
+ const OP = proto.SyncdMutation.SyncdOperation
13
+
14
+ const mutationKeys = keydata => {
15
+ const keys = expandAppStateKeys(keydata)
16
+ return { indexKey: keys.indexKey, valueEncryptionKey: keys.valueEncryptionKey, valueMacKey: keys.valueMacKey, snapshotMacKey: keys.snapshotMacKey, patchMacKey: keys.patchMacKey }
17
+ }
18
+
19
+ const generateMac = (operation, data, keyId, key) => {
20
+ const opByte = operation === OP.SET ? 0x01 : 0x02
21
+ const keyIdBuffer = typeof keyId === 'string' ? Buffer.from(keyId, 'base64') : keyId
22
+ const keyData = new Uint8Array(1 + keyIdBuffer.length)
23
+ keyData[0] = opByte
24
+ keyData.set(keyIdBuffer, 1)
25
+ const last = new Uint8Array(8)
26
+ last[7] = keyData.length
27
+ const total = new Uint8Array(keyData.length + data.length + last.length)
28
+ total.set(keyData, 0); total.set(data, keyData.length); total.set(last, keyData.length + data.length)
29
+ return hmacSign(total, key, 'sha512').subarray(0, 32)
30
+ }
31
+
32
+ const to64BitNetworkOrder = e => { const buff = Buffer.alloc(8); buff.writeUint32BE(e, 4); return buff }
33
+
34
+ export const makeLtHashGenerator = ({ indexValueMap, hash }) => {
35
+ indexValueMap = { ...indexValueMap }
36
+ const addBuffs = [], subBuffs = []
37
+ return {
38
+ mix: ({ indexMac, valueMac, operation }) => {
39
+ const key = Buffer.from(indexMac).toString('base64')
40
+ const prev = indexValueMap[key]
41
+ if (operation === OP.REMOVE) { if (!prev) return; delete indexValueMap[key] }
42
+ else { addBuffs.push(valueMac); indexValueMap[key] = { valueMac } }
43
+ if (prev) subBuffs.push(prev.valueMac)
44
+ },
45
+ finish: async () => ({ hash: Buffer.from(LT_HASH_ANTI_TAMPERING.subtractThenAdd(hash, subBuffs, addBuffs)), indexValueMap })
46
+ }
47
+ }
48
+
49
+ const generateSnapshotMac = (lthash, version, name, key) => hmacSign(Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')]), key, 'sha256')
50
+ const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => hmacSign(Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')]), key)
51
+
52
+ export const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
53
+ export const ensureLTHashStateVersion = state => { if (typeof state.version !== 'number' || isNaN(state.version)) state.version = 0; return state }
54
+
55
+ export const MAX_SYNC_ATTEMPTS = 2
56
+ export const isMissingKeyError = error => error?.data?.isMissingKey === true
57
+ export const isAppStateSyncIrrecoverable = (error, attempts) => attempts >= MAX_SYNC_ATTEMPTS || error?.name === 'TypeError'
58
+
59
+ export const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
60
+ const key = myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
61
+ if (!key) throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } })
62
+ const encKeyId = Buffer.from(myAppStateKeyId, 'base64')
63
+ state = { ...state, indexValueMap: { ...state.indexValueMap } }
64
+ const indexBuffer = Buffer.from(JSON.stringify(index))
65
+ const dataProto = proto.SyncActionData.fromObject({ index: indexBuffer, value: syncAction, padding: new Uint8Array(0), version: apiVersion })
66
+ const encoded = proto.SyncActionData.encode(dataProto).finish()
67
+ const keyValue = mutationKeys(key.keyData)
68
+ const encValue = aesEncrypt(encoded, keyValue.valueEncryptionKey)
69
+ const valueMac = generateMac(operation, encValue, encKeyId, keyValue.valueMacKey)
70
+ const indexMac = hmacSign(indexBuffer, keyValue.indexKey)
71
+ const generator = makeLtHashGenerator(state)
72
+ generator.mix({ indexMac, valueMac, operation })
73
+ Object.assign(state, await generator.finish())
74
+ state.version += 1
75
+ const snapshotMac = generateSnapshotMac(state.hash, state.version, type, keyValue.snapshotMacKey)
76
+ const patch = {
77
+ patchMac: generatePatchMac(snapshotMac, [valueMac], state.version, type, keyValue.patchMacKey),
78
+ snapshotMac,
79
+ keyId: { id: encKeyId },
80
+ mutations: [{ operation, record: { index: { blob: indexMac }, value: { blob: Buffer.concat([encValue, valueMac]) }, keyId: { id: encKeyId } } }]
81
+ }
82
+ state.indexValueMap[indexMac.toString('base64')] = { valueMac }
83
+ return { patch, state }
84
+ }
85
+
86
+ export const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
87
+ const ltGenerator = makeLtHashGenerator(initialState)
88
+ const derivedKeyCache = new Map()
89
+
90
+ async function getKey(keyId) {
91
+ const base64Key = Buffer.from(keyId).toString('base64')
92
+ if (derivedKeyCache.has(base64Key)) return derivedKeyCache.get(base64Key)
93
+ const keyEnc = await getAppStateSyncKey(base64Key)
94
+ if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true, msgMutations } })
95
+ const keys = mutationKeys(keyEnc.keyData)
96
+ derivedKeyCache.set(base64Key, keys)
97
+ return keys
98
+ }
99
+
100
+ for (const msgMutation of msgMutations) {
101
+ const operation = 'operation' in msgMutation ? msgMutation.operation : OP.SET
102
+ const record = 'record' in msgMutation && msgMutation.record ? msgMutation.record : msgMutation
103
+ let key
104
+ try { key = await getKey(record.keyId.id) } catch (err) { if (isMissingKeyError(err)) throw err; continue }
105
+ if (!record?.value?.blob) continue
106
+ const content = record.value.blob
107
+ const encContent = content.subarray(0, -32)
108
+ const ogValueMac = content.subarray(-32)
109
+ if (validateMacs) {
110
+ const contentHmac = generateMac(operation, encContent, record.keyId.id, key.valueMacKey)
111
+ if (Buffer.compare(contentHmac, ogValueMac) !== 0) continue
112
+ }
113
+ let result
114
+ try { result = aesDecrypt(encContent, key.valueEncryptionKey) } catch { continue }
115
+ const syncAction = proto.SyncActionData.decode(result)
116
+ if (validateMacs) {
117
+ const hmac = hmacSign(syncAction.index, key.indexKey)
118
+ if (Buffer.compare(hmac, record.index.blob) !== 0) throw new Boom('HMAC index verification failed')
119
+ }
120
+ onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
121
+ ltGenerator.mix({ indexMac: record.index.blob, valueMac: ogValueMac, operation })
122
+ }
123
+ return ltGenerator.finish()
124
+ }
125
+
126
+ export const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onMutation, validateMacs) => {
127
+ if (validateMacs) {
128
+ const base64Key = Buffer.from(msg.keyId.id).toString('base64')
129
+ const mainKeyObj = await getAppStateSyncKey(base64Key)
130
+ if (!mainKeyObj) throw new Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } })
131
+ const mainKey = mutationKeys(mainKeyObj.keyData)
132
+ const mutationmacs = msg.mutations.map(m => m.record.value.blob.slice(-32))
133
+ const patchMac = generatePatchMac(msg.snapshotMac, mutationmacs, toNumber(msg.version.version), name, mainKey.patchMacKey)
134
+ if (Buffer.compare(patchMac, msg.patchMac) !== 0) throw new Boom('Invalid patch mac')
135
+ }
136
+ return decodeSyncdMutations(msg.mutations, initialState, getAppStateSyncKey, onMutation, validateMacs)
137
+ }
138
+
139
+ export const extractSyncdPatches = async (result, options) => {
140
+ const syncNode = getBinaryNodeChild(result, 'sync')
141
+ const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
142
+ const final = {}
143
+ await Promise.all(collectionNodes.map(async collectionNode => {
144
+ const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
145
+ const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
146
+ const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
147
+ const syncds = []
148
+ const name = collectionNode.attrs.name
149
+ const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
150
+ let snapshot
151
+ if (snapshotNode?.content) {
152
+ if (!Buffer.isBuffer(snapshotNode)) snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
153
+ const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content)
154
+ snapshot = proto.SyncdSnapshot.decode(await downloadExternalBlob(blobRef, options))
155
+ }
156
+ for (let { content } of patches) {
157
+ if (!content) continue
158
+ if (!Buffer.isBuffer(content)) content = Buffer.from(Object.values(content))
159
+ const syncd = proto.SyncdPatch.decode(content)
160
+ if (!syncd.version) syncd.version = { version: +collectionNode.attrs.version + 1 }
161
+ syncds.push(syncd)
162
+ }
163
+ final[name] = { patches: syncds, hasMorePatches, snapshot }
164
+ }))
165
+ return final
166
+ }
167
+
168
+ export const downloadExternalBlob = async (blob, options) => {
169
+ const stream = await downloadContentFromMessage(blob, 'md-app-state', { options })
170
+ const bufferArray = []
171
+ for await (const chunk of stream) bufferArray.push(chunk)
172
+ return Buffer.concat(bufferArray)
173
+ }
174
+
175
+ export const downloadExternalPatch = async (blob, options) => {
176
+ const buffer = await downloadExternalBlob(blob, options)
177
+ return proto.SyncdMutations.decode(buffer)
178
+ }
179
+
180
+ export const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVersionNumber, validateMacs = true, logger) => {
181
+ const newState = newLTHashState()
182
+ newState.version = toNumber(snapshot.version.version)
183
+ const mutationMap = {}
184
+ const areMutationsRequired = minimumVersionNumber === undefined || newState.version > minimumVersionNumber
185
+ const onMutation = areMutationsRequired ? mutation => { const index = mutation.syncAction.index?.toString(); mutationMap[index] = mutation } : () => { }
186
+ const { hash, indexValueMap } = await decodeSyncdMutations(snapshot.records, newState, getAppStateSyncKey, onMutation, validateMacs)
187
+ newState.hash = hash
188
+ newState.indexValueMap = indexValueMap
189
+ if (validateMacs) {
190
+ const base64Key = Buffer.from(snapshot.keyId.id).toString('base64')
191
+ const keyEnc = await getAppStateSyncKey(base64Key)
192
+ if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
193
+ const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, mutationKeys(keyEnc.keyData).snapshotMacKey)
194
+ // soft failure — MAC mismatch on snapshot doesn't abort; state is still usable
195
+ if (Buffer.compare(snapshot.mac, computedSnapshotMac) !== 0) logger?.warn({ name, version: newState.version }, 'LTHash verification failed on snapshot, continuing with partial state')
196
+ }
197
+ return { state: newState, mutationMap }
198
+ }
199
+
200
+ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, options, minimumVersionNumber, logger, validateMacs = true) => {
201
+ const newState = { ...initial, indexValueMap: { ...initial.indexValueMap } }
202
+ const mutationMap = {}
203
+ for (const syncd of syncds) {
204
+ const { version, keyId, snapshotMac } = syncd
205
+ if (syncd.externalMutations) {
206
+ logger?.trace({ name, version }, 'downloading external patch')
207
+ const ref = await downloadExternalPatch(syncd.externalMutations, options)
208
+ logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
209
+ syncd.mutations?.push(...ref.mutations)
210
+ }
211
+ const patchVersion = toNumber(version.version)
212
+ newState.version = patchVersion
213
+ const shouldMutate = minimumVersionNumber === undefined || patchVersion > minimumVersionNumber
214
+ const onMutation = shouldMutate ? mutation => { const index = mutation.syncAction.index?.toString(); mutationMap[index] = mutation } : () => { }
215
+ let decodeResult
216
+ try { decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, onMutation, validateMacs) }
217
+ catch (err) { if (isMissingKeyError(err)) throw err; logger?.warn({ name, version: patchVersion, error: err.message }, 'failed to decode patch, skipping'); continue }
218
+ newState.hash = decodeResult.hash
219
+ newState.indexValueMap = decodeResult.indexValueMap
220
+ if (validateMacs) {
221
+ const base64Key = Buffer.from(keyId.id).toString('base64')
222
+ const keyEnc = await getAppStateSyncKey(base64Key)
223
+ if (!keyEnc) throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } })
224
+ const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, mutationKeys(keyEnc.keyData).snapshotMacKey)
225
+ // MAC mismatch mid-patch-stream means state is corrupted from this point; stop processing
226
+ if (Buffer.compare(snapshotMac, computedSnapshotMac) !== 0) { logger?.warn({ name, version: newState.version }, 'LTHash verification failed, skipping remaining patches'); break }
227
+ }
228
+ syncd.mutations = []
229
+ }
230
+ return { state: newState, mutationMap }
231
+ }
232
+
233
+ export const chatModificationToAppPatch = (mod, jid) => {
234
+ const getMessageRange = lastMessages => {
235
+ if (!Array.isArray(lastMessages)) return lastMessages
236
+ const lastMsg = lastMessages[lastMessages.length - 1]
237
+ return {
238
+ lastMessageTimestamp: lastMsg?.messageTimestamp,
239
+ messages: lastMessages?.length ? lastMessages.map(m => {
240
+ if (!m.key?.id || !m.key?.remoteJid) throw new Boom('Incomplete key', { statusCode: 400, data: m })
241
+ if (isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) throw new Boom('Expected not from me message to have participant', { statusCode: 400, data: m })
242
+ if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
243
+ if (m.key.participant) m.key.participant = jidNormalizedUser(m.key.participant)
244
+ return m
245
+ }) : undefined
246
+ }
247
+ }
248
+
249
+ let patch
250
+ if ('mute' in mod) patch = { syncAction: { muteAction: { muted: !!mod.mute, muteEndTimestamp: mod.mute || undefined } }, index: ['mute', jid], type: 'regular_high', apiVersion: 2, operation: OP.SET }
251
+ else if ('archive' in mod) patch = { syncAction: { archiveChatAction: { archived: !!mod.archive, messageRange: getMessageRange(mod.lastMessages) } }, index: ['archive', jid], type: 'regular_low', apiVersion: 3, operation: OP.SET }
252
+ else if ('markRead' in mod) patch = { syncAction: { markChatAsReadAction: { read: mod.markRead, messageRange: getMessageRange(mod.lastMessages) } }, index: ['markChatAsRead', jid], type: 'regular_low', apiVersion: 3, operation: OP.SET }
253
+ else if ('deleteForMe' in mod) { const { timestamp, key, deleteMedia } = mod.deleteForMe; patch = { syncAction: { deleteMessageForMeAction: { deleteMedia, messageTimestamp: timestamp } }, index: ['deleteMessageForMe', jid, key.id, key.fromMe ? '1' : '0', '0'], type: 'regular_high', apiVersion: 3, operation: OP.SET } }
254
+ else if ('clear' in mod) patch = { syncAction: { clearChatAction: { messageRange: getMessageRange(mod.lastMessages) } }, index: ['clearChat', jid, '1', '0'], type: 'regular_high', apiVersion: 6, operation: OP.SET }
255
+ else if ('pin' in mod) patch = { syncAction: { pinAction: { pinned: !!mod.pin } }, index: ['pin_v1', jid], type: 'regular_low', apiVersion: 5, operation: OP.SET }
256
+ else if ('contact' in mod) patch = { syncAction: { contactAction: mod.contact || {} }, index: ['contact', jid], type: 'critical_unblock_low', apiVersion: 2, operation: mod.contact ? OP.SET : OP.REMOVE }
257
+ else if ('disableLinkPreviews' in mod) patch = { syncAction: { privacySettingDisableLinkPreviewsAction: mod.disableLinkPreviews || {} }, index: ['setting_disableLinkPreviews'], type: 'regular', apiVersion: 8, operation: OP.SET }
258
+ else if ('star' in mod) { const key = mod.star.messages[0]; patch = { syncAction: { starAction: { starred: !!mod.star.star } }, index: ['star', jid, key.id, key.fromMe ? '1' : '0', '0'], type: 'regular_low', apiVersion: 2, operation: OP.SET } }
259
+ else if ('delete' in mod) patch = { syncAction: { deleteChatAction: { messageRange: getMessageRange(mod.lastMessages) } }, index: ['deleteChat', jid, '1'], type: 'regular_high', apiVersion: 6, operation: OP.SET }
260
+ else if ('pushNameSetting' in mod) patch = { syncAction: { pushNameSetting: { name: mod.pushNameSetting } }, index: ['setting_pushName'], type: 'critical_block', apiVersion: 1, operation: OP.SET }
261
+ else if ('quickReply' in mod) patch = { syncAction: { quickReplyAction: { count: 0, deleted: mod.quickReply.deleted || false, keywords: [], message: mod.quickReply.message || '', shortcut: mod.quickReply.shortcut || '' } }, index: ['quick_reply', mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000))], type: 'regular', apiVersion: 2, operation: OP.SET }
262
+ else if ('addLabel' in mod) patch = { syncAction: { labelEditAction: { name: mod.addLabel.name, color: mod.addLabel.color, predefinedId: mod.addLabel.predefinedId, deleted: mod.addLabel.deleted } }, index: ['label_edit', mod.addLabel.id], type: 'regular', apiVersion: 3, operation: OP.SET }
263
+ else if ('addChatLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: true } }, index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid], type: 'regular', apiVersion: 3, operation: OP.SET }
264
+ else if ('removeChatLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: false } }, index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid], type: 'regular', apiVersion: 3, operation: OP.SET }
265
+ else if ('addMessageLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: true } }, index: [LabelAssociationType.Message, mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId, '0', '0'], type: 'regular', apiVersion: 3, operation: OP.SET }
266
+ else if ('removeMessageLabel' in mod) patch = { syncAction: { labelAssociationAction: { labeled: false } }, index: [LabelAssociationType.Message, mod.removeMessageLabel.labelId, jid, mod.removeMessageLabel.messageId, '0', '0'], type: 'regular', apiVersion: 3, operation: OP.SET }
267
+ else throw new Boom('not supported')
268
+
269
+ patch.syncAction.timestamp = Date.now()
270
+ return patch
271
+ }
272
+
273
+ export const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
274
+ const isInitialSync = !!initialSyncOpts
275
+ const accountSettings = initialSyncOpts?.accountSettings
276
+ logger?.trace({ syncAction, initialSync: isInitialSync }, 'processing sync action')
277
+ const { syncAction: { value: action }, index: [type, id, msgId, fromMe] } = syncAction
278
+
279
+ const getChatUpdateConditional = (id, msgRange) => !isInitialSync ? undefined : data => {
280
+ const chat = data.historySets.chats[id] || data.chatUpserts[id]
281
+ if (!chat) return false
282
+ if (!msgRange) return true
283
+ const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
284
+ return lastMsgTimestamp >= Number(chat?.lastMessageRecvTimestamp || 0)
285
+ }
286
+
287
+ if (action?.muteAction) ev.emit('chats.update', [{ id, muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
288
+ else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') { const archiveAction = action?.archiveChatAction; ev.emit('chats.update', [{ id, archived: archiveAction ? archiveAction.archived : type === 'archive', conditional: getChatUpdateConditional(id, !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange) }]) }
289
+ else if (action?.markChatAsReadAction) ev.emit('chats.update', [{ id, unreadCount: isInitialSync && action.markChatAsReadAction.read ? null : action.markChatAsReadAction?.read ? 0 : -1, conditional: getChatUpdateConditional(id, action.markChatAsReadAction?.messageRange) }])
290
+ else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') ev.emit('messages.delete', { keys: [{ remoteJid: id, id: msgId, fromMe: fromMe === '1' }] })
291
+ else if (action?.contactAction) emitSyncActionResults(ev, processContactAction(action.contactAction, id, logger))
292
+ else if (action?.pushNameSetting) { const name = action?.pushNameSetting?.name; if (name && me?.name !== name) ev.emit('creds.update', { me: { ...me, name } }) }
293
+ else if (action?.pinAction) ev.emit('chats.update', [{ id, pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
294
+ else if (action?.unarchiveChatsSetting) { const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats; ev.emit('creds.update', { accountSettings: { unarchiveChats } }); if (accountSettings) accountSettings.unarchiveChats = unarchiveChats }
295
+ else if (action?.starAction || type === 'star') { let starred = action?.starAction?.starred; if (typeof starred !== 'boolean') starred = syncAction.index[syncAction.index.length - 1] === '1'; ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { starred } }]) }
296
+ else if (action?.deleteChatAction || type === 'deleteChat') { if (!isInitialSync) ev.emit('chats.delete', [id]) }
297
+ else if (action?.labelEditAction) { const { name, color, deleted, predefinedId } = action.labelEditAction; ev.emit('labels.edit', { id, name, color, deleted, predefinedId: predefinedId ? String(predefinedId) : undefined }) }
298
+ else if (action?.labelAssociationAction) ev.emit('labels.association', { type: action.labelAssociationAction.labeled ? 'add' : 'remove', association: type === LabelAssociationType.Chat ? { type: LabelAssociationType.Chat, chatId: syncAction.index[2], labelId: syncAction.index[1] } : { type: LabelAssociationType.Message, chatId: syncAction.index[2], messageId: syncAction.index[3], labelId: syncAction.index[1] } })
299
+ else if (action?.localeSetting?.locale) ev.emit('settings.update', { setting: 'locale', value: action.localeSetting.locale })
300
+ else if (action?.timeFormatAction) ev.emit('settings.update', { setting: 'timeFormat', value: action.timeFormatAction })
301
+ else if (action?.pnForLidChatAction?.pnJid) ev.emit('lid-mapping.update', { lid: id, pn: action.pnForLidChatAction.pnJid })
302
+ else if (action?.privacySettingRelayAllCalls) ev.emit('settings.update', { setting: 'privacySettingRelayAllCalls', value: action.privacySettingRelayAllCalls })
303
+ else if (action?.statusPrivacy) ev.emit('settings.update', { setting: 'statusPrivacy', value: action.statusPrivacy })
304
+ else if (action?.lockChatAction) ev.emit('chats.lock', { id, locked: !!action.lockChatAction.locked })
305
+ else if (action?.privacySettingDisableLinkPreviewsAction) ev.emit('settings.update', { setting: 'disableLinkPreviews', value: action.privacySettingDisableLinkPreviewsAction })
306
+ else if (action?.notificationActivitySettingAction?.notificationActivitySetting) ev.emit('settings.update', { setting: 'notificationActivitySetting', value: action.notificationActivitySettingAction.notificationActivitySetting })
307
+ else if (action?.lidContactAction) ev.emit('contacts.upsert', [{ id, name: action.lidContactAction.fullName || action.lidContactAction.firstName || action.lidContactAction.username || undefined, username: action.lidContactAction.username || undefined, lid: id, phoneNumber: undefined }])
308
+ else if (action?.privacySettingChannelsPersonalisedRecommendationAction) ev.emit('settings.update', { setting: 'channelsPersonalisedRecommendation', value: action.privacySettingChannelsPersonalisedRecommendationAction })
309
+ else if (action?.aiThreadRenameAction) ev.emit('chats.update', [{ id, name: action.aiThreadRenameAction.newTitle || undefined, conditional: getChatUpdateConditional(id, undefined) }])
310
+ else if (action?.threadPinAction) ev.emit('chats.update', [{ id, pinned: action.threadPinAction.pinned ? toNumber(action.timestamp) : null, conditional: getChatUpdateConditional(id, undefined) }])
311
+ else if (action?.newsletterSavedInterestsAction) ev.emit('settings.update', { setting: 'newsletterSavedInterests', value: action.newsletterSavedInterestsAction.newsletterSavedInterests })
312
+ else if (action?.interactiveMessageAction) ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { interactiveMessageAction: action.interactiveMessageAction } }])
313
+ else if (action?.deviceCapabilities || action?.deviceCapabilitiesV2) ev.emit('creds.update', { deviceCapabilities: action.deviceCapabilities || action.deviceCapabilitiesV2 })
314
+ else if (action?.nctSaltSyncAction) ev.emit('creds.update', { nctSalt: action.nctSaltSyncAction.salt })
315
+ else if (action?.bubbleLockMessageAction) ev.emit('messages.update', [{ key: { remoteJid: id, id: msgId, fromMe: fromMe === '1' }, update: { bubbleLocked: !!action.bubbleLockMessageAction.locked } }])
316
+ else logger?.debug({ syncAction, id }, 'unprocessable update')
317
+ }
@@ -0,0 +1,20 @@
1
+ export const CompanionWebClientType = Object.freeze({ UNKNOWN: 0, CHROME: 1, EDGE: 2, FIREFOX: 3, IE: 4, OPERA: 5, SAFARI: 6, ELECTRON: 7, UWP: 8, OTHER_WEB_CLIENT: 9 })
2
+
3
+ const BROWSER_TO_COMPANION = {
4
+ Chrome: CompanionWebClientType.CHROME,
5
+ Edge: CompanionWebClientType.EDGE,
6
+ Firefox: CompanionWebClientType.FIREFOX,
7
+ IE: CompanionWebClientType.IE,
8
+ Opera: CompanionWebClientType.OPERA,
9
+ Safari: CompanionWebClientType.SAFARI
10
+ }
11
+
12
+ export const getCompanionWebClientType = ([os, browserName]) => {
13
+ if (browserName === 'Desktop') return os === 'Windows' ? CompanionWebClientType.UWP : CompanionWebClientType.ELECTRON
14
+ return BROWSER_TO_COMPANION[browserName] ?? CompanionWebClientType.OTHER_WEB_CLIENT
15
+ }
16
+
17
+ export const getCompanionPlatformId = browser => getCompanionWebClientType(browser).toString()
18
+
19
+ export const buildPairingQRData = (ref, noiseKeyB64, identityKeyB64, advB64, browser) =>
20
+ 'https://wa.me/settings/linked_devices#' + [ref, noiseKeyB64, identityKeyB64, advB64, getCompanionPlatformId(browser)].join(',')
@@ -0,0 +1,38 @@
1
+ import { proto } from '../../WAProto/index.js'
2
+
3
+ export const decodeConsumerApplication = buffer => {
4
+ if (!Buffer.isBuffer(buffer) && !(buffer instanceof Uint8Array)) throw new TypeError('decodeConsumerApplication: buffer must be Buffer or Uint8Array')
5
+ return proto.ConsumerApplication.decode(buffer)
6
+ }
7
+
8
+ const textOf = mt => (mt && typeof mt.text === 'string' ? mt.text : undefined)
9
+
10
+ const mapContent = content => {
11
+ if (!content) return null
12
+ switch (content.content) {
13
+ case 'messageText': return { conversation: textOf(content.messageText) ?? '' }
14
+ case 'extendedTextMessage': { const e = content.extendedTextMessage; return { extendedTextMessage: { text: textOf(e.text), matchedText: e.matchedText, canonicalUrl: e.canonicalUrl, description: e.description, title: e.title } } }
15
+ case 'imageMessage': return { imageMessage: { mediaPayload: content.imageMessage.image, caption: textOf(content.imageMessage.caption) } }
16
+ case 'videoMessage': return { videoMessage: { mediaPayload: content.videoMessage.video, caption: textOf(content.videoMessage.caption) } }
17
+ case 'audioMessage': return { audioMessage: { mediaPayload: content.audioMessage.audio, ptt: content.audioMessage.ptt } }
18
+ case 'documentMessage': return { documentMessage: { mediaPayload: content.documentMessage.document, fileName: content.documentMessage.fileName } }
19
+ case 'stickerMessage': return { stickerMessage: { mediaPayload: content.stickerMessage.sticker } }
20
+ case 'contactMessage': return { contactMessage: { mediaPayload: content.contactMessage.contact } }
21
+ case 'contactsArrayMessage': return { contactsArrayMessage: { displayName: content.contactsArrayMessage.displayName, contacts: content.contactsArrayMessage.contacts || [] } }
22
+ case 'locationMessage': { const loc = content.locationMessage.location || {}; return { locationMessage: { degreesLatitude: loc.degreesLatitude, degreesLongitude: loc.degreesLongitude, name: loc.name } } }
23
+ case 'liveLocationMessage': { const loc = content.liveLocationMessage.location || {}; return { liveLocationMessage: { degreesLatitude: loc.degreesLatitude, degreesLongitude: loc.degreesLongitude, caption: textOf(content.liveLocationMessage.caption) } } }
24
+ case 'reactionMessage': return { reactionMessage: content.reactionMessage }
25
+ default: return null
26
+ }
27
+ }
28
+
29
+ // primary use: newsletter/channel message decoding pipeline
30
+ export const consumerApplicationToMessage = app => {
31
+ const payload = app?.payload
32
+ if (!payload) return null
33
+ switch (payload.payload) {
34
+ case 'content': return mapContent(payload.content)
35
+ case 'applicationData': { const ad = payload.applicationData; if (ad?.applicationContent === 'revoke' && ad.revoke?.key) return { protocolMessage: { key: ad.revoke.key, type: proto.Message.ProtocolMessage.Type.REVOKE } }; return null }
36
+ default: return null
37
+ }
38
+ }
@@ -0,0 +1,52 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
2
+ import { calculateAgreement, calculateSignature, verifySignature as rustVerifySignature, generateKeyPair as rustGenerateKeyPair, hkdf, md5 } from 'whatsapp-rust-bridge'
3
+ import { KEY_BUNDLE_TYPE } from '../Defaults/index.js'
4
+
5
+ export { hkdf, md5 }
6
+
7
+ const GCM_TAG_LENGTH = 16 // 128 >> 3
8
+
9
+ export const generateSignalPubKey = pubKey => pubKey.length === 33 ? pubKey : Buffer.concat([KEY_BUNDLE_TYPE, pubKey])
10
+
11
+ export const Curve = {
12
+ generateKeyPair: () => { const { pubKey, privKey } = rustGenerateKeyPair(); return { private: Buffer.from(privKey), public: Buffer.from(pubKey.slice(1)) } },
13
+ sharedKey: (privateKey, publicKey) => Buffer.from(calculateAgreement(generateSignalPubKey(publicKey), privateKey)),
14
+ sign: (privateKey, buf) => calculateSignature(privateKey, buf),
15
+ verify: (pubKey, message, signature) => { try { return rustVerifySignature(generateSignalPubKey(pubKey), message, signature) } catch { return false } }
16
+ }
17
+
18
+ export const signedKeyPair = (identityKeyPair, keyId) => {
19
+ const preKey = Curve.generateKeyPair()
20
+ const pubKey = generateSignalPubKey(preKey.public)
21
+ return { keyPair: preKey, signature: Curve.sign(identityKeyPair.private, pubKey), keyId }
22
+ }
23
+
24
+ export const aesEncryptGCM = (plaintext, key, iv, additionalData) => {
25
+ const cipher = createCipheriv('aes-256-gcm', key, iv)
26
+ cipher.setAAD(additionalData)
27
+ return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()])
28
+ }
29
+
30
+ export const aesDecryptGCM = (ciphertext, key, iv, additionalData) => {
31
+ const decipher = createDecipheriv('aes-256-gcm', key, iv)
32
+ decipher.setAAD(additionalData)
33
+ decipher.setAuthTag(ciphertext.subarray(-GCM_TAG_LENGTH))
34
+ return Buffer.concat([decipher.update(ciphertext.subarray(0, -GCM_TAG_LENGTH)), decipher.final()])
35
+ }
36
+
37
+ export const aesEncryptCTR = (plaintext, key, iv) => { const c = createCipheriv('aes-256-ctr', key, iv); return Buffer.concat([c.update(plaintext), c.final()]) }
38
+ export const aesDecryptCTR = (ciphertext, key, iv) => { const d = createDecipheriv('aes-256-ctr', key, iv); return Buffer.concat([d.update(ciphertext), d.final()]) }
39
+
40
+ export const aesDecryptWithIV = (buffer, key, IV) => { const d = createDecipheriv('aes-256-cbc', key, IV); return Buffer.concat([d.update(buffer), d.final()]) }
41
+ export const aesDecrypt = (buffer, key) => aesDecryptWithIV(buffer.subarray(16), key, buffer.subarray(0, 16))
42
+
43
+ export const aesEncryptWithIV = (buffer, key, IV) => { const c = createCipheriv('aes-256-cbc', key, IV); return Buffer.concat([c.update(buffer), c.final()]) }
44
+ export const aesEncrypt = (buffer, key) => { const IV = randomBytes(16); return Buffer.concat([IV, ...(() => { const c = createCipheriv('aes-256-cbc', key, IV); return [c.update(buffer), c.final()] })()]) }
45
+
46
+ export const hmacSign = (buffer, key, variant = 'sha256') => createHmac(variant, key).update(buffer).digest()
47
+ export const sha256 = buffer => createHash('sha256').update(buffer).digest()
48
+
49
+ export const derivePairingCodeKey = async (pairingCode, salt) => {
50
+ const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(pairingCode), { name: 'PBKDF2' }, false, ['deriveBits'])
51
+ return Buffer.from(await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: salt instanceof Uint8Array ? salt : new Uint8Array(salt), iterations: 2 << 16, hash: 'SHA-256' }, keyMaterial, 256))
52
+ }
@@ -0,0 +1,197 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { proto } from '../../WAProto/index.js'
3
+ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidGroup, isJidMetaAI, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser } from '../WABinary/index.js'
4
+ import { unpadRandomMax16 } from './generics.js'
5
+
6
+ export const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node'
7
+ export const MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled'
8
+ export const ACCOUNT_RESTRICTED_TEXT = 'Your account has been restricted'
9
+
10
+ export const DECRYPTION_RETRY_CONFIG = { maxRetries: 3, baseDelayMs: 100, sessionRecordErrors: ['No session record', 'SessionError: No session record'] }
11
+
12
+ export const NACK_REASONS = {
13
+ ParsingError: 487, UnrecognizedStanza: 488, UnrecognizedStanzaClass: 489, UnrecognizedStanzaType: 490,
14
+ InvalidProtobuf: 491, InvalidHostedCompanionStanza: 493, MissingMessageSecret: 495,
15
+ SignalErrorOldCounter: 496, MessageDeletedOnPeer: 499, UnhandledError: 500,
16
+ UnsupportedAdminRevoke: 550, UnsupportedLIDGroup: 551, DBOperationFailed: 552
17
+ }
18
+
19
+ export const SERVER_ERROR_CODES = { MessageAccountRestriction: '463', SmaxInvalid: '479' }
20
+
21
+ export const getDecryptionJid = async (sender, repository, logger) => {
22
+ if (isJidNewsletter(sender) || isJidStatusBroadcast(sender) || isJidMetaAI(sender)) return sender
23
+ if (isLidUser(sender) || isHostedLidUser(sender)) return sender
24
+ return (await repository.lidMapping.getLIDForPN(sender)) || sender
25
+ }
26
+
27
+ const storeMappingFromEnvelope = async (stanza, sender, repository, decryptionJid, logger, meId, meLid) => {
28
+ const { senderAlt } = extractAddressingContext(stanza)
29
+ if (!senderAlt) return
30
+ if (isLidUser(senderAlt) && isPnUser(sender) && decryptionJid === sender) {
31
+ if (areJidsSameUser(sender, meId) || areJidsSameUser(senderAlt, meLid)) return
32
+ try {
33
+ await repository.lidMapping.storeLIDPNMappings([{ lid: senderAlt, pn: sender }])
34
+ repository.migrateSession(sender, senderAlt).catch(err => logger?.warn?.({ sender, senderAlt, err }, 'Failed to migrate session (PN→LID)'))
35
+ logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope (PN→LID)')
36
+ } catch (error) { logger.warn({ sender, senderAlt, error }, 'Failed to store LID mapping (PN→LID)') }
37
+ } else if (isPnUser(senderAlt) && isLidUser(sender)) {
38
+ if (areJidsSameUser(sender, meLid) || areJidsSameUser(senderAlt, meId)) return
39
+ try {
40
+ await repository.lidMapping.storeLIDPNMappings([{ lid: sender, pn: senderAlt }])
41
+ repository.migrateSession(senderAlt, sender).catch(err => logger?.warn?.({ sender, senderAlt, err }, 'Failed to migrate session (LID→PN)'))
42
+ logger.debug({ sender, senderAlt }, 'Stored LID mapping from envelope (LID→PN)')
43
+ } catch (error) { logger.warn({ sender, senderAlt, error }, 'Failed to store LID mapping (LID→PN)') }
44
+ }
45
+ }
46
+
47
+ export const extractAddressingContext = stanza => {
48
+ const sender = stanza.attrs.participant || stanza.attrs.from
49
+ const addressingMode = stanza.attrs.addressing_mode || (sender?.endsWith('lid') ? 'lid' : 'pn')
50
+ let senderAlt, recipientAlt
51
+ if (addressingMode === 'lid') {
52
+ senderAlt = stanza.attrs.participant_pn || stanza.attrs.sender_pn || stanza.attrs.peer_recipient_pn
53
+ recipientAlt = stanza.attrs.recipient_pn
54
+ } else {
55
+ senderAlt = stanza.attrs.participant_lid || stanza.attrs.sender_lid || stanza.attrs.peer_recipient_lid
56
+ recipientAlt = stanza.attrs.recipient_lid
57
+ }
58
+ return { addressingMode, senderAlt, recipientAlt }
59
+ }
60
+
61
+ const findChildByTag = (node, tag) => Array.isArray(node?.content) ? node.content.find(c => c?.tag === tag) : undefined
62
+
63
+ export const extractNewsletterMessageMeta = stanza => {
64
+ const meta = findChildByTag(stanza, 'meta')
65
+ if (!meta) return undefined
66
+ const result = {}
67
+ const adminProfile = findChildByTag(meta, 'admin_profile')
68
+ if (adminProfile) {
69
+ const name = findChildByTag(adminProfile, 'name')
70
+ const picture = findChildByTag(adminProfile, 'picture')
71
+ const content = name?.content
72
+ result.adminProfile = {
73
+ id: adminProfile.attrs?.id,
74
+ name: typeof content === 'string' ? content : content instanceof Uint8Array ? Buffer.from(content).toString('utf-8') : undefined,
75
+ pictureId: picture?.attrs?.id,
76
+ pictureDirectPath: picture?.attrs?.direct_path
77
+ }
78
+ }
79
+ if (findChildByTag(meta, 'paid_partnership')) result.paidPartnership = true
80
+ if (findChildByTag(meta, 'ai_content')) result.aiContent = true
81
+ return result
82
+ }
83
+
84
+ export const decodeMessageNode = (stanza, meId, meLid) => {
85
+ let msgType, chatId, author, fromMe = false
86
+ const { id: msgId, from, participant, recipient } = stanza.attrs
87
+ const addressingContext = extractAddressingContext(stanza)
88
+ const isMe = jid => areJidsSameUser(jid, meId)
89
+ const isMeLid = jid => areJidsSameUser(jid, meLid)
90
+
91
+ if (isPnUser(from) || isLidUser(from) || isHostedLidUser(from) || isHostedPnUser(from)) {
92
+ if (isMe(from) || isMeLid(from)) fromMe = true
93
+ chatId = (recipient && !isJidMetaAI(recipient)) ? (fromMe ? recipient : (() => { throw new Boom('recipient present, but msg not from me', { data: stanza }) })()) : from
94
+ msgType = 'chat'; author = from
95
+ } else if (isJidGroup(from)) {
96
+ if (!participant) throw new Boom('No participant in group message')
97
+ if (isMe(participant) || isMeLid(participant)) fromMe = true
98
+ msgType = 'group'; author = participant; chatId = from
99
+ } else if (isJidBroadcast(from)) {
100
+ if (!participant) throw new Boom('No participant in group message')
101
+ const isParticipantMe = isMe(participant)
102
+ msgType = isJidStatusBroadcast(from) ? (isParticipantMe ? 'direct_peer_status' : 'other_status') : (isParticipantMe ? 'peer_broadcast' : 'other_broadcast')
103
+ fromMe = isParticipantMe; chatId = from; author = participant
104
+ } else if (isJidNewsletter(from)) {
105
+ msgType = 'newsletter'; chatId = from; author = from
106
+ if (isMe(from) || isMeLid(from)) fromMe = true
107
+ } else throw new Boom('Unknown message type', { data: stanza })
108
+
109
+ const key = {
110
+ remoteJid: chatId,
111
+ remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
112
+ fromMe, id: msgId, participant,
113
+ participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined,
114
+ addressingMode: addressingContext.addressingMode,
115
+ ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
116
+ }
117
+ const fullMessage = { key, messageTimestamp: +stanza.attrs.t, pushName: stanza.attrs?.notify, broadcast: isJidBroadcast(from) }
118
+ if (key.fromMe) fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK
119
+ return { fullMessage, author, sender: msgType === 'chat' ? author : chatId }
120
+ }
121
+
122
+ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
123
+ const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)
124
+ return {
125
+ fullMessage,
126
+ category: stanza.attrs.category,
127
+ author,
128
+ async decrypt() {
129
+ let decryptables = 0
130
+ if (Array.isArray(stanza.content)) {
131
+ for (const { tag, attrs, content } of stanza.content) {
132
+ if (tag === 'verified_name' && content instanceof Uint8Array) {
133
+ const cert = proto.VerifiedNameCertificate.decode(content)
134
+ fullMessage.verifiedBizName = proto.VerifiedNameCertificate.Details.decode(cert.details).verifiedName
135
+ }
136
+ if (tag === 'unavailable' && attrs.type === 'view_once') fullMessage.key.isViewOnce = true
137
+ if (tag !== 'enc' && tag !== 'plaintext') continue
138
+ if (!(content instanceof Uint8Array)) continue
139
+ decryptables++
140
+ const decryptionJid = await getDecryptionJid(author, repository, logger)
141
+ const { senderAlt } = extractAddressingContext(stanza)
142
+ const decryptionAltJid = senderAlt ? await getDecryptionJid(senderAlt, repository, logger) : null
143
+ if (tag !== 'plaintext') storeMappingFromEnvelope(stanza, author, repository, decryptionJid, logger, meId, meLid).catch(err => logger?.warn?.({ err }, 'storeMappingFromEnvelope failed'))
144
+ try {
145
+ const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type
146
+ switch (e2eType) {
147
+ case 'skmsg':
148
+ try {
149
+ fullMessage._msgBuffer = await repository.decryptGroupMessage({ group: sender, authorJid: author, msg: content })
150
+ } catch (decryptErr) {
151
+ const errMsg = decryptErr?.message || decryptErr?.toString() || ''
152
+ if (errMsg.includes('memory access out of bounds')) {
153
+ console.error('[Signal] Stale sender key — group:', sender, 'author:', author, 'err:', errMsg)
154
+ await repository.deleteSenderKey(sender, author).catch(e => console.error('[Signal] Failed to delete sender key:', e))
155
+ }
156
+ throw decryptErr
157
+ }
158
+ break
159
+ case 'pkmsg':
160
+ case 'msg': {
161
+ const buf = await repository.decryptMessage({ jid: decryptionJid, type: e2eType, ciphertext: content })
162
+ if (buf === null) return // DuplicatedMessage — libsignal handled silently
163
+ fullMessage._msgBuffer = buf
164
+ break
165
+ }
166
+ case 'plaintext':
167
+ fullMessage._msgBuffer = content
168
+ break
169
+ case 'msmsg':
170
+ if (!stanza.attrs._msmsgDecrypted) throw new Error('msmsg type but no pre-decrypted message available')
171
+ { let msg = stanza.attrs._msmsgDecrypted; msg = msg.deviceSentMessage?.message || msg; fullMessage.message ? Object.assign(fullMessage.message, msg) : (fullMessage.message = msg) }
172
+ continue
173
+ default:
174
+ throw new Error(`Unknown e2e type: ${e2eType}`)
175
+ }
176
+ let msg = proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(fullMessage._msgBuffer) : fullMessage._msgBuffer)
177
+ msg = msg.deviceSentMessage?.message || msg
178
+ if (msg.senderKeyDistributionMessage) {
179
+ await repository.processSenderKeyDistributionMessage({ authorJid: author, item: msg.senderKeyDistributionMessage })
180
+ .catch(err => logger.error({ key: fullMessage.key, err }, 'failed to process sender key distribution message'))
181
+ }
182
+ fullMessage.message ? Object.assign(fullMessage.message, msg) : (fullMessage.message = msg)
183
+ const viewOnceInner = msg?.viewOnceMessage?.message || msg?.viewOnceMessageV2?.message || msg?.viewOnceMessageV2Extension?.message
184
+ if (viewOnceInner?.imageMessage?.viewOnce || viewOnceInner?.videoMessage?.viewOnce || viewOnceInner?.audioMessage?.viewOnce) fullMessage.key.isViewOnce = true
185
+ } catch (err) {
186
+ const errStr = err?.message || (typeof err === 'string' ? err : '') || ''
187
+ const isExpected = errStr.includes('InvalidPreKeyId') || errStr.includes('SessionNotFound') || errStr.includes('InvalidMessage') || errStr.includes('no sender key state') || errStr.includes('memory access out of bounds') || errStr.includes('old counter') || errStr.includes('DuplicatedMessage') || errStr.includes('BadMac')
188
+ ; (isExpected ? logger?.debug?.bind(logger) : logger?.error?.bind(logger))?.({ key: fullMessage.key, err, errStr, messageType: tag === 'plaintext' ? 'plaintext' : attrs.type, sender, author }, 'failed to decrypt message')
189
+ fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
190
+ fullMessage.messageStubParameters = [errStr]
191
+ }
192
+ }
193
+ }
194
+ if (!decryptables) { fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT; fullMessage.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT] }
195
+ }
196
+ }
197
+ }