@nexustechpro/baileys 2.0.2 → 2.0.5
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/lib/Defaults/baileys-version.json +6 -2
- package/lib/Defaults/index.js +172 -172
- package/lib/Signal/libsignal.js +380 -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 +909 -963
- package/lib/Socket/communities.js +430 -430
- package/lib/Socket/groups.js +342 -342
- package/lib/Socket/index.js +22 -22
- package/lib/Socket/messages-recv.js +777 -743
- package/lib/Socket/messages-send.js +295 -305
- package/lib/Socket/mex.js +50 -50
- package/lib/Socket/newsletter.js +148 -148
- package/lib/Socket/nexus-handler.js +75 -261
- package/lib/Socket/socket.js +709 -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 +28 -28
- 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 +12 -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 +694 -764
- package/lib/Utils/crypto.js +109 -135
- package/lib/Utils/decode-wa-message.js +310 -314
- package/lib/Utils/event-buffer.js +547 -547
- package/lib/Utils/generics.js +297 -297
- package/lib/Utils/history.js +91 -83
- package/lib/Utils/index.js +21 -20
- package/lib/Utils/key-store.js +17 -0
- package/lib/Utils/link-preview.js +97 -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 +534 -534
- package/lib/Utils/messages.js +705 -705
- package/lib/Utils/noise-handler.js +255 -255
- package/lib/Utils/pre-key-manager.js +105 -105
- package/lib/Utils/process-message.js +412 -412
- package/lib/Utils/signal.js +160 -158
- package/lib/Utils/use-multi-file-auth-state.js +120 -120
- package/lib/Utils/validate-connection.js +194 -194
- package/lib/WABinary/constants.js +1300 -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 +5 -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 +66 -66
- package/package.json +171 -144
- 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,7 +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/
|
|
7
|
+
import { getUrlInfo, migrateIndexKey } from '../Utils/index.js'
|
|
8
8
|
import { makeKeyedMutex } from '../Utils/make-mutex.js'
|
|
9
9
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js'
|
|
10
10
|
import { makeNewsletterSocket } from './newsletter.js'
|
|
@@ -22,10 +22,47 @@ const {
|
|
|
22
22
|
|
|
23
23
|
const {
|
|
24
24
|
areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
|
|
25
|
-
isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
|
|
25
|
+
isJidBroadcast, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
|
|
26
26
|
getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType
|
|
27
27
|
} = WABinary
|
|
28
28
|
|
|
29
|
+
const TC_TOKEN_BUCKET_DURATION = 604800 // 7 days
|
|
30
|
+
const TC_TOKEN_NUM_BUCKETS = 4 // ~28-day rolling window
|
|
31
|
+
|
|
32
|
+
const isTcTokenExpired = (timestamp) => {
|
|
33
|
+
if (timestamp === null || timestamp === undefined) return true
|
|
34
|
+
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
|
|
35
|
+
if (isNaN(ts)) return true
|
|
36
|
+
const now = Math.floor(Date.now() / 1000)
|
|
37
|
+
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
|
|
38
|
+
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1)
|
|
39
|
+
return ts < (cutoffBucket * TC_TOKEN_BUCKET_DURATION)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const shouldSendNewTcToken = (senderTimestamp) => {
|
|
43
|
+
if (senderTimestamp === undefined) return true
|
|
44
|
+
const now = Math.floor(Date.now() / 1000)
|
|
45
|
+
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
|
|
46
|
+
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION)
|
|
47
|
+
return currentBucket > senderBucket
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolveTcTokenJid = async (jid, getLIDForPN) => {
|
|
51
|
+
if (isLidUser(jid)) return jid
|
|
52
|
+
const lid = await getLIDForPN(jid)
|
|
53
|
+
return lid ?? jid
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resolveIssuanceJid = async (jid, issueToLid, getLIDForPN, getPNForLID) => {
|
|
57
|
+
if (issueToLid) {
|
|
58
|
+
if (isLidUser(jid)) return jid
|
|
59
|
+
return (await getLIDForPN(jid)) ?? jid
|
|
60
|
+
}
|
|
61
|
+
if (!isLidUser(jid)) return jid
|
|
62
|
+
if (getPNForLID) return (await getPNForLID(jid)) ?? jid
|
|
63
|
+
return jid
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
export const makeMessagesSocket = (config) => {
|
|
30
67
|
const {
|
|
31
68
|
logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview,
|
|
@@ -43,13 +80,12 @@ export const makeMessagesSocket = (config) => {
|
|
|
43
80
|
const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
44
81
|
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
|
45
82
|
const encryptionMutex = makeKeyedMutex()
|
|
83
|
+
|
|
84
|
+
// Prevents duplicate TC token IQ requests from concurrent sends
|
|
85
|
+
const inFlightTcTokenIssuance = new Set()
|
|
86
|
+
|
|
46
87
|
let mediaConn
|
|
47
88
|
|
|
48
|
-
// ─────────────────────────────────────────────
|
|
49
|
-
// MEDIA CONNECTION
|
|
50
|
-
// Fetches and caches the WhatsApp media upload connection.
|
|
51
|
-
// Refreshes automatically when TTL expires or forced.
|
|
52
|
-
// ─────────────────────────────────────────────
|
|
53
89
|
const refreshMediaConn = async (forceGet = false) => {
|
|
54
90
|
const media = await mediaConn
|
|
55
91
|
if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
|
|
@@ -68,10 +104,8 @@ export const makeMessagesSocket = (config) => {
|
|
|
68
104
|
return mediaConn
|
|
69
105
|
}
|
|
70
106
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Sends read receipts and delivery confirmations.
|
|
74
|
-
// ─────────────────────────────────────────────
|
|
107
|
+
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
|
|
108
|
+
|
|
75
109
|
const sendReceipt = async (jid, participant, messageIds, type) => {
|
|
76
110
|
if (!messageIds?.length) throw new Boom('missing ids in receipt')
|
|
77
111
|
const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
|
|
@@ -104,11 +138,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
104
138
|
await sendReceipts(keys, privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
|
|
105
139
|
}
|
|
106
140
|
|
|
107
|
-
// ─────────────────────────────────────────────
|
|
108
|
-
// DEVICE & SESSION MANAGEMENT
|
|
109
|
-
// Fetches participant devices via USync and manages
|
|
110
|
-
// Signal protocol sessions for encryption.
|
|
111
|
-
// ─────────────────────────────────────────────
|
|
112
141
|
const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
|
|
113
142
|
const deviceResults = []
|
|
114
143
|
if (!useCache) logger.debug('not using cache for devices')
|
|
@@ -117,7 +146,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
117
146
|
const decoded = jidDecode(jid)
|
|
118
147
|
const user = decoded?.user
|
|
119
148
|
const device = decoded?.device
|
|
120
|
-
// Already has a device number — push directly
|
|
121
149
|
if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
|
|
122
150
|
return { jid: jidNormalizedUser(jid), user }
|
|
123
151
|
}).filter(Boolean)
|
|
@@ -144,7 +172,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
144
172
|
|
|
145
173
|
if (!toFetch.length) return deviceResults
|
|
146
174
|
|
|
147
|
-
// Track which JIDs are LID-based so we can encode them correctly
|
|
148
175
|
const requestedLidUsers = new Set()
|
|
149
176
|
for (const jid of toFetch) {
|
|
150
177
|
if (isLidUser(jid) || isHostedLidUser(jid)) {
|
|
@@ -153,23 +180,21 @@ export const makeMessagesSocket = (config) => {
|
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
|
|
156
|
-
const
|
|
157
|
-
for (const jid of toFetch)
|
|
183
|
+
const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
|
|
184
|
+
for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
|
|
158
185
|
|
|
159
|
-
const result = await sock.executeUSyncQuery(
|
|
186
|
+
const result = await sock.executeUSyncQuery(usyncQuery)
|
|
160
187
|
if (result) {
|
|
161
188
|
const lidResults = result.list.filter(a => !!a.lid)
|
|
162
189
|
if (lidResults.length > 0) {
|
|
163
190
|
logger.trace('Storing LID maps from device call')
|
|
164
191
|
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')
|
|
192
|
+
try {
|
|
193
|
+
const lids = lidResults.map(a => a.lid)
|
|
194
|
+
if (lids.length) await assertSessions(lids, false)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
|
|
197
|
+
}
|
|
173
198
|
}
|
|
174
199
|
|
|
175
200
|
const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
|
|
@@ -187,27 +212,23 @@ export const makeMessagesSocket = (config) => {
|
|
|
187
212
|
}
|
|
188
213
|
}
|
|
189
214
|
|
|
190
|
-
// Cache results
|
|
191
215
|
if (userDevicesCache.mset) {
|
|
192
216
|
await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
|
|
193
217
|
} else {
|
|
194
218
|
for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
|
195
219
|
}
|
|
196
220
|
|
|
197
|
-
// Persist device lists for session migration
|
|
221
|
+
// Persist device lists for session migration (capped at 500 users)
|
|
198
222
|
const userDeviceUpdates = {}
|
|
199
223
|
for (const [userId, devices] of Object.entries(deviceMap)) {
|
|
200
224
|
if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
|
201
225
|
}
|
|
202
226
|
if (Object.keys(userDeviceUpdates).length > 0) {
|
|
203
227
|
try {
|
|
204
|
-
const
|
|
205
|
-
const currentBatch = existingData?.['_index'] || {}
|
|
228
|
+
const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
|
|
206
229
|
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')
|
|
230
|
+
await authState.keys.set({ 'device-list': { 'index': mergedBatch } })
|
|
231
|
+
logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(mergedBatch).length }, 'stored user device lists')
|
|
211
232
|
} catch (error) {
|
|
212
233
|
logger.warn({ error }, 'failed to store user device lists')
|
|
213
234
|
}
|
|
@@ -217,47 +238,26 @@ export const makeMessagesSocket = (config) => {
|
|
|
217
238
|
}
|
|
218
239
|
|
|
219
240
|
const assertSessions = async (jids, force) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
peerSessionsCache.set(signalId, sessionValidation.exists)
|
|
231
|
-
if (sessionValidation.exists && !force) continue
|
|
241
|
+
let didFetchNewSession = false
|
|
242
|
+
let jidsRequiringFetch = []
|
|
243
|
+
if (force) {
|
|
244
|
+
jidsRequiringFetch = jids
|
|
245
|
+
} else {
|
|
246
|
+
// assertSessions
|
|
247
|
+
const sessionBatch = await migrateIndexKey(authState.keys, 'session') // sessions live in index blob, not individual files
|
|
248
|
+
for (const jid of jids) {
|
|
249
|
+
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
|
250
|
+
if (!sessionBatch[signalId]) jidsRequiringFetch.push(jid)
|
|
232
251
|
}
|
|
233
|
-
jidsRequiringFetch.push(jid)
|
|
234
252
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))
|
|
243
|
-
) || []).map(a => a.lid)
|
|
244
|
-
]
|
|
245
|
-
|
|
246
|
-
logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
|
|
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
|
|
253
|
+
if (jidsRequiringFetch.length) {
|
|
254
|
+
logger.debug({ jidsRequiringFetch }, 'fetching sessions')
|
|
255
|
+
const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: jidsRequiringFetch.map(jid => ({ tag: 'user', attrs: { jid } })) }] })
|
|
256
|
+
await parseAndInjectE2ESessions(result, signalRepository)
|
|
257
|
+
didFetchNewSession = true
|
|
258
|
+
}
|
|
259
|
+
return didFetchNewSession
|
|
255
260
|
}
|
|
256
|
-
|
|
257
|
-
// ─────────────────────────────────────────────
|
|
258
|
-
// PEER DATA OPERATIONS
|
|
259
|
-
// Used for history sync requests and placeholder resends.
|
|
260
|
-
// ─────────────────────────────────────────────
|
|
261
261
|
const sendPeerDataOperationMessage = async (pdoMessage) => {
|
|
262
262
|
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
263
263
|
return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
|
|
@@ -265,18 +265,24 @@ export const makeMessagesSocket = (config) => {
|
|
|
265
265
|
}, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return unixTimestampSeconds() - Number(tokenData.timestamp) > TOKEN_EXPIRY_TTL
|
|
268
|
+
// Issues our TC token to a contact so they can send us private messages. Fire-and-forget.
|
|
269
|
+
const issuePrivacyTokens = async (jids, timestamp) => {
|
|
270
|
+
const t = (timestamp ?? unixTimestampSeconds()).toString()
|
|
271
|
+
return query({
|
|
272
|
+
tag: 'iq',
|
|
273
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
274
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
275
|
+
})
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
|
|
278
|
+
// Fetches TC tokens from the server for the given JIDs and stores them locally.
|
|
279
|
+
const getPrivacyTokens = async (jids) => {
|
|
280
|
+
const t = unixTimestampSeconds().toString()
|
|
281
|
+
const result = await query({
|
|
282
|
+
tag: 'iq',
|
|
283
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
284
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
285
|
+
})
|
|
280
286
|
const tokens = {}
|
|
281
287
|
const tokenList = getBinaryNodeChild(result, 'tokens')
|
|
282
288
|
if (tokenList) {
|
|
@@ -285,25 +291,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
285
291
|
if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
|
|
286
292
|
}
|
|
287
293
|
}
|
|
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
294
|
if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
|
|
300
295
|
return tokens
|
|
301
296
|
}
|
|
302
297
|
|
|
303
|
-
// ─────────────────────────────────────────────
|
|
304
|
-
// GROUP MEMBER LABEL
|
|
305
|
-
// Sets a custom label for a member in a group.
|
|
306
|
-
// ─────────────────────────────────────────────
|
|
307
298
|
const updateMemberLabel = (jid, memberLabel) => {
|
|
308
299
|
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
309
300
|
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
@@ -315,20 +306,19 @@ export const makeMessagesSocket = (config) => {
|
|
|
315
306
|
}, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
|
|
316
307
|
}
|
|
317
308
|
|
|
318
|
-
// ─────────────────────────────────────────────
|
|
319
|
-
// MESSAGE TYPE DETECTION
|
|
320
|
-
// Classifies messages for proper stanza type attribute.
|
|
321
|
-
// ─────────────────────────────────────────────
|
|
322
309
|
const getMessageType = (msg) => {
|
|
323
310
|
const message = normalizeMessageContent(msg)
|
|
311
|
+
if (!message) return 'text'
|
|
324
312
|
if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
|
|
325
|
-
if (message.reactionMessage) return 'reaction'
|
|
313
|
+
if (message.reactionMessage || message.encReactionMessage) return 'reaction'
|
|
326
314
|
if (message.eventMessage) return 'event'
|
|
327
315
|
if (getMediaType(message)) return 'media'
|
|
328
316
|
return 'text'
|
|
329
317
|
}
|
|
330
318
|
|
|
331
319
|
const getMediaType = (message) => {
|
|
320
|
+
const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
|
|
321
|
+
if (inner) return getMediaType(inner)
|
|
332
322
|
if (message.imageMessage) return 'image'
|
|
333
323
|
if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
|
|
334
324
|
if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
|
|
@@ -352,11 +342,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
352
342
|
if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
|
|
353
343
|
}
|
|
354
344
|
|
|
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
345
|
const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
|
|
361
346
|
if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
|
|
362
347
|
|
|
@@ -373,7 +358,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
373
358
|
if (!jid) return null
|
|
374
359
|
let msgToEncrypt = patchedMessage
|
|
375
360
|
|
|
376
|
-
// Use
|
|
361
|
+
// Use DSM for own linked devices so they can read the message
|
|
377
362
|
if (dsmMessage) {
|
|
378
363
|
const { user: targetUser } = jidDecode(jid)
|
|
379
364
|
const { user: ownPnUser } = jidDecode(meId)
|
|
@@ -389,7 +374,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
389
374
|
return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
|
|
390
375
|
})
|
|
391
376
|
} catch (err) {
|
|
392
|
-
logger.
|
|
377
|
+
logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session, will retry on next interaction')
|
|
393
378
|
return null
|
|
394
379
|
}
|
|
395
380
|
})
|
|
@@ -398,17 +383,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
398
383
|
return { nodes, shouldIncludeDeviceIdentity }
|
|
399
384
|
}
|
|
400
385
|
|
|
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
386
|
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
|
|
413
387
|
const meId = authState.creds.me.id
|
|
414
388
|
const meLid = authState.creds.me?.lid
|
|
@@ -418,7 +392,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
418
392
|
const isLid = server === 'lid'
|
|
419
393
|
const isNewsletter = server === 'newsletter'
|
|
420
394
|
|
|
421
|
-
// Choose sender identity (PN or LID) based on destination
|
|
422
395
|
let activeSender = meId
|
|
423
396
|
let groupAddressingMode = 'pn'
|
|
424
397
|
if (isGroup && !isStatus) {
|
|
@@ -473,7 +446,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
473
446
|
const mediaType = getMediaType(message)
|
|
474
447
|
if (mediaType) extraAttrs.mediatype = mediaType
|
|
475
448
|
|
|
476
|
-
// ── Newsletter: plaintext encoding, no encryption ──
|
|
477
449
|
if (isNewsletter) {
|
|
478
450
|
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
|
479
451
|
binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
|
|
@@ -482,11 +454,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
482
454
|
return
|
|
483
455
|
}
|
|
484
456
|
|
|
485
|
-
if (messages
|
|
457
|
+
if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
|
|
486
458
|
extraAttrs['decrypt-fail'] = 'hide'
|
|
487
459
|
}
|
|
488
460
|
|
|
489
|
-
// ── Group / Status: sender key (SKDM + skmsg) ──
|
|
490
461
|
if ((isGroup || isStatus) && !isRetryResend) {
|
|
491
462
|
const [groupData] = await Promise.all([
|
|
492
463
|
(async () => {
|
|
@@ -495,10 +466,9 @@ export const makeMessagesSocket = (config) => {
|
|
|
495
466
|
else if (!isStatus) groupData = await groupMetadata(jid)
|
|
496
467
|
return groupData
|
|
497
468
|
})(),
|
|
498
|
-
Promise.resolve({}) // senderKeyMap
|
|
469
|
+
Promise.resolve({}) // senderKeyMap always empty — forces fresh SKDM every send
|
|
499
470
|
])
|
|
500
471
|
|
|
501
|
-
// Build participant list
|
|
502
472
|
const participantsList = []
|
|
503
473
|
if (isStatus) {
|
|
504
474
|
if (statusJidList?.length) participantsList.push(...statusJidList)
|
|
@@ -515,9 +485,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
515
485
|
const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
|
|
516
486
|
devices.push(...additionalDevices)
|
|
517
487
|
|
|
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.
|
|
488
|
+
// Force Device 0 inclusion — USync sometimes omits it for LID groups
|
|
521
489
|
for (const pJid of participantsList) {
|
|
522
490
|
const decoded = jidDecode(pJid)
|
|
523
491
|
if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
|
|
@@ -533,7 +501,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
533
501
|
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
534
502
|
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
|
|
535
503
|
|
|
536
|
-
// Send SKDM to ALL devices every time (senderKeyMap is always {})
|
|
537
504
|
const senderKeyRecipients = devices
|
|
538
505
|
.filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
|
|
539
506
|
.map(d => d.jid)
|
|
@@ -549,7 +516,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
549
516
|
|
|
550
517
|
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
|
|
551
518
|
|
|
552
|
-
// ── Group Retry: direct pairwise re-encrypt for specific participant ──
|
|
553
519
|
} else if ((isGroup || isStatus) && isRetryResend) {
|
|
554
520
|
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
555
521
|
if (!groupData && !isStatus) await groupMetadata(jid)
|
|
@@ -565,17 +531,19 @@ export const makeMessagesSocket = (config) => {
|
|
|
565
531
|
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
566
532
|
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
|
|
567
533
|
|
|
568
|
-
// Send fresh SKDM directly to the requesting participant
|
|
569
534
|
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
570
535
|
await assertSessions([participant.jid])
|
|
571
536
|
const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
|
|
572
537
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
|
|
573
538
|
participants.push(...skResult.nodes)
|
|
574
539
|
|
|
575
|
-
|
|
540
|
+
// For retry resend, encrypt directly to the requesting participant
|
|
541
|
+
const isParticipantLid = isLidUser(participant.jid)
|
|
542
|
+
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
|
|
543
|
+
const encodedMsg = isMe ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } }) : encodeWAMessage(message)
|
|
544
|
+
const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: encodedMsg, jid: participant.jid })
|
|
576
545
|
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
|
|
577
546
|
|
|
578
|
-
// ── DM / LID: standard pairwise encryption ──
|
|
579
547
|
} else {
|
|
580
548
|
let ownId = meId
|
|
581
549
|
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
@@ -593,7 +561,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
593
561
|
}
|
|
594
562
|
|
|
595
563
|
if (additionalAttributes?.category !== 'peer') {
|
|
596
|
-
// Preserve Device 0 entries before USync refetch which may omit them
|
|
597
564
|
const device0Entries = devices.filter(d => d.device === 0)
|
|
598
565
|
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
599
566
|
devices.length = 0
|
|
@@ -603,7 +570,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
603
570
|
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
604
571
|
devices.push(...device0Entries, ...sessionDevices)
|
|
605
572
|
|
|
606
|
-
// Explicitly fetch sender's linked devices if not returned by USync
|
|
607
573
|
if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
|
|
608
574
|
const senderDevices = await getUSyncDevices([senderIdentity], true, false)
|
|
609
575
|
const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
|
|
@@ -639,7 +605,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
639
605
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
|
640
606
|
}
|
|
641
607
|
|
|
642
|
-
// ── Build final stanza ──
|
|
643
608
|
if (participants.length) {
|
|
644
609
|
if (additionalAttributes?.category === 'peer') {
|
|
645
610
|
const peerNode = participants[0]?.content?.[0]
|
|
@@ -670,41 +635,74 @@ export const makeMessagesSocket = (config) => {
|
|
|
670
635
|
stanza.attrs.to = destinationJid
|
|
671
636
|
}
|
|
672
637
|
|
|
673
|
-
|
|
674
|
-
|
|
638
|
+
let didPushAdditional = false
|
|
639
|
+
|
|
640
|
+
if (!isNewsletter && buttonType && !isStatus) {
|
|
675
641
|
const buttonsNode = getButtonArgs(messages)
|
|
676
642
|
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
677
|
-
if (filteredButtons) {
|
|
678
|
-
|
|
679
|
-
|
|
643
|
+
if (filteredButtons) {
|
|
644
|
+
stanza.content.push(...additionalNodes)
|
|
645
|
+
didPushAdditional = true
|
|
646
|
+
} else {
|
|
647
|
+
stanza.content.push(...buttonsNode)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!didPushAdditional && additionalNodes?.length > 0) {
|
|
680
652
|
stanza.content.push(...additionalNodes)
|
|
681
653
|
}
|
|
682
654
|
|
|
683
|
-
// Attach device identity for LID groups and pkmsg sessions
|
|
684
655
|
if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
|
|
685
656
|
stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
|
|
686
657
|
logger.debug({ jid }, 'adding device identity')
|
|
687
658
|
}
|
|
688
659
|
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
660
|
+
// TC token handling for 1:1 messages
|
|
661
|
+
const isPeerMessage = additionalAttributes?.category === 'peer'
|
|
662
|
+
const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
|
|
663
|
+
if (is1on1) {
|
|
664
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
665
|
+
const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
|
|
666
|
+
|
|
667
|
+
const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
668
|
+
const existingEntry = contactTcTokenData[tcTokenJid]
|
|
669
|
+
let tcTokenBuffer = existingEntry?.token
|
|
670
|
+
|
|
671
|
+
// Clear expired tokens
|
|
672
|
+
if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
|
|
673
|
+
logger.debug({ jid: destinationJid }, 'tctoken expired, clearing')
|
|
674
|
+
tcTokenBuffer = undefined
|
|
695
675
|
try {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
676
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null } })
|
|
677
|
+
} catch { }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (tcTokenBuffer?.length) {
|
|
681
|
+
stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Fire-and-forget: issue our token to the contact after send
|
|
685
|
+
const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
|
|
686
|
+
if (!isProtocolMsg && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
|
|
687
|
+
inFlightTcTokenIssuance.add(tcTokenJid)
|
|
688
|
+
const issueTimestamp = unixTimestampSeconds()
|
|
689
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
690
|
+
const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
|
|
691
|
+
resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
|
|
692
|
+
.then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
|
|
693
|
+
.then(async () => {
|
|
694
|
+
const currentData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
695
|
+
const current = currentData[tcTokenJid]
|
|
696
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: { ...current, senderTimestamp: issueTimestamp } } })
|
|
697
|
+
})
|
|
698
|
+
.catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
|
|
699
|
+
.finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
|
|
701
700
|
}
|
|
702
|
-
if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
703
701
|
}
|
|
704
702
|
|
|
705
|
-
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
|
703
|
+
logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
|
|
706
704
|
await sendNode(stanza)
|
|
707
|
-
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid,
|
|
705
|
+
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
|
|
708
706
|
|
|
709
707
|
}, activeSender)
|
|
710
708
|
|
|
@@ -722,21 +720,131 @@ export const makeMessagesSocket = (config) => {
|
|
|
722
720
|
}
|
|
723
721
|
}
|
|
724
722
|
|
|
725
|
-
const
|
|
726
|
-
const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me })
|
|
723
|
+
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
724
|
const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
|
|
728
725
|
|
|
726
|
+
const sendMessage = async (jid, content, options = {}) => {
|
|
727
|
+
const meId = authState.creds.me.id
|
|
728
|
+
const meLid = authState.creds.me?.lid
|
|
729
|
+
const { server } = jidDecode(jid)
|
|
730
|
+
const isGroup = server === 'g.us'
|
|
731
|
+
const isDestinationLid = server === 'lid'
|
|
732
|
+
const useCache = options.useCachedGroupMetadata !== false
|
|
733
|
+
const { quoted } = options
|
|
734
|
+
|
|
735
|
+
let activeSender = meId
|
|
736
|
+
let addressingMode = 'pn'
|
|
737
|
+
if (isGroup) {
|
|
738
|
+
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
739
|
+
addressingMode = groupData?.addressingMode || 'lid'
|
|
740
|
+
if (addressingMode === 'lid' && meLid) activeSender = meLid
|
|
741
|
+
} else if (isDestinationLid && meLid) {
|
|
742
|
+
activeSender = meLid
|
|
743
|
+
addressingMode = 'lid'
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Unwrap shorthand `interactive` key
|
|
747
|
+
if (content.interactive && !content.interactiveMessage) {
|
|
748
|
+
const { interactive, ...rest } = content
|
|
749
|
+
content = { ...rest, interactiveMessage: interactive }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const messageType = nexus.detectType(content)
|
|
753
|
+
if (messageType) return await nexus.processMessage(content, jid, quoted)
|
|
754
|
+
|
|
755
|
+
if (content.disappearingMessagesInChat && isJidGroup(jid)) {
|
|
756
|
+
const value = typeof content.disappearingMessagesInChat === 'boolean'
|
|
757
|
+
? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
|
|
758
|
+
: content.disappearingMessagesInChat
|
|
759
|
+
await groupToggleEphemeral(jid, value)
|
|
760
|
+
return
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (content.delete) {
|
|
764
|
+
const deleteKey = content.delete
|
|
765
|
+
if (!deleteKey.remoteJid || !deleteKey.id) {
|
|
766
|
+
logger.error({ deleteKey }, 'Invalid delete key: missing remoteJid or id')
|
|
767
|
+
throw new Boom('Delete key must have remoteJid and id', { statusCode: 400 })
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const { server: deleteServer } = jidDecode(deleteKey.remoteJid)
|
|
771
|
+
let deleteAddressingMode = deleteServer === 'lid' ? 'lid' : 'pn'
|
|
772
|
+
if (isJidGroup(deleteKey.remoteJid)) {
|
|
773
|
+
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(deleteKey.remoteJid) : undefined
|
|
774
|
+
deleteAddressingMode = groupData?.addressingMode || 'lid'
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let normalizedParticipant = deleteKey.participant
|
|
778
|
+
if (deleteKey.fromMe || isJidGroup(deleteKey.remoteJid)) {
|
|
779
|
+
const senderJid = (deleteAddressingMode === 'lid' && meLid) ? meLid : meId
|
|
780
|
+
normalizedParticipant = jidNormalizedUser(senderJid)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
content.delete = {
|
|
784
|
+
remoteJid: deleteKey.remoteJid,
|
|
785
|
+
fromMe: deleteKey.fromMe === true || deleteKey.fromMe === 'true',
|
|
786
|
+
id: deleteKey.id,
|
|
787
|
+
...(normalizedParticipant ? { participant: jidNormalizedUser(normalizedParticipant) } : {}),
|
|
788
|
+
addressingMode: deleteAddressingMode
|
|
789
|
+
}
|
|
790
|
+
logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const fullMsg = await generateWAMessage(jid, content, {
|
|
794
|
+
logger,
|
|
795
|
+
userJid: jidNormalizedUser(activeSender),
|
|
796
|
+
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
797
|
+
getProfilePicUrl: sock.profilePictureUrl,
|
|
798
|
+
getCallLink: sock.createCallLink,
|
|
799
|
+
upload: waUploadToServer,
|
|
800
|
+
mediaCache: config.mediaCache,
|
|
801
|
+
options: config.options,
|
|
802
|
+
messageId: generateMessageIDV2(activeSender),
|
|
803
|
+
...options
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
const additionalAttributes = {}, additionalNodes = []
|
|
807
|
+
if (content.delete) {
|
|
808
|
+
additionalAttributes.edit = isJidGroup(content.delete.remoteJid) ? '8' : '7'
|
|
809
|
+
} else if (content.edit) {
|
|
810
|
+
additionalAttributes.edit = '1'
|
|
811
|
+
} else if (content.pin) {
|
|
812
|
+
additionalAttributes.edit = '2'
|
|
813
|
+
}
|
|
814
|
+
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
815
|
+
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
816
|
+
if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
|
|
817
|
+
|
|
818
|
+
// Pre-fetch TC token for new DM contacts
|
|
819
|
+
if (!isJidGroup(jid) && !isJidStatusBroadcast(jid) && !isJidBroadcast(jid) && !content.disappearingMessagesInChat) {
|
|
820
|
+
const existingToken = await authState.keys.get('tctoken', [jid])
|
|
821
|
+
if (!existingToken[jid]) {
|
|
822
|
+
try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
|
|
823
|
+
catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
await relayMessage(jid, fullMsg.message, {
|
|
828
|
+
messageId: fullMsg.key.id,
|
|
829
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
830
|
+
additionalAttributes,
|
|
831
|
+
statusJidList: options.statusJidList,
|
|
832
|
+
additionalNodes
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
if (config.emitOwnEvents) {
|
|
836
|
+
process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
|
|
837
|
+
}
|
|
838
|
+
return fullMsg
|
|
839
|
+
}
|
|
840
|
+
|
|
729
841
|
return {
|
|
730
842
|
...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
|
-
// ─────────────────────────────────────────────
|
|
843
|
+
getPrivacyTokens, issuePrivacyTokens, assertSessions, relayMessage,
|
|
844
|
+
sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
|
|
845
|
+
waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
|
|
846
|
+
createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
|
|
847
|
+
|
|
740
848
|
updateMediaMessage: async (message) => {
|
|
741
849
|
const content = assertMediaContent(message.message)
|
|
742
850
|
const mediaKey = content.mediaKey
|
|
@@ -767,11 +875,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
767
875
|
return message
|
|
768
876
|
},
|
|
769
877
|
|
|
770
|
-
// ─────────────────────────────────────────────
|
|
771
|
-
// SEND STATUS MENTIONS
|
|
772
|
-
// Broadcasts a status update and notifies mentioned
|
|
773
|
-
// users or groups via statusMentionMessage protocol.
|
|
774
|
-
// ─────────────────────────────────────────────
|
|
775
878
|
sendStatusMentions: async (content, jids = []) => {
|
|
776
879
|
const userJid = jidNormalizedUser(authState.creds.me.id)
|
|
777
880
|
const allUsers = new Set([userJid])
|
|
@@ -825,7 +928,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
825
928
|
return msg
|
|
826
929
|
},
|
|
827
930
|
|
|
828
|
-
// Nexus handler shortcuts
|
|
931
|
+
// Nexus handler shortcuts
|
|
829
932
|
sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
|
|
830
933
|
sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
|
|
831
934
|
sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
|
|
@@ -838,133 +941,20 @@ export const makeMessagesSocket = (config) => {
|
|
|
838
941
|
sendCarouselMessage: (jid, data, quoted) => nexus.handleCarousel({ carouselMessage: data }, jid, quoted),
|
|
839
942
|
sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
|
|
840
943
|
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
|
-
}
|
|
944
|
+
sendMessage,
|
|
945
|
+
// Shorthand wrappers
|
|
946
|
+
sendText: (jid, text, options = {}) => sendMessage(jid, { text, ...options }, options),
|
|
947
|
+
sendImage: (jid, image, caption = '', options = {}) => sendMessage(jid, { image, caption, ...options }, options),
|
|
948
|
+
sendVideo: (jid, video, caption = '', options = {}) => sendMessage(jid, { video, caption, ...options }, options),
|
|
949
|
+
sendDocument: (jid, document, caption = '', options = {}) => sendMessage(jid, { document, caption, ...options }, options),
|
|
950
|
+
sendAudio: (jid, audio, options = {}) => sendMessage(jid, { audio, ...options }, options),
|
|
951
|
+
sendLocation: (jid, { degreesLatitude, degreesLongitude, name, url, address } = {}, options = {}) =>
|
|
952
|
+
sendMessage(jid, { location: { degreesLatitude, degreesLongitude, name, url, address }, ...options }, options),
|
|
953
|
+
sendPoll: (jid, name, pollVote = [], multiSelect = false, options = {}) =>
|
|
954
|
+
sendMessage(jid, { poll: { name, values: pollVote, selectableOptionsCount: multiSelect ? pollVote.length : 0 }, ...options }, options),
|
|
955
|
+
sendReaction: (jid, key, reaction, options = {}) => sendMessage(jid, { react: { text: reaction, key }, ...options }, options),
|
|
956
|
+
sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
|
|
957
|
+
sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
|
|
958
|
+
sendForward: (jid, message, options = {}) => sendMessage(jid, { forward: message, force: options.force }, options),
|
|
969
959
|
}
|
|
970
960
|
}
|