@zervonex/baileys 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3204 -3203
- package/WAProto/fetcher.js +1 -1
- package/WAProto/index.js +87 -87
- package/lib/Builders/AIRich.js +642 -589
- package/lib/Builders/builders.js +5 -4
- package/lib/Defaults/index.js +2 -1
- package/lib/Socket/index.js +14 -14
- package/lib/Socket/messages-recv.js +980 -903
- package/lib/Socket/messages-send.js +707 -692
- package/lib/Socket/newsletter.js +781 -751
- package/lib/Socket/socket.js +0 -1
- package/lib/Socket/{nexus-handler.js → zervonex-handler.js} +54 -54
- package/lib/Utils/chat-utils.js +1 -0
- package/lib/Utils/index.js +38 -39
- package/lib/Utils/messages.js +30 -20
- package/lib/Utils/spam-report-utils.js +134 -141
- package/lib/Utils/tc-token-utils.js +125 -73
- package/lib/index.js +51 -52
- package/package.json +11 -7
- package/lib/Utils/key-store.js +0 -3
|
@@ -1,693 +1,708 @@
|
|
|
1
|
-
import NodeCache from '@cacheable/node-cache'
|
|
2
|
-
import { Boom } from '@hapi/boom'
|
|
3
|
-
import { randomBytes } from 'crypto'
|
|
4
|
-
import { proto } from '../../WAProto/index.js'
|
|
5
|
-
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
|
|
6
|
-
import { getRandomHex } from '../Builders/index.js'
|
|
7
|
-
import {
|
|
8
|
-
aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent,
|
|
9
|
-
buildMergedTcTokenIndexWrite, decryptMediaRetryData, delay,
|
|
10
|
-
encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage,
|
|
11
|
-
encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2,
|
|
12
|
-
generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent,
|
|
13
|
-
getMessageReportingToken, getStatusCodeForMediaRetry, getUrlFromDirectPath,
|
|
14
|
-
getUrlInfo, getWAUploadToServer, isTcTokenExpired, makeKeyedMutex,
|
|
15
|
-
makeMutex, MessageRetryManager, normalizeMessageContent,
|
|
16
|
-
parseAndInjectE2ESessions, preSeedTcToken, resolveIssuanceJid,
|
|
17
|
-
resolveTcTokenJid, shouldIncludeReportingToken, shouldSendNewTcToken,
|
|
18
|
-
storeTcTokensFromIqResult, unixTimestampSeconds,
|
|
19
|
-
} from '../Utils/index.js'
|
|
20
|
-
import {
|
|
21
|
-
areJidsSameUser, getBinaryFilteredButtons, getBinaryNodeChild,
|
|
22
|
-
getBinaryNodeChildren, getButtonArgs, getButtonType, isHostedLidUser,
|
|
23
|
-
isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isJidStatusBroadcast,
|
|
24
|
-
isJidUser, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser,
|
|
25
|
-
S_WHATSAPP_NET, STORIES_JID, getBinaryFilteredBizBot,
|
|
26
|
-
} from '../WABinary/index.js'
|
|
27
|
-
import { USyncQuery, USyncUser } from '../WAUSync/index.js'
|
|
28
|
-
import { makeNewsletterSocket } from './newsletter.js'
|
|
29
|
-
|
|
30
|
-
export const makeMessagesSocket = (config) => {
|
|
31
|
-
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, getMessage, aiLabel: configAiLabel } = config
|
|
32
|
-
|
|
33
|
-
const sock = makeNewsletterSocket(config)
|
|
34
|
-
const { ev, authState, processingMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, placeholderResendCache } = sock
|
|
35
|
-
|
|
36
|
-
const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
37
|
-
const devicesMutex = makeMutex()
|
|
38
|
-
const encryptionMutex = makeKeyedMutex()
|
|
39
|
-
const mediaConnMutex = makeKeyedMutex()
|
|
40
|
-
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
|
41
|
-
const inFlightTcTokenIssuance = new Set()
|
|
42
|
-
|
|
43
|
-
let mediaConn
|
|
44
|
-
|
|
45
|
-
const refreshMediaConn = async (forceGet = false) => mediaConnMutex.mutex('media-conn', async () => {
|
|
46
|
-
const media = await mediaConn
|
|
47
|
-
if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
|
|
48
|
-
mediaConn = (async () => {
|
|
49
|
-
const result = await query({ tag: 'iq', attrs: { type: 'set', xmlns: 'w:m', to: S_WHATSAPP_NET }, content: [{ tag: 'media_conn', attrs: {} }] })
|
|
50
|
-
const node = getBinaryNodeChild(result, 'media_conn')
|
|
51
|
-
return { hosts: getBinaryNodeChildren(node, 'host').map(({ attrs }) => ({ hostname: attrs.hostname, maxContentLengthBytes: +attrs.maxContentLengthBytes })), auth: node.attrs.auth, ttl: +node.attrs.ttl, fetchDate: new Date() }
|
|
52
|
-
})()
|
|
53
|
-
logger.debug('fetched media conn')
|
|
54
|
-
}
|
|
55
|
-
return mediaConn
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
|
|
59
|
-
|
|
60
|
-
// ── Receipts ──────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
const sendReceipt = async (jid, participant, messageIds, type) => {
|
|
63
|
-
if (!messageIds?.length) throw new Boom('missing ids in receipt')
|
|
64
|
-
const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
|
|
65
|
-
const isReadType = type === 'read' || type === 'read-self'
|
|
66
|
-
if (isReadType) node.attrs.t = unixTimestampSeconds().toString()
|
|
67
|
-
if (isJidStatusBroadcast(jid) && !participant && getMessage) {
|
|
68
|
-
try { const msg = await getMessage({ remoteJid: jid, id: messageIds[0], fromMe: false }); participant = msg?.key?.participant || msg?.participant || msg?.key?.remoteJid } catch (err) { logger.debug({ err, jid }, 'failed to resolve status receipt participant') }
|
|
69
|
-
}
|
|
70
|
-
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) { node.attrs.recipient = jid; node.attrs.to = participant }
|
|
71
|
-
else if (isJidStatusBroadcast(jid) && participant) { node.attrs.to = jid; node.attrs.participant = participant }
|
|
72
|
-
else { node.attrs.to = jid; if (participant) node.attrs.participant = participant }
|
|
73
|
-
if (type) node.attrs.type = type
|
|
74
|
-
if (messageIds.length > 1) node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
|
|
75
|
-
logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt')
|
|
76
|
-
await sendNode(node)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const sendReceipts = async (keys, type) => { for (const { jid, participant, messageIds } of aggregateMessageKeysNotFromMe(keys)) await sendReceipt(jid, participant, messageIds, type) }
|
|
80
|
-
|
|
81
|
-
const readMessages = async (keys) => {
|
|
82
|
-
const privacySettings = await fetchPrivacySettings()
|
|
83
|
-
const hasStatusKey = keys.some(k => isJidStatusBroadcast(k.remoteJid))
|
|
84
|
-
await sendReceipts(keys, hasStatusKey ? 'read' : (privacySettings.readreceipts === 'all' ? 'read' : 'read-self'))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Device resolution ─────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
|
|
90
|
-
const deviceResults = []
|
|
91
|
-
const jidsWithUser = jids.map(jid => {
|
|
92
|
-
const { user, device } = jidDecode(jid) || {}
|
|
93
|
-
if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
|
|
94
|
-
return { jid: jidNormalizedUser(jid), user }
|
|
95
|
-
}).filter(Boolean)
|
|
96
|
-
|
|
97
|
-
let mgetDevices
|
|
98
|
-
if (useCache && userDevicesCache.mget) mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
|
|
99
|
-
|
|
100
|
-
const toFetch = []
|
|
101
|
-
for (const { jid, user } of jidsWithUser) {
|
|
102
|
-
if (useCache) {
|
|
103
|
-
const devices = mgetDevices?.[user] || (userDevicesCache.mget ? undefined : await userDevicesCache.get(user))
|
|
104
|
-
if (devices) deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) })))
|
|
105
|
-
else toFetch.push(jid)
|
|
106
|
-
} else {
|
|
107
|
-
toFetch.push(jid)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!toFetch.length) return deviceResults
|
|
112
|
-
|
|
113
|
-
const requestedLidUsers = new Set()
|
|
114
|
-
for (const jid of toFetch) { const user = jidDecode(jid)?.user; if ((isLidUser(jid) || isHostedLidUser(jid)) && user) requestedLidUsers.add(user) }
|
|
115
|
-
|
|
116
|
-
const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
|
|
117
|
-
for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
|
|
118
|
-
|
|
119
|
-
const result = await sock.executeUSyncQuery(usyncQuery)
|
|
120
|
-
if (result) {
|
|
121
|
-
const lidResults = result.list.filter(a => !!a.lid)
|
|
122
|
-
if (lidResults.length > 0) {
|
|
123
|
-
await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
|
|
124
|
-
try { if (lidResults.length) await assertSessions(lidResults.map(a => a.lid), true) } catch (e) { logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs') }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const extracted = extractDeviceJids(result.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
|
|
128
|
-
const deviceMap = {}
|
|
129
|
-
for (const item of extracted) { deviceMap[item.user] = deviceMap[item.user] || []; deviceMap[item.user].push(item) }
|
|
130
|
-
|
|
131
|
-
for (const [user, userDevices] of Object.entries(deviceMap)) {
|
|
132
|
-
const isLid = requestedLidUsers.has(user)
|
|
133
|
-
for (const item of userDevices) deviceResults.push({ ...item, jid: jidEncode(isLid ? user : item.user, item.server, item.device) })
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await devicesMutex.mutex(async () => {
|
|
137
|
-
if (userDevicesCache.mset) await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
|
|
138
|
-
else for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
const userDeviceUpdates = {}
|
|
142
|
-
for (const [userId, devices] of Object.entries(deviceMap)) if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
|
143
|
-
if (Object.keys(userDeviceUpdates).length > 0) { try { await authState.keys.set({ 'device-list': userDeviceUpdates }) } catch (error) { logger.warn({ error }, 'failed to store user device lists') } }
|
|
144
|
-
}
|
|
145
|
-
return deviceResults
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const assertSessions = async (jids, force) => {
|
|
149
|
-
let jidsRequiringFetch = []
|
|
150
|
-
if (force) { jidsRequiringFetch = jids }
|
|
151
|
-
else {
|
|
152
|
-
const signalIds = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
|
|
153
|
-
const sessionBatch = await authState.keys.get('session', signalIds)
|
|
154
|
-
for (let i = 0; i < jids.length; i++) if (!sessionBatch[signalIds[i]]) jidsRequiringFetch.push(jids[i])
|
|
155
|
-
}
|
|
156
|
-
if (!jidsRequiringFetch.length) return false
|
|
157
|
-
const pnJids = jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))
|
|
158
|
-
const mappedLids = (await signalRepository.lidMapping.getLIDsForPNs(pnJids)) || []
|
|
159
|
-
const wireJids = [...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)), ...mappedLids.map(a => a.lid)]
|
|
160
|
-
logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
|
|
161
|
-
const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => ({ tag: 'user', attrs: { jid, ...(force ? { reason: 'identity' } : {}) } })) }] })
|
|
162
|
-
await parseAndInjectE2ESessions(result, signalRepository)
|
|
163
|
-
return true
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Privacy tokens ────────────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
const issuePrivacyTokens = async (jids, timestamp) => {
|
|
169
|
-
const t = (timestamp ?? unixTimestampSeconds()).toString()
|
|
170
|
-
return query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' }, content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }] })
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const getPrivacyTokens = async (jids) => {
|
|
174
|
-
const t = unixTimestampSeconds().toString()
|
|
175
|
-
const result = await query({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
if (message.
|
|
203
|
-
if (message.
|
|
204
|
-
if (message.
|
|
205
|
-
if (message.
|
|
206
|
-
if (message.
|
|
207
|
-
if (message.
|
|
208
|
-
if (message.
|
|
209
|
-
if (message.
|
|
210
|
-
if (message.
|
|
211
|
-
if (message.
|
|
212
|
-
if (message.
|
|
213
|
-
if (message.
|
|
214
|
-
if (message.
|
|
215
|
-
if (message.
|
|
216
|
-
if (message.
|
|
217
|
-
if (message.
|
|
218
|
-
if (message.
|
|
219
|
-
if (
|
|
220
|
-
if (/https:\/\/wa\.me\/
|
|
221
|
-
if (message.extendedTextMessage?.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (message
|
|
228
|
-
if (message.
|
|
229
|
-
if (message.
|
|
230
|
-
if (
|
|
231
|
-
return '
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
let shouldIncludeDeviceIdentity = false
|
|
245
|
-
const meId = authState.creds.me.id
|
|
246
|
-
const meLid = authState.creds.me?.lid
|
|
247
|
-
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const { user:
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (deliveryMode === '
|
|
444
|
-
if (deliveryMode === '
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
])
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1
|
+
import NodeCache from '@cacheable/node-cache'
|
|
2
|
+
import { Boom } from '@hapi/boom'
|
|
3
|
+
import { randomBytes } from 'crypto'
|
|
4
|
+
import { proto } from '../../WAProto/index.js'
|
|
5
|
+
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
|
|
6
|
+
import { getRandomHex } from '../Builders/index.js'
|
|
7
|
+
import {
|
|
8
|
+
aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent,
|
|
9
|
+
buildMergedTcTokenIndexWrite, decryptMediaRetryData, delay,
|
|
10
|
+
encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage,
|
|
11
|
+
encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2,
|
|
12
|
+
generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent,
|
|
13
|
+
getMessageReportingToken, getStatusCodeForMediaRetry, getUrlFromDirectPath,
|
|
14
|
+
getUrlInfo, getWAUploadToServer, isTcTokenExpired, makeKeyedMutex,
|
|
15
|
+
makeMutex, MessageRetryManager, normalizeMessageContent, buildFanoutTcTokenNode,
|
|
16
|
+
parseAndInjectE2ESessions, preSeedTcToken, resolveIssuanceJid,
|
|
17
|
+
resolveTcTokenJid, shouldIncludeReportingToken, shouldSendNewTcToken,
|
|
18
|
+
storeTcTokensFromIqResult, unixTimestampSeconds, buildContentBindingNode
|
|
19
|
+
} from '../Utils/index.js'
|
|
20
|
+
import {
|
|
21
|
+
areJidsSameUser, getBinaryFilteredButtons, getBinaryNodeChild,
|
|
22
|
+
getBinaryNodeChildren, getButtonArgs, getButtonType, isHostedLidUser,
|
|
23
|
+
isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isJidStatusBroadcast,
|
|
24
|
+
isJidUser, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser,
|
|
25
|
+
S_WHATSAPP_NET, STORIES_JID, getBinaryFilteredBizBot,
|
|
26
|
+
} from '../WABinary/index.js'
|
|
27
|
+
import { USyncQuery, USyncUser } from '../WAUSync/index.js'
|
|
28
|
+
import { makeNewsletterSocket } from './newsletter.js'
|
|
29
|
+
|
|
30
|
+
export const makeMessagesSocket = (config) => {
|
|
31
|
+
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, getMessage, aiLabel: configAiLabel } = config
|
|
32
|
+
|
|
33
|
+
const sock = makeNewsletterSocket(config)
|
|
34
|
+
const { ev, authState, processingMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, placeholderResendCache } = sock
|
|
35
|
+
|
|
36
|
+
const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
37
|
+
const devicesMutex = makeMutex()
|
|
38
|
+
const encryptionMutex = makeKeyedMutex()
|
|
39
|
+
const mediaConnMutex = makeKeyedMutex()
|
|
40
|
+
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
|
41
|
+
const inFlightTcTokenIssuance = new Set()
|
|
42
|
+
|
|
43
|
+
let mediaConn
|
|
44
|
+
|
|
45
|
+
const refreshMediaConn = async (forceGet = false) => mediaConnMutex.mutex('media-conn', async () => {
|
|
46
|
+
const media = await mediaConn
|
|
47
|
+
if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
|
|
48
|
+
mediaConn = (async () => {
|
|
49
|
+
const result = await query({ tag: 'iq', attrs: { type: 'set', xmlns: 'w:m', to: S_WHATSAPP_NET }, content: [{ tag: 'media_conn', attrs: {} }] })
|
|
50
|
+
const node = getBinaryNodeChild(result, 'media_conn')
|
|
51
|
+
return { hosts: getBinaryNodeChildren(node, 'host').map(({ attrs }) => ({ hostname: attrs.hostname, maxContentLengthBytes: +attrs.maxContentLengthBytes })), auth: node.attrs.auth, ttl: +node.attrs.ttl, fetchDate: new Date() }
|
|
52
|
+
})()
|
|
53
|
+
logger.debug('fetched media conn')
|
|
54
|
+
}
|
|
55
|
+
return mediaConn
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
|
|
59
|
+
|
|
60
|
+
// ── Receipts ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const sendReceipt = async (jid, participant, messageIds, type) => {
|
|
63
|
+
if (!messageIds?.length) throw new Boom('missing ids in receipt')
|
|
64
|
+
const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
|
|
65
|
+
const isReadType = type === 'read' || type === 'read-self'
|
|
66
|
+
if (isReadType) node.attrs.t = unixTimestampSeconds().toString()
|
|
67
|
+
if (isJidStatusBroadcast(jid) && !participant && getMessage) {
|
|
68
|
+
try { const msg = await getMessage({ remoteJid: jid, id: messageIds[0], fromMe: false }); participant = msg?.key?.participant || msg?.participant || msg?.key?.remoteJid } catch (err) { logger.debug({ err, jid }, 'failed to resolve status receipt participant') }
|
|
69
|
+
}
|
|
70
|
+
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) { node.attrs.recipient = jid; node.attrs.to = participant }
|
|
71
|
+
else if (isJidStatusBroadcast(jid) && participant) { node.attrs.to = jid; node.attrs.participant = participant }
|
|
72
|
+
else { node.attrs.to = jid; if (participant) node.attrs.participant = participant }
|
|
73
|
+
if (type) node.attrs.type = type
|
|
74
|
+
if (messageIds.length > 1) node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
|
|
75
|
+
logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt')
|
|
76
|
+
await sendNode(node)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sendReceipts = async (keys, type) => { for (const { jid, participant, messageIds } of aggregateMessageKeysNotFromMe(keys)) await sendReceipt(jid, participant, messageIds, type) }
|
|
80
|
+
|
|
81
|
+
const readMessages = async (keys) => {
|
|
82
|
+
const privacySettings = await fetchPrivacySettings()
|
|
83
|
+
const hasStatusKey = keys.some(k => isJidStatusBroadcast(k.remoteJid))
|
|
84
|
+
await sendReceipts(keys, hasStatusKey ? 'read' : (privacySettings.readreceipts === 'all' ? 'read' : 'read-self'))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Device resolution ─────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
|
|
90
|
+
const deviceResults = []
|
|
91
|
+
const jidsWithUser = jids.map(jid => {
|
|
92
|
+
const { user, device } = jidDecode(jid) || {}
|
|
93
|
+
if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
|
|
94
|
+
return { jid: jidNormalizedUser(jid), user }
|
|
95
|
+
}).filter(Boolean)
|
|
96
|
+
|
|
97
|
+
let mgetDevices
|
|
98
|
+
if (useCache && userDevicesCache.mget) mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
|
|
99
|
+
|
|
100
|
+
const toFetch = []
|
|
101
|
+
for (const { jid, user } of jidsWithUser) {
|
|
102
|
+
if (useCache) {
|
|
103
|
+
const devices = mgetDevices?.[user] || (userDevicesCache.mget ? undefined : await userDevicesCache.get(user))
|
|
104
|
+
if (devices) deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) })))
|
|
105
|
+
else toFetch.push(jid)
|
|
106
|
+
} else {
|
|
107
|
+
toFetch.push(jid)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!toFetch.length) return deviceResults
|
|
112
|
+
|
|
113
|
+
const requestedLidUsers = new Set()
|
|
114
|
+
for (const jid of toFetch) { const user = jidDecode(jid)?.user; if ((isLidUser(jid) || isHostedLidUser(jid)) && user) requestedLidUsers.add(user) }
|
|
115
|
+
|
|
116
|
+
const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
|
|
117
|
+
for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
|
|
118
|
+
|
|
119
|
+
const result = await sock.executeUSyncQuery(usyncQuery)
|
|
120
|
+
if (result) {
|
|
121
|
+
const lidResults = result.list.filter(a => !!a.lid)
|
|
122
|
+
if (lidResults.length > 0) {
|
|
123
|
+
await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
|
|
124
|
+
try { if (lidResults.length) await assertSessions(lidResults.map(a => a.lid), true) } catch (e) { logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs') }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const extracted = extractDeviceJids(result.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
|
|
128
|
+
const deviceMap = {}
|
|
129
|
+
for (const item of extracted) { deviceMap[item.user] = deviceMap[item.user] || []; deviceMap[item.user].push(item) }
|
|
130
|
+
|
|
131
|
+
for (const [user, userDevices] of Object.entries(deviceMap)) {
|
|
132
|
+
const isLid = requestedLidUsers.has(user)
|
|
133
|
+
for (const item of userDevices) deviceResults.push({ ...item, jid: jidEncode(isLid ? user : item.user, item.server, item.device) })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await devicesMutex.mutex(async () => {
|
|
137
|
+
if (userDevicesCache.mset) await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
|
|
138
|
+
else for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const userDeviceUpdates = {}
|
|
142
|
+
for (const [userId, devices] of Object.entries(deviceMap)) if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
|
143
|
+
if (Object.keys(userDeviceUpdates).length > 0) { try { await authState.keys.set({ 'device-list': userDeviceUpdates }) } catch (error) { logger.warn({ error }, 'failed to store user device lists') } }
|
|
144
|
+
}
|
|
145
|
+
return deviceResults
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const assertSessions = async (jids, force) => {
|
|
149
|
+
let jidsRequiringFetch = []
|
|
150
|
+
if (force) { jidsRequiringFetch = jids }
|
|
151
|
+
else {
|
|
152
|
+
const signalIds = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
|
|
153
|
+
const sessionBatch = await authState.keys.get('session', signalIds)
|
|
154
|
+
for (let i = 0; i < jids.length; i++) if (!sessionBatch[signalIds[i]]) jidsRequiringFetch.push(jids[i])
|
|
155
|
+
}
|
|
156
|
+
if (!jidsRequiringFetch.length) return false
|
|
157
|
+
const pnJids = jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))
|
|
158
|
+
const mappedLids = (await signalRepository.lidMapping.getLIDsForPNs(pnJids)) || []
|
|
159
|
+
const wireJids = [...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)), ...mappedLids.map(a => a.lid)]
|
|
160
|
+
logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
|
|
161
|
+
const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => ({ tag: 'user', attrs: { jid, ...(force ? { reason: 'identity' } : {}) } })) }] })
|
|
162
|
+
await parseAndInjectE2ESessions(result, signalRepository)
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Privacy tokens ────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const issuePrivacyTokens = async (jids, timestamp) => {
|
|
169
|
+
const t = (timestamp ?? unixTimestampSeconds()).toString()
|
|
170
|
+
return query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' }, content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }] })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const getPrivacyTokens = async (jids) => {
|
|
174
|
+
const t = unixTimestampSeconds().toString()
|
|
175
|
+
const result = await query({
|
|
176
|
+
tag: 'iq',
|
|
177
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
178
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }],
|
|
179
|
+
})
|
|
180
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: jids[0] ? jidNormalizedUser(jids[0]) : undefined, keys: authState.keys, getLIDForPN: signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping) })
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Protocol helpers ──────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
const sendPeerDataOperationMessage = async (pdoMessage) => {
|
|
187
|
+
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
188
|
+
return relayMessage(jidNormalizedUser(authState.creds.me.id), { protocolMessage: { peerDataOperationRequestMessage: pdoMessage, type: proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE } }, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const updateMemberLabel = (jid, memberLabel) => {
|
|
192
|
+
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
193
|
+
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
194
|
+
return relayMessage(jid, { protocolMessage: { type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE, memberLabel: { label: memberLabel.slice(0, 30), labelTimestamp: unixTimestampSeconds() } } }, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Message type helpers ──────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
const getMediaType = (message) => {
|
|
200
|
+
const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
|
|
201
|
+
if (inner) return getMediaType(inner)
|
|
202
|
+
if (message.imageMessage) return 'image'
|
|
203
|
+
if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
|
|
204
|
+
if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
|
|
205
|
+
if (message.audioMessage) return message.audioMessage.ptt ? 'ptt' : 'audio'
|
|
206
|
+
if (message.ptvMessage) return 'ptv'
|
|
207
|
+
if (message.albumMessage) return 'collection'
|
|
208
|
+
if (message.contactMessage) return 'vcard'
|
|
209
|
+
if (message.documentMessage) return 'document'
|
|
210
|
+
if (message.stickerPackMessage) return 'sticker_pack'
|
|
211
|
+
if (message.contactsArrayMessage) return 'contact_array'
|
|
212
|
+
if (message.locationMessage) return 'location'
|
|
213
|
+
if (message.liveLocationMessage) return 'livelocation'
|
|
214
|
+
if (message.listMessage) return 'list'
|
|
215
|
+
if (message.listResponseMessage) return 'list_response'
|
|
216
|
+
if (message.buttonsResponseMessage) return 'buttons_response'
|
|
217
|
+
if (message.orderMessage) return 'order'
|
|
218
|
+
if (message.productMessage) return 'product'
|
|
219
|
+
if (message.interactiveResponseMessage) return 'native_flow_response'
|
|
220
|
+
if (/https:\/\/wa\.me\/c\/\d+/.test(message.extendedTextMessage?.text)) return 'cataloglink'
|
|
221
|
+
if (/https:\/\/wa\.me\/p\/\d+\/\d+/.test(message.extendedTextMessage?.text)) return 'productlink'
|
|
222
|
+
if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const getMessageType = (msg) => {
|
|
226
|
+
const message = normalizeMessageContent(msg)
|
|
227
|
+
if (!message) return 'text'
|
|
228
|
+
if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3 || message.pollCreationMessageV5 || message.pollCreationMessageV6 || message.pollUpdateMessage) return 'poll'
|
|
229
|
+
if (message.reactionMessage || message.encReactionMessage) return 'reaction'
|
|
230
|
+
if (message.eventMessage) return 'event'
|
|
231
|
+
if (getMediaType(message)) return 'media'
|
|
232
|
+
return 'text'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const getUrlInfoFn = (text) => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 4000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined })
|
|
236
|
+
|
|
237
|
+
// ── Encryption ────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage, tcFanoutState) => {
|
|
240
|
+
if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
|
|
241
|
+
if (typeof signalRepository.preLoadKeys === 'function') await signalRepository.preLoadKeys(recipientJids)
|
|
242
|
+
const patched = await patchMessageBeforeSending(message, recipientJids)
|
|
243
|
+
const patchedMessages = Array.isArray(patched) ? patched : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
|
|
244
|
+
let shouldIncludeDeviceIdentity = false
|
|
245
|
+
const meId = authState.creds.me.id
|
|
246
|
+
const meLid = authState.creds.me?.lid
|
|
247
|
+
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
|
248
|
+
const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
|
|
249
|
+
try {
|
|
250
|
+
if (!jid) return null
|
|
251
|
+
let msgToEncrypt = patchedMessage
|
|
252
|
+
if (dsmMessage) {
|
|
253
|
+
const { user: targetUser } = jidDecode(jid)
|
|
254
|
+
const { user: ownPnUser } = jidDecode(meId)
|
|
255
|
+
const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
|
|
256
|
+
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
257
|
+
if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
|
|
258
|
+
}
|
|
259
|
+
const bytes = encodeWAMessage(msgToEncrypt)
|
|
260
|
+
return await encryptionMutex.mutex(jid, async () => {
|
|
261
|
+
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
|
|
262
|
+
if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
|
|
263
|
+
const toContent = [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }]
|
|
264
|
+
if (tcFanoutState && jidDecode(jid)?.device === 0) {
|
|
265
|
+
const fanoutNode = buildFanoutTcTokenNode({ tokenBuffer: tcFanoutState.buffer, recipientsSeen: tcFanoutState.seen, userJid: jidDecode(jid)?.user })
|
|
266
|
+
if (fanoutNode) { logger.debug({ jid, userJid: jidDecode(jid)?.user }, 'attaching fanout tctoken to <to> node'); toContent.push(fanoutNode) }
|
|
267
|
+
}
|
|
268
|
+
return { tag: 'to', attrs: { jid }, content: toContent }
|
|
269
|
+
})
|
|
270
|
+
} catch (err) { logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session'); return null }
|
|
271
|
+
})
|
|
272
|
+
const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
|
|
273
|
+
if (recipientJids.length > 0 && nodes.length === 0) throw new Boom('All encryptions failed', { statusCode: 500 })
|
|
274
|
+
return { nodes, shouldIncludeDeviceIdentity }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── relayMessage ──────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted, deliveryMode = 'toAll', aiLabel: msgAiLabel, ai: msgAi } = {} = {} = {} = {}) => {
|
|
280
|
+
jid = jidNormalizedUser(jid)
|
|
281
|
+
const meId = authState.creds.me.id
|
|
282
|
+
const meLid = authState.creds.me?.lid
|
|
283
|
+
const { user, server } = jidDecode(jid)
|
|
284
|
+
const isGroup = server === 'g.us'
|
|
285
|
+
const isStatus = isJidStatusBroadcast(jid)
|
|
286
|
+
const isLid = server === 'lid'
|
|
287
|
+
const isNewsletter = server === 'newsletter'
|
|
288
|
+
|
|
289
|
+
let activeSender = meId
|
|
290
|
+
let groupAddressingMode = 'pn'
|
|
291
|
+
if (isGroup && !isStatus) {
|
|
292
|
+
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
293
|
+
groupAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
294
|
+
if (groupAddressingMode === 'lid' && meLid) activeSender = meLid
|
|
295
|
+
} else if (isLid && meLid) {
|
|
296
|
+
activeSender = meLid
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const isRetryResend = Boolean(participant?.jid)
|
|
300
|
+
let shouldIncludeDeviceIdentity = isRetryResend
|
|
301
|
+
let finalMsgId = msgId
|
|
302
|
+
|
|
303
|
+
const hasProtoMessage = Object.keys(message).some(k => k.endsWith('Message') || k === 'conversation')
|
|
304
|
+
if (!hasProtoMessage) {
|
|
305
|
+
const generatedMsg = await generateWAMessage(jid, message, { logger, userJid: jidNormalizedUser(activeSender), getUrlInfo: getUrlInfoFn, getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, messageId: finalMsgId || generateMessageIDV2(activeSender), quoted })
|
|
306
|
+
message = generatedMsg.message
|
|
307
|
+
if (!finalMsgId) finalMsgId = generatedMsg.key.id
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
finalMsgId = finalMsgId || generateMessageIDV2(activeSender)
|
|
311
|
+
useUserDevicesCache = useUserDevicesCache !== false
|
|
312
|
+
useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
|
|
313
|
+
|
|
314
|
+
const participants = []
|
|
315
|
+
const destinationJid = !isStatus ? jid : 'status@broadcast'
|
|
316
|
+
const binaryNodeContent = []
|
|
317
|
+
const devices = []
|
|
318
|
+
const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
|
|
319
|
+
const extraAttrs = {}
|
|
320
|
+
const messages = normalizeMessageContent(message)
|
|
321
|
+
const buttonType = getButtonType(messages)
|
|
322
|
+
let hasDeviceFanoutFalse = false
|
|
323
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
324
|
+
|
|
325
|
+
if (participant) {
|
|
326
|
+
if (!isGroup && !isStatus) hasDeviceFanoutFalse = true
|
|
327
|
+
const { user, device } = jidDecode(participant.jid)
|
|
328
|
+
devices.push({ user, device, jid: participant.jid })
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
await authState.keys.transaction(async () => {
|
|
332
|
+
const mediaType = getMediaType(message)
|
|
333
|
+
if (mediaType) extraAttrs.mediatype = mediaType
|
|
334
|
+
|
|
335
|
+
if (isNewsletter) {
|
|
336
|
+
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
|
337
|
+
binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
|
|
338
|
+
await sendNode({ tag: 'message', attrs: { to: jid, id: finalMsgId, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent })
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) extraAttrs['decrypt-fail'] = 'hide'
|
|
343
|
+
|
|
344
|
+
if ((isGroup || isStatus) && !isRetryResend) {
|
|
345
|
+
const groupData = await (async () => { let data = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined; if (!data && !isStatus) data = await groupMetadata(jid); return data })()
|
|
346
|
+
const participantsList = []
|
|
347
|
+
|
|
348
|
+
if (isStatus) {
|
|
349
|
+
if (statusJidList?.length) participantsList.push(...statusJidList.map(j => jidNormalizedUser(j)).filter(j => jidDecode(j)?.user !== jidDecode(meId)?.user))
|
|
350
|
+
} else {
|
|
351
|
+
if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData.addressingMode || groupAddressingMode }
|
|
352
|
+
additionalAttributes = { ...additionalAttributes, addressing_mode: groupAddressingMode }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
356
|
+
|
|
357
|
+
const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
|
|
358
|
+
devices.push(...additionalDevices)
|
|
359
|
+
|
|
360
|
+
// Force device 0 — USync can omit it for LID groups
|
|
361
|
+
for (const pJid of participantsList) { const decoded = jidDecode(pJid); if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) devices.push({ user: decoded.user, device: 0, server: decoded.server, domainType: decoded.domainType, jid: jidEncode(decoded.user, decoded.server, 0) }) }
|
|
362
|
+
|
|
363
|
+
const patched = await patchMessageBeforeSending(message)
|
|
364
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
365
|
+
|
|
366
|
+
const bytes = encodeWAMessage(patched)
|
|
367
|
+
const gAddressingMode = additionalAttributes?.addressing_mode || groupAddressingMode
|
|
368
|
+
const gSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
369
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: gSenderIdentity })
|
|
370
|
+
|
|
371
|
+
const senderKeyRecipients = devices.filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99).map(d => d.jid)
|
|
372
|
+
if (senderKeyRecipients.length) {
|
|
373
|
+
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
374
|
+
await assertSessions(senderKeyRecipients)
|
|
375
|
+
const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
|
|
376
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
|
|
377
|
+
participants.push(...result.nodes)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
|
|
381
|
+
|
|
382
|
+
} else if ((isGroup || isStatus) && isRetryResend) {
|
|
383
|
+
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
384
|
+
if (!groupData && !isStatus) await groupMetadata(jid)
|
|
385
|
+
if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
386
|
+
additionalAttributes = { ...additionalAttributes, addressing_mode: groupData?.addressingMode || 'lid' }
|
|
387
|
+
|
|
388
|
+
const patched = await patchMessageBeforeSending(message)
|
|
389
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
390
|
+
|
|
391
|
+
const bytes = encodeWAMessage(patched)
|
|
392
|
+
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
393
|
+
const gSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
394
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: gSenderIdentity })
|
|
395
|
+
|
|
396
|
+
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
397
|
+
await assertSessions([participant.jid])
|
|
398
|
+
const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
|
|
399
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
|
|
400
|
+
participants.push(...skResult.nodes)
|
|
401
|
+
|
|
402
|
+
const isParticipantLid = isLidUser(participant.jid)
|
|
403
|
+
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
|
|
404
|
+
const encodedMsg = isMe ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } }) : encodeWAMessage(message)
|
|
405
|
+
const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: encodedMsg, jid: participant.jid })
|
|
406
|
+
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
|
|
407
|
+
|
|
408
|
+
} else {
|
|
409
|
+
const ownId = isLid && meLid ? meLid : meId
|
|
410
|
+
const { user: ownUser } = jidDecode(ownId)
|
|
411
|
+
const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
412
|
+
|
|
413
|
+
devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
|
|
414
|
+
if (user !== ownUser) {
|
|
415
|
+
const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
416
|
+
const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
|
|
417
|
+
devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!participant && additionalAttributes?.category !== 'peer') {
|
|
421
|
+
const device0Entries = devices.filter(d => d.device === 0)
|
|
422
|
+
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
423
|
+
devices.length = 0
|
|
424
|
+
const senderIdentity = isLid && meLid ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined) : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
|
|
425
|
+
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
426
|
+
const seenJids = new Set()
|
|
427
|
+
for (const d of [...device0Entries, ...sessionDevices]) { if (!seenJids.has(d.jid)) { seenJids.add(d.jid); devices.push(d) } }
|
|
428
|
+
if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
|
|
429
|
+
const senderDevices = await getUSyncDevices([senderIdentity], true, false)
|
|
430
|
+
devices.push(...senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser))
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const meRecipients = [], otherRecipients = []
|
|
435
|
+
const { user: mePnUser } = jidDecode(meId)
|
|
436
|
+
const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
|
|
437
|
+
|
|
438
|
+
for (const { user: devUser, jid: devJid } of devices) {
|
|
439
|
+
if (devJid === meId || (meLid && devJid === meLid)) continue
|
|
440
|
+
const isOwnUser = devUser === mePnUser || devUser === meLidUser
|
|
441
|
+
const isCompanion = devJid.includes(':')
|
|
442
|
+
|
|
443
|
+
if (deliveryMode === 'toMainOnly' && isCompanion) continue
|
|
444
|
+
if (deliveryMode === 'toRecipientMain' && !isOwnUser && isCompanion) continue
|
|
445
|
+
if (deliveryMode === 'toSelf' && !isOwnUser) continue
|
|
446
|
+
if (deliveryMode === 'toRecipient' && isOwnUser) continue
|
|
447
|
+
if (deliveryMode === 'toSelfMain' && (!isOwnUser || isCompanion)) continue
|
|
448
|
+
if (deliveryMode === 'toRecipientMainOnly' && (isOwnUser || isCompanion)) continue
|
|
449
|
+
|
|
450
|
+
if (isOwnUser) meRecipients.push(devJid); else otherRecipients.push(devJid)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await assertSessions([...meRecipients, ...otherRecipients])
|
|
454
|
+
const tcFanoutJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
|
|
455
|
+
const tcFanoutEntry = (await authState.keys.get('tctoken', [tcFanoutJid]))[tcFanoutJid]
|
|
456
|
+
const tcFanoutBuffer = tcFanoutEntry?.token?.length && !isTcTokenExpired(tcFanoutEntry?.timestamp) ? tcFanoutEntry.token : undefined
|
|
457
|
+
const tcFanoutState = tcFanoutBuffer ? { buffer: tcFanoutBuffer, seen: new Set() } : undefined
|
|
458
|
+
const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
|
|
459
|
+
createParticipantNodes(meRecipients, meMsg || message, extraAttrs, undefined, tcFanoutState),
|
|
460
|
+
createParticipantNodes(otherRecipients, message, extraAttrs, meMsg, tcFanoutState),
|
|
461
|
+
])
|
|
462
|
+
participants.push(...meNodes, ...otherNodes)
|
|
463
|
+
if (meRecipients.length > 0 || otherRecipients.length > 0) extraAttrs.phash = generateParticipantHashV2([...meRecipients, ...otherRecipients])
|
|
464
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (participants.length) {
|
|
468
|
+
if (additionalAttributes?.category === 'peer') { const peerNode = participants[0]?.content?.[0]; if (peerNode) binaryNodeContent.push(peerNode) }
|
|
469
|
+
else binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const stanza = {
|
|
473
|
+
tag: 'message',
|
|
474
|
+
attrs: { id: finalMsgId, to: destinationJid, type: getMessageType(message), ...((isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}), ...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}), ...(additionalAttributes || {}) },
|
|
475
|
+
content: binaryNodeContent,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (participant) {
|
|
479
|
+
if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid; stanza.attrs.participant = participant.jid }
|
|
480
|
+
else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid; stanza.attrs.recipient = destinationJid }
|
|
481
|
+
else stanza.attrs.to = participant.jid
|
|
482
|
+
} else {
|
|
483
|
+
stanza.attrs.to = destinationJid
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let didPushAdditional = false
|
|
487
|
+
if (!isNewsletter && buttonType && !isStatus) {
|
|
488
|
+
const buttonsNode = getButtonArgs(messages)
|
|
489
|
+
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
490
|
+
if (filteredButtons) { stanza.content.push(...additionalNodes); didPushAdditional = true }
|
|
491
|
+
else stanza.content.push(...buttonsNode)
|
|
492
|
+
}
|
|
493
|
+
if (!didPushAdditional && additionalNodes?.length > 0) stanza.content.push(...additionalNodes)
|
|
494
|
+
|
|
495
|
+
if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
|
|
496
|
+
|
|
497
|
+
const isPeerMessage = additionalAttributes?.category === 'peer'
|
|
498
|
+
const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
|
|
499
|
+
if (is1on1) {
|
|
500
|
+
const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
|
|
501
|
+
const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
502
|
+
const existingEntry = contactTcTokenData[tcTokenJid]
|
|
503
|
+
let tcTokenBuffer = existingEntry?.token
|
|
504
|
+
|
|
505
|
+
if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
|
|
506
|
+
tcTokenBuffer = undefined
|
|
507
|
+
const cleared = existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null
|
|
508
|
+
try { await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } }) } catch (err) { logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup') }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) {
|
|
512
|
+
try { const seeded = await preSeedTcToken({ authState, jid: destinationJid, getLIDForPN, logger }); tcTokenBuffer = seeded.token } catch (err) { logger.warn({ jid: destinationJid, err: err?.message }, 'local tctoken generation failed') }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (tcTokenBuffer?.length && (tcTokenBuffer.length < 1 || tcTokenBuffer.length > 120)) tcTokenBuffer = undefined
|
|
516
|
+
if (tcTokenBuffer?.length && sock.serverProps?.privacyTokenOn1to1) {
|
|
517
|
+
logger.debug({ tcTokenJid, bufLen: tcTokenBuffer.length, stanzaId: finalMsgId }, 'attaching 1:1 tctoken to stanza')
|
|
518
|
+
stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
519
|
+
}
|
|
520
|
+
const mediaType = getMediaType(message)
|
|
521
|
+
if (mediaType === 'url') {
|
|
522
|
+
const contentBindingNode = buildContentBindingNode({ userJid: destinationJid, contentBindingMap: sock.contentBindingMap, messageType: 'url' })
|
|
523
|
+
if (contentBindingNode) stanza.content.push(contentBindingNode)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
|
|
527
|
+
const isBotOrPSA = isJidBot(destinationJid) || isJidMetaAI(destinationJid)
|
|
528
|
+
if (!isProtocolMsg && !isBotOrPSA && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
|
|
529
|
+
inFlightTcTokenIssuance.add(tcTokenJid)
|
|
530
|
+
const issueTimestamp = unixTimestampSeconds()
|
|
531
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
532
|
+
const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
|
|
533
|
+
resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
|
|
534
|
+
.then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
|
|
535
|
+
.then(async (result) => {
|
|
536
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
|
|
537
|
+
const fresh = (await authState.keys.get('tctoken', [tcTokenJid]))[tcTokenJid]
|
|
538
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
|
|
539
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: { ...fresh, senderTimestamp: issueTimestamp }, ...indexWrite } })
|
|
540
|
+
})
|
|
541
|
+
.catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
|
|
542
|
+
.finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!isNewsletter && !isRetryResend && messages?.messageContextInfo?.messageSecret && shouldIncludeReportingToken(messages)) {
|
|
547
|
+
try {
|
|
548
|
+
const encoded = encodeWAMessage(messages)
|
|
549
|
+
const reportingKey = { id: finalMsgId, fromMe: true, remoteJid: destinationJid, participant: participant?.jid }
|
|
550
|
+
const reportingNode = await getMessageReportingToken(encoded, messages, reportingKey)
|
|
551
|
+
if (reportingNode) stanza.content.push(reportingNode)
|
|
552
|
+
} catch (error) { logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token') }
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const aiLabel = msgAiLabel ?? msgAi ?? configAiLabel ?? false
|
|
556
|
+
if (aiLabel && !isGroup && !isStatus && !isNewsletter) {
|
|
557
|
+
const alreadyHasBizBot = getBinaryFilteredBizBot(additionalNodes || []) || getBinaryFilteredBizBot(stanza.content)
|
|
558
|
+
if (!alreadyHasBizBot) stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
|
|
562
|
+
await sendNode(stanza)
|
|
563
|
+
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
|
|
564
|
+
|
|
565
|
+
}, activeSender)
|
|
566
|
+
|
|
567
|
+
const isSelf = areJidsSameUser(jid, meId) || (meLid && areJidsSameUser(jid, meLid))
|
|
568
|
+
return { key: { remoteJid: jid, fromMe: true, id: finalMsgId, participant: (isGroup || isSelf) ? jidNormalizedUser(activeSender) : undefined, addressingMode: (isLid || (isGroup && groupAddressingMode === 'lid')) ? 'lid' : 'pn' }, messageId: finalMsgId }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
|
|
572
|
+
|
|
573
|
+
// ── sendMessage ───────────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
const sendMessage = async (jid, content, options = {}) => {
|
|
576
|
+
const meId = authState.creds.me.id
|
|
577
|
+
const meLid = authState.creds.me?.lid
|
|
578
|
+
const { server } = jidDecode(jid)
|
|
579
|
+
const isGroup = server === 'g.us'
|
|
580
|
+
const isDestinationLid = server === 'lid'
|
|
581
|
+
const useCache = options.useCachedGroupMetadata !== false
|
|
582
|
+
|
|
583
|
+
let activeSender = meId
|
|
584
|
+
if (isGroup) {
|
|
585
|
+
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
586
|
+
const addressingMode = groupData?.addressingMode || 'lid'
|
|
587
|
+
if (addressingMode === 'lid' && meLid) activeSender = meLid
|
|
588
|
+
} else if (isDestinationLid && meLid) {
|
|
589
|
+
activeSender = meLid
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (content.interactive && !content.interactiveMessage) { const { interactive, ...rest } = content; content = { ...rest, interactiveMessage: interactive } }
|
|
593
|
+
|
|
594
|
+
if (content.disappearingMessagesInChat && isJidGroup(jid)) {
|
|
595
|
+
const value = typeof content.disappearingMessagesInChat === 'boolean' ? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : content.disappearingMessagesInChat
|
|
596
|
+
await groupToggleEphemeral(jid, value); return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let ephemeralDuration = options.ephemeralExpiration
|
|
600
|
+
if (!ephemeralDuration) {
|
|
601
|
+
if (isGroup) { const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined; if (groupData?.ephemeralDuration > 0) ephemeralDuration = groupData.ephemeralDuration }
|
|
602
|
+
else { const chatEphemeral = await authState.keys.get('chat-ephemeral', [jid]); if (chatEphemeral?.[jid]?.expiration > 0) ephemeralDuration = chatEphemeral[jid].expiration }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const fullMsg = await generateWAMessage(jid, content, { logger, userJid: jidNormalizedUser(activeSender), getUrlInfo: getUrlInfoFn, getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, messageId: generateMessageIDV2(activeSender), ...options, ephemeralExpiration: ephemeralDuration })
|
|
606
|
+
|
|
607
|
+
const additionalAttributes = {}, additionalNodes = []
|
|
608
|
+
if (content.delete) { const fromMe = content.delete?.fromMe; const isGroupDelete = isJidGroup(content.delete?.remoteJid); additionalAttributes.edit = (isGroupDelete && !fromMe) ? '8' : '7' }
|
|
609
|
+
else if (content.edit) additionalAttributes.edit = '1'
|
|
610
|
+
else if (content.pin) additionalAttributes.edit = '2'
|
|
611
|
+
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
612
|
+
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
613
|
+
|
|
614
|
+
await relayMessage(jid, fullMsg.message, { messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata, additionalAttributes, statusJidList: options.statusJidList, additionalNodes, deliveryMode: options.deliveryMode, aiLabel: options.aiLabel ?? options.ai })
|
|
615
|
+
if (config.emitOwnEvents) process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
|
|
616
|
+
return fullMsg
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── updateMediaMessage ────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
const updateMediaMessage = async (message) => {
|
|
622
|
+
const content = assertMediaContent(message.message)
|
|
623
|
+
const mediaKey = content.mediaKey
|
|
624
|
+
const meId = authState.creds.me.id
|
|
625
|
+
const node = await encryptMediaRetryRequest(message.key, mediaKey, meId)
|
|
626
|
+
let error
|
|
627
|
+
await Promise.all([sendNode(node), waitForMsgMediaUpdate(async (update) => {
|
|
628
|
+
const result = update.find(c => c.key.id === message.key.id)
|
|
629
|
+
if (result) {
|
|
630
|
+
if (result.error) { error = result.error }
|
|
631
|
+
else {
|
|
632
|
+
try {
|
|
633
|
+
const media = await decryptMediaRetryData(result.media, mediaKey, result.key.id)
|
|
634
|
+
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) throw new Boom(`Media re-upload failed (${proto.MediaRetryNotification.ResultType[media.result]})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
|
|
635
|
+
content.directPath = media.directPath
|
|
636
|
+
content.url = getUrlFromDirectPath(content.directPath)
|
|
637
|
+
} catch (err) { error = err }
|
|
638
|
+
}
|
|
639
|
+
return true
|
|
640
|
+
}
|
|
641
|
+
})])
|
|
642
|
+
if (error) throw error
|
|
643
|
+
ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }])
|
|
644
|
+
return message
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ── sendStatusMentions ────────────────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
const sendStatusMentions = async (content, jids = []) => {
|
|
650
|
+
const userJid = jidNormalizedUser(authState.creds.me.id)
|
|
651
|
+
const allUsers = new Set([userJid])
|
|
652
|
+
for (const id of jids) {
|
|
653
|
+
if (isJidGroup(id)) {
|
|
654
|
+
try { const metadata = await cachedGroupMetadata?.(id) || await groupMetadata(id); metadata.participants.forEach(p => allUsers.add(jidNormalizedUser(p.id))) }
|
|
655
|
+
catch (error) { logger.error({ id, error }, 'failed to fetch group metadata for status mentions') }
|
|
656
|
+
} else if (isJidUser(id)) {
|
|
657
|
+
allUsers.add(jidNormalizedUser(id))
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const isMedia = content.image || content.video || content.audio
|
|
662
|
+
const isAudio = !!content.audio
|
|
663
|
+
const msgContent = { ...content }
|
|
664
|
+
if (isMedia && !isAudio) { if (msgContent.text) { msgContent.caption = msgContent.text; delete msgContent.text }; delete msgContent.ptt; delete msgContent.font; delete msgContent.backgroundColor; delete msgContent.textColor }
|
|
665
|
+
if (isAudio) { delete msgContent.text; delete msgContent.caption; delete msgContent.font; delete msgContent.textColor }
|
|
666
|
+
|
|
667
|
+
const msg = await generateWAMessage(STORIES_JID, msgContent, { logger, userJid, getUrlInfo: getUrlInfoFn, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, font: !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined, textColor: !isMedia ? (content.textColor || getRandomHex()) : undefined, backgroundColor: (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined, ptt: isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined })
|
|
668
|
+
|
|
669
|
+
await relayMessage(STORIES_JID, msg.message, { messageId: msg.key.id, statusJidList: Array.from(allUsers), additionalNodes: [{ tag: 'meta', attrs: {}, content: [{ tag: 'mentioned_users', attrs: {}, content: jids.map(jid => ({ tag: 'to', attrs: { jid: jidNormalizedUser(jid) } })) }] }] })
|
|
670
|
+
|
|
671
|
+
for (const id of jids) {
|
|
672
|
+
try {
|
|
673
|
+
const normalizedId = jidNormalizedUser(id)
|
|
674
|
+
const isPrivate = isJidUser(normalizedId)
|
|
675
|
+
const type = isPrivate ? 'statusMentionMessage' : 'groupStatusMentionMessage'
|
|
676
|
+
const protocolMessage = { [type]: { message: { protocolMessage: { key: msg.key, type: 25 } } }, messageContextInfo: { messageSecret: randomBytes(32) } }
|
|
677
|
+
const statusMsg = await generateWAMessageFromContent(normalizedId, protocolMessage, {})
|
|
678
|
+
await relayMessage(normalizedId, statusMsg.message, { additionalNodes: [{ tag: 'meta', attrs: isPrivate ? { is_status_mention: 'true' } : { is_group_status_mention: 'true' } }] })
|
|
679
|
+
await delay(2000)
|
|
680
|
+
} catch (error) { logger.error({ id, error }, 'failed to send status mention') }
|
|
681
|
+
}
|
|
682
|
+
return msg
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
...sock,
|
|
687
|
+
getPrivacyTokens,
|
|
688
|
+
issuePrivacyTokens,
|
|
689
|
+
assertSessions,
|
|
690
|
+
relayMessage,
|
|
691
|
+
sendReceipt,
|
|
692
|
+
sendReceipts,
|
|
693
|
+
readMessages,
|
|
694
|
+
refreshMediaConn,
|
|
695
|
+
waUploadToServer,
|
|
696
|
+
sendPeerDataOperationMessage,
|
|
697
|
+
createParticipantNodes,
|
|
698
|
+
getUSyncDevices,
|
|
699
|
+
messageRetryManager,
|
|
700
|
+
updateMemberLabel,
|
|
701
|
+
userDevicesCache,
|
|
702
|
+
devicesMutex,
|
|
703
|
+
placeholderResendCache,
|
|
704
|
+
updateMediaMessage,
|
|
705
|
+
sendStatusMentions,
|
|
706
|
+
sendMessage,
|
|
707
|
+
}
|
|
693
708
|
}
|