@zervonex/baileys 0.0.0-stage → 3.0.1

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  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 +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -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 +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -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 +316 -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 +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. package/package.json +216 -5
@@ -0,0 +1,76 @@
1
+ import PQueue from 'p-queue'
2
+
3
+ export class PreKeyManager {
4
+ constructor(store, logger) {
5
+ this.store = store
6
+ this.logger = logger
7
+ this.queues = new Map()
8
+ }
9
+
10
+ getQueue(keyType) {
11
+ if (!this.queues.has(keyType)) this.queues.set(keyType, new PQueue({ concurrency: 1 }))
12
+ return this.queues.get(keyType)
13
+ }
14
+
15
+ async processOperations(data, keyType, transactionCache, mutations, isInTransaction) {
16
+ const keyData = data[keyType]
17
+ if (!keyData) return
18
+ return this.getQueue(keyType).add(async () => {
19
+ transactionCache[keyType] ||= {}
20
+ mutations[keyType] ||= {}
21
+
22
+ const deletions = []
23
+ const updates = {}
24
+ for (const keyId in keyData) {
25
+ if (keyData[keyId] === null) deletions.push(keyId)
26
+ else updates[keyId] = keyData[keyId]
27
+ }
28
+
29
+ if (Object.keys(updates).length > 0) {
30
+ Object.assign(transactionCache[keyType], updates)
31
+ Object.assign(mutations[keyType], updates)
32
+ }
33
+
34
+ if (deletions.length > 0) await this.processDeletions(keyType, deletions, transactionCache, mutations, isInTransaction)
35
+ })
36
+ }
37
+
38
+ async processDeletions(keyType, ids, transactionCache, mutations, isInTransaction) {
39
+ if (isInTransaction) {
40
+ for (const keyId of ids) {
41
+ if (transactionCache[keyType]?.[keyId]) {
42
+ transactionCache[keyType][keyId] = null
43
+ mutations[keyType][keyId] = null
44
+ } else {
45
+ this.logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`)
46
+ }
47
+ }
48
+ } else {
49
+ const existingKeys = await this.store.get(keyType, ids)
50
+ for (const keyId of ids) {
51
+ if (existingKeys[keyId]) {
52
+ transactionCache[keyType][keyId] = null
53
+ mutations[keyType][keyId] = null
54
+ } else {
55
+ this.logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ async validateDeletions(data, keyType) {
62
+ const keyData = data[keyType]
63
+ if (!keyData) return
64
+ return this.getQueue(keyType).add(async () => {
65
+ const deletionIds = Object.keys(keyData).filter(id => keyData[id] === null)
66
+ if (deletionIds.length === 0) return
67
+ const existingKeys = await this.store.get(keyType, deletionIds)
68
+ for (const keyId of deletionIds) {
69
+ if (!existingKeys[keyId]) {
70
+ this.logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
71
+ delete data[keyType][keyId]
72
+ }
73
+ }
74
+ })
75
+ }
76
+ }
@@ -0,0 +1,399 @@
1
+ import { proto } from '../../WAProto/index.js'
2
+ import { Boom } from '@hapi/boom'
3
+ import { WAMessageStubType } from '../Types/index.js'
4
+ import { getContentType, normalizeMessageContent } from './messages.js'
5
+ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJidStatusBroadcast, isLidUser, jidDecode, jidEncode, jidNormalizedUser } from '../WABinary/index.js'
6
+ import { aesDecryptGCM, hmacSign } from './crypto.js'
7
+ import { getKeyAuthor, toNumber } from './generics.js'
8
+ import { downloadAndProcessHistorySyncNotification } from './history.js'
9
+ import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js'
10
+
11
+ const REAL_MSG_STUB_TYPES = new Set([
12
+ WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
13
+ WAMessageStubType.CALL_MISSED_GROUP_VOICE,
14
+ WAMessageStubType.CALL_MISSED_VIDEO,
15
+ WAMessageStubType.CALL_MISSED_VOICE,
16
+ ])
17
+
18
+ const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD])
19
+
20
+ // Self-only protocol types: drop if not fromMe to prevent local-state spoofing.
21
+ // Cross-user types (REVOKE, MESSAGE_EDIT, EPHEMERAL_SETTING, GROUP_MEMBER_LABEL_CHANGE) must NOT be listed here.
22
+ const SELF_ONLY_PROTOCOL_TYPES = new Set([
23
+ proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
24
+ proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
25
+ proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC,
26
+ proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE,
27
+ ])
28
+
29
+ async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
30
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
31
+ const candidates = []
32
+ for (const chat of chats) {
33
+ const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0
34
+ if (!chat.tcToken?.length || ts <= 0) continue
35
+ const jid = jidNormalizedUser(chat.id)
36
+ const storageJid = await resolveTcTokenJid(jid, getLIDForPN)
37
+ candidates.push({ storageJid, token: Buffer.from(chat.tcToken), ts, senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined })
38
+ }
39
+ if (!candidates.length) return
40
+
41
+ const existing = await keyStore.get('tctoken', candidates.map(c => c.storageJid))
42
+ const entries = {}
43
+ for (const c of candidates) {
44
+ const existingTs = existing[c.storageJid]?.timestamp ? Number(existing[c.storageJid].timestamp) : 0
45
+ if (existingTs > 0 && existingTs >= c.ts) continue
46
+ entries[c.storageJid] = { ...existing[c.storageJid], token: c.token, timestamp: String(c.ts), ...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {}) }
47
+ }
48
+ if (!Object.keys(entries).length) return
49
+
50
+ try {
51
+ const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries)) // keeps __index in sync for cross-session pruning
52
+ await keyStore.set({ tctoken: { ...entries, ...indexWrite } })
53
+ } catch (err) {
54
+ logger?.warn({ err }, 'failed to store tctokens from history sync')
55
+ }
56
+ }
57
+
58
+ export const cleanMessage = (message, meId, meLid) => {
59
+ if (isHostedPnUser(message.key.remoteJid) || isHostedLidUser(message.key.remoteJid)) {
60
+ message.key.remoteJid = jidEncode(jidDecode(message.key?.remoteJid)?.user, isHostedPnUser(message.key.remoteJid) ? 's.whatsapp.net' : 'lid')
61
+ } else {
62
+ message.key.remoteJid = jidNormalizedUser(message.key.remoteJid)
63
+ }
64
+ if (isHostedPnUser(message.key.participant) || isHostedLidUser(message.key.participant)) {
65
+ message.key.participant = jidEncode(jidDecode(message.key.participant)?.user, isHostedPnUser(message.key.participant) ? 's.whatsapp.net' : 'lid')
66
+ } else {
67
+ message.key.participant = jidNormalizedUser(message.key.participant)
68
+ }
69
+
70
+ const content = normalizeMessageContent(message.message)
71
+ if (content?.reactionMessage) normaliseKey(content.reactionMessage.key)
72
+ if (content?.pollUpdateMessage) normaliseKey(content.pollUpdateMessage.pollCreationMessageKey)
73
+
74
+ function normaliseKey(msgKey) {
75
+ if (message.key.fromMe) return
76
+ msgKey.fromMe = !msgKey.fromMe
77
+ ? areJidsSameUser(msgKey.participant || msgKey.remoteJid, meId) || areJidsSameUser(msgKey.participant || msgKey.remoteJid, meLid)
78
+ : false
79
+ msgKey.remoteJid = message.key.remoteJid // TODO: investigate inconsistencies
80
+ msgKey.participant = msgKey.participant || message.key.participant
81
+ }
82
+ }
83
+
84
+ export const isRealMessage = message => {
85
+ const normalizedContent = normalizeMessageContent(message.message)
86
+ return (
87
+ (!!normalizedContent || REAL_MSG_STUB_TYPES.has(message.messageStubType) || REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType)) &&
88
+ !!getContentType(normalizedContent) &&
89
+ !normalizedContent?.protocolMessage &&
90
+ !normalizedContent?.reactionMessage &&
91
+ !normalizedContent?.pollUpdateMessage
92
+ )
93
+ }
94
+
95
+ export const shouldIncrementChatUnread = message => !message.key.fromMe && !message.messageStubType
96
+
97
+ export const getChatId = ({ remoteJid, participant, fromMe }) => {
98
+ if (!remoteJid) throw new Boom('Cannot derive chat id: message key is missing remoteJid', { data: { remoteJid, participant, fromMe } })
99
+ if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
100
+ if (!participant) throw new Boom('Cannot derive chat id: broadcast message key is missing participant', { data: { remoteJid, fromMe } })
101
+ return participant
102
+ }
103
+ return remoteJid
104
+ }
105
+
106
+ export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsgId, pollEncKey, voterJid }) {
107
+ const toBinary = txt => Buffer.from(txt)
108
+ const sign = Buffer.concat([toBinary(pollMsgId), toBinary(pollCreatorJid), toBinary(voterJid), toBinary('Poll Vote'), new Uint8Array([1])])
109
+ const decKey = hmacSign(sign, hmacSign(pollEncKey, new Uint8Array(32), 'sha256'), 'sha256')
110
+ return proto.Message.PollVoteMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, toBinary(`${pollMsgId}\u0000${voterJid}`)))
111
+ }
112
+
113
+ export function decryptEventResponse({ encPayload, encIv }, { eventCreatorJid, eventMsgId, eventEncKey, responderJid }) {
114
+ const toBinary = txt => Buffer.from(txt)
115
+ const sign = Buffer.concat([toBinary(eventMsgId), toBinary(eventCreatorJid), toBinary(responderJid), toBinary('Event Response'), new Uint8Array([1])])
116
+ const decKey = hmacSign(sign, hmacSign(eventEncKey, new Uint8Array(32), 'sha256'), 'sha256')
117
+ return proto.Message.EventResponseMessage.decode(aesDecryptGCM(encPayload, decKey, encIv, toBinary(`${eventMsgId}\u0000${responderJid}`)))
118
+ }
119
+
120
+ export const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, signalRepository, keyStore, logger, options, getMessage }) => {
121
+ const meId = creds.me.id
122
+ const { accountSettings } = creds
123
+ const chat = { id: jidNormalizedUser(getChatId(message.key)) }
124
+ const isRealMsg = isRealMessage(message)
125
+
126
+ if (isRealMsg) {
127
+ chat.messages = [{ message }]
128
+ chat.conversationTimestamp = toNumber(message.messageTimestamp)
129
+ if (shouldIncrementChatUnread(message)) chat.unreadCount = (chat.unreadCount || 0) + 1
130
+ }
131
+
132
+ const content = normalizeMessageContent(message.message)
133
+
134
+ if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
135
+ chat.archived = false
136
+ chat.readOnly = false
137
+ }
138
+
139
+ const protocolMsg = content?.protocolMessage
140
+ if (protocolMsg) {
141
+ if (protocolMsg.type !== null && protocolMsg.type !== undefined && SELF_ONLY_PROTOCOL_TYPES.has(protocolMsg.type) && !message.key.fromMe) {
142
+ 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')
143
+ return
144
+ }
145
+
146
+ switch (protocolMsg.type) {
147
+ case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION: {
148
+ const histNotification = protocolMsg.historySyncNotification
149
+ const isLatest = !creds.processedHistoryMessages?.length
150
+ if (shouldProcessHistoryMsg) {
151
+ if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
152
+ ev.emit('creds.update', { processedHistoryMessages: [...(creds.processedHistoryMessages || []), { key: message.key, messageTimestamp: message.messageTimestamp }] })
153
+ }
154
+ const data = await downloadAndProcessHistorySyncNotification(histNotification, options, logger)
155
+ if (data.lidPnMappings?.length) {
156
+ await signalRepository.lidMapping.storeLIDPNMappings(data.lidPnMappings).catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'))
157
+ }
158
+ await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger)
159
+ ev.emit('messaging-history.set', { ...data, isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined, chunkOrder: histNotification.chunkOrder, peerDataRequestSessionId: histNotification.peerDataRequestSessionId })
160
+ }
161
+ break
162
+ }
163
+
164
+ case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE: {
165
+ const keys = protocolMsg.appStateSyncKeyShare.keys
166
+ if (keys?.length) {
167
+ let newAppStateSyncKeyId = ''
168
+ await keyStore.transaction(async () => {
169
+ const newKeys = []
170
+ for (const { keyData, keyId } of keys) {
171
+ const strKeyId = Buffer.from(keyId.keyId).toString('base64')
172
+ newKeys.push(strKeyId)
173
+ await keyStore.set({ 'app-state-sync-key': { [strKeyId]: keyData } })
174
+ newAppStateSyncKeyId = strKeyId
175
+ }
176
+ }, meId)
177
+ ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId })
178
+ }
179
+ break
180
+ }
181
+
182
+ case proto.Message.ProtocolMessage.Type.REVOKE: {
183
+ ev.emit('messages.update', [{ key: { ...message.key, id: protocolMsg.key.id }, update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key } }])
184
+ break
185
+ }
186
+
187
+ case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING: {
188
+ Object.assign(chat, { ephemeralSettingTimestamp: toNumber(message.messageTimestamp), ephemeralExpiration: protocolMsg.ephemeralExpiration || null })
189
+ break
190
+ }
191
+
192
+ case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT: {
193
+ ev.emit('messages.update', [{
194
+ key: { ...message.key, id: protocolMsg.key?.id }, // key is in sender's perspective; fromMe flip happens at read time
195
+ update: {
196
+ message: { editedMessage: { message: protocolMsg.editedMessage } },
197
+ messageTimestamp: protocolMsg.timestampMs ? Math.floor(toNumber(protocolMsg.timestampMs) / 1000) : message.messageTimestamp,
198
+ },
199
+ }])
200
+ break
201
+ }
202
+
203
+ case proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE: {
204
+ const labelAssociationMsg = protocolMsg.memberLabel
205
+ if (labelAssociationMsg?.label) {
206
+ ev.emit('group.member-tag.update', { groupId: chat.id, label: labelAssociationMsg.label, participant: message.key.participant, participantAlt: message.key.participantAlt, messageTimestamp: Number(message.messageTimestamp) })
207
+ }
208
+ break
209
+ }
210
+
211
+ case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE: {
212
+ const response = protocolMsg.peerDataOperationRequestResponseMessage
213
+ if (!response) break
214
+ for (const result of (response.peerDataOperationResult || [])) {
215
+ const retryResponse = result?.placeholderMessageResendResponse
216
+ if (!retryResponse?.webMessageInfoBytes) continue
217
+ try {
218
+ const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes)
219
+ const msgId = webMessageInfo.key?.id
220
+ const cachedData = msgId ? await placeholderResendCache?.get(msgId) : undefined // preserves LID, timestamps etc. that phone may omit in PDO response
221
+ if (msgId) await placeholderResendCache?.del(msgId)
222
+ let finalMsg
223
+ if (cachedData && typeof cachedData === 'object') {
224
+ cachedData.message = webMessageInfo.message
225
+ if (webMessageInfo.messageTimestamp) cachedData.messageTimestamp = webMessageInfo.messageTimestamp
226
+ finalMsg = cachedData
227
+ } else {
228
+ finalMsg = webMessageInfo
229
+ }
230
+ ev.emit('messages.upsert', { messages: [finalMsg], type: 'notify', requestId: response.stanzaId })
231
+ } catch (err) {
232
+ logger?.warn({ err, stanzaId: response.stanzaId }, 'failed to decode placeholder resend response')
233
+ }
234
+ }
235
+ break
236
+ }
237
+
238
+ case proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC: {
239
+ const encodedPayload = protocolMsg.lidMigrationMappingSyncMessage?.encodedMappingPayload
240
+ const { pnToLidMappings, chatDbMigrationTimestamp } = proto.LIDMigrationMappingSyncPayload.decode(encodedPayload)
241
+ const pairs = []
242
+ for (const { pn, latestLid, assignedLid } of pnToLidMappings) {
243
+ pairs.push({ lid: `${latestLid || assignedLid}@lid`, pn: `${pn}@s.whatsapp.net` }) // latestLid takes priority over assignedLid
244
+ }
245
+ await signalRepository.lidMapping.storeLIDPNMappings(pairs)
246
+ for (const { pn, lid } of pairs) await signalRepository.migrateSession(pn, lid)
247
+ break
248
+ }
249
+
250
+ case proto.Message.ProtocolMessage.Type.STOP_GENERATION_MESSAGE: {
251
+ ev.emit('messages.update', [{ key: { remoteJid: message.key.remoteJid, id: protocolMsg.key?.id }, update: { generationStopped: true } }])
252
+ break
253
+ }
254
+
255
+ case proto.Message.ProtocolMessage.Type.CHAT_THEME_SETTING: {
256
+ ev.emit('chats.update', [{ id: message.key.remoteJid, theme: protocolMsg.chatThemeSetting }])
257
+ break
258
+ }
259
+
260
+ case proto.Message.ProtocolMessage.Type.MARK_AS_VERIFIED_ACTION: {
261
+ ev.emit('contacts.update', [{ id: message.key.remoteJid, verified: protocolMsg.markAsVerifiedAction?.verified }])
262
+ break
263
+ }
264
+
265
+ case proto.Message.ProtocolMessage.Type.MESSAGE_UNSCHEDULE: {
266
+ ev.emit('messages.update', [{ key: { remoteJid: message.key.remoteJid, id: protocolMsg.key?.id }, update: { messageStubType: WAMessageStubType.SCHEDULED_MESSAGE_CREATED, scheduled: false } }])
267
+ break
268
+ }
269
+ }
270
+ } else if (content?.reactionMessage) {
271
+ ev.emit('messages.reaction', [{ reaction: { ...content.reactionMessage, key: message.key }, key: content.reactionMessage?.key }])
272
+ } /*else if (content?.pollUpdateMessage) {
273
+ const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey
274
+ const pollMsg = await getMessage(creationMsgKey)
275
+ if (pollMsg) {
276
+ try {
277
+ const meIdNormalised = jidNormalizedUser(meId)
278
+ const pollCreatorJid = getKeyAuthor(creationMsgKey, meIdNormalised)
279
+ const voterJid = getKeyAuthor(message.key, meIdNormalised)
280
+ const pollEncKey = pollMsg.messageContextInfo?.messageSecret
281
+ if (!pollEncKey) {
282
+ logger?.warn({ creationMsgKey }, 'poll vote: missing messageSecret for decryption')
283
+ } else {
284
+ const voteMsg = decryptPollVote(content.pollUpdateMessage.vote, { pollEncKey, pollCreatorJid, pollMsgId: creationMsgKey.id, voterJid })
285
+ ev.emit('messages.update', [{ key: creationMsgKey, update: { pollUpdates: [{ pollUpdateMessageKey: message.key, vote: voteMsg, senderTimestampMs: toNumber(content.pollUpdateMessage.senderTimestampMs) }] } }])
286
+ }
287
+ } catch (err) {
288
+ logger?.warn({ err, creationMsgKey }, 'failed to decrypt poll vote')
289
+ }
290
+ } else {
291
+ logger?.warn({ creationMsgKey }, 'poll creation message not found, cannot decrypt update')
292
+ }
293
+ }*/ else if (content?.encEventResponseMessage) {
294
+ const encEventResponse = content.encEventResponseMessage
295
+ const creationMsgKey = encEventResponse.eventCreationMessageKey
296
+ const eventMsg = await getMessage(creationMsgKey)
297
+ if (eventMsg) {
298
+ try {
299
+ const meIdNormalised = jidNormalizedUser(meId)
300
+ const eventCreatorKey = creationMsgKey.participant || creationMsgKey.remoteJid
301
+ const eventCreatorPn = isLidUser(eventCreatorKey) ? await signalRepository.lidMapping.getPNForLID(eventCreatorKey) : eventCreatorKey // event creator must be PN; resolve from LID if necessary
302
+ const eventCreatorJid = getKeyAuthor({ remoteJid: jidNormalizedUser(eventCreatorPn), fromMe: meIdNormalised === eventCreatorPn }, meIdNormalised)
303
+ const responderJid = getKeyAuthor(message.key, meIdNormalised)
304
+ const eventEncKey = eventMsg?.messageContextInfo?.messageSecret
305
+ if (!eventEncKey) {
306
+ logger?.warn({ creationMsgKey }, 'event response: missing messageSecret for decryption')
307
+ } else {
308
+ const responseMsg = decryptEventResponse(encEventResponse, { eventEncKey, eventCreatorJid, eventMsgId: creationMsgKey.id, responderJid })
309
+ ev.emit('messages.update', [{ key: creationMsgKey, update: { eventResponses: [{ eventResponseMessageKey: message.key, senderTimestampMs: responseMsg.timestampMs, response: responseMsg }] } }])
310
+ }
311
+ } catch (err) {
312
+ logger?.warn({ err, creationMsgKey }, 'failed to decrypt event response')
313
+ }
314
+ } else {
315
+ logger?.warn({ creationMsgKey }, 'event creation message not found, cannot decrypt response')
316
+ }
317
+ } else if (message.messageStubType) {
318
+ const jid = message.key?.remoteJid
319
+ let participants
320
+ const emitParticipantsUpdate = action => ev.emit('group-participants.update', { id: jid, author: message.key.participant, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername, participants, action })
321
+ const emitGroupUpdate = update => ev.emit('groups.update', [{ id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt, authorUsername: message.key.participantUsername }])
322
+ 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 })
323
+
324
+ const participantsIncludesMe = () => participants.find(p => areJidsSameUser(meId, p.phoneNumber)) // TODO: ADD SUPPORT FOR LID
325
+
326
+ switch (message.messageStubType) {
327
+ case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
328
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || []
329
+ emitParticipantsUpdate('modify')
330
+ break
331
+ case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
332
+ case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
333
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || []
334
+ emitParticipantsUpdate('remove')
335
+ if (participantsIncludesMe()) chat.readOnly = true
336
+ break
337
+ case WAMessageStubType.GROUP_PARTICIPANT_ADD:
338
+ case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
339
+ case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
340
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || []
341
+ if (participantsIncludesMe()) chat.readOnly = false
342
+ emitParticipantsUpdate('add')
343
+ break
344
+ case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
345
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || []
346
+ emitParticipantsUpdate('demote')
347
+ break
348
+ case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
349
+ participants = message.messageStubParameters.map(a => JSON.parse(a)) || []
350
+ emitParticipantsUpdate('promote')
351
+ break
352
+ case WAMessageStubType.GROUP_CHANGE_ANNOUNCE: {
353
+ const announceValue = message.messageStubParameters?.[0]
354
+ emitGroupUpdate({ announce: announceValue === 'true' || announceValue === 'on' })
355
+ break
356
+ }
357
+ case WAMessageStubType.GROUP_CHANGE_RESTRICT: {
358
+ const restrictValue = message.messageStubParameters?.[0]
359
+ emitGroupUpdate({ restrict: restrictValue === 'true' || restrictValue === 'on' })
360
+ break
361
+ }
362
+ case WAMessageStubType.GROUP_CHANGE_SUBJECT: {
363
+ const name = message.messageStubParameters?.[0]
364
+ chat.name = name
365
+ emitGroupUpdate({ subject: name })
366
+ break
367
+ }
368
+ case WAMessageStubType.GROUP_CHANGE_DESCRIPTION: {
369
+ const description = message.messageStubParameters?.[0]
370
+ chat.description = description
371
+ emitGroupUpdate({ desc: description })
372
+ break
373
+ }
374
+ case WAMessageStubType.GROUP_CHANGE_INVITE_LINK: {
375
+ emitGroupUpdate({ inviteCode: message.messageStubParameters?.[0] })
376
+ break
377
+ }
378
+ case WAMessageStubType.GROUP_MEMBER_ADD_MODE: {
379
+ const memberAddValue = message.messageStubParameters?.[0]
380
+ emitGroupUpdate({ memberAddMode: memberAddValue === 'all_member_add' })
381
+ break
382
+ }
383
+ case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE: {
384
+ const approvalMode = message.messageStubParameters?.[0]
385
+ emitGroupUpdate({ joinApprovalMode: approvalMode === 'on' })
386
+ break
387
+ }
388
+ case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD: {
389
+ const participant = JSON.parse(message.messageStubParameters?.[0])
390
+ emitGroupRequestJoin(participant, message.messageStubParameters?.[1], message.messageStubParameters?.[2])
391
+ break
392
+ }
393
+ }
394
+ }
395
+
396
+ if (Object.keys(chat).length > 1) ev.emit('chats.update', [chat])
397
+ }
398
+
399
+ export default processMessage
@@ -0,0 +1,171 @@
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
+ { f: 97, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }] }, { f: 4, s: [{ f: 21 }, { f: 22 }] }] }, // richResponseMessage
33
+ { f: 98, s: [{ f: 1 }, { f: 2 }] }, // statusNotificationMessage
34
+ { f: 100, s: [{ f: 1, m: true }] }, // botTaskMessage
35
+ { f: 101, s: [{ f: 1, m: true }] }, // questionMessage
36
+ { f: 103, s: [{ f: 1, m: true }] }, // groupStatusMessageV2
37
+ { f: 104, s: [{ f: 1, m: true }] }, // botForwardedMessage
38
+ { f: 105, s: [{ f: 1 }, { f: 2 }] }, // statusQuestionAnswerMessage
39
+ { f: 107, s: [{ f: 1 }, { f: 2 }] }, // questionResponseMessage
40
+ { f: 109, s: [{ f: 1 }, { f: 2 }, { f: 3 }, { f: 4 }] }, // statusQuotedMessage
41
+ { f: 111, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] }, { f: 8, s: [{ f: 1 }, { f: 2 }] }] }, // pollCreationMessageV5
42
+ { f: 115, s: [{ f: 1 }, { f: 2, s: [{ f: 1 }] }, { f: 3, s: [{ f: 21 }, { f: 22 }] }] }, // pollResultSnapshotMessageV3
43
+ { f: 119, s: [{ f: 2 }, { f: 3, s: [{ f: 1 }, { f: 2 }] }, { f: 5, s: [{ f: 21 }, { f: 22 }] }, { f: 8, s: [{ f: 1 }, { f: 2 }] }] }, // pollCreationMessageV6
44
+ { f: 125, s: [{ f: 1 }, { f: 2, s: [{ f: 1 }] }, { f: 3 }, { f: 4 }] }, // splitPaymentMessage
45
+ { f: 129, s: [{ f: 1, m: true }] }, // musicMessage
46
+ ]
47
+
48
+ const EMPTY_MAP = new Map()
49
+ const ENC_SECRET_REPORT_TOKEN = 'Report Token'
50
+ const WIRE = { VARINT: 0, FIXED64: 1, BYTES: 2, FIXED32: 5 }
51
+
52
+ const compileReportingFields = fields => {
53
+ const map = new Map()
54
+ for (const f of fields) map.set(f.f, { m: f.m, children: f.s ? compileReportingFields(f.s) : undefined })
55
+ return map
56
+ }
57
+
58
+ const compiledReportingFields = compileReportingFields(reportingFields)
59
+
60
+ const decodeVarint = (buffer, offset) => {
61
+ let value = 0, bytes = 0, shift = 0
62
+ while (offset + bytes < buffer.length) {
63
+ const current = buffer[offset + bytes]
64
+ value |= (current & 0x7f) << shift
65
+ bytes++
66
+ if ((current & 0x80) === 0) return { value, bytes, ok: true }
67
+ shift += 7
68
+ if (shift > 35) return { value: 0, bytes: 0, ok: false }
69
+ }
70
+ return { value: 0, bytes: 0, ok: false }
71
+ }
72
+
73
+ const encodeVarint = value => {
74
+ const parts = []
75
+ let remaining = value >>> 0
76
+ while (remaining > 0x7f) { parts.push((remaining & 0x7f) | 0x80); remaining >>>= 7 }
77
+ parts.push(remaining)
78
+ return Buffer.from(parts)
79
+ }
80
+
81
+ const extractReportingTokenContent = (data, cfg) => {
82
+ const out = []
83
+ let i = 0
84
+ while (i < data.length) {
85
+ const tag = decodeVarint(data, i)
86
+ if (!tag.ok) return null
87
+ const fieldNum = tag.value >> 3
88
+ const wireType = tag.value & 0x7
89
+ const fieldStart = i
90
+ i += tag.bytes
91
+ const fieldCfg = cfg.get(fieldNum)
92
+ const pushSlice = end => { if (end > data.length) return false; out.push({ num: fieldNum, bytes: data.subarray(fieldStart, end) }); i = end; return true }
93
+ const skip = end => { if (end > data.length) return false; i = end; return true }
94
+
95
+ if (wireType === WIRE.VARINT) {
96
+ const v = decodeVarint(data, i)
97
+ if (!v.ok) return null
98
+ const end = i + v.bytes
99
+ if (!fieldCfg) { if (!skip(end)) return null; continue }
100
+ if (!pushSlice(end)) return null
101
+ continue
102
+ }
103
+ if (wireType === WIRE.FIXED64) {
104
+ const end = i + 8
105
+ if (!fieldCfg) { if (!skip(end)) return null; continue }
106
+ if (!pushSlice(end)) return null
107
+ continue
108
+ }
109
+ if (wireType === WIRE.FIXED32) {
110
+ const end = i + 4
111
+ if (!fieldCfg) { if (!skip(end)) return null; continue }
112
+ if (!pushSlice(end)) return null
113
+ continue
114
+ }
115
+ if (wireType === WIRE.BYTES) {
116
+ const len = decodeVarint(data, i)
117
+ if (!len.ok) return null
118
+ const valStart = i + len.bytes
119
+ const valEnd = valStart + len.value
120
+ if (valEnd > data.length) return null
121
+ if (!fieldCfg) { i = valEnd; continue }
122
+ if (fieldCfg.m || fieldCfg.children) {
123
+ const sub = extractReportingTokenContent(data.subarray(valStart, valEnd), fieldCfg.children ?? EMPTY_MAP)
124
+ if (sub === null) return null
125
+ if (sub.length > 0) {
126
+ out.push({ num: fieldNum, bytes: Buffer.concat([encodeVarint(tag.value), encodeVarint(sub.length), sub]) })
127
+ }
128
+ i = valEnd
129
+ continue
130
+ }
131
+ out.push({ num: fieldNum, bytes: data.subarray(fieldStart, valEnd) })
132
+ i = valEnd
133
+ continue
134
+ }
135
+ return null
136
+ }
137
+ if (out.length === 0) return Buffer.alloc(0)
138
+ out.sort((a, b) => a.num - b.num)
139
+ return Buffer.concat(out.map(f => f.bytes))
140
+ }
141
+
142
+ const generateMsgSecretKey = (modificationType, origMsgId, origMsgSender, modificationSender, origMsgSecret) => {
143
+ const useCaseSecret = Buffer.concat([
144
+ Buffer.from(origMsgId, 'utf8'),
145
+ Buffer.from(origMsgSender, 'utf8'),
146
+ Buffer.from(modificationSender, 'utf8'),
147
+ Buffer.from(modificationType, 'utf8'),
148
+ ])
149
+ return hkdf(origMsgSecret, 32, { info: useCaseSecret.toString('latin1') })
150
+ }
151
+
152
+ export const shouldIncludeReportingToken = message =>
153
+ !message.reactionMessage && !message.encReactionMessage &&
154
+ !message.encEventResponseMessage && !message.pollUpdateMessage &&
155
+ !message.statusStickerInteractionMessage && !message.encCommentMessage
156
+
157
+ export const getMessageReportingToken = async (msgProtobuf, message, key) => {
158
+ const msgSecret = message.messageContextInfo?.messageSecret
159
+ if (!msgSecret || !key.id) return null
160
+ const from = key.fromMe ? key.remoteJid : key.participant || key.remoteJid
161
+ const to = key.fromMe ? key.participant || key.remoteJid : key.remoteJid
162
+ const reportingSecret = generateMsgSecretKey(ENC_SECRET_REPORT_TOKEN, key.id, from, to, msgSecret)
163
+ const content = extractReportingTokenContent(msgProtobuf, compiledReportingFields)
164
+ if (!content || content.length === 0) return null
165
+ const reportingToken = createHmac('sha256', reportingSecret).update(content).digest().subarray(0, 16)
166
+ return {
167
+ tag: 'reporting',
168
+ attrs: {},
169
+ content: [{ tag: 'reporting_token', attrs: { v: '2' }, content: reportingToken }],
170
+ }
171
+ }