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