@zervonex/baileys 0.0.0-stage → 3.0.1

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