@nexustechpro/baileys 1.1.9 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1745 -1745
- package/lib/Socket/messages-send.js +629 -345
- package/lib/Utils/link-preview.js +46 -36
- package/lib/Utils/messages-media.js +155 -311
- package/lib/Utils/messages.js +43 -43
- package/lib/index.js +2 -2
- package/package.json +20 -20
|
@@ -34,7 +34,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
34
34
|
} = config
|
|
35
35
|
|
|
36
36
|
const sock = makeNewsletterSocket(config)
|
|
37
|
-
const {
|
|
37
|
+
const {
|
|
38
|
+
ev, authState, processingMutex, signalRepository, upsertMessage, query,
|
|
39
|
+
fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral
|
|
40
|
+
} = sock
|
|
38
41
|
|
|
39
42
|
const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
40
43
|
const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
@@ -42,7 +45,11 @@ export const makeMessagesSocket = (config) => {
|
|
|
42
45
|
const encryptionMutex = makeKeyedMutex()
|
|
43
46
|
let mediaConn
|
|
44
47
|
|
|
45
|
-
//
|
|
48
|
+
// ─────────────────────────────────────────────
|
|
49
|
+
// MEDIA CONNECTION
|
|
50
|
+
// Fetches and caches the WhatsApp media upload connection.
|
|
51
|
+
// Refreshes automatically when TTL expires or forced.
|
|
52
|
+
// ─────────────────────────────────────────────
|
|
46
53
|
const refreshMediaConn = async (forceGet = false) => {
|
|
47
54
|
const media = await mediaConn
|
|
48
55
|
if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
|
|
@@ -51,7 +58,9 @@ export const makeMessagesSocket = (config) => {
|
|
|
51
58
|
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
|
|
52
59
|
return {
|
|
53
60
|
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({ hostname: attrs.hostname, maxContentLengthBytes: +attrs.maxContentLengthBytes })),
|
|
54
|
-
auth: mediaConnNode.attrs.auth,
|
|
61
|
+
auth: mediaConnNode.attrs.auth,
|
|
62
|
+
ttl: +mediaConnNode.attrs.ttl,
|
|
63
|
+
fetchDate: new Date()
|
|
55
64
|
}
|
|
56
65
|
})()
|
|
57
66
|
logger.debug('fetched media conn')
|
|
@@ -59,23 +68,35 @@ export const makeMessagesSocket = (config) => {
|
|
|
59
68
|
return mediaConn
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
//
|
|
71
|
+
// ─────────────────────────────────────────────
|
|
72
|
+
// RECEIPTS
|
|
73
|
+
// Sends read receipts and delivery confirmations.
|
|
74
|
+
// ─────────────────────────────────────────────
|
|
63
75
|
const sendReceipt = async (jid, participant, messageIds, type) => {
|
|
64
76
|
if (!messageIds?.length) throw new Boom('missing ids in receipt')
|
|
65
77
|
const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
|
|
66
78
|
const isReadReceipt = type === 'read' || type === 'read-self'
|
|
67
79
|
if (isReadReceipt) node.attrs.t = unixTimestampSeconds().toString()
|
|
68
|
-
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
|
|
69
|
-
|
|
80
|
+
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
|
|
81
|
+
node.attrs.recipient = jid
|
|
82
|
+
node.attrs.to = participant
|
|
83
|
+
} else {
|
|
84
|
+
node.attrs.to = jid
|
|
85
|
+
if (participant) node.attrs.participant = participant
|
|
86
|
+
}
|
|
70
87
|
if (type) node.attrs.type = type
|
|
71
|
-
if (messageIds.length > 1)
|
|
88
|
+
if (messageIds.length > 1) {
|
|
89
|
+
node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
|
|
90
|
+
}
|
|
72
91
|
logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt')
|
|
73
92
|
await sendNode(node)
|
|
74
93
|
}
|
|
75
94
|
|
|
76
95
|
const sendReceipts = async (keys, type) => {
|
|
77
96
|
const recps = aggregateMessageKeysNotFromMe(keys)
|
|
78
|
-
for (const { jid, participant, messageIds } of recps)
|
|
97
|
+
for (const { jid, participant, messageIds } of recps) {
|
|
98
|
+
await sendReceipt(jid, participant, messageIds, type)
|
|
99
|
+
}
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
const readMessages = async (keys) => {
|
|
@@ -83,7 +104,11 @@ export const makeMessagesSocket = (config) => {
|
|
|
83
104
|
await sendReceipts(keys, privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
//
|
|
107
|
+
// ─────────────────────────────────────────────
|
|
108
|
+
// DEVICE & SESSION MANAGEMENT
|
|
109
|
+
// Fetches participant devices via USync and manages
|
|
110
|
+
// Signal protocol sessions for encryption.
|
|
111
|
+
// ─────────────────────────────────────────────
|
|
87
112
|
const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
|
|
88
113
|
const deviceResults = []
|
|
89
114
|
if (!useCache) logger.debug('not using cache for devices')
|
|
@@ -92,26 +117,41 @@ export const makeMessagesSocket = (config) => {
|
|
|
92
117
|
const decoded = jidDecode(jid)
|
|
93
118
|
const user = decoded?.user
|
|
94
119
|
const device = decoded?.device
|
|
120
|
+
// Already has a device number — push directly
|
|
95
121
|
if (typeof device === 'number' && device >= 0 && user) { deviceResults.push({ user, device, jid }); return null }
|
|
96
122
|
return { jid: jidNormalizedUser(jid), user }
|
|
97
123
|
}).filter(Boolean)
|
|
98
124
|
|
|
99
125
|
let mgetDevices
|
|
100
|
-
if (useCache && userDevicesCache.mget)
|
|
126
|
+
if (useCache && userDevicesCache.mget) {
|
|
127
|
+
mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
|
|
128
|
+
}
|
|
101
129
|
|
|
102
130
|
const toFetch = []
|
|
103
131
|
for (const { jid, user } of jidsWithUser) {
|
|
104
132
|
if (useCache) {
|
|
105
133
|
const devices = mgetDevices?.[user] || (userDevicesCache.mget ? undefined : await userDevicesCache.get(user))
|
|
106
|
-
if (devices) {
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
if (devices) {
|
|
135
|
+
deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) })))
|
|
136
|
+
logger.trace({ user }, 'using cache for devices')
|
|
137
|
+
} else {
|
|
138
|
+
toFetch.push(jid)
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
toFetch.push(jid)
|
|
142
|
+
}
|
|
109
143
|
}
|
|
110
144
|
|
|
111
145
|
if (!toFetch.length) return deviceResults
|
|
112
146
|
|
|
147
|
+
// Track which JIDs are LID-based so we can encode them correctly
|
|
113
148
|
const requestedLidUsers = new Set()
|
|
114
|
-
for (const jid of toFetch)
|
|
149
|
+
for (const jid of toFetch) {
|
|
150
|
+
if (isLidUser(jid) || isHostedLidUser(jid)) {
|
|
151
|
+
const user = jidDecode(jid)?.user
|
|
152
|
+
if (user) requestedLidUsers.add(user)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
115
155
|
|
|
116
156
|
const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
|
|
117
157
|
for (const jid of toFetch) query.withUser(new USyncUser().withId(jid))
|
|
@@ -119,10 +159,14 @@ export const makeMessagesSocket = (config) => {
|
|
|
119
159
|
const result = await sock.executeUSyncQuery(query)
|
|
120
160
|
if (result) {
|
|
121
161
|
const lidResults = result.list.filter(a => !!a.lid)
|
|
122
|
-
if (lidResults.length > 0) {
|
|
162
|
+
if (lidResults.length > 0) {
|
|
163
|
+
logger.trace('Storing LID maps from device call')
|
|
164
|
+
await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Pre-assert sessions for newly discovered LID users
|
|
123
168
|
try {
|
|
124
169
|
const lids = lidResults.map(a => a.lid)
|
|
125
|
-
// Re-fetch sessions during device lookup to ensure fresh state
|
|
126
170
|
if (lids.length) await assertSessions(lids, false)
|
|
127
171
|
} catch (e) {
|
|
128
172
|
logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
|
|
@@ -130,62 +174,90 @@ export const makeMessagesSocket = (config) => {
|
|
|
130
174
|
|
|
131
175
|
const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
|
|
132
176
|
const deviceMap = {}
|
|
133
|
-
for (const item of extracted) {
|
|
177
|
+
for (const item of extracted) {
|
|
178
|
+
deviceMap[item.user] = deviceMap[item.user] || []
|
|
179
|
+
deviceMap[item.user]?.push(item)
|
|
180
|
+
}
|
|
134
181
|
|
|
135
182
|
for (const [user, userDevices] of Object.entries(deviceMap)) {
|
|
136
|
-
const
|
|
183
|
+
const isLid = requestedLidUsers.has(user)
|
|
137
184
|
for (const item of userDevices) {
|
|
138
|
-
const finalJid =
|
|
185
|
+
const finalJid = isLid ? jidEncode(user, item.server, item.device) : jidEncode(item.user, item.server, item.device)
|
|
139
186
|
deviceResults.push({ ...item, jid: finalJid })
|
|
140
187
|
}
|
|
141
188
|
}
|
|
142
189
|
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
// Cache results
|
|
191
|
+
if (userDevicesCache.mset) {
|
|
192
|
+
await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
|
|
193
|
+
} else {
|
|
194
|
+
for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
|
195
|
+
}
|
|
145
196
|
|
|
197
|
+
// Persist device lists for session migration support (capped at 500 users)
|
|
146
198
|
const userDeviceUpdates = {}
|
|
147
|
-
for (const [userId, devices] of Object.entries(deviceMap))
|
|
148
|
-
|
|
199
|
+
for (const [userId, devices] of Object.entries(deviceMap)) {
|
|
200
|
+
if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
|
201
|
+
}
|
|
149
202
|
if (Object.keys(userDeviceUpdates).length > 0) {
|
|
150
203
|
try {
|
|
151
204
|
const existingData = await authState.keys.get('device-list', ['_index'])
|
|
152
205
|
const currentBatch = existingData?.['_index'] || {}
|
|
153
206
|
const mergedBatch = { ...currentBatch, ...userDeviceUpdates }
|
|
154
|
-
const userKeys = Object.keys(mergedBatch).sort()
|
|
155
207
|
const trimmedBatch = {}
|
|
156
|
-
|
|
208
|
+
Object.keys(mergedBatch).sort().slice(-500).forEach(userId => { trimmedBatch[userId] = mergedBatch[userId] })
|
|
157
209
|
await authState.keys.set({ 'device-list': { '_index': trimmedBatch } })
|
|
158
210
|
logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(trimmedBatch).length }, 'stored user device lists')
|
|
159
|
-
} catch (error) {
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logger.warn({ error }, 'failed to store user device lists')
|
|
213
|
+
}
|
|
160
214
|
}
|
|
161
215
|
}
|
|
162
216
|
return deviceResults
|
|
163
217
|
}
|
|
164
218
|
|
|
165
219
|
const assertSessions = async (jids, force) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
220
|
+
const uniqueJids = [...new Set(jids)]
|
|
221
|
+
const jidsRequiringFetch = []
|
|
222
|
+
|
|
223
|
+
for (const jid of uniqueJids) {
|
|
224
|
+
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
|
225
|
+
const cachedSession = peerSessionsCache.get(signalId)
|
|
226
|
+
if (cachedSession !== undefined) {
|
|
227
|
+
if (cachedSession && !force) continue
|
|
228
|
+
} else {
|
|
229
|
+
const sessionValidation = await signalRepository.validateSession(jid)
|
|
230
|
+
peerSessionsCache.set(signalId, sessionValidation.exists)
|
|
231
|
+
if (sessionValidation.exists && !force) continue
|
|
232
|
+
}
|
|
233
|
+
jidsRequiringFetch.push(jid)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!jidsRequiringFetch.length) return false
|
|
237
|
+
|
|
238
|
+
// Resolve LID JIDs for wire-level session fetch
|
|
239
|
+
const wireJids = [
|
|
240
|
+
...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)),
|
|
241
|
+
...(await signalRepository.lidMapping.getLIDsForPNs(
|
|
242
|
+
jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))
|
|
243
|
+
) || []).map(a => a.lid)
|
|
244
|
+
]
|
|
177
245
|
|
|
178
|
-
if (jidsRequiringFetch.length) {
|
|
179
|
-
const wireJids = [...jidsRequiringFetch.filter(jid => isLidUser(jid) || isHostedLidUser(jid)), ...(await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => isPnUser(jid) || isHostedPnUser(jid))) || []).map(a => a.lid)]
|
|
180
246
|
logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
|
|
181
|
-
const result = await query({
|
|
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
|
+
})
|
|
182
252
|
await parseAndInjectE2ESessions(result, signalRepository)
|
|
183
|
-
didFetchNewSession = true
|
|
184
253
|
for (const wireJid of wireJids) peerSessionsCache.set(signalRepository.jidToSignalProtocolAddress(wireJid), true)
|
|
254
|
+
return true
|
|
185
255
|
}
|
|
186
|
-
return didFetchNewSession
|
|
187
|
-
}
|
|
188
256
|
|
|
257
|
+
// ─────────────────────────────────────────────
|
|
258
|
+
// PEER DATA OPERATIONS
|
|
259
|
+
// Used for history sync requests and placeholder resends.
|
|
260
|
+
// ─────────────────────────────────────────────
|
|
189
261
|
const sendPeerDataOperationMessage = async (pdoMessage) => {
|
|
190
262
|
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
191
263
|
return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
|
|
@@ -193,62 +265,60 @@ export const makeMessagesSocket = (config) => {
|
|
|
193
265
|
}, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
|
|
194
266
|
}
|
|
195
267
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
for (const node of tokenNodes) {
|
|
202
|
-
const jid = node.attrs.jid
|
|
203
|
-
const token = node.content
|
|
204
|
-
if (jid && token) tokens[jid] = { token, timestamp: Number(unixTimestampSeconds()) }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return tokens
|
|
208
|
-
}
|
|
209
|
-
const updateMemberLabel = (jid, memberLabel) => {
|
|
210
|
-
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
211
|
-
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
212
|
-
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 }] })
|
|
213
|
-
}
|
|
268
|
+
// ─────────────────────────────────────────────
|
|
269
|
+
// TC TOKEN (Trusted Contact Token)
|
|
270
|
+
// Used for end-to-end privacy verification with contacts.
|
|
271
|
+
// ─────────────────────────────────────────────
|
|
272
|
+
const TOKEN_EXPIRY_TTL = 24 * 60 * 60 // 24 hours in seconds
|
|
214
273
|
|
|
215
|
-
const
|
|
216
|
-
if (!
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
let shouldIncludeDeviceIdentity = false
|
|
220
|
-
const meId = authState.creds.me.id
|
|
221
|
-
const meLid = authState.creds.me?.lid
|
|
222
|
-
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
|
274
|
+
const isTokenExpired = (tokenData) => {
|
|
275
|
+
if (!tokenData || !tokenData.timestamp) return true
|
|
276
|
+
return unixTimestampSeconds() - Number(tokenData.timestamp) > TOKEN_EXPIRY_TTL
|
|
277
|
+
}
|
|
223
278
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const {
|
|
230
|
-
|
|
231
|
-
const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
|
|
232
|
-
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
233
|
-
if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
|
|
279
|
+
const parseTCTokens = (result) => {
|
|
280
|
+
const tokens = {}
|
|
281
|
+
const tokenList = getBinaryNodeChild(result, 'tokens')
|
|
282
|
+
if (tokenList) {
|
|
283
|
+
for (const node of getBinaryNodeChildren(tokenList, 'token')) {
|
|
284
|
+
const { jid, content } = { jid: node.attrs.jid, content: node.content }
|
|
285
|
+
if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
|
|
234
286
|
}
|
|
235
|
-
const bytes = encodeWAMessage(msgToEncrypt)
|
|
236
|
-
return await encryptionMutex.mutex(jid, async () => {
|
|
237
|
-
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
|
|
238
|
-
if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
|
|
239
|
-
return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
|
|
240
|
-
})
|
|
241
|
-
} catch (err) {
|
|
242
|
-
logger.error({jid, err }, 'Failed to encrypt for recipient')
|
|
243
|
-
return null
|
|
244
287
|
}
|
|
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' } })) }]
|
|
245
297
|
})
|
|
298
|
+
const tokens = parseTCTokens(result)
|
|
299
|
+
if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
|
|
300
|
+
return tokens
|
|
301
|
+
}
|
|
246
302
|
|
|
247
|
-
|
|
248
|
-
|
|
303
|
+
// ─────────────────────────────────────────────
|
|
304
|
+
// GROUP MEMBER LABEL
|
|
305
|
+
// Sets a custom label for a member in a group.
|
|
306
|
+
// ─────────────────────────────────────────────
|
|
307
|
+
const updateMemberLabel = (jid, memberLabel) => {
|
|
308
|
+
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
309
|
+
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
310
|
+
return relayMessage(jid, {
|
|
311
|
+
protocolMessage: {
|
|
312
|
+
type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE,
|
|
313
|
+
memberLabel: { label: memberLabel.slice(0, 30), labelTimestamp: unixTimestampSeconds() }
|
|
314
|
+
}
|
|
315
|
+
}, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
|
|
249
316
|
}
|
|
250
317
|
|
|
251
|
-
//
|
|
318
|
+
// ─────────────────────────────────────────────
|
|
319
|
+
// MESSAGE TYPE DETECTION
|
|
320
|
+
// Classifies messages for proper stanza type attribute.
|
|
321
|
+
// ─────────────────────────────────────────────
|
|
252
322
|
const getMessageType = (msg) => {
|
|
253
323
|
const message = normalizeMessageContent(msg)
|
|
254
324
|
if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
|
|
@@ -282,266 +352,375 @@ export const makeMessagesSocket = (config) => {
|
|
|
282
352
|
if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
|
|
283
353
|
}
|
|
284
354
|
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// Check if message is already in proper WAMessage format
|
|
294
|
-
const hasProtoMessageType = Object.keys(message).some(key => key.endsWith('Message') || key === 'conversation')
|
|
295
|
-
|
|
296
|
-
if (!hasProtoMessageType) {
|
|
297
|
-
logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
|
|
298
|
-
const generatedMsg = await generateWAMessage(jid, message, { logger, userJid: meId, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }), getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, messageId: finalMsgId || generateMessageIDV2(meId), quoted: quoted })
|
|
299
|
-
message = generatedMsg.message
|
|
300
|
-
if (!finalMsgId) finalMsgId = generatedMsg.key.id
|
|
301
|
-
logger.debug({ msgId: finalMsgId, jid }, 'message auto-generated successfully')
|
|
302
|
-
}
|
|
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
|
+
const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
|
|
361
|
+
if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
|
|
303
362
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
363
|
+
const patched = await patchMessageBeforeSending(message, recipientJids)
|
|
364
|
+
const patchedMessages = Array.isArray(patched) ? patched : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
|
|
365
|
+
let shouldIncludeDeviceIdentity = false
|
|
366
|
+
|
|
367
|
+
const meId = authState.creds.me.id
|
|
368
|
+
const meLid = authState.creds.me?.lid
|
|
369
|
+
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
|
370
|
+
|
|
371
|
+
const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
|
|
372
|
+
try {
|
|
373
|
+
if (!jid) return null
|
|
374
|
+
let msgToEncrypt = patchedMessage
|
|
375
|
+
|
|
376
|
+
// Use Device Sent Message (DSM) for own linked devices so they can read the message
|
|
377
|
+
if (dsmMessage) {
|
|
378
|
+
const { user: targetUser } = jidDecode(jid)
|
|
379
|
+
const { user: ownPnUser } = jidDecode(meId)
|
|
380
|
+
const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
|
|
381
|
+
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
382
|
+
if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const bytes = encodeWAMessage(msgToEncrypt)
|
|
386
|
+
return await encryptionMutex.mutex(jid, async () => {
|
|
387
|
+
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
|
|
388
|
+
if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
|
|
389
|
+
return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
|
|
390
|
+
})
|
|
391
|
+
} catch (err) {
|
|
392
|
+
logger.error({ jid, err }, 'Failed to encrypt for recipient')
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
|
|
398
|
+
return { nodes, shouldIncludeDeviceIdentity }
|
|
328
399
|
}
|
|
329
400
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
|
|
413
|
+
const meId = authState.creds.me.id
|
|
414
|
+
const meLid = authState.creds.me?.lid
|
|
415
|
+
const { user, server } = jidDecode(jid)
|
|
416
|
+
const isGroup = server === 'g.us'
|
|
417
|
+
const isStatus = jid === 'status@broadcast'
|
|
418
|
+
const isLid = server === 'lid'
|
|
419
|
+
const isNewsletter = server === 'newsletter'
|
|
420
|
+
|
|
421
|
+
// Choose sender identity (PN or LID) based on destination
|
|
422
|
+
let activeSender = meId
|
|
423
|
+
let groupAddressingMode = 'pn'
|
|
424
|
+
if (isGroup && !isStatus) {
|
|
425
|
+
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
426
|
+
groupAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
427
|
+
if (groupAddressingMode === 'lid' && meLid) activeSender = meLid
|
|
428
|
+
} else if (isLid && meLid) {
|
|
429
|
+
activeSender = meLid
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const isRetryResend = Boolean(participant?.jid)
|
|
433
|
+
let shouldIncludeDeviceIdentity = isRetryResend
|
|
434
|
+
let finalMsgId = msgId
|
|
333
435
|
|
|
334
|
-
if
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
436
|
+
// Auto-generate WAMessage from raw content if needed
|
|
437
|
+
const hasProtoMessageType = Object.keys(message).some(key => key.endsWith('Message') || key === 'conversation')
|
|
438
|
+
if (!hasProtoMessageType) {
|
|
439
|
+
logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
|
|
440
|
+
const generatedMsg = await generateWAMessage(jid, message, {
|
|
441
|
+
logger, userJid: jidNormalizedUser(activeSender),
|
|
442
|
+
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
443
|
+
getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink,
|
|
444
|
+
upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
|
|
445
|
+
messageId: finalMsgId || generateMessageIDV2(activeSender), quoted
|
|
446
|
+
})
|
|
447
|
+
message = generatedMsg.message
|
|
448
|
+
if (!finalMsgId) finalMsgId = generatedMsg.key.id
|
|
449
|
+
logger.debug({ msgId: finalMsgId, jid }, 'message auto-generated successfully')
|
|
342
450
|
}
|
|
343
451
|
|
|
344
|
-
|
|
452
|
+
finalMsgId = finalMsgId || generateMessageIDV2(activeSender)
|
|
453
|
+
useUserDevicesCache = useUserDevicesCache !== false
|
|
454
|
+
useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
|
|
455
|
+
|
|
456
|
+
const participants = []
|
|
457
|
+
const destinationJid = !isStatus ? jid : 'status@broadcast'
|
|
458
|
+
const binaryNodeContent = []
|
|
459
|
+
const devices = []
|
|
460
|
+
const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
|
|
461
|
+
const extraAttrs = {}
|
|
462
|
+
const messages = normalizeMessageContent(message)
|
|
463
|
+
const buttonType = getButtonType(messages)
|
|
464
|
+
|
|
465
|
+
let hasDeviceFanoutFalse = false
|
|
466
|
+
if (participant) {
|
|
467
|
+
if (!isGroup && !isStatus) hasDeviceFanoutFalse = true
|
|
468
|
+
const { user, device } = jidDecode(participant.jid)
|
|
469
|
+
devices.push({ user, device, jid: participant.jid })
|
|
470
|
+
}
|
|
345
471
|
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
472
|
+
await authState.keys.transaction(async () => {
|
|
473
|
+
const mediaType = getMediaType(message)
|
|
474
|
+
if (mediaType) extraAttrs.mediatype = mediaType
|
|
475
|
+
|
|
476
|
+
// ── Newsletter: plaintext encoding, no encryption ──
|
|
477
|
+
if (isNewsletter) {
|
|
478
|
+
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
|
479
|
+
binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
|
|
480
|
+
await sendNode({ tag: 'message', attrs: { to: jid, id: finalMsgId, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent })
|
|
481
|
+
logger.debug({ msgId: finalMsgId }, `sending newsletter message to ${jid}`)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (messages.pinInChatMessage || messages.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
|
|
486
|
+
extraAttrs['decrypt-fail'] = 'hide'
|
|
487
|
+
}
|
|
356
488
|
|
|
357
|
-
|
|
489
|
+
// ── Group / Status: sender key (SKDM + skmsg) ──
|
|
490
|
+
if ((isGroup || isStatus) && !isRetryResend) {
|
|
491
|
+
const [groupData] = await Promise.all([
|
|
492
|
+
(async () => {
|
|
493
|
+
let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
494
|
+
if (groupData?.participants) logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
|
|
495
|
+
else if (!isStatus) groupData = await groupMetadata(jid)
|
|
496
|
+
return groupData
|
|
497
|
+
})(),
|
|
498
|
+
Promise.resolve({}) // senderKeyMap intentionally always empty — forces fresh SKDM every send
|
|
499
|
+
])
|
|
500
|
+
|
|
501
|
+
// Build participant list
|
|
358
502
|
const participantsList = []
|
|
359
|
-
if (isStatus) {
|
|
360
|
-
|
|
503
|
+
if (isStatus) {
|
|
504
|
+
if (statusJidList?.length) participantsList.push(...statusJidList)
|
|
505
|
+
} else {
|
|
361
506
|
let groupAddressingMode = 'lid'
|
|
362
507
|
if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData?.addressingMode || groupAddressingMode }
|
|
363
508
|
additionalAttributes = { ...additionalAttributes, addressing_mode: groupAddressingMode }
|
|
364
509
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for (const jid of participantsList) {
|
|
369
|
-
const { user, server } = jidDecode(jid)
|
|
370
|
-
if (user) {
|
|
371
|
-
device0EntriesGroup.push({ user, device: 0, jid: jidEncode(user, server, 0) })
|
|
372
|
-
}
|
|
510
|
+
|
|
511
|
+
if (groupData?.ephemeralDuration > 0) {
|
|
512
|
+
additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
373
513
|
}
|
|
374
|
-
|
|
514
|
+
|
|
375
515
|
const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
for
|
|
380
|
-
|
|
381
|
-
|
|
516
|
+
devices.push(...additionalDevices)
|
|
517
|
+
|
|
518
|
+
// Ensure Device 0 (primary phone) is always included for every participant.
|
|
519
|
+
// USync sometimes omits Device 0 for LID groups to save bandwidth,
|
|
520
|
+
// which would cause recipients to miss the SKDM on first send.
|
|
521
|
+
for (const pJid of participantsList) {
|
|
522
|
+
const decoded = jidDecode(pJid)
|
|
523
|
+
if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
|
|
524
|
+
devices.push({ user: decoded.user, device: 0, server: decoded.server, domainType: decoded.domainType, jid: jidEncode(decoded.user, decoded.server, 0) })
|
|
525
|
+
}
|
|
382
526
|
}
|
|
383
|
-
devices.push(...Array.from(deviceMap.values()))
|
|
384
|
-
}
|
|
385
527
|
|
|
386
|
-
|
|
528
|
+
const patched = await patchMessageBeforeSending(message)
|
|
529
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
530
|
+
|
|
531
|
+
const bytes = encodeWAMessage(patched)
|
|
532
|
+
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
533
|
+
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
534
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
|
|
535
|
+
|
|
536
|
+
// Send SKDM to ALL devices every time (senderKeyMap is always {})
|
|
537
|
+
const senderKeyRecipients = devices
|
|
538
|
+
.filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
|
|
539
|
+
.map(d => d.jid)
|
|
540
|
+
|
|
541
|
+
if (senderKeyRecipients.length) {
|
|
542
|
+
logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
|
|
543
|
+
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
544
|
+
await assertSessions(senderKeyRecipients)
|
|
545
|
+
const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, {})
|
|
546
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
|
|
547
|
+
participants.push(...result.nodes)
|
|
548
|
+
}
|
|
387
549
|
|
|
388
|
-
|
|
389
|
-
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
550
|
+
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
|
|
390
551
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
552
|
+
// ── Group Retry: direct pairwise re-encrypt for specific participant ──
|
|
553
|
+
} else if ((isGroup || isStatus) && isRetryResend) {
|
|
554
|
+
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
555
|
+
if (!groupData && !isStatus) await groupMetadata(jid)
|
|
394
556
|
|
|
395
|
-
|
|
557
|
+
if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
558
|
+
additionalAttributes = { ...additionalAttributes, addressing_mode: groupData?.addressingMode || 'lid' }
|
|
396
559
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const deviceJid = device.jid
|
|
400
|
-
const hasKey = !!senderKeyMap[deviceJid]
|
|
401
|
-
if ((!hasKey || !!participant) && !isHostedLidUser(deviceJid) && !isHostedPnUser(deviceJid) && device.device !== 99) { senderKeyRecipients.push(deviceJid); senderKeyMap[deviceJid] = true }
|
|
402
|
-
}
|
|
560
|
+
const patched = await patchMessageBeforeSending(message)
|
|
561
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
403
562
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
563
|
+
const bytes = encodeWAMessage(patched)
|
|
564
|
+
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
565
|
+
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
566
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
|
|
567
|
+
|
|
568
|
+
// Send fresh SKDM directly to the requesting participant
|
|
407
569
|
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
408
|
-
await assertSessions(
|
|
409
|
-
const
|
|
410
|
-
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity ||
|
|
411
|
-
participants.push(...
|
|
412
|
-
}
|
|
570
|
+
await assertSessions([participant.jid])
|
|
571
|
+
const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
|
|
572
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
|
|
573
|
+
participants.push(...skResult.nodes)
|
|
413
574
|
|
|
414
|
-
if (isRetryResend) {
|
|
415
575
|
const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: bytes, jid: participant?.jid })
|
|
416
576
|
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
|
|
577
|
+
|
|
578
|
+
// ── DM / LID: standard pairwise encryption ──
|
|
417
579
|
} else {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
} else {
|
|
422
|
-
let ownId = meId
|
|
423
|
-
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
580
|
+
let ownId = meId
|
|
581
|
+
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
424
582
|
|
|
425
|
-
|
|
583
|
+
const { user: ownUser } = jidDecode(ownId)
|
|
426
584
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
585
|
+
if (!participant) {
|
|
586
|
+
const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
587
|
+
devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
|
|
430
588
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
589
|
+
if (user !== ownUser) {
|
|
590
|
+
const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
591
|
+
const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
|
|
592
|
+
devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
|
|
593
|
+
}
|
|
436
594
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
595
|
+
if (additionalAttributes?.category !== 'peer') {
|
|
596
|
+
// Preserve Device 0 entries before USync refetch which may omit them
|
|
597
|
+
const device0Entries = devices.filter(d => d.device === 0)
|
|
598
|
+
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
599
|
+
devices.length = 0
|
|
600
|
+
const senderIdentity = isLid && meLid
|
|
601
|
+
? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
|
|
602
|
+
: jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
|
|
603
|
+
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
604
|
+
devices.push(...device0Entries, ...sessionDevices)
|
|
605
|
+
|
|
606
|
+
// Explicitly fetch sender's linked devices if not returned by USync
|
|
607
|
+
if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
|
|
608
|
+
const senderDevices = await getUSyncDevices([senderIdentity], true, false)
|
|
609
|
+
const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
|
|
610
|
+
if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
|
|
611
|
+
}
|
|
451
612
|
}
|
|
452
613
|
}
|
|
453
|
-
}
|
|
454
614
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
615
|
+
const allRecipients = [], meRecipients = [], otherRecipients = []
|
|
616
|
+
const { user: mePnUser } = jidDecode(meId)
|
|
617
|
+
const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
|
|
618
|
+
|
|
619
|
+
for (const { user, jid } of devices) {
|
|
620
|
+
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
621
|
+
if (isExactSenderDevice) continue
|
|
622
|
+
const isMe = user === mePnUser || user === meLidUser
|
|
623
|
+
if (isMe) meRecipients.push(jid)
|
|
624
|
+
else otherRecipients.push(jid)
|
|
625
|
+
allRecipients.push(jid)
|
|
626
|
+
}
|
|
467
627
|
|
|
468
|
-
|
|
628
|
+
await assertSessions(allRecipients)
|
|
469
629
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
630
|
+
const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
|
|
631
|
+
createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
|
|
632
|
+
createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
|
|
633
|
+
])
|
|
474
634
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
635
|
+
participants.push(...meNodes, ...otherNodes)
|
|
636
|
+
if (meRecipients.length > 0 || otherRecipients.length > 0) {
|
|
637
|
+
extraAttrs.phash = generateParticipantHashV2([...meRecipients, ...otherRecipients])
|
|
638
|
+
}
|
|
639
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
|
640
|
+
}
|
|
479
641
|
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
642
|
+
// ── Build final stanza ──
|
|
643
|
+
if (participants.length) {
|
|
644
|
+
if (additionalAttributes?.category === 'peer') {
|
|
645
|
+
const peerNode = participants[0]?.content?.[0]
|
|
646
|
+
if (peerNode) binaryNodeContent.push(peerNode)
|
|
647
|
+
} else {
|
|
648
|
+
binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
|
|
649
|
+
}
|
|
650
|
+
}
|
|
484
651
|
|
|
485
|
-
|
|
652
|
+
const stanza = {
|
|
653
|
+
tag: 'message',
|
|
654
|
+
attrs: {
|
|
655
|
+
id: finalMsgId,
|
|
656
|
+
to: destinationJid,
|
|
657
|
+
type: getMessageType(message),
|
|
658
|
+
...(isLid || (isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
|
|
659
|
+
...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}),
|
|
660
|
+
...(additionalAttributes || {})
|
|
661
|
+
},
|
|
662
|
+
content: binaryNodeContent
|
|
663
|
+
}
|
|
486
664
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (!isNewsletter && buttonType) {
|
|
495
|
-
const buttonsNode = getButtonArgs(messages)
|
|
496
|
-
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
497
|
-
if (filteredButtons) { stanza.content.push(...additionalNodes); additionalAlready = true }
|
|
498
|
-
else stanza.content.push(buttonsNode)
|
|
499
|
-
}
|
|
665
|
+
if (participant) {
|
|
666
|
+
if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid; stanza.attrs.participant = participant.jid }
|
|
667
|
+
else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid; stanza.attrs.recipient = destinationJid }
|
|
668
|
+
else stanza.attrs.to = participant.jid
|
|
669
|
+
} else {
|
|
670
|
+
stanza.attrs.to = destinationJid
|
|
671
|
+
}
|
|
500
672
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
673
|
+
// Attach button metadata if needed
|
|
674
|
+
if (!isNewsletter && buttonType) {
|
|
675
|
+
const buttonsNode = getButtonArgs(messages)
|
|
676
|
+
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
677
|
+
if (filteredButtons) { stanza.content.push(...additionalNodes) }
|
|
678
|
+
else stanza.content.push(buttonsNode)
|
|
679
|
+
} else if (additionalNodes?.length > 0) {
|
|
680
|
+
stanza.content.push(...additionalNodes)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Attach device identity for LID groups and pkmsg sessions
|
|
684
|
+
if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
|
|
685
|
+
stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
|
|
686
|
+
logger.debug({ jid }, 'adding device identity')
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Attach TC token for DM messages (auto-refresh if expired)
|
|
690
|
+
if (!isGroup && !isRetryResend && !isStatus) {
|
|
691
|
+
const contactTcTokenData = await authState.keys.get('tctoken', [destinationJid])
|
|
692
|
+
let tcTokenBuffer = contactTcTokenData[destinationJid]?.token
|
|
693
|
+
if (isTokenExpired(contactTcTokenData[destinationJid])) {
|
|
694
|
+
logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
|
|
695
|
+
try {
|
|
696
|
+
const freshTokens = await getPrivacyTokens([destinationJid])
|
|
697
|
+
tcTokenBuffer = freshTokens[destinationJid]?.token
|
|
698
|
+
} catch (err) {
|
|
699
|
+
logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
703
|
+
}
|
|
521
704
|
|
|
522
705
|
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
|
523
706
|
await sendNode(stanza)
|
|
524
707
|
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, msgId, message)
|
|
525
|
-
}, meId)
|
|
526
|
-
|
|
527
|
-
return {key: {remoteJid: jid, fromMe: true, id: finalMsgId, participant: isGroup ? authState.creds.me.id : undefined}, messageId: finalMsgId}
|
|
528
|
-
}
|
|
529
708
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
709
|
+
}, activeSender)
|
|
710
|
+
|
|
711
|
+
const isSelf = areJidsSameUser(jid, meId) || (meLid && areJidsSameUser(jid, meLid))
|
|
712
|
+
const returnParticipant = (isGroup || isSelf) ? jidNormalizedUser(activeSender) : undefined
|
|
713
|
+
return {
|
|
714
|
+
key: {
|
|
715
|
+
remoteJid: jid,
|
|
716
|
+
fromMe: true,
|
|
717
|
+
id: finalMsgId,
|
|
718
|
+
participant: returnParticipant,
|
|
719
|
+
addressingMode: (isLid || (isGroup && groupAddressingMode === 'lid')) ? 'lid' : 'pn'
|
|
720
|
+
},
|
|
721
|
+
messageId: finalMsgId
|
|
722
|
+
}
|
|
536
723
|
}
|
|
537
|
-
|
|
538
|
-
const getPrivacyTokens = async (jids) => {
|
|
539
|
-
const t = unixTimestampSeconds().toString()
|
|
540
|
-
const result = await 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' } })) }] })
|
|
541
|
-
const tokens = parseTCTokens(result)
|
|
542
|
-
if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
|
|
543
|
-
return tokens
|
|
544
|
-
}
|
|
545
724
|
|
|
546
725
|
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
|
|
547
726
|
const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me })
|
|
@@ -549,10 +728,15 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
549
728
|
|
|
550
729
|
return {
|
|
551
730
|
...sock,
|
|
552
|
-
getPrivacyTokens, assertSessions, relayMessage, sendReceipt, sendReceipts, nexus,
|
|
553
|
-
refreshMediaConn, waUploadToServer, fetchPrivacySettings,
|
|
554
|
-
createParticipantNodes, getUSyncDevices,
|
|
555
|
-
|
|
731
|
+
getPrivacyTokens, assertSessions, relayMessage, sendReceipt, sendReceipts, nexus,
|
|
732
|
+
readMessages, refreshMediaConn, waUploadToServer, fetchPrivacySettings,
|
|
733
|
+
sendPeerDataOperationMessage, createParticipantNodes, getUSyncDevices,
|
|
734
|
+
messageRetryManager, updateMemberLabel,
|
|
735
|
+
|
|
736
|
+
// ─────────────────────────────────────────────
|
|
737
|
+
// UPDATE MEDIA MESSAGE
|
|
738
|
+
// Re-requests media upload for expired/missing media.
|
|
739
|
+
// ─────────────────────────────────────────────
|
|
556
740
|
updateMediaMessage: async (message) => {
|
|
557
741
|
const content = assertMediaContent(message.message)
|
|
558
742
|
const mediaKey = content.mediaKey
|
|
@@ -562,11 +746,14 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
562
746
|
await Promise.all([sendNode(node), waitForMsgMediaUpdate(async (update) => {
|
|
563
747
|
const result = update.find(c => c.key.id === message.key.id)
|
|
564
748
|
if (result) {
|
|
565
|
-
if (result.error)
|
|
566
|
-
|
|
749
|
+
if (result.error) {
|
|
750
|
+
error = result.error
|
|
751
|
+
} else {
|
|
567
752
|
try {
|
|
568
753
|
const media = await decryptMediaRetryData(result.media, mediaKey, result.key.id)
|
|
569
|
-
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS)
|
|
754
|
+
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
|
|
755
|
+
throw new Boom(`Media re-upload failed (${proto.MediaRetryNotification.ResultType[media.result]})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
|
|
756
|
+
}
|
|
570
757
|
content.directPath = media.directPath
|
|
571
758
|
content.url = getUrlFromDirectPath(content.directPath)
|
|
572
759
|
logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful')
|
|
@@ -580,34 +767,50 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
580
767
|
return message
|
|
581
768
|
},
|
|
582
769
|
|
|
770
|
+
// ─────────────────────────────────────────────
|
|
771
|
+
// SEND STATUS MENTIONS
|
|
772
|
+
// Broadcasts a status update and notifies mentioned
|
|
773
|
+
// users or groups via statusMentionMessage protocol.
|
|
774
|
+
// ─────────────────────────────────────────────
|
|
583
775
|
sendStatusMentions: async (content, jids = []) => {
|
|
584
776
|
const userJid = jidNormalizedUser(authState.creds.me.id)
|
|
585
777
|
const allUsers = new Set([userJid])
|
|
586
778
|
for (const id of jids) {
|
|
587
|
-
if (isJidGroup(id)) {
|
|
588
|
-
|
|
779
|
+
if (isJidGroup(id)) {
|
|
780
|
+
try { const metadata = await cachedGroupMetadata(id) || await groupMetadata(id); metadata.participants.forEach(p => allUsers.add(jidNormalizedUser(p.id))) }
|
|
781
|
+
catch (error) { logger.error(`Error getting metadata for ${id}: ${error}`) }
|
|
782
|
+
} else if (isJidUser(id)) {
|
|
783
|
+
allUsers.add(jidNormalizedUser(id))
|
|
784
|
+
}
|
|
589
785
|
}
|
|
590
|
-
|
|
786
|
+
|
|
591
787
|
const getRandomHex = () => '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')
|
|
592
788
|
const isMedia = content.image || content.video || content.audio
|
|
593
789
|
const isAudio = !!content.audio
|
|
594
790
|
const msgContent = { ...content }
|
|
595
791
|
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 }
|
|
596
792
|
if (isAudio) { delete msgContent.text; delete msgContent.caption; delete msgContent.font; delete msgContent.textColor }
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const backgroundColor = (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined
|
|
600
|
-
const ptt = isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined
|
|
601
|
-
let msg, mediaHandle
|
|
793
|
+
|
|
794
|
+
let msg
|
|
602
795
|
try {
|
|
603
796
|
msg = await generateWAMessage(STORIES_JID, msgContent, {
|
|
604
797
|
logger, userJid,
|
|
605
798
|
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
606
|
-
upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts });
|
|
607
|
-
mediaCache: config.mediaCache, options: config.options,
|
|
799
|
+
upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts }); return up },
|
|
800
|
+
mediaCache: config.mediaCache, options: config.options,
|
|
801
|
+
font: !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined,
|
|
802
|
+
textColor: !isMedia ? (content.textColor || getRandomHex()) : undefined,
|
|
803
|
+
backgroundColor: (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined,
|
|
804
|
+
ptt: isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined
|
|
608
805
|
})
|
|
609
806
|
} catch (error) { logger.error(`Error generating message: ${error}`); throw error }
|
|
610
|
-
|
|
807
|
+
|
|
808
|
+
await relayMessage(STORIES_JID, msg.message, {
|
|
809
|
+
messageId: msg.key.id,
|
|
810
|
+
statusJidList: Array.from(allUsers),
|
|
811
|
+
additionalNodes: [{ tag: 'meta', attrs: {}, content: [{ tag: 'mentioned_users', attrs: {}, content: jids.map(jid => ({ tag: 'to', attrs: { jid: jidNormalizedUser(jid) } })) }] }]
|
|
812
|
+
})
|
|
813
|
+
|
|
611
814
|
for (const id of jids) {
|
|
612
815
|
try {
|
|
613
816
|
const normalizedId = jidNormalizedUser(id)
|
|
@@ -622,6 +825,7 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
622
825
|
return msg
|
|
623
826
|
},
|
|
624
827
|
|
|
828
|
+
// Nexus handler shortcuts for rich message types
|
|
625
829
|
sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
|
|
626
830
|
sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
|
|
627
831
|
sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
|
|
@@ -635,51 +839,131 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
635
839
|
sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
|
|
636
840
|
stickerPackMessage: (jid, data, options) => nexus.handleStickerPack(data, jid, options?.quoted),
|
|
637
841
|
|
|
842
|
+
// ─────────────────────────────────────────────
|
|
843
|
+
// SEND MESSAGE
|
|
844
|
+
// Main public API for sending any message type.
|
|
845
|
+
// Handles delete normalization, LID/PN identity selection,
|
|
846
|
+
// message generation, and relay.
|
|
847
|
+
// ─────────────────────────────────────────────
|
|
638
848
|
sendMessage: async (jid, content, options = {}) => {
|
|
639
|
-
const
|
|
849
|
+
const meId = authState.creds.me.id
|
|
850
|
+
const meLid = authState.creds.me?.lid
|
|
851
|
+
const { server } = jidDecode(jid)
|
|
852
|
+
const isGroup = server === 'g.us'
|
|
853
|
+
const isDestinationLid = server === 'lid'
|
|
854
|
+
const useCache = options.useCachedGroupMetadata !== false
|
|
640
855
|
const { quoted } = options
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
644
876
|
const messageType = nexus.detectType(content)
|
|
645
877
|
if (messageType) return await nexus.processMessage(content, jid, quoted)
|
|
646
878
|
|
|
879
|
+
// Handle disappearing message toggle for groups
|
|
647
880
|
if (content.disappearingMessagesInChat && isJidGroup(jid)) {
|
|
648
|
-
const value = typeof content.disappearingMessagesInChat === 'boolean'
|
|
881
|
+
const value = typeof content.disappearingMessagesInChat === 'boolean'
|
|
882
|
+
? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
|
|
883
|
+
: content.disappearingMessagesInChat
|
|
649
884
|
await groupToggleEphemeral(jid, value)
|
|
650
885
|
return
|
|
651
886
|
}
|
|
652
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
|
|
653
922
|
const fullMsg = await generateWAMessage(jid, content, {
|
|
654
|
-
logger,
|
|
923
|
+
logger,
|
|
924
|
+
userJid: jidNormalizedUser(activeSender),
|
|
655
925
|
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
656
|
-
getProfilePicUrl: sock.profilePictureUrl,
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
659
933
|
})
|
|
660
934
|
|
|
935
|
+
// Build additional stanza attributes
|
|
661
936
|
const additionalAttributes = {}, additionalNodes = []
|
|
662
|
-
if (content.delete)
|
|
663
|
-
|
|
664
|
-
else if (content.
|
|
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
|
+
}
|
|
665
944
|
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
666
945
|
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
667
946
|
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
+
}
|
|
676
955
|
|
|
677
956
|
await relayMessage(jid, fullMsg.message, {
|
|
678
|
-
messageId: fullMsg.key.id,
|
|
679
|
-
|
|
957
|
+
messageId: fullMsg.key.id,
|
|
958
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
959
|
+
additionalAttributes,
|
|
960
|
+
statusJidList: options.statusJidList,
|
|
961
|
+
additionalNodes
|
|
680
962
|
})
|
|
681
963
|
|
|
682
|
-
if (config.emitOwnEvents)
|
|
964
|
+
if (config.emitOwnEvents) {
|
|
965
|
+
process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
|
|
966
|
+
}
|
|
683
967
|
return fullMsg
|
|
684
968
|
}
|
|
685
969
|
}
|