@nexustechpro/baileys 2.0.1 → 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.
@@ -34,7 +34,10 @@ export const makeMessagesSocket = (config) => {
34
34
  } = config
35
35
 
36
36
  const sock = makeNewsletterSocket(config)
37
- const { ev, authState, processingMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral } = sock
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
- // ===== MEDIA CONNECTION =====
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, ttl: +mediaConnNode.attrs.ttl, fetchDate: new Date()
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
- // ===== RECEIPTS =====
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))) { node.attrs.recipient = jid; node.attrs.to = participant }
69
- else { node.attrs.to = jid; if (participant) node.attrs.participant = participant }
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) node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
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) await sendReceipt(jid, participant, messageIds, type)
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
- // ===== DEVICES & SESSIONS =====
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) mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
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) { deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) }))); logger.trace({ user }, 'using cache for devices') }
107
- else toFetch.push(jid)
108
- } else toFetch.push(jid)
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) if (isLidUser(jid) || isHostedLidUser(jid)) { const user = jidDecode(jid)?.user; if (user) requestedLidUsers.add(user) }
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) { logger.trace('Storing LID maps from device call'); await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id }))) }
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) { deviceMap[item.user] = deviceMap[item.user] || []; deviceMap[item.user]?.push(item) }
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 isLidUser = requestedLidUsers.has(user)
183
+ const isLid = requestedLidUsers.has(user)
137
184
  for (const item of userDevices) {
138
- const finalJid = isLidUser ? jidEncode(user, item.server, item.device) : jidEncode(item.user, item.server, item.device)
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
- if (userDevicesCache.mset) await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
144
- else for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
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)) if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
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
- userKeys.slice(-500).forEach(userId => { trimmedBatch[userId] = mergedBatch[userId] })
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) { logger.warn({ error }, 'failed to store user device lists') }
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
- let didFetchNewSession = false
167
- const uniqueJids = [...new Set(jids)]
168
- const jidsRequiringFetch = []
169
-
170
- for (const jid of uniqueJids) {
171
- const signalId = signalRepository.jidToSignalProtocolAddress(jid)
172
- const cachedSession = peerSessionsCache.get(signalId)
173
- if (cachedSession !== undefined) { if (cachedSession && !force) continue } // ← Add && !force
174
- else { const sessionValidation = await signalRepository.validateSession(jid); peerSessionsCache.set(signalId, sessionValidation.exists); if (sessionValidation.exists && !force) continue } // ← Add && !force
175
- jidsRequiringFetch.push(jid)
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({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: wireJids.map(jid => { const attrs = { jid }; if (force) attrs.reason = 'identity'; return { tag: 'user', attrs } }) }] })
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
- const parseTCTokens = (result) => {
197
- const tokens = {}
198
- const tokenList = getBinaryNodeChild(result, 'tokens')
199
- if (tokenList) {
200
- const tokenNodes = getBinaryNodeChildren(tokenList, 'token')
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 createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
216
- if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
217
- const patched = await patchMessageBeforeSending(message, recipientJids)
218
- const patchedMessages = Array.isArray(patched) ? patched : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
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
- const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
225
- try {
226
- if (!jid) return null
227
- let msgToEncrypt = patchedMessage
228
- if (dsmMessage) {
229
- const { user: targetUser } = jidDecode(jid)
230
- const { user: ownPnUser } = jidDecode(meId)
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
- const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
248
- return { nodes, shouldIncludeDeviceIdentity }
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
- // ===== MESSAGE HELPERS =====
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
- // ===== RELAY MESSAGE =====
286
- const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
287
- const meId = authState.creds.me.id
288
- const meLid = authState.creds.me?.lid
289
- const isRetryResend = Boolean(participant?.jid)
290
- let shouldIncludeDeviceIdentity = isRetryResend
291
- let finalMsgId = msgId
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
- const { user, server } = jidDecode(jid)
305
- const isGroup = server === 'g.us'
306
- const isStatus = jid === 'status@broadcast'
307
- const isLid = server === 'lid'
308
- const isNewsletter = server === 'newsletter'
309
-
310
- finalMsgId = finalMsgId || generateMessageIDV2(meId)
311
- useUserDevicesCache = useUserDevicesCache !== false
312
- useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
313
-
314
- const participants = []
315
- const destinationJid = !isStatus ? jid : 'status@broadcast'
316
- const binaryNodeContent = []
317
- const devices = []
318
-
319
- const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
320
- const extraAttrs = {}
321
- const messages = normalizeMessageContent(message)
322
- const buttonType = getButtonType(messages)
323
-
324
- if (participant) {
325
- if (!isGroup && !isStatus) additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
326
- const { user, device } = jidDecode(participant.jid)
327
- devices.push({ user, device, jid: participant.jid })
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
- await authState.keys.transaction(async () => {
331
- const mediaType = getMediaType(message)
332
- if (mediaType) extraAttrs.mediatype = mediaType
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 (isNewsletter) {
335
- const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
336
- const bytes = encodeNewsletterMessage(patched)
337
- binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: bytes })
338
- const stanza = { tag: 'message', attrs: { to: jid, id: finalMsgId, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent }
339
- logger.debug({ msgId: finalMsgId }, `sending newsletter message to ${jid}`)
340
- await sendNode(stanza)
341
- return
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
- if (messages.pinInChatMessage || messages.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) extraAttrs['decrypt-fail'] = 'hide'
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
- if (isGroup || isStatus) {
347
- const [groupData, senderKeyMap] = await Promise.all([
348
- (async () => {
349
- let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
350
- if (groupData?.participants) logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
351
- else if (!isStatus) groupData = await groupMetadata(jid)
352
- return groupData
353
- })(),
354
- (async () => !participant && !isStatus ? (await authState.keys.get('sender-key-memory', [jid]))[jid] || {} : {})()
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
- if (!participant) {
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) { if (statusJidList?.length) participantsList.push(...statusJidList) }
360
- else {
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
- // DEVICE 0 PRESERVATION FOR GROUPS: Initialize device 0 for all participants
367
- const device0EntriesGroup = []
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
- // Combine device 0 entries with fetched devices, avoiding duplicates
377
- const deviceMap = new Map()
378
- for (const d of device0EntriesGroup) deviceMap.set(`${d.user}:${d.device}`, d)
379
- for (const d of additionalDevices) {
380
- const key = `${d.user}:${d.device}`
381
- if (!deviceMap.has(key)) deviceMap.set(key, d)
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
- if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
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
- const patched = await patchMessageBeforeSending(message)
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
- const bytes = encodeWAMessage(patched)
392
- const groupAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
393
- const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
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
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
557
+ if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
558
+ additionalAttributes = { ...additionalAttributes, addressing_mode: groupData?.addressingMode || 'lid' }
396
559
 
397
- const senderKeyRecipients = []
398
- for (const device of devices) {
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
- // Assert sessions once for sender key recipients ONLY to avoid concurrent conflicts
405
- if (senderKeyRecipients.length) {
406
- logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
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(senderKeyRecipients)
409
- const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
410
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
411
- participants.push(...result.nodes)
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
- binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
419
- await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
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
- const { user: ownUser } = jidDecode(ownId)
583
+ const { user: ownUser } = jidDecode(ownId)
426
584
 
427
- if (!participant) {
428
- const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
429
- devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
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
- if (user !== ownUser) {
432
- const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
433
- const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
434
- devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
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
- if (additionalAttributes?.category !== 'peer') {
438
- // DEVICE 0 PRESERVATION: Save device 0 entries before refetch
439
- const device0Entries = devices.filter(d => d.device === 0)
440
- const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
441
- devices.length = 0
442
- const senderIdentity = isLid && meLid ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined) : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
443
- // Fetch both sender and recipient devices to ensure complete enumeration
444
- const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
445
- devices.push(...device0Entries, ...sessionDevices)
446
- // If sender devices weren't enumerated, explicitly fetch them
447
- if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
448
- const senderDevices = await getUSyncDevices([senderIdentity], true, false)
449
- const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
450
- if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
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
- const allRecipients = [], meRecipients = [], otherRecipients = []
456
- const { user: mePnUser } = jidDecode(meId)
457
- const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
458
-
459
- for (const { user, jid } of devices) {
460
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
461
- if (isExactSenderDevice) continue
462
- const isMe = user === mePnUser || user === meLidUser
463
- if (isMe) meRecipients.push(jid)
464
- else otherRecipients.push(jid)
465
- allRecipients.push(jid)
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
- await assertSessions(allRecipients)
628
+ await assertSessions(allRecipients)
469
629
 
470
- const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
471
- createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
472
- createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
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
- participants.push(...meNodes, ...otherNodes)
476
- if (meRecipients.length > 0 || otherRecipients.length > 0) extraAttrs.phash = generateParticipantHashV2([...meRecipients, ...otherRecipients])
477
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
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
- if (participants.length) {
481
- if (additionalAttributes?.category === 'peer') { const peerNode = participants[0]?.content?.[0]; if (peerNode) binaryNodeContent.push(peerNode) }
482
- else binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
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
- const stanza = { tag: 'message', attrs: { id: finalMsgId, to: destinationJid, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent }
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
- if (participant) {
488
- if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid; stanza.attrs.participant = participant.jid }
489
- else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid; stanza.attrs.recipient = destinationJid }
490
- else stanza.attrs.to = participant.jid
491
- } else stanza.attrs.to = destinationJid
492
-
493
- let additionalAlready = false
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
- if (shouldIncludeDeviceIdentity) { stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) }); logger.debug({ jid }, 'adding device identity') }
502
- if (additionalNodes?.length > 0 && !additionalAlready) stanza.content.push(...additionalNodes)
503
- // Add TCToken support with expiration validation
504
- if (!isGroup && !isRetryResend && !isStatus) {
505
- const contactTcTokenData = await authState.keys.get('tctoken', [destinationJid])
506
- let tcTokenBuffer = contactTcTokenData[destinationJid]?.token
507
-
508
- // Check if token is expired
509
- if (isTokenExpired(contactTcTokenData[destinationJid])) {
510
- logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
511
- try {
512
- const freshTokens = await getPrivacyTokens([destinationJid])
513
- tcTokenBuffer = freshTokens[destinationJid]?.token
514
- } catch (err) {
515
- logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
516
- }
517
- }
518
-
519
- if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
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
- const TOKEN_EXPIRY_TTL = 24 * 60 * 60 // 24 hours in seconds
531
-
532
- const isTokenExpired = (tokenData) => {
533
- if (!tokenData || !tokenData.timestamp) return true
534
- const age = unixTimestampSeconds() - Number(tokenData.timestamp)
535
- return age > TOKEN_EXPIRY_TTL
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, readMessages,
553
- refreshMediaConn, waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
554
- createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
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) error = result.error
566
- else {
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) throw new Boom(`Media re-upload failed (${proto.MediaRetryNotification.ResultType[media.result]})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
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)) { try { const metadata = await cachedGroupMetadata(id) || await groupMetadata(id); metadata.participants.forEach(p => allUsers.add(jidNormalizedUser(p.id))) } catch (error) { logger.error(`Error getting metadata for ${id}: ${error}`) } }
588
- else if (isJidUser(id)) allUsers.add(jidNormalizedUser(id))
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
- const uniqueUsers = Array.from(allUsers)
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
- const font = !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined
598
- const textColor = !isMedia ? (content.textColor || getRandomHex()) : undefined
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 }); mediaHandle = up.handle; return up },
607
- mediaCache: config.mediaCache, options: config.options, font, textColor, backgroundColor, ptt
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
- await relayMessage(STORIES_JID, msg.message, { messageId: msg.key.id, statusJidList: uniqueUsers, additionalNodes: [{ tag: 'meta', attrs: {}, content: [{ tag: 'mentioned_users', attrs: {}, content: jids.map(jid => ({ tag: 'to', attrs: { jid: jidNormalizedUser(jid) } })) }] }] })
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 userJid = authState.creds.me.id
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
- if (content.interactive && !content.interactiveMessage) { const { interactive, ...rest } = content; content = { ...rest, interactiveMessage: interactive } }
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' ? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : content.disappearingMessagesInChat
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, userJid,
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, getCallLink: sock.createCallLink,
657
- upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
658
- messageId: generateMessageIDV2(sock.user?.id), ...options
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) additionalAttributes.edit = isJidGroup(content.delete?.remoteJid) && !content.delete?.fromMe ? '8' : '7'
663
- else if (content.edit) additionalAttributes.edit = '1'
664
- else if (content.pin) additionalAttributes.edit = '2'
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
- // Auto-fetch TCToken for new contacts
669
- if (!isJidGroup(jid) && !content.disappearingMessagesInChat) {
670
- const existingToken = await authState.keys.get('tctoken', [jid])
671
- if (!existingToken[jid]) {
672
- try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
673
- catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
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, useCachedGroupMetadata: options.useCachedGroupMetadata,
679
- additionalAttributes, statusJidList: options.statusJidList, additionalNodes
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) process.nextTick(() => { processingMutex.mutex(() => upsertMessage(fullMsg, 'append')) })
964
+ if (config.emitOwnEvents) {
965
+ process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
966
+ }
683
967
  return fullMsg
684
968
  }
685
969
  }