@nexustechpro/baileys 2.0.2 → 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.
- package/LICENSE +1 -1
- package/README.md +924 -1299
- package/WAProto/index.js +22 -18
- package/lib/Defaults/baileys-version.json +6 -2
- package/lib/Defaults/index.js +173 -172
- package/lib/Signal/libsignal.js +395 -292
- package/lib/Signal/lid-mapping.js +264 -171
- package/lib/Socket/Client/index.js +2 -2
- package/lib/Socket/Client/types.js +10 -10
- package/lib/Socket/Client/websocket.js +45 -310
- package/lib/Socket/business.js +375 -375
- package/lib/Socket/chats.js +916 -963
- package/lib/Socket/communities.js +430 -430
- package/lib/Socket/groups.js +342 -342
- package/lib/Socket/index.js +21 -22
- package/lib/Socket/messages-recv.js +963 -743
- package/lib/Socket/messages-send.js +273 -321
- package/lib/Socket/mex.js +50 -50
- package/lib/Socket/newsletter.js +148 -148
- package/lib/Socket/nexus-handler.js +296 -247
- package/lib/Socket/registration.js +50 -33
- package/lib/Socket/socket.js +872 -1201
- package/lib/Store/index.js +5 -5
- package/lib/Store/make-cache-manager-store.js +81 -81
- package/lib/Store/make-in-memory-store.js +416 -416
- package/lib/Store/make-ordered-dictionary.js +81 -81
- package/lib/Store/object-repository.js +30 -30
- package/lib/Types/Auth.js +1 -1
- package/lib/Types/Bussines.js +1 -1
- package/lib/Types/Call.js +1 -1
- package/lib/Types/Chat.js +7 -7
- package/lib/Types/Contact.js +1 -1
- package/lib/Types/Events.js +1 -1
- package/lib/Types/GroupMetadata.js +1 -1
- package/lib/Types/Label.js +24 -24
- package/lib/Types/LabelAssociation.js +6 -6
- package/lib/Types/Message.js +10 -10
- package/lib/Types/Newsletter.js +37 -29
- package/lib/Types/Product.js +1 -1
- package/lib/Types/Signal.js +1 -1
- package/lib/Types/Socket.js +2 -2
- package/lib/Types/State.js +55 -12
- package/lib/Types/USync.js +1 -1
- package/lib/Types/index.js +25 -25
- package/lib/Utils/auth-utils.js +264 -256
- package/lib/Utils/baileys-event-stream.js +55 -55
- package/lib/Utils/browser-utils.js +27 -27
- package/lib/Utils/business.js +228 -230
- package/lib/Utils/chat-utils.js +726 -764
- package/lib/Utils/companion-reg-client-utils.js +34 -0
- package/lib/Utils/crypto.js +109 -135
- package/lib/Utils/decode-wa-message.js +342 -314
- package/lib/Utils/event-buffer.js +547 -547
- package/lib/Utils/generics.js +295 -297
- package/lib/Utils/history.js +91 -83
- package/lib/Utils/index.js +25 -20
- package/lib/Utils/key-store.js +17 -0
- package/lib/Utils/link-preview.js +107 -98
- package/lib/Utils/logger.js +2 -2
- package/lib/Utils/lt-hash.js +47 -47
- package/lib/Utils/make-mutex.js +39 -39
- package/lib/Utils/message-retry-manager.js +148 -148
- package/lib/Utils/messages-media.js +579 -535
- package/lib/Utils/messages.js +821 -706
- package/lib/Utils/noise-handler.js +255 -255
- package/lib/Utils/pre-key-manager.js +105 -105
- package/lib/Utils/process-message.js +430 -412
- package/lib/Utils/reporting-utils.js +155 -0
- package/lib/Utils/signal.js +191 -159
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +162 -0
- package/lib/Utils/use-multi-file-auth-state.js +120 -120
- package/lib/Utils/validate-connection.js +194 -194
- package/lib/WABinary/constants.js +1306 -1300
- package/lib/WABinary/decode.js +237 -237
- package/lib/WABinary/encode.js +232 -232
- package/lib/WABinary/generic-utils.js +252 -211
- package/lib/WABinary/index.js +6 -5
- package/lib/WABinary/jid-utils.js +279 -95
- package/lib/WABinary/types.js +1 -1
- package/lib/WAM/BinaryInfo.js +9 -9
- package/lib/WAM/constants.js +22852 -22852
- package/lib/WAM/encode.js +149 -149
- package/lib/WAM/index.js +3 -3
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
- package/lib/WAUSync/Protocols/index.js +4 -4
- package/lib/WAUSync/USyncQuery.js +93 -93
- package/lib/WAUSync/USyncUser.js +22 -22
- package/lib/WAUSync/index.js +3 -3
- package/lib/index.js +65 -66
- package/package.json +172 -143
- package/lib/Signal/Group/ciphertext-message.js +0 -12
- package/lib/Signal/Group/group-session-builder.js +0 -30
- package/lib/Signal/Group/group_cipher.js +0 -100
- package/lib/Signal/Group/index.js +0 -12
- package/lib/Signal/Group/keyhelper.js +0 -18
- package/lib/Signal/Group/sender-chain-key.js +0 -26
- package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
- package/lib/Signal/Group/sender-key-message.js +0 -66
- package/lib/Signal/Group/sender-key-name.js +0 -48
- package/lib/Signal/Group/sender-key-record.js +0 -41
- package/lib/Signal/Group/sender-key-state.js +0 -84
- package/lib/Signal/Group/sender-message-key.js +0 -26
|
@@ -4,8 +4,7 @@ import * as Utils from '../Utils/index.js'
|
|
|
4
4
|
import { proto } from '../../WAProto/index.js'
|
|
5
5
|
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
|
|
6
6
|
import * as WABinary from '../WABinary/index.js'
|
|
7
|
-
import { getUrlInfo } from '../Utils/
|
|
8
|
-
import { makeKeyedMutex } from '../Utils/make-mutex.js'
|
|
7
|
+
import { getUrlInfo, migrateIndexKey, getMessageReportingToken, shouldIncludeReportingToken, buildMergedTcTokenIndexWrite, isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid, shouldSendNewTcToken, storeTcTokensFromIqResult, makeKeyedMutex, makeMutex } from '../Utils/index.js'
|
|
9
8
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js'
|
|
10
9
|
import { makeNewsletterSocket } from './newsletter.js'
|
|
11
10
|
import NexusHandler from './nexus-handler.js'
|
|
@@ -22,16 +21,12 @@ const {
|
|
|
22
21
|
|
|
23
22
|
const {
|
|
24
23
|
areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
|
|
25
|
-
isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
|
|
26
|
-
getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType
|
|
24
|
+
isJidBroadcast, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
|
|
25
|
+
getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType, isJidBot, isJidMetaAI
|
|
27
26
|
} = WABinary
|
|
28
27
|
|
|
29
28
|
export const makeMessagesSocket = (config) => {
|
|
30
|
-
const {
|
|
31
|
-
logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview,
|
|
32
|
-
options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata,
|
|
33
|
-
enableRecentMessageCache, maxMsgRetryCount
|
|
34
|
-
} = config
|
|
29
|
+
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, getMessage } = config
|
|
35
30
|
|
|
36
31
|
const sock = makeNewsletterSocket(config)
|
|
37
32
|
const {
|
|
@@ -40,16 +35,15 @@ export const makeMessagesSocket = (config) => {
|
|
|
40
35
|
} = sock
|
|
41
36
|
|
|
42
37
|
const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
43
|
-
const
|
|
38
|
+
const devicesMutex = makeMutex()
|
|
44
39
|
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
|
45
40
|
const encryptionMutex = makeKeyedMutex()
|
|
41
|
+
|
|
42
|
+
// Prevents duplicate TC token IQ requests from concurrent sends
|
|
43
|
+
const inFlightTcTokenIssuance = new Set()
|
|
44
|
+
|
|
46
45
|
let mediaConn
|
|
47
46
|
|
|
48
|
-
// ─────────────────────────────────────────────
|
|
49
|
-
// MEDIA CONNECTION
|
|
50
|
-
// Fetches and caches the WhatsApp media upload connection.
|
|
51
|
-
// Refreshes automatically when TTL expires or forced.
|
|
52
|
-
// ─────────────────────────────────────────────
|
|
53
47
|
const refreshMediaConn = async (forceGet = false) => {
|
|
54
48
|
const media = await mediaConn
|
|
55
49
|
if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
|
|
@@ -68,18 +62,26 @@ export const makeMessagesSocket = (config) => {
|
|
|
68
62
|
return mediaConn
|
|
69
63
|
}
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Sends read receipts and delivery confirmations.
|
|
74
|
-
// ─────────────────────────────────────────────
|
|
65
|
+
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
|
|
66
|
+
|
|
75
67
|
const sendReceipt = async (jid, participant, messageIds, type) => {
|
|
76
68
|
if (!messageIds?.length) throw new Boom('missing ids in receipt')
|
|
77
69
|
const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
|
|
78
70
|
const isReadReceipt = type === 'read' || type === 'read-self'
|
|
79
71
|
if (isReadReceipt) node.attrs.t = unixTimestampSeconds().toString()
|
|
72
|
+
if (isJidStatusBroadcast(jid) && !participant && getMessage) {
|
|
73
|
+
try {
|
|
74
|
+
const msg = await getMessage({ remoteJid: jid, id: messageIds[0], fromMe: false })
|
|
75
|
+
participant = msg?.key?.participant || msg?.participant || msg?.key?.remoteJid
|
|
76
|
+
logger.debug({ jid, resolvedParticipant: participant }, 'resolved status receipt participant from message store')
|
|
77
|
+
} catch (err) { logger.debug({ err, jid }, 'failed to resolve status receipt participant') }
|
|
78
|
+
}
|
|
80
79
|
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
|
|
81
80
|
node.attrs.recipient = jid
|
|
82
81
|
node.attrs.to = participant
|
|
82
|
+
} else if (isJidStatusBroadcast(jid) && participant) {
|
|
83
|
+
node.attrs.to = jid
|
|
84
|
+
node.attrs.participant = participant
|
|
83
85
|
} else {
|
|
84
86
|
node.attrs.to = jid
|
|
85
87
|
if (participant) node.attrs.participant = participant
|
|
@@ -101,14 +103,11 @@ export const makeMessagesSocket = (config) => {
|
|
|
101
103
|
|
|
102
104
|
const readMessages = async (keys) => {
|
|
103
105
|
const privacySettings = await fetchPrivacySettings()
|
|
104
|
-
|
|
106
|
+
const hasStatusKey = keys.some(k => isJidStatusBroadcast(k.remoteJid))
|
|
107
|
+
const type = hasStatusKey ? 'read' : (privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
|
|
108
|
+
await sendReceipts(keys, type)
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
// ─────────────────────────────────────────────
|
|
108
|
-
// DEVICE & SESSION MANAGEMENT
|
|
109
|
-
// Fetches participant devices via USync and manages
|
|
110
|
-
// Signal protocol sessions for encryption.
|
|
111
|
-
// ─────────────────────────────────────────────
|
|
112
111
|
const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
|
|
113
112
|
const deviceResults = []
|
|
114
113
|
if (!useCache) logger.debug('not using cache for devices')
|
|
@@ -117,7 +116,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
117
116
|
const decoded = jidDecode(jid)
|
|
118
117
|
const user = decoded?.user
|
|
119
118
|
const device = decoded?.device
|
|
120
|
-
// Already has a device number — push directly
|
|
121
119
|
if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
|
|
122
120
|
return { jid: jidNormalizedUser(jid), user }
|
|
123
121
|
}).filter(Boolean)
|
|
@@ -144,7 +142,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
144
142
|
|
|
145
143
|
if (!toFetch.length) return deviceResults
|
|
146
144
|
|
|
147
|
-
// Track which JIDs are LID-based so we can encode them correctly
|
|
148
145
|
const requestedLidUsers = new Set()
|
|
149
146
|
for (const jid of toFetch) {
|
|
150
147
|
if (isLidUser(jid) || isHostedLidUser(jid)) {
|
|
@@ -153,23 +150,21 @@ export const makeMessagesSocket = (config) => {
|
|
|
153
150
|
}
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
const
|
|
157
|
-
for (const jid of toFetch)
|
|
153
|
+
const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
|
|
154
|
+
for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
|
|
158
155
|
|
|
159
|
-
const result = await sock.executeUSyncQuery(
|
|
156
|
+
const result = await sock.executeUSyncQuery(usyncQuery)
|
|
160
157
|
if (result) {
|
|
161
158
|
const lidResults = result.list.filter(a => !!a.lid)
|
|
162
159
|
if (lidResults.length > 0) {
|
|
163
160
|
logger.trace('Storing LID maps from device call')
|
|
164
161
|
await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} catch (e) {
|
|
172
|
-
logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
|
|
162
|
+
try {
|
|
163
|
+
const lids = lidResults.map(a => a.lid)
|
|
164
|
+
if (lids.length) await assertSessions(lids, false)
|
|
165
|
+
} catch (e) {
|
|
166
|
+
logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
|
|
167
|
+
}
|
|
173
168
|
}
|
|
174
169
|
|
|
175
170
|
const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
|
|
@@ -187,27 +182,25 @@ export const makeMessagesSocket = (config) => {
|
|
|
187
182
|
}
|
|
188
183
|
}
|
|
189
184
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
await devicesMutex.mutex(async () => {
|
|
186
|
+
if (userDevicesCache.mset) {
|
|
187
|
+
await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
|
|
188
|
+
} else {
|
|
189
|
+
for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
|
190
|
+
}
|
|
191
|
+
})
|
|
196
192
|
|
|
197
|
-
// Persist device lists for session migration
|
|
193
|
+
// Persist device lists for session migration (capped at 500 users)
|
|
198
194
|
const userDeviceUpdates = {}
|
|
199
195
|
for (const [userId, devices] of Object.entries(deviceMap)) {
|
|
200
196
|
if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
|
201
197
|
}
|
|
202
198
|
if (Object.keys(userDeviceUpdates).length > 0) {
|
|
203
199
|
try {
|
|
204
|
-
const
|
|
205
|
-
const currentBatch = existingData?.['_index'] || {}
|
|
200
|
+
const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
|
|
206
201
|
const mergedBatch = { ...currentBatch, ...userDeviceUpdates }
|
|
207
|
-
|
|
208
|
-
Object.keys(
|
|
209
|
-
await authState.keys.set({ 'device-list': { '_index': trimmedBatch } })
|
|
210
|
-
logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(trimmedBatch).length }, 'stored user device lists')
|
|
202
|
+
await authState.keys.set({ 'device-list': { 'index': mergedBatch } })
|
|
203
|
+
logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(mergedBatch).length }, 'stored user device lists')
|
|
211
204
|
} catch (error) {
|
|
212
205
|
logger.warn({ error }, 'failed to store user device lists')
|
|
213
206
|
}
|
|
@@ -217,47 +210,30 @@ export const makeMessagesSocket = (config) => {
|
|
|
217
210
|
}
|
|
218
211
|
|
|
219
212
|
const assertSessions = async (jids, force) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const sessionValidation = await signalRepository.validateSession(jid)
|
|
230
|
-
peerSessionsCache.set(signalId, sessionValidation.exists)
|
|
231
|
-
if (sessionValidation.exists && !force) continue
|
|
213
|
+
let didFetchNewSession = false
|
|
214
|
+
let jidsRequiringFetch = []
|
|
215
|
+
if (force) {
|
|
216
|
+
jidsRequiringFetch = jids
|
|
217
|
+
} else {
|
|
218
|
+
const sessionBatch = await migrateIndexKey(authState.keys, 'session')
|
|
219
|
+
for (const jid of jids) {
|
|
220
|
+
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
|
221
|
+
if (!sessionBatch[signalId]) jidsRequiringFetch.push(jid)
|
|
232
222
|
}
|
|
233
|
-
jidsRequiringFetch.push(jid)
|
|
234
223
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const result = await query({
|
|
248
|
-
tag: 'iq',
|
|
249
|
-
attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET },
|
|
250
|
-
content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => { const attrs = { jid }; if (force) attrs.reason = 'identity'; return { tag: 'user', attrs } }) }]
|
|
251
|
-
})
|
|
252
|
-
await parseAndInjectE2ESessions(result, signalRepository)
|
|
253
|
-
for (const wireJid of wireJids) peerSessionsCache.set(signalRepository.jidToSignalProtocolAddress(wireJid), true)
|
|
254
|
-
return true
|
|
224
|
+
if (jidsRequiringFetch.length) {
|
|
225
|
+
const wireJids = [
|
|
226
|
+
...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)),
|
|
227
|
+
...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid)))) || []).map(a => a.lid)
|
|
228
|
+
]
|
|
229
|
+
const fetchTargets = wireJids.length ? wireJids : jidsRequiringFetch
|
|
230
|
+
logger.debug({ jidsRequiringFetch, fetchTargets }, 'fetching sessions')
|
|
231
|
+
const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: fetchTargets.map(jid => ({ tag: 'user', attrs: { jid, ...(force ? { reason: 'identity' } : {}) } })) }] })
|
|
232
|
+
await parseAndInjectE2ESessions(result, signalRepository)
|
|
233
|
+
didFetchNewSession = true
|
|
234
|
+
}
|
|
235
|
+
return didFetchNewSession
|
|
255
236
|
}
|
|
256
|
-
|
|
257
|
-
// ─────────────────────────────────────────────
|
|
258
|
-
// PEER DATA OPERATIONS
|
|
259
|
-
// Used for history sync requests and placeholder resends.
|
|
260
|
-
// ─────────────────────────────────────────────
|
|
261
237
|
const sendPeerDataOperationMessage = async (pdoMessage) => {
|
|
262
238
|
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
263
239
|
return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
|
|
@@ -265,18 +241,24 @@ export const makeMessagesSocket = (config) => {
|
|
|
265
241
|
}, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
|
|
266
242
|
}
|
|
267
243
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return unixTimestampSeconds() - Number(tokenData.timestamp) > TOKEN_EXPIRY_TTL
|
|
244
|
+
// Issues our TC token to a contact so they can send us private messages. Fire-and-forget.
|
|
245
|
+
const issuePrivacyTokens = async (jids, timestamp) => {
|
|
246
|
+
const t = (timestamp ?? unixTimestampSeconds()).toString()
|
|
247
|
+
return query({
|
|
248
|
+
tag: 'iq',
|
|
249
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
250
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
251
|
+
})
|
|
277
252
|
}
|
|
278
253
|
|
|
279
|
-
|
|
254
|
+
// Fetches TC tokens from the server for the given JIDs and stores them locally.
|
|
255
|
+
const getPrivacyTokens = async (jids) => {
|
|
256
|
+
const t = unixTimestampSeconds().toString()
|
|
257
|
+
const result = await query({
|
|
258
|
+
tag: 'iq',
|
|
259
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
260
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
261
|
+
})
|
|
280
262
|
const tokens = {}
|
|
281
263
|
const tokenList = getBinaryNodeChild(result, 'tokens')
|
|
282
264
|
if (tokenList) {
|
|
@@ -285,25 +267,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
285
267
|
if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
|
|
286
268
|
}
|
|
287
269
|
}
|
|
288
|
-
return tokens
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const getPrivacyTokens = async (jids) => {
|
|
292
|
-
const t = unixTimestampSeconds().toString()
|
|
293
|
-
const result = await query({
|
|
294
|
-
tag: 'iq',
|
|
295
|
-
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
296
|
-
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
297
|
-
})
|
|
298
|
-
const tokens = parseTCTokens(result)
|
|
299
270
|
if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
|
|
300
271
|
return tokens
|
|
301
272
|
}
|
|
302
273
|
|
|
303
|
-
// ─────────────────────────────────────────────
|
|
304
|
-
// GROUP MEMBER LABEL
|
|
305
|
-
// Sets a custom label for a member in a group.
|
|
306
|
-
// ─────────────────────────────────────────────
|
|
307
274
|
const updateMemberLabel = (jid, memberLabel) => {
|
|
308
275
|
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
309
276
|
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
@@ -315,20 +282,19 @@ export const makeMessagesSocket = (config) => {
|
|
|
315
282
|
}, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
|
|
316
283
|
}
|
|
317
284
|
|
|
318
|
-
// ─────────────────────────────────────────────
|
|
319
|
-
// MESSAGE TYPE DETECTION
|
|
320
|
-
// Classifies messages for proper stanza type attribute.
|
|
321
|
-
// ─────────────────────────────────────────────
|
|
322
285
|
const getMessageType = (msg) => {
|
|
323
286
|
const message = normalizeMessageContent(msg)
|
|
287
|
+
if (!message) return 'text'
|
|
324
288
|
if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
|
|
325
|
-
if (message.reactionMessage) return 'reaction'
|
|
289
|
+
if (message.reactionMessage || message.encReactionMessage) return 'reaction'
|
|
326
290
|
if (message.eventMessage) return 'event'
|
|
327
291
|
if (getMediaType(message)) return 'media'
|
|
328
292
|
return 'text'
|
|
329
293
|
}
|
|
330
294
|
|
|
331
295
|
const getMediaType = (message) => {
|
|
296
|
+
const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
|
|
297
|
+
if (inner) return getMediaType(inner)
|
|
332
298
|
if (message.imageMessage) return 'image'
|
|
333
299
|
if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
|
|
334
300
|
if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
|
|
@@ -352,11 +318,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
352
318
|
if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
|
|
353
319
|
}
|
|
354
320
|
|
|
355
|
-
// ─────────────────────────────────────────────
|
|
356
|
-
// PARTICIPANT NODE CREATION
|
|
357
|
-
// Encrypts a message for each recipient JID and
|
|
358
|
-
// wraps it in a binary <to> node for the stanza.
|
|
359
|
-
// ─────────────────────────────────────────────
|
|
360
321
|
const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
|
|
361
322
|
if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
|
|
362
323
|
|
|
@@ -373,7 +334,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
373
334
|
if (!jid) return null
|
|
374
335
|
let msgToEncrypt = patchedMessage
|
|
375
336
|
|
|
376
|
-
// Use
|
|
337
|
+
// Use DSM for own linked devices so they can read the message
|
|
377
338
|
if (dsmMessage) {
|
|
378
339
|
const { user: targetUser } = jidDecode(jid)
|
|
379
340
|
const { user: ownPnUser } = jidDecode(meId)
|
|
@@ -389,7 +350,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
389
350
|
return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
|
|
390
351
|
})
|
|
391
352
|
} catch (err) {
|
|
392
|
-
logger.
|
|
353
|
+
logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session, will retry on next interaction')
|
|
393
354
|
return null
|
|
394
355
|
}
|
|
395
356
|
})
|
|
@@ -398,17 +359,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
398
359
|
return { nodes, shouldIncludeDeviceIdentity }
|
|
399
360
|
}
|
|
400
361
|
|
|
401
|
-
// ─────────────────────────────────────────────
|
|
402
|
-
// RELAY MESSAGE
|
|
403
|
-
// Core message sending function. Handles groups, DMs,
|
|
404
|
-
// newsletters, status broadcasts, and retries.
|
|
405
|
-
//
|
|
406
|
-
// Key design decisions:
|
|
407
|
-
// - senderKeyMap always starts empty (Promise.resolve({})) so
|
|
408
|
-
// SKDM is always sent fresh — prevents "waiting for message"
|
|
409
|
-
// errors caused by stale persisted sender key memory.
|
|
410
|
-
// - LID vs PN identity is resolved per group's addressingMode.
|
|
411
|
-
// ─────────────────────────────────────────────
|
|
412
362
|
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
|
|
413
363
|
const meId = authState.creds.me.id
|
|
414
364
|
const meLid = authState.creds.me?.lid
|
|
@@ -418,7 +368,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
418
368
|
const isLid = server === 'lid'
|
|
419
369
|
const isNewsletter = server === 'newsletter'
|
|
420
370
|
|
|
421
|
-
// Choose sender identity (PN or LID) based on destination
|
|
422
371
|
let activeSender = meId
|
|
423
372
|
let groupAddressingMode = 'pn'
|
|
424
373
|
if (isGroup && !isStatus) {
|
|
@@ -460,6 +409,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
460
409
|
const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
|
|
461
410
|
const extraAttrs = {}
|
|
462
411
|
const messages = normalizeMessageContent(message)
|
|
412
|
+
const reportingMessage = messages
|
|
463
413
|
const buttonType = getButtonType(messages)
|
|
464
414
|
|
|
465
415
|
let hasDeviceFanoutFalse = false
|
|
@@ -473,7 +423,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
473
423
|
const mediaType = getMediaType(message)
|
|
474
424
|
if (mediaType) extraAttrs.mediatype = mediaType
|
|
475
425
|
|
|
476
|
-
// ── Newsletter: plaintext encoding, no encryption ──
|
|
477
426
|
if (isNewsletter) {
|
|
478
427
|
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
|
479
428
|
binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
|
|
@@ -482,11 +431,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
482
431
|
return
|
|
483
432
|
}
|
|
484
433
|
|
|
485
|
-
if (messages
|
|
434
|
+
if (messages?.pinInChatMessage || messages?.keepInChatMessage || (message.reactionMessage && !isStatus) || message.protocolMessage?.editedMessage) {
|
|
486
435
|
extraAttrs['decrypt-fail'] = 'hide'
|
|
487
436
|
}
|
|
488
437
|
|
|
489
|
-
// ── Group / Status: sender key (SKDM + skmsg) ──
|
|
490
438
|
if ((isGroup || isStatus) && !isRetryResend) {
|
|
491
439
|
const [groupData] = await Promise.all([
|
|
492
440
|
(async () => {
|
|
@@ -495,10 +443,9 @@ export const makeMessagesSocket = (config) => {
|
|
|
495
443
|
else if (!isStatus) groupData = await groupMetadata(jid)
|
|
496
444
|
return groupData
|
|
497
445
|
})(),
|
|
498
|
-
Promise.resolve({}) // senderKeyMap
|
|
446
|
+
Promise.resolve({}) // senderKeyMap always empty — forces fresh SKDM every send
|
|
499
447
|
])
|
|
500
448
|
|
|
501
|
-
// Build participant list
|
|
502
449
|
const participantsList = []
|
|
503
450
|
if (isStatus) {
|
|
504
451
|
if (statusJidList?.length) participantsList.push(...statusJidList)
|
|
@@ -515,9 +462,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
515
462
|
const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
|
|
516
463
|
devices.push(...additionalDevices)
|
|
517
464
|
|
|
518
|
-
//
|
|
519
|
-
// USync sometimes omits Device 0 for LID groups to save bandwidth,
|
|
520
|
-
// which would cause recipients to miss the SKDM on first send.
|
|
465
|
+
// Force Device 0 inclusion — USync sometimes omits it for LID groups
|
|
521
466
|
for (const pJid of participantsList) {
|
|
522
467
|
const decoded = jidDecode(pJid)
|
|
523
468
|
if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
|
|
@@ -529,11 +474,11 @@ export const makeMessagesSocket = (config) => {
|
|
|
529
474
|
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
530
475
|
|
|
531
476
|
const bytes = encodeWAMessage(patched)
|
|
477
|
+
const bytesU8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
532
478
|
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
533
479
|
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
534
|
-
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data:
|
|
480
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytesU8, meId: groupSenderIdentity })
|
|
535
481
|
|
|
536
|
-
// Send SKDM to ALL devices every time (senderKeyMap is always {})
|
|
537
482
|
const senderKeyRecipients = devices
|
|
538
483
|
.filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
|
|
539
484
|
.map(d => d.jid)
|
|
@@ -549,7 +494,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
549
494
|
|
|
550
495
|
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
|
|
551
496
|
|
|
552
|
-
// ── Group Retry: direct pairwise re-encrypt for specific participant ──
|
|
553
497
|
} else if ((isGroup || isStatus) && isRetryResend) {
|
|
554
498
|
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
555
499
|
if (!groupData && !isStatus) await groupMetadata(jid)
|
|
@@ -561,21 +505,24 @@ export const makeMessagesSocket = (config) => {
|
|
|
561
505
|
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
562
506
|
|
|
563
507
|
const bytes = encodeWAMessage(patched)
|
|
508
|
+
const bytesU8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
564
509
|
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
565
510
|
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
566
|
-
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data:
|
|
511
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytesU8, meId: groupSenderIdentity })
|
|
567
512
|
|
|
568
|
-
// Send fresh SKDM directly to the requesting participant
|
|
569
513
|
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
570
514
|
await assertSessions([participant.jid])
|
|
571
515
|
const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
|
|
572
516
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
|
|
573
517
|
participants.push(...skResult.nodes)
|
|
574
518
|
|
|
575
|
-
|
|
519
|
+
// For retry resend, encrypt directly to the requesting participant
|
|
520
|
+
const isParticipantLid = isLidUser(participant.jid)
|
|
521
|
+
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
|
|
522
|
+
const encodedMsg = isMe ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } }) : encodeWAMessage(message)
|
|
523
|
+
const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: encodedMsg, jid: participant.jid })
|
|
576
524
|
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
|
|
577
525
|
|
|
578
|
-
// ── DM / LID: standard pairwise encryption ──
|
|
579
526
|
} else {
|
|
580
527
|
let ownId = meId
|
|
581
528
|
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
@@ -593,7 +540,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
593
540
|
}
|
|
594
541
|
|
|
595
542
|
if (additionalAttributes?.category !== 'peer') {
|
|
596
|
-
// Preserve Device 0 entries before USync refetch which may omit them
|
|
597
543
|
const device0Entries = devices.filter(d => d.device === 0)
|
|
598
544
|
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
599
545
|
devices.length = 0
|
|
@@ -603,7 +549,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
603
549
|
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
604
550
|
devices.push(...device0Entries, ...sessionDevices)
|
|
605
551
|
|
|
606
|
-
// Explicitly fetch sender's linked devices if not returned by USync
|
|
607
552
|
if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
|
|
608
553
|
const senderDevices = await getUSyncDevices([senderIdentity], true, false)
|
|
609
554
|
const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
|
|
@@ -639,7 +584,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
639
584
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
|
640
585
|
}
|
|
641
586
|
|
|
642
|
-
// ── Build final stanza ──
|
|
643
587
|
if (participants.length) {
|
|
644
588
|
if (additionalAttributes?.category === 'peer') {
|
|
645
589
|
const peerNode = participants[0]?.content?.[0]
|
|
@@ -670,41 +614,90 @@ export const makeMessagesSocket = (config) => {
|
|
|
670
614
|
stanza.attrs.to = destinationJid
|
|
671
615
|
}
|
|
672
616
|
|
|
673
|
-
|
|
674
|
-
|
|
617
|
+
let didPushAdditional = false
|
|
618
|
+
|
|
619
|
+
if (!isNewsletter && buttonType && !isStatus) {
|
|
675
620
|
const buttonsNode = getButtonArgs(messages)
|
|
676
621
|
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
677
|
-
if (filteredButtons) {
|
|
678
|
-
|
|
679
|
-
|
|
622
|
+
if (filteredButtons) {
|
|
623
|
+
stanza.content.push(...additionalNodes)
|
|
624
|
+
didPushAdditional = true
|
|
625
|
+
} else {
|
|
626
|
+
stanza.content.push(...buttonsNode)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (!didPushAdditional && additionalNodes?.length > 0) {
|
|
680
631
|
stanza.content.push(...additionalNodes)
|
|
681
632
|
}
|
|
682
633
|
|
|
683
|
-
// Attach device identity for LID groups and pkmsg sessions
|
|
684
634
|
if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
|
|
685
635
|
stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
|
|
686
636
|
logger.debug({ jid }, 'adding device identity')
|
|
687
637
|
}
|
|
688
638
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
|
|
639
|
+
const isPeerMessage = additionalAttributes?.category === 'peer'
|
|
640
|
+
const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
|
|
641
|
+
if (is1on1) {
|
|
642
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
643
|
+
const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
|
|
644
|
+
const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
645
|
+
const existingEntry = contactTcTokenData[tcTokenJid]
|
|
646
|
+
let tcTokenBuffer = existingEntry?.token
|
|
647
|
+
if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
|
|
648
|
+
logger.debug({ jid: destinationJid, timestamp: existingEntry?.timestamp }, 'tctoken expired, clearing')
|
|
649
|
+
tcTokenBuffer = undefined
|
|
650
|
+
const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
|
|
651
|
+
try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
|
|
652
|
+
}
|
|
653
|
+
if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
654
|
+
const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
|
|
655
|
+
const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
|
|
656
|
+
if (!isProtocolMsg && !isBotOrPSA && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
|
|
657
|
+
inFlightTcTokenIssuance.add(tcTokenJid)
|
|
658
|
+
const issueTimestamp = unixTimestampSeconds()
|
|
659
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
660
|
+
const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
|
|
661
|
+
resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
|
|
662
|
+
.then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
|
|
663
|
+
.then(async (result) => {
|
|
664
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
|
|
665
|
+
const currentData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
666
|
+
const currentEntry = currentData[tcTokenJid]
|
|
667
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
|
|
668
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: { token: Buffer.alloc(0), ...currentEntry, senderTimestamp: issueTimestamp }, ...indexWrite } })
|
|
669
|
+
})
|
|
670
|
+
.catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
|
|
671
|
+
.finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (
|
|
675
|
+
!isNewsletter &&
|
|
676
|
+
!isRetryResend &&
|
|
677
|
+
reportingMessage?.messageContextInfo?.messageSecret &&
|
|
678
|
+
shouldIncludeReportingToken(reportingMessage)
|
|
679
|
+
) {
|
|
680
|
+
try {
|
|
681
|
+
const encoded = encodeWAMessage(reportingMessage)
|
|
682
|
+
const reportingKey = {
|
|
683
|
+
id: finalMsgId,
|
|
684
|
+
fromMe: true,
|
|
685
|
+
remoteJid: destinationJid,
|
|
686
|
+
participant: participant?.jid
|
|
700
687
|
}
|
|
688
|
+
const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
|
|
689
|
+
if (reportingNode) {
|
|
690
|
+
stanza.content.push(reportingNode)
|
|
691
|
+
logger.trace({ jid }, 'added reporting token to message')
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
|
|
701
695
|
}
|
|
702
|
-
if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
703
696
|
}
|
|
704
697
|
|
|
705
|
-
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
|
698
|
+
logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
|
|
706
699
|
await sendNode(stanza)
|
|
707
|
-
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid,
|
|
700
|
+
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
|
|
708
701
|
|
|
709
702
|
}, activeSender)
|
|
710
703
|
|
|
@@ -722,21 +715,98 @@ export const makeMessagesSocket = (config) => {
|
|
|
722
715
|
}
|
|
723
716
|
}
|
|
724
717
|
|
|
725
|
-
const
|
|
726
|
-
const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me })
|
|
718
|
+
const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }) })
|
|
727
719
|
const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
|
|
728
720
|
|
|
721
|
+
const sendMessage = async (jid, content, options = {}) => {
|
|
722
|
+
const meId = authState.creds.me.id
|
|
723
|
+
const meLid = authState.creds.me?.lid
|
|
724
|
+
const { server } = jidDecode(jid)
|
|
725
|
+
const isGroup = server === 'g.us'
|
|
726
|
+
const isDestinationLid = server === 'lid'
|
|
727
|
+
const useCache = options.useCachedGroupMetadata !== false
|
|
728
|
+
const { quoted } = options
|
|
729
|
+
|
|
730
|
+
let activeSender = meId
|
|
731
|
+
let addressingMode = 'pn'
|
|
732
|
+
if (isGroup) {
|
|
733
|
+
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
734
|
+
addressingMode = groupData?.addressingMode || 'lid'
|
|
735
|
+
if (addressingMode === 'lid' && meLid) activeSender = meLid
|
|
736
|
+
} else if (isDestinationLid && meLid) {
|
|
737
|
+
activeSender = meLid
|
|
738
|
+
addressingMode = 'lid'
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Unwrap shorthand `interactive` key
|
|
742
|
+
if (content.interactive && !content.interactiveMessage) {
|
|
743
|
+
const { interactive, ...rest } = content
|
|
744
|
+
content = { ...rest, interactiveMessage: interactive }
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// never route status reactions through nexus
|
|
748
|
+
const isStatusJid = jid === 'status@broadcast'
|
|
749
|
+
const messageType = !isStatusJid && nexus.detectType(content)
|
|
750
|
+
if (messageType) return await nexus.processMessage(content, jid, quoted)
|
|
751
|
+
|
|
752
|
+
if (content.disappearingMessagesInChat && isJidGroup(jid)) {
|
|
753
|
+
const value = typeof content.disappearingMessagesInChat === 'boolean'
|
|
754
|
+
? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
|
|
755
|
+
: content.disappearingMessagesInChat
|
|
756
|
+
await groupToggleEphemeral(jid, value)
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
const fullMsg = await generateWAMessage(jid, content, {
|
|
762
|
+
logger,
|
|
763
|
+
userJid: jidNormalizedUser(activeSender),
|
|
764
|
+
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
765
|
+
getProfilePicUrl: sock.profilePictureUrl,
|
|
766
|
+
getCallLink: sock.createCallLink,
|
|
767
|
+
upload: waUploadToServer,
|
|
768
|
+
mediaCache: config.mediaCache,
|
|
769
|
+
options: config.options,
|
|
770
|
+
messageId: generateMessageIDV2(activeSender),
|
|
771
|
+
...options
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
const additionalAttributes = {}, additionalNodes = []
|
|
775
|
+
if (content.delete) {
|
|
776
|
+
const fromMe = content.delete?.fromMe
|
|
777
|
+
const isGroupDelete = isJidGroup(content.delete?.remoteJid)
|
|
778
|
+
additionalAttributes.edit = (isGroupDelete && !fromMe) ? '8' : '7'
|
|
779
|
+
} else if (content.edit) {
|
|
780
|
+
additionalAttributes.edit = '1'
|
|
781
|
+
} else if (content.pin) {
|
|
782
|
+
additionalAttributes.edit = '2'
|
|
783
|
+
}
|
|
784
|
+
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
785
|
+
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
786
|
+
if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
|
|
787
|
+
|
|
788
|
+
await relayMessage(jid, fullMsg.message, {
|
|
789
|
+
messageId: fullMsg.key.id,
|
|
790
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
791
|
+
additionalAttributes,
|
|
792
|
+
statusJidList: options.statusJidList,
|
|
793
|
+
additionalNodes
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
if (config.emitOwnEvents) {
|
|
797
|
+
process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
|
|
798
|
+
}
|
|
799
|
+
return fullMsg
|
|
800
|
+
}
|
|
801
|
+
|
|
729
802
|
return {
|
|
730
803
|
...sock,
|
|
731
|
-
getPrivacyTokens, assertSessions, relayMessage,
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
messageRetryManager, updateMemberLabel,
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
// UPDATE MEDIA MESSAGE
|
|
738
|
-
// Re-requests media upload for expired/missing media.
|
|
739
|
-
// ─────────────────────────────────────────────
|
|
804
|
+
getPrivacyTokens, issuePrivacyTokens, assertSessions, relayMessage,
|
|
805
|
+
sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
|
|
806
|
+
waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
|
|
807
|
+
createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
|
|
808
|
+
userDevicesCache, devicesMutex,
|
|
809
|
+
|
|
740
810
|
updateMediaMessage: async (message) => {
|
|
741
811
|
const content = assertMediaContent(message.message)
|
|
742
812
|
const mediaKey = content.mediaKey
|
|
@@ -767,11 +837,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
767
837
|
return message
|
|
768
838
|
},
|
|
769
839
|
|
|
770
|
-
// ─────────────────────────────────────────────
|
|
771
|
-
// SEND STATUS MENTIONS
|
|
772
|
-
// Broadcasts a status update and notifies mentioned
|
|
773
|
-
// users or groups via statusMentionMessage protocol.
|
|
774
|
-
// ─────────────────────────────────────────────
|
|
775
840
|
sendStatusMentions: async (content, jids = []) => {
|
|
776
841
|
const userJid = jidNormalizedUser(authState.creds.me.id)
|
|
777
842
|
const allUsers = new Set([userJid])
|
|
@@ -825,7 +890,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
825
890
|
return msg
|
|
826
891
|
},
|
|
827
892
|
|
|
828
|
-
// Nexus handler shortcuts
|
|
893
|
+
// Nexus handler shortcuts
|
|
829
894
|
sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
|
|
830
895
|
sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
|
|
831
896
|
sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
|
|
@@ -838,133 +903,20 @@ export const makeMessagesSocket = (config) => {
|
|
|
838
903
|
sendCarouselMessage: (jid, data, quoted) => nexus.handleCarousel({ carouselMessage: data }, jid, quoted),
|
|
839
904
|
sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
|
|
840
905
|
stickerPackMessage: (jid, data, options) => nexus.handleStickerPack(data, jid, options?.quoted),
|
|
841
|
-
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
// Resolve sender identity for this destination
|
|
858
|
-
let activeSender = meId
|
|
859
|
-
let addressingMode = 'pn'
|
|
860
|
-
if (isGroup) {
|
|
861
|
-
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
862
|
-
addressingMode = groupData?.addressingMode || 'lid'
|
|
863
|
-
if (addressingMode === 'lid' && meLid) activeSender = meLid
|
|
864
|
-
} else if (isDestinationLid && meLid) {
|
|
865
|
-
activeSender = meLid
|
|
866
|
-
addressingMode = 'lid'
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Unwrap shorthand `interactive` key
|
|
870
|
-
if (content.interactive && !content.interactiveMessage) {
|
|
871
|
-
const { interactive, ...rest } = content
|
|
872
|
-
content = { ...rest, interactiveMessage: interactive }
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Delegate rich message types to NexusHandler
|
|
876
|
-
const messageType = nexus.detectType(content)
|
|
877
|
-
if (messageType) return await nexus.processMessage(content, jid, quoted)
|
|
878
|
-
|
|
879
|
-
// Handle disappearing message toggle for groups
|
|
880
|
-
if (content.disappearingMessagesInChat && isJidGroup(jid)) {
|
|
881
|
-
const value = typeof content.disappearingMessagesInChat === 'boolean'
|
|
882
|
-
? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
|
|
883
|
-
: content.disappearingMessagesInChat
|
|
884
|
-
await groupToggleEphemeral(jid, value)
|
|
885
|
-
return
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// ── Normalize delete key ──
|
|
889
|
-
// Groups require a participant field matching the sender's LID or PN identity.
|
|
890
|
-
// edit="7" = delete own message, edit="8" = admin delete another's message.
|
|
891
|
-
if (content.delete) {
|
|
892
|
-
const deleteKey = content.delete
|
|
893
|
-
if (!deleteKey.remoteJid || !deleteKey.id) {
|
|
894
|
-
logger.error({ deleteKey }, 'Invalid delete key: missing remoteJid or id')
|
|
895
|
-
throw new Boom('Delete key must have remoteJid and id', { statusCode: 400 })
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const { server: deleteServer } = jidDecode(deleteKey.remoteJid)
|
|
899
|
-
let deleteAddressingMode = deleteServer === 'lid' ? 'lid' : 'pn'
|
|
900
|
-
if (isJidGroup(deleteKey.remoteJid)) {
|
|
901
|
-
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(deleteKey.remoteJid) : undefined
|
|
902
|
-
deleteAddressingMode = groupData?.addressingMode || 'lid'
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
let normalizedParticipant = deleteKey.participant
|
|
906
|
-
if (deleteKey.fromMe || isJidGroup(deleteKey.remoteJid)) {
|
|
907
|
-
const senderJid = (deleteAddressingMode === 'lid' && meLid) ? meLid : meId
|
|
908
|
-
normalizedParticipant = jidNormalizedUser(senderJid)
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
content.delete = {
|
|
912
|
-
remoteJid: deleteKey.remoteJid,
|
|
913
|
-
fromMe: deleteKey.fromMe === true || deleteKey.fromMe === 'true',
|
|
914
|
-
id: deleteKey.id,
|
|
915
|
-
...(normalizedParticipant ? { participant: jidNormalizedUser(normalizedParticipant) } : {}),
|
|
916
|
-
addressingMode: deleteAddressingMode
|
|
917
|
-
}
|
|
918
|
-
logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Generate the full WAMessage proto
|
|
922
|
-
const fullMsg = await generateWAMessage(jid, content, {
|
|
923
|
-
logger,
|
|
924
|
-
userJid: jidNormalizedUser(activeSender),
|
|
925
|
-
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
926
|
-
getProfilePicUrl: sock.profilePictureUrl,
|
|
927
|
-
getCallLink: sock.createCallLink,
|
|
928
|
-
upload: waUploadToServer,
|
|
929
|
-
mediaCache: config.mediaCache,
|
|
930
|
-
options: config.options,
|
|
931
|
-
messageId: generateMessageIDV2(activeSender),
|
|
932
|
-
...options
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
// Build additional stanza attributes
|
|
936
|
-
const additionalAttributes = {}, additionalNodes = []
|
|
937
|
-
if (content.delete) {
|
|
938
|
-
additionalAttributes.edit = isJidGroup(content.delete.remoteJid) && !content.delete.fromMe ? '8' : '7'
|
|
939
|
-
} else if (content.edit) {
|
|
940
|
-
additionalAttributes.edit = '1'
|
|
941
|
-
} else if (content.pin) {
|
|
942
|
-
additionalAttributes.edit = '2'
|
|
943
|
-
}
|
|
944
|
-
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
945
|
-
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
946
|
-
|
|
947
|
-
// Fetch TC token for new DM contacts
|
|
948
|
-
if (!isJidGroup(jid) && !content.disappearingMessagesInChat) {
|
|
949
|
-
const existingToken = await authState.keys.get('tctoken', [jid])
|
|
950
|
-
if (!existingToken[jid]) {
|
|
951
|
-
try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
|
|
952
|
-
catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
await relayMessage(jid, fullMsg.message, {
|
|
957
|
-
messageId: fullMsg.key.id,
|
|
958
|
-
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
959
|
-
additionalAttributes,
|
|
960
|
-
statusJidList: options.statusJidList,
|
|
961
|
-
additionalNodes
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
if (config.emitOwnEvents) {
|
|
965
|
-
process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
|
|
966
|
-
}
|
|
967
|
-
return fullMsg
|
|
968
|
-
}
|
|
906
|
+
sendMessage,
|
|
907
|
+
// Shorthand wrappers
|
|
908
|
+
sendText: (jid, text, options = {}) => sendMessage(jid, { text, ...options }, options),
|
|
909
|
+
sendImage: (jid, image, caption = '', options = {}) => sendMessage(jid, { image, caption, ...options }, options),
|
|
910
|
+
sendVideo: (jid, video, caption = '', options = {}) => sendMessage(jid, { video, caption, ...options }, options),
|
|
911
|
+
sendDocument: (jid, document, caption = '', options = {}) => sendMessage(jid, { document, caption, ...options }, options),
|
|
912
|
+
sendAudio: (jid, audio, options = {}) => sendMessage(jid, { audio, ...options }, options),
|
|
913
|
+
sendLocation: (jid, { degreesLatitude, degreesLongitude, name, url, address } = {}, options = {}) =>
|
|
914
|
+
sendMessage(jid, { location: { degreesLatitude, degreesLongitude, name, url, address }, ...options }, options),
|
|
915
|
+
sendPoll: (jid, name, pollVote = [], multiSelect = false, options = {}) =>
|
|
916
|
+
sendMessage(jid, { poll: { name, values: pollVote, selectableOptionsCount: multiSelect ? pollVote.length : 0 }, ...options }, options),
|
|
917
|
+
sendReaction: (jid, key, reaction, options = {}) => sendMessage(jid, { react: { text: reaction, key }, ...options }, options),
|
|
918
|
+
sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
|
|
919
|
+
sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
|
|
920
|
+
sendForward: (jid, message, options = {}) => sendMessage(jid, { forward: message, force: options.force }, options),
|
|
969
921
|
}
|
|
970
922
|
}
|