@nexustechpro/baileys 2.0.1 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +924 -1299
- package/lib/Defaults/baileys-version.json +6 -2
- package/lib/Defaults/index.js +172 -172
- package/lib/Signal/libsignal.js +380 -292
- package/lib/Signal/lid-mapping.js +264 -171
- package/lib/Socket/Client/index.js +2 -2
- package/lib/Socket/Client/types.js +10 -10
- package/lib/Socket/Client/websocket.js +45 -310
- package/lib/Socket/business.js +375 -375
- package/lib/Socket/chats.js +909 -963
- package/lib/Socket/communities.js +430 -430
- package/lib/Socket/groups.js +342 -342
- package/lib/Socket/index.js +22 -22
- package/lib/Socket/messages-recv.js +777 -743
- package/lib/Socket/messages-send.js +667 -393
- package/lib/Socket/mex.js +50 -50
- package/lib/Socket/newsletter.js +148 -148
- package/lib/Socket/nexus-handler.js +75 -261
- package/lib/Socket/socket.js +709 -1201
- package/lib/Store/index.js +5 -5
- package/lib/Store/make-cache-manager-store.js +81 -81
- package/lib/Store/make-in-memory-store.js +416 -416
- package/lib/Store/make-ordered-dictionary.js +81 -81
- package/lib/Store/object-repository.js +30 -30
- package/lib/Types/Auth.js +1 -1
- package/lib/Types/Bussines.js +1 -1
- package/lib/Types/Call.js +1 -1
- package/lib/Types/Chat.js +7 -7
- package/lib/Types/Contact.js +1 -1
- package/lib/Types/Events.js +1 -1
- package/lib/Types/GroupMetadata.js +1 -1
- package/lib/Types/Label.js +24 -24
- package/lib/Types/LabelAssociation.js +6 -6
- package/lib/Types/Message.js +10 -10
- package/lib/Types/Newsletter.js +28 -28
- package/lib/Types/Product.js +1 -1
- package/lib/Types/Signal.js +1 -1
- package/lib/Types/Socket.js +2 -2
- package/lib/Types/State.js +12 -12
- package/lib/Types/USync.js +1 -1
- package/lib/Types/index.js +25 -25
- package/lib/Utils/auth-utils.js +264 -256
- package/lib/Utils/baileys-event-stream.js +55 -55
- package/lib/Utils/browser-utils.js +27 -27
- package/lib/Utils/business.js +228 -230
- package/lib/Utils/chat-utils.js +694 -764
- package/lib/Utils/crypto.js +109 -135
- package/lib/Utils/decode-wa-message.js +310 -314
- package/lib/Utils/event-buffer.js +547 -547
- package/lib/Utils/generics.js +297 -297
- package/lib/Utils/history.js +91 -83
- package/lib/Utils/index.js +21 -20
- package/lib/Utils/key-store.js +17 -0
- package/lib/Utils/link-preview.js +97 -88
- package/lib/Utils/logger.js +2 -2
- package/lib/Utils/lt-hash.js +47 -47
- package/lib/Utils/make-mutex.js +39 -39
- package/lib/Utils/message-retry-manager.js +148 -148
- package/lib/Utils/messages-media.js +534 -532
- package/lib/Utils/messages.js +705 -705
- package/lib/Utils/noise-handler.js +255 -255
- package/lib/Utils/pre-key-manager.js +105 -105
- package/lib/Utils/process-message.js +412 -412
- package/lib/Utils/signal.js +160 -158
- package/lib/Utils/use-multi-file-auth-state.js +120 -120
- package/lib/Utils/validate-connection.js +194 -194
- package/lib/WABinary/constants.js +1300 -1300
- package/lib/WABinary/decode.js +237 -237
- package/lib/WABinary/encode.js +232 -232
- package/lib/WABinary/generic-utils.js +252 -211
- package/lib/WABinary/index.js +5 -5
- package/lib/WABinary/jid-utils.js +279 -95
- package/lib/WABinary/types.js +1 -1
- package/lib/WAM/BinaryInfo.js +9 -9
- package/lib/WAM/constants.js +22852 -22852
- package/lib/WAM/encode.js +149 -149
- package/lib/WAM/index.js +3 -3
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
- package/lib/WAUSync/Protocols/index.js +4 -4
- package/lib/WAUSync/USyncQuery.js +93 -93
- package/lib/WAUSync/USyncUser.js +22 -22
- package/lib/WAUSync/index.js +3 -3
- package/lib/index.js +66 -66
- package/package.json +171 -144
- package/lib/Signal/Group/ciphertext-message.js +0 -12
- package/lib/Signal/Group/group-session-builder.js +0 -30
- package/lib/Signal/Group/group_cipher.js +0 -100
- package/lib/Signal/Group/index.js +0 -12
- package/lib/Signal/Group/keyhelper.js +0 -18
- package/lib/Signal/Group/sender-chain-key.js +0 -26
- package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
- package/lib/Signal/Group/sender-key-message.js +0 -66
- package/lib/Signal/Group/sender-key-name.js +0 -48
- package/lib/Signal/Group/sender-key-record.js +0 -41
- package/lib/Signal/Group/sender-key-state.js +0 -84
- package/lib/Signal/Group/sender-message-key.js +0 -26
|
@@ -4,7 +4,7 @@ import * as Utils from '../Utils/index.js'
|
|
|
4
4
|
import { proto } from '../../WAProto/index.js'
|
|
5
5
|
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
|
|
6
6
|
import * as WABinary from '../WABinary/index.js'
|
|
7
|
-
import { getUrlInfo } from '../Utils/
|
|
7
|
+
import { getUrlInfo, migrateIndexKey } from '../Utils/index.js'
|
|
8
8
|
import { makeKeyedMutex } from '../Utils/make-mutex.js'
|
|
9
9
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js'
|
|
10
10
|
import { makeNewsletterSocket } from './newsletter.js'
|
|
@@ -22,10 +22,47 @@ const {
|
|
|
22
22
|
|
|
23
23
|
const {
|
|
24
24
|
areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser,
|
|
25
|
-
isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
|
|
25
|
+
isJidBroadcast, isJidGroup, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET,
|
|
26
26
|
getBinaryFilteredButtons, STORIES_JID, isJidUser, getButtonArgs, getButtonType
|
|
27
27
|
} = WABinary
|
|
28
28
|
|
|
29
|
+
const TC_TOKEN_BUCKET_DURATION = 604800 // 7 days
|
|
30
|
+
const TC_TOKEN_NUM_BUCKETS = 4 // ~28-day rolling window
|
|
31
|
+
|
|
32
|
+
const isTcTokenExpired = (timestamp) => {
|
|
33
|
+
if (timestamp === null || timestamp === undefined) return true
|
|
34
|
+
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
|
|
35
|
+
if (isNaN(ts)) return true
|
|
36
|
+
const now = Math.floor(Date.now() / 1000)
|
|
37
|
+
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
|
|
38
|
+
const cutoffBucket = currentBucket - (TC_TOKEN_NUM_BUCKETS - 1)
|
|
39
|
+
return ts < (cutoffBucket * TC_TOKEN_BUCKET_DURATION)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const shouldSendNewTcToken = (senderTimestamp) => {
|
|
43
|
+
if (senderTimestamp === undefined) return true
|
|
44
|
+
const now = Math.floor(Date.now() / 1000)
|
|
45
|
+
const currentBucket = Math.floor(now / TC_TOKEN_BUCKET_DURATION)
|
|
46
|
+
const senderBucket = Math.floor(senderTimestamp / TC_TOKEN_BUCKET_DURATION)
|
|
47
|
+
return currentBucket > senderBucket
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolveTcTokenJid = async (jid, getLIDForPN) => {
|
|
51
|
+
if (isLidUser(jid)) return jid
|
|
52
|
+
const lid = await getLIDForPN(jid)
|
|
53
|
+
return lid ?? jid
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resolveIssuanceJid = async (jid, issueToLid, getLIDForPN, getPNForLID) => {
|
|
57
|
+
if (issueToLid) {
|
|
58
|
+
if (isLidUser(jid)) return jid
|
|
59
|
+
return (await getLIDForPN(jid)) ?? jid
|
|
60
|
+
}
|
|
61
|
+
if (!isLidUser(jid)) return jid
|
|
62
|
+
if (getPNForLID) return (await getPNForLID(jid)) ?? jid
|
|
63
|
+
return jid
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
export const makeMessagesSocket = (config) => {
|
|
30
67
|
const {
|
|
31
68
|
logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview,
|
|
@@ -34,15 +71,21 @@ export const makeMessagesSocket = (config) => {
|
|
|
34
71
|
} = config
|
|
35
72
|
|
|
36
73
|
const sock = makeNewsletterSocket(config)
|
|
37
|
-
const {
|
|
74
|
+
const {
|
|
75
|
+
ev, authState, processingMutex, signalRepository, upsertMessage, query,
|
|
76
|
+
fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral
|
|
77
|
+
} = sock
|
|
38
78
|
|
|
39
79
|
const userDevicesCache = config.userDevicesCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
40
80
|
const peerSessionsCache = new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, useClones: false })
|
|
41
81
|
const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null
|
|
42
82
|
const encryptionMutex = makeKeyedMutex()
|
|
83
|
+
|
|
84
|
+
// Prevents duplicate TC token IQ requests from concurrent sends
|
|
85
|
+
const inFlightTcTokenIssuance = new Set()
|
|
86
|
+
|
|
43
87
|
let mediaConn
|
|
44
88
|
|
|
45
|
-
// ===== MEDIA CONNECTION =====
|
|
46
89
|
const refreshMediaConn = async (forceGet = false) => {
|
|
47
90
|
const media = await mediaConn
|
|
48
91
|
if (!media || forceGet || Date.now() - media.fetchDate.getTime() > media.ttl * 1000) {
|
|
@@ -51,7 +94,9 @@ export const makeMessagesSocket = (config) => {
|
|
|
51
94
|
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
|
|
52
95
|
return {
|
|
53
96
|
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({ hostname: attrs.hostname, maxContentLengthBytes: +attrs.maxContentLengthBytes })),
|
|
54
|
-
auth: mediaConnNode.attrs.auth,
|
|
97
|
+
auth: mediaConnNode.attrs.auth,
|
|
98
|
+
ttl: +mediaConnNode.attrs.ttl,
|
|
99
|
+
fetchDate: new Date()
|
|
55
100
|
}
|
|
56
101
|
})()
|
|
57
102
|
logger.debug('fetched media conn')
|
|
@@ -59,23 +104,33 @@ export const makeMessagesSocket = (config) => {
|
|
|
59
104
|
return mediaConn
|
|
60
105
|
}
|
|
61
106
|
|
|
62
|
-
|
|
107
|
+
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
|
|
108
|
+
|
|
63
109
|
const sendReceipt = async (jid, participant, messageIds, type) => {
|
|
64
110
|
if (!messageIds?.length) throw new Boom('missing ids in receipt')
|
|
65
111
|
const node = { tag: 'receipt', attrs: { id: messageIds[0] } }
|
|
66
112
|
const isReadReceipt = type === 'read' || type === 'read-self'
|
|
67
113
|
if (isReadReceipt) node.attrs.t = unixTimestampSeconds().toString()
|
|
68
|
-
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
|
|
69
|
-
|
|
114
|
+
if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
|
|
115
|
+
node.attrs.recipient = jid
|
|
116
|
+
node.attrs.to = participant
|
|
117
|
+
} else {
|
|
118
|
+
node.attrs.to = jid
|
|
119
|
+
if (participant) node.attrs.participant = participant
|
|
120
|
+
}
|
|
70
121
|
if (type) node.attrs.type = type
|
|
71
|
-
if (messageIds.length > 1)
|
|
122
|
+
if (messageIds.length > 1) {
|
|
123
|
+
node.content = [{ tag: 'list', attrs: {}, content: messageIds.slice(1).map(id => ({ tag: 'item', attrs: { id } })) }]
|
|
124
|
+
}
|
|
72
125
|
logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt')
|
|
73
126
|
await sendNode(node)
|
|
74
127
|
}
|
|
75
128
|
|
|
76
129
|
const sendReceipts = async (keys, type) => {
|
|
77
130
|
const recps = aggregateMessageKeysNotFromMe(keys)
|
|
78
|
-
for (const { jid, participant, messageIds } of recps)
|
|
131
|
+
for (const { jid, participant, messageIds } of recps) {
|
|
132
|
+
await sendReceipt(jid, participant, messageIds, type)
|
|
133
|
+
}
|
|
79
134
|
}
|
|
80
135
|
|
|
81
136
|
const readMessages = async (keys) => {
|
|
@@ -83,7 +138,6 @@ export const makeMessagesSocket = (config) => {
|
|
|
83
138
|
await sendReceipts(keys, privacySettings.readreceipts === 'all' ? 'read' : 'read-self')
|
|
84
139
|
}
|
|
85
140
|
|
|
86
|
-
// ===== DEVICES & SESSIONS =====
|
|
87
141
|
const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
|
|
88
142
|
const deviceResults = []
|
|
89
143
|
if (!useCache) logger.debug('not using cache for devices')
|
|
@@ -97,95 +151,113 @@ export const makeMessagesSocket = (config) => {
|
|
|
97
151
|
}).filter(Boolean)
|
|
98
152
|
|
|
99
153
|
let mgetDevices
|
|
100
|
-
if (useCache && userDevicesCache.mget)
|
|
154
|
+
if (useCache && userDevicesCache.mget) {
|
|
155
|
+
mgetDevices = await userDevicesCache.mget(jidsWithUser.map(j => j?.user).filter(Boolean))
|
|
156
|
+
}
|
|
101
157
|
|
|
102
158
|
const toFetch = []
|
|
103
159
|
for (const { jid, user } of jidsWithUser) {
|
|
104
160
|
if (useCache) {
|
|
105
161
|
const devices = mgetDevices?.[user] || (userDevicesCache.mget ? undefined : await userDevicesCache.get(user))
|
|
106
|
-
if (devices) {
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
if (devices) {
|
|
163
|
+
deviceResults.push(...devices.map(d => ({ ...d, jid: jidEncode(d.user, d.server, d.device) })))
|
|
164
|
+
logger.trace({ user }, 'using cache for devices')
|
|
165
|
+
} else {
|
|
166
|
+
toFetch.push(jid)
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
toFetch.push(jid)
|
|
170
|
+
}
|
|
109
171
|
}
|
|
110
172
|
|
|
111
173
|
if (!toFetch.length) return deviceResults
|
|
112
174
|
|
|
113
175
|
const requestedLidUsers = new Set()
|
|
114
|
-
for (const jid of toFetch)
|
|
176
|
+
for (const jid of toFetch) {
|
|
177
|
+
if (isLidUser(jid) || isHostedLidUser(jid)) {
|
|
178
|
+
const user = jidDecode(jid)?.user
|
|
179
|
+
if (user) requestedLidUsers.add(user)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
115
182
|
|
|
116
|
-
const
|
|
117
|
-
for (const jid of toFetch)
|
|
183
|
+
const usyncQuery = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
|
|
184
|
+
for (const jid of toFetch) usyncQuery.withUser(new USyncUser().withId(jid))
|
|
118
185
|
|
|
119
|
-
const result = await sock.executeUSyncQuery(
|
|
186
|
+
const result = await sock.executeUSyncQuery(usyncQuery)
|
|
120
187
|
if (result) {
|
|
121
188
|
const lidResults = result.list.filter(a => !!a.lid)
|
|
122
|
-
if (lidResults.length > 0) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
if (lidResults.length > 0) {
|
|
190
|
+
logger.trace('Storing LID maps from device call')
|
|
191
|
+
await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
|
|
192
|
+
try {
|
|
193
|
+
const lids = lidResults.map(a => a.lid)
|
|
194
|
+
if (lids.length) await assertSessions(lids, false)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
|
|
197
|
+
}
|
|
129
198
|
}
|
|
130
199
|
|
|
131
200
|
const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices)
|
|
132
201
|
const deviceMap = {}
|
|
133
|
-
for (const item of extracted) {
|
|
202
|
+
for (const item of extracted) {
|
|
203
|
+
deviceMap[item.user] = deviceMap[item.user] || []
|
|
204
|
+
deviceMap[item.user]?.push(item)
|
|
205
|
+
}
|
|
134
206
|
|
|
135
207
|
for (const [user, userDevices] of Object.entries(deviceMap)) {
|
|
136
|
-
const
|
|
208
|
+
const isLid = requestedLidUsers.has(user)
|
|
137
209
|
for (const item of userDevices) {
|
|
138
|
-
const finalJid =
|
|
210
|
+
const finalJid = isLid ? jidEncode(user, item.server, item.device) : jidEncode(item.user, item.server, item.device)
|
|
139
211
|
deviceResults.push({ ...item, jid: finalJid })
|
|
140
212
|
}
|
|
141
213
|
}
|
|
142
214
|
|
|
143
|
-
if (userDevicesCache.mset)
|
|
144
|
-
|
|
215
|
+
if (userDevicesCache.mset) {
|
|
216
|
+
await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
|
|
217
|
+
} else {
|
|
218
|
+
for (const key in deviceMap) if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
|
|
219
|
+
}
|
|
145
220
|
|
|
221
|
+
// Persist device lists for session migration (capped at 500 users)
|
|
146
222
|
const userDeviceUpdates = {}
|
|
147
|
-
for (const [userId, devices] of Object.entries(deviceMap))
|
|
148
|
-
|
|
223
|
+
for (const [userId, devices] of Object.entries(deviceMap)) {
|
|
224
|
+
if (devices?.length > 0) userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
|
|
225
|
+
}
|
|
149
226
|
if (Object.keys(userDeviceUpdates).length > 0) {
|
|
150
227
|
try {
|
|
151
|
-
const
|
|
152
|
-
const currentBatch = existingData?.['_index'] || {}
|
|
228
|
+
const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
|
|
153
229
|
const mergedBatch = { ...currentBatch, ...userDeviceUpdates }
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} catch (error) { logger.warn({ error }, 'failed to store user device lists') }
|
|
230
|
+
await authState.keys.set({ 'device-list': { 'index': mergedBatch } })
|
|
231
|
+
logger.debug({ userCount: Object.keys(userDeviceUpdates).length, batchSize: Object.keys(mergedBatch).length }, 'stored user device lists')
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.warn({ error }, 'failed to store user device lists')
|
|
234
|
+
}
|
|
160
235
|
}
|
|
161
236
|
}
|
|
162
237
|
return deviceResults
|
|
163
238
|
}
|
|
164
239
|
|
|
165
240
|
const assertSessions = async (jids, force) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
let didFetchNewSession = false
|
|
242
|
+
let jidsRequiringFetch = []
|
|
243
|
+
if (force) {
|
|
244
|
+
jidsRequiringFetch = jids
|
|
245
|
+
} else {
|
|
246
|
+
// assertSessions
|
|
247
|
+
const sessionBatch = await migrateIndexKey(authState.keys, 'session') // sessions live in index blob, not individual files
|
|
248
|
+
for (const jid of jids) {
|
|
249
|
+
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
|
|
250
|
+
if (!sessionBatch[signalId]) jidsRequiringFetch.push(jid)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (jidsRequiringFetch.length) {
|
|
254
|
+
logger.debug({ jidsRequiringFetch }, 'fetching sessions')
|
|
255
|
+
const result = await query({ tag: 'iq', attrs: { xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET }, content: [{ tag: 'key', attrs: {}, content: jidsRequiringFetch.map(jid => ({ tag: 'user', attrs: { jid } })) }] })
|
|
256
|
+
await parseAndInjectE2ESessions(result, signalRepository)
|
|
257
|
+
didFetchNewSession = true
|
|
258
|
+
}
|
|
259
|
+
return didFetchNewSession
|
|
185
260
|
}
|
|
186
|
-
return didFetchNewSession
|
|
187
|
-
}
|
|
188
|
-
|
|
189
261
|
const sendPeerDataOperationMessage = async (pdoMessage) => {
|
|
190
262
|
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
191
263
|
return await relayMessage(jidNormalizedUser(authState.creds.me.id), {
|
|
@@ -193,72 +265,60 @@ export const makeMessagesSocket = (config) => {
|
|
|
193
265
|
}, { additionalAttributes: { category: 'peer', push_priority: 'high_force' }, additionalNodes: [{ tag: 'meta', attrs: { appdata: 'default' } }] })
|
|
194
266
|
}
|
|
195
267
|
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (jid && token) tokens[jid] = { token, timestamp: Number(unixTimestampSeconds()) }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return tokens
|
|
208
|
-
}
|
|
209
|
-
const updateMemberLabel = (jid, memberLabel) => {
|
|
210
|
-
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
211
|
-
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
212
|
-
return relayMessage(jid, { protocolMessage: { type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE, memberLabel: { label: memberLabel.slice(0, 30), labelTimestamp: unixTimestampSeconds() } } }, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
|
|
268
|
+
// Issues our TC token to a contact so they can send us private messages. Fire-and-forget.
|
|
269
|
+
const issuePrivacyTokens = async (jids, timestamp) => {
|
|
270
|
+
const t = (timestamp ?? unixTimestampSeconds()).toString()
|
|
271
|
+
return query({
|
|
272
|
+
tag: 'iq',
|
|
273
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
274
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
275
|
+
})
|
|
213
276
|
}
|
|
214
277
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const { user: targetUser } = jidDecode(jid)
|
|
230
|
-
const { user: ownPnUser } = jidDecode(meId)
|
|
231
|
-
const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
|
|
232
|
-
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
233
|
-
if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
|
|
278
|
+
// Fetches TC tokens from the server for the given JIDs and stores them locally.
|
|
279
|
+
const getPrivacyTokens = async (jids) => {
|
|
280
|
+
const t = unixTimestampSeconds().toString()
|
|
281
|
+
const result = await query({
|
|
282
|
+
tag: 'iq',
|
|
283
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' },
|
|
284
|
+
content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }]
|
|
285
|
+
})
|
|
286
|
+
const tokens = {}
|
|
287
|
+
const tokenList = getBinaryNodeChild(result, 'tokens')
|
|
288
|
+
if (tokenList) {
|
|
289
|
+
for (const node of getBinaryNodeChildren(tokenList, 'token')) {
|
|
290
|
+
const { jid, content } = { jid: node.attrs.jid, content: node.content }
|
|
291
|
+
if (jid && content) tokens[jid] = { token: content, timestamp: Number(unixTimestampSeconds()) }
|
|
234
292
|
}
|
|
235
|
-
const bytes = encodeWAMessage(msgToEncrypt)
|
|
236
|
-
return await encryptionMutex.mutex(jid, async () => {
|
|
237
|
-
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
|
|
238
|
-
if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
|
|
239
|
-
return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
|
|
240
|
-
})
|
|
241
|
-
} catch (err) {
|
|
242
|
-
logger.error({jid, err }, 'Failed to encrypt for recipient')
|
|
243
|
-
return null
|
|
244
293
|
}
|
|
245
|
-
})
|
|
294
|
+
if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
|
|
295
|
+
return tokens
|
|
296
|
+
}
|
|
246
297
|
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
const updateMemberLabel = (jid, memberLabel) => {
|
|
299
|
+
if (!memberLabel || typeof memberLabel !== 'string') throw new Error('Member label must be a non-empty string')
|
|
300
|
+
if (!isJidGroup(jid)) throw new Error('Member labels can only be set in groups')
|
|
301
|
+
return relayMessage(jid, {
|
|
302
|
+
protocolMessage: {
|
|
303
|
+
type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE,
|
|
304
|
+
memberLabel: { label: memberLabel.slice(0, 30), labelTimestamp: unixTimestampSeconds() }
|
|
305
|
+
}
|
|
306
|
+
}, { additionalNodes: [{ tag: 'meta', attrs: { tag_reason: 'user_update', appdata: 'member_tag' }, content: undefined }] })
|
|
249
307
|
}
|
|
250
308
|
|
|
251
|
-
// ===== MESSAGE HELPERS =====
|
|
252
309
|
const getMessageType = (msg) => {
|
|
253
310
|
const message = normalizeMessageContent(msg)
|
|
311
|
+
if (!message) return 'text'
|
|
254
312
|
if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) return 'poll'
|
|
255
|
-
if (message.reactionMessage) return 'reaction'
|
|
313
|
+
if (message.reactionMessage || message.encReactionMessage) return 'reaction'
|
|
256
314
|
if (message.eventMessage) return 'event'
|
|
257
315
|
if (getMediaType(message)) return 'media'
|
|
258
316
|
return 'text'
|
|
259
317
|
}
|
|
260
318
|
|
|
261
319
|
const getMediaType = (message) => {
|
|
320
|
+
const inner = message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.viewOnceMessageV2Extension?.message
|
|
321
|
+
if (inner) return getMediaType(inner)
|
|
262
322
|
if (message.imageMessage) return 'image'
|
|
263
323
|
if (message.stickerMessage) return message.stickerMessage.isLottie ? '1p_sticker' : message.stickerMessage.isAvatar ? 'avatar_sticker' : 'sticker'
|
|
264
324
|
if (message.videoMessage) return message.videoMessage.gifPlayback ? 'gif' : 'video'
|
|
@@ -282,275 +342,507 @@ export const makeMessagesSocket = (config) => {
|
|
|
282
342
|
if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) return 'url'
|
|
283
343
|
}
|
|
284
344
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const meId = authState.creds.me.id
|
|
288
|
-
const meLid = authState.creds.me?.lid
|
|
289
|
-
const isRetryResend = Boolean(participant?.jid)
|
|
290
|
-
let shouldIncludeDeviceIdentity = isRetryResend
|
|
291
|
-
let finalMsgId = msgId
|
|
292
|
-
|
|
293
|
-
// Check if message is already in proper WAMessage format
|
|
294
|
-
const hasProtoMessageType = Object.keys(message).some(key => key.endsWith('Message') || key === 'conversation')
|
|
295
|
-
|
|
296
|
-
if (!hasProtoMessageType) {
|
|
297
|
-
logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
|
|
298
|
-
const generatedMsg = await generateWAMessage(jid, message, { logger, userJid: meId, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }), getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink, upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options, messageId: finalMsgId || generateMessageIDV2(meId), quoted: quoted })
|
|
299
|
-
message = generatedMsg.message
|
|
300
|
-
if (!finalMsgId) finalMsgId = generatedMsg.key.id
|
|
301
|
-
logger.debug({ msgId: finalMsgId, jid }, 'message auto-generated successfully')
|
|
302
|
-
}
|
|
345
|
+
const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
|
|
346
|
+
if (!recipientJids.length) return { nodes: [], shouldIncludeDeviceIdentity: false }
|
|
303
347
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
348
|
+
const patched = await patchMessageBeforeSending(message, recipientJids)
|
|
349
|
+
const patchedMessages = Array.isArray(patched) ? patched : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
|
|
350
|
+
let shouldIncludeDeviceIdentity = false
|
|
351
|
+
|
|
352
|
+
const meId = authState.creds.me.id
|
|
353
|
+
const meLid = authState.creds.me?.lid
|
|
354
|
+
const meLidUser = meLid ? jidDecode(meLid)?.user : null
|
|
355
|
+
|
|
356
|
+
const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
|
|
357
|
+
try {
|
|
358
|
+
if (!jid) return null
|
|
359
|
+
let msgToEncrypt = patchedMessage
|
|
360
|
+
|
|
361
|
+
// Use DSM for own linked devices so they can read the message
|
|
362
|
+
if (dsmMessage) {
|
|
363
|
+
const { user: targetUser } = jidDecode(jid)
|
|
364
|
+
const { user: ownPnUser } = jidDecode(meId)
|
|
365
|
+
const isOwnUser = targetUser === ownPnUser || (meLidUser && targetUser === meLidUser)
|
|
366
|
+
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
367
|
+
if (isOwnUser && !isExactSenderDevice) { msgToEncrypt = dsmMessage; logger.debug({ jid, targetUser }, 'Using DSM for own device') }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const bytes = encodeWAMessage(msgToEncrypt)
|
|
371
|
+
return await encryptionMutex.mutex(jid, async () => {
|
|
372
|
+
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
|
|
373
|
+
if (type === 'pkmsg') shouldIncludeDeviceIdentity = true
|
|
374
|
+
return { tag: 'to', attrs: { jid }, content: [{ tag: 'enc', attrs: { v: '2', type, ...(extraAttrs || {}) }, content: ciphertext }] }
|
|
375
|
+
})
|
|
376
|
+
} catch (err) {
|
|
377
|
+
logger.warn({ jid, err: err?.message || err }, 'Failed to encrypt for recipient — no session, will retry on next interaction')
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const nodes = (await Promise.all(encryptionPromises)).filter(Boolean)
|
|
383
|
+
return { nodes, shouldIncludeDeviceIdentity }
|
|
328
384
|
}
|
|
329
385
|
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
386
|
+
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList, quoted } = {}) => {
|
|
387
|
+
const meId = authState.creds.me.id
|
|
388
|
+
const meLid = authState.creds.me?.lid
|
|
389
|
+
const { user, server } = jidDecode(jid)
|
|
390
|
+
const isGroup = server === 'g.us'
|
|
391
|
+
const isStatus = jid === 'status@broadcast'
|
|
392
|
+
const isLid = server === 'lid'
|
|
393
|
+
const isNewsletter = server === 'newsletter'
|
|
394
|
+
|
|
395
|
+
let activeSender = meId
|
|
396
|
+
let groupAddressingMode = 'pn'
|
|
397
|
+
if (isGroup && !isStatus) {
|
|
398
|
+
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
399
|
+
groupAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
400
|
+
if (groupAddressingMode === 'lid' && meLid) activeSender = meLid
|
|
401
|
+
} else if (isLid && meLid) {
|
|
402
|
+
activeSender = meLid
|
|
403
|
+
}
|
|
333
404
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
405
|
+
const isRetryResend = Boolean(participant?.jid)
|
|
406
|
+
let shouldIncludeDeviceIdentity = isRetryResend
|
|
407
|
+
let finalMsgId = msgId
|
|
408
|
+
|
|
409
|
+
// Auto-generate WAMessage from raw content if needed
|
|
410
|
+
const hasProtoMessageType = Object.keys(message).some(key => key.endsWith('Message') || key === 'conversation')
|
|
411
|
+
if (!hasProtoMessageType) {
|
|
412
|
+
logger.debug({ jid }, 'relayMessage: auto-generating message from raw content')
|
|
413
|
+
const generatedMsg = await generateWAMessage(jid, message, {
|
|
414
|
+
logger, userJid: jidNormalizedUser(activeSender),
|
|
415
|
+
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
416
|
+
getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink,
|
|
417
|
+
upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
|
|
418
|
+
messageId: finalMsgId || generateMessageIDV2(activeSender), quoted
|
|
419
|
+
})
|
|
420
|
+
message = generatedMsg.message
|
|
421
|
+
if (!finalMsgId) finalMsgId = generatedMsg.key.id
|
|
422
|
+
logger.debug({ msgId: finalMsgId, jid }, 'message auto-generated successfully')
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
finalMsgId = finalMsgId || generateMessageIDV2(activeSender)
|
|
426
|
+
useUserDevicesCache = useUserDevicesCache !== false
|
|
427
|
+
useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
|
|
428
|
+
|
|
429
|
+
const participants = []
|
|
430
|
+
const destinationJid = !isStatus ? jid : 'status@broadcast'
|
|
431
|
+
const binaryNodeContent = []
|
|
432
|
+
const devices = []
|
|
433
|
+
const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo }
|
|
434
|
+
const extraAttrs = {}
|
|
435
|
+
const messages = normalizeMessageContent(message)
|
|
436
|
+
const buttonType = getButtonType(messages)
|
|
437
|
+
|
|
438
|
+
let hasDeviceFanoutFalse = false
|
|
439
|
+
if (participant) {
|
|
440
|
+
if (!isGroup && !isStatus) hasDeviceFanoutFalse = true
|
|
441
|
+
const { user, device } = jidDecode(participant.jid)
|
|
442
|
+
devices.push({ user, device, jid: participant.jid })
|
|
342
443
|
}
|
|
343
444
|
|
|
344
|
-
|
|
445
|
+
await authState.keys.transaction(async () => {
|
|
446
|
+
const mediaType = getMediaType(message)
|
|
447
|
+
if (mediaType) extraAttrs.mediatype = mediaType
|
|
345
448
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
449
|
+
if (isNewsletter) {
|
|
450
|
+
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
|
451
|
+
binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: encodeNewsletterMessage(patched) })
|
|
452
|
+
await sendNode({ tag: 'message', attrs: { to: jid, id: finalMsgId, type: getMessageType(message), ...(additionalAttributes || {}) }, content: binaryNodeContent })
|
|
453
|
+
logger.debug({ msgId: finalMsgId }, `sending newsletter message to ${jid}`)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (messages?.pinInChatMessage || messages?.keepInChatMessage || message.reactionMessage || message.protocolMessage?.editedMessage) {
|
|
458
|
+
extraAttrs['decrypt-fail'] = 'hide'
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if ((isGroup || isStatus) && !isRetryResend) {
|
|
462
|
+
const [groupData] = await Promise.all([
|
|
463
|
+
(async () => {
|
|
464
|
+
let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
465
|
+
if (groupData?.participants) logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
|
|
466
|
+
else if (!isStatus) groupData = await groupMetadata(jid)
|
|
467
|
+
return groupData
|
|
468
|
+
})(),
|
|
469
|
+
Promise.resolve({}) // senderKeyMap always empty — forces fresh SKDM every send
|
|
470
|
+
])
|
|
356
471
|
|
|
357
|
-
if (!participant) {
|
|
358
472
|
const participantsList = []
|
|
359
|
-
if (isStatus) {
|
|
360
|
-
|
|
473
|
+
if (isStatus) {
|
|
474
|
+
if (statusJidList?.length) participantsList.push(...statusJidList)
|
|
475
|
+
} else {
|
|
361
476
|
let groupAddressingMode = 'lid'
|
|
362
477
|
if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData?.addressingMode || groupAddressingMode }
|
|
363
478
|
additionalAttributes = { ...additionalAttributes, addressing_mode: groupAddressingMode }
|
|
364
479
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for (const jid of participantsList) {
|
|
369
|
-
const { user, server } = jidDecode(jid)
|
|
370
|
-
if (user) {
|
|
371
|
-
device0EntriesGroup.push({ user, device: 0, jid: jidEncode(user, server, 0) })
|
|
372
|
-
}
|
|
480
|
+
|
|
481
|
+
if (groupData?.ephemeralDuration > 0) {
|
|
482
|
+
additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
373
483
|
}
|
|
374
|
-
|
|
484
|
+
|
|
375
485
|
const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
for (const
|
|
380
|
-
const
|
|
381
|
-
if (!
|
|
486
|
+
devices.push(...additionalDevices)
|
|
487
|
+
|
|
488
|
+
// Force Device 0 inclusion — USync sometimes omits it for LID groups
|
|
489
|
+
for (const pJid of participantsList) {
|
|
490
|
+
const decoded = jidDecode(pJid)
|
|
491
|
+
if (decoded?.user && !devices.some(d => d.user === decoded.user && d.device === 0)) {
|
|
492
|
+
devices.push({ user: decoded.user, device: 0, server: decoded.server, domainType: decoded.domainType, jid: jidEncode(decoded.user, decoded.server, 0) })
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const patched = await patchMessageBeforeSending(message)
|
|
497
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
498
|
+
|
|
499
|
+
const bytes = encodeWAMessage(patched)
|
|
500
|
+
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
501
|
+
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
502
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
|
|
503
|
+
|
|
504
|
+
const senderKeyRecipients = devices
|
|
505
|
+
.filter(d => !isHostedLidUser(d.jid) && !isHostedPnUser(d.jid) && d.device !== 99)
|
|
506
|
+
.map(d => d.jid)
|
|
507
|
+
|
|
508
|
+
if (senderKeyRecipients.length) {
|
|
509
|
+
logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
|
|
510
|
+
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
511
|
+
await assertSessions(senderKeyRecipients)
|
|
512
|
+
const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, {})
|
|
513
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
|
|
514
|
+
participants.push(...result.nodes)
|
|
382
515
|
}
|
|
383
|
-
devices.push(...Array.from(deviceMap.values()))
|
|
384
|
-
}
|
|
385
516
|
|
|
386
|
-
|
|
517
|
+
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...extraAttrs }, content: ciphertext })
|
|
387
518
|
|
|
388
|
-
|
|
389
|
-
|
|
519
|
+
} else if ((isGroup || isStatus) && isRetryResend) {
|
|
520
|
+
const groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
521
|
+
if (!groupData && !isStatus) await groupMetadata(jid)
|
|
390
522
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
|
|
523
|
+
if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
524
|
+
additionalAttributes = { ...additionalAttributes, addressing_mode: groupData?.addressingMode || 'lid' }
|
|
394
525
|
|
|
395
|
-
|
|
526
|
+
const patched = await patchMessageBeforeSending(message)
|
|
527
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching not supported in groups')
|
|
396
528
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
if ((!hasKey || !!participant) && !isHostedLidUser(deviceJid) && !isHostedPnUser(deviceJid) && device.device !== 99) { senderKeyRecipients.push(deviceJid); senderKeyMap[deviceJid] = true }
|
|
402
|
-
}
|
|
529
|
+
const bytes = encodeWAMessage(patched)
|
|
530
|
+
const gAddressingMode = additionalAttributes?.addressing_mode || groupData?.addressingMode || 'lid'
|
|
531
|
+
const groupSenderIdentity = gAddressingMode === 'lid' && meLid ? meLid : meId
|
|
532
|
+
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({ group: destinationJid, data: bytes, meId: groupSenderIdentity })
|
|
403
533
|
|
|
404
|
-
// Assert sessions once for sender key recipients ONLY to avoid concurrent conflicts
|
|
405
|
-
if (senderKeyRecipients.length) {
|
|
406
|
-
logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
|
|
407
534
|
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
408
|
-
await assertSessions(
|
|
409
|
-
const
|
|
410
|
-
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity ||
|
|
411
|
-
participants.push(...
|
|
535
|
+
await assertSessions([participant.jid])
|
|
536
|
+
const skResult = await createParticipantNodes([participant.jid], senderKeyMsg, {})
|
|
537
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || skResult.shouldIncludeDeviceIdentity
|
|
538
|
+
participants.push(...skResult.nodes)
|
|
539
|
+
|
|
540
|
+
// For retry resend, encrypt directly to the requesting participant
|
|
541
|
+
const isParticipantLid = isLidUser(participant.jid)
|
|
542
|
+
const isMe = areJidsSameUser(participant.jid, isParticipantLid ? meLid : meId)
|
|
543
|
+
const encodedMsg = isMe ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } }) : encodeWAMessage(message)
|
|
544
|
+
const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({ data: encodedMsg, jid: participant.jid })
|
|
545
|
+
binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type, count: participant.count.toString() }, content: encryptedContent })
|
|
546
|
+
|
|
547
|
+
} else {
|
|
548
|
+
let ownId = meId
|
|
549
|
+
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
550
|
+
|
|
551
|
+
const { user: ownUser } = jidDecode(ownId)
|
|
552
|
+
|
|
553
|
+
if (!participant) {
|
|
554
|
+
const targetUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
555
|
+
devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
|
|
556
|
+
|
|
557
|
+
if (user !== ownUser) {
|
|
558
|
+
const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
|
|
559
|
+
const ownUserForAddressing = isLid && meLid ? jidDecode(meLid).user : jidDecode(meId).user
|
|
560
|
+
devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (additionalAttributes?.category !== 'peer') {
|
|
564
|
+
const device0Entries = devices.filter(d => d.device === 0)
|
|
565
|
+
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
566
|
+
devices.length = 0
|
|
567
|
+
const senderIdentity = isLid && meLid
|
|
568
|
+
? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
|
|
569
|
+
: jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
|
|
570
|
+
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
571
|
+
devices.push(...device0Entries, ...sessionDevices)
|
|
572
|
+
|
|
573
|
+
if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
|
|
574
|
+
const senderDevices = await getUSyncDevices([senderIdentity], true, false)
|
|
575
|
+
const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
|
|
576
|
+
if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const allRecipients = [], meRecipients = [], otherRecipients = []
|
|
582
|
+
const { user: mePnUser } = jidDecode(meId)
|
|
583
|
+
const { user: meLidUser } = meLid ? jidDecode(meLid) : { user: null }
|
|
584
|
+
|
|
585
|
+
for (const { user, jid } of devices) {
|
|
586
|
+
const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
|
|
587
|
+
if (isExactSenderDevice) continue
|
|
588
|
+
const isMe = user === mePnUser || user === meLidUser
|
|
589
|
+
if (isMe) meRecipients.push(jid)
|
|
590
|
+
else otherRecipients.push(jid)
|
|
591
|
+
allRecipients.push(jid)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
await assertSessions(allRecipients)
|
|
595
|
+
|
|
596
|
+
const [{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 }, { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }] = await Promise.all([
|
|
597
|
+
createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
|
|
598
|
+
createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
|
|
599
|
+
])
|
|
600
|
+
|
|
601
|
+
participants.push(...meNodes, ...otherNodes)
|
|
602
|
+
if (meRecipients.length > 0 || otherRecipients.length > 0) {
|
|
603
|
+
extraAttrs.phash = generateParticipantHashV2([...meRecipients, ...otherRecipients])
|
|
604
|
+
}
|
|
605
|
+
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
|
412
606
|
}
|
|
413
607
|
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
608
|
+
if (participants.length) {
|
|
609
|
+
if (additionalAttributes?.category === 'peer') {
|
|
610
|
+
const peerNode = participants[0]?.content?.[0]
|
|
611
|
+
if (peerNode) binaryNodeContent.push(peerNode)
|
|
612
|
+
} else {
|
|
613
|
+
binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const stanza = {
|
|
618
|
+
tag: 'message',
|
|
619
|
+
attrs: {
|
|
620
|
+
id: finalMsgId,
|
|
621
|
+
to: destinationJid,
|
|
622
|
+
type: getMessageType(message),
|
|
623
|
+
...(isLid || (isGroup && groupAddressingMode === 'lid') ? { addressing_mode: 'lid' } : {}),
|
|
624
|
+
...(hasDeviceFanoutFalse ? { device_fanout: 'false' } : {}),
|
|
625
|
+
...(additionalAttributes || {})
|
|
626
|
+
},
|
|
627
|
+
content: binaryNodeContent
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (participant) {
|
|
631
|
+
if (isJidGroup(destinationJid)) { stanza.attrs.to = destinationJid; stanza.attrs.participant = participant.jid }
|
|
632
|
+
else if (areJidsSameUser(participant.jid, meId)) { stanza.attrs.to = participant.jid; stanza.attrs.recipient = destinationJid }
|
|
633
|
+
else stanza.attrs.to = participant.jid
|
|
417
634
|
} else {
|
|
418
|
-
|
|
419
|
-
|
|
635
|
+
stanza.attrs.to = destinationJid
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let didPushAdditional = false
|
|
639
|
+
|
|
640
|
+
if (!isNewsletter && buttonType && !isStatus) {
|
|
641
|
+
const buttonsNode = getButtonArgs(messages)
|
|
642
|
+
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
643
|
+
if (filteredButtons) {
|
|
644
|
+
stanza.content.push(...additionalNodes)
|
|
645
|
+
didPushAdditional = true
|
|
646
|
+
} else {
|
|
647
|
+
stanza.content.push(...buttonsNode)
|
|
648
|
+
}
|
|
420
649
|
}
|
|
421
|
-
} else {
|
|
422
|
-
let ownId = meId
|
|
423
|
-
if (isLid && meLid) { ownId = meLid; logger.debug({ to: jid, ownId }, 'Using LID identity') }
|
|
424
650
|
|
|
425
|
-
|
|
651
|
+
if (!didPushAdditional && additionalNodes?.length > 0) {
|
|
652
|
+
stanza.content.push(...additionalNodes)
|
|
653
|
+
}
|
|
426
654
|
|
|
427
|
-
if (!
|
|
428
|
-
|
|
429
|
-
|
|
655
|
+
if ((shouldIncludeDeviceIdentity || (meLid && (isLid || (isGroup && groupAddressingMode === 'lid')))) && !isNewsletter) {
|
|
656
|
+
stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
|
|
657
|
+
logger.debug({ jid }, 'adding device identity')
|
|
658
|
+
}
|
|
430
659
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
660
|
+
// TC token handling for 1:1 messages
|
|
661
|
+
const isPeerMessage = additionalAttributes?.category === 'peer'
|
|
662
|
+
const is1on1 = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
|
|
663
|
+
if (is1on1) {
|
|
664
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
665
|
+
const tcTokenJid = await resolveTcTokenJid(destinationJid, getLIDForPN)
|
|
666
|
+
|
|
667
|
+
const contactTcTokenData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
668
|
+
const existingEntry = contactTcTokenData[tcTokenJid]
|
|
669
|
+
let tcTokenBuffer = existingEntry?.token
|
|
670
|
+
|
|
671
|
+
// Clear expired tokens
|
|
672
|
+
if (tcTokenBuffer?.length && isTcTokenExpired(existingEntry?.timestamp)) {
|
|
673
|
+
logger.debug({ jid: destinationJid }, 'tctoken expired, clearing')
|
|
674
|
+
tcTokenBuffer = undefined
|
|
675
|
+
try {
|
|
676
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: existingEntry?.senderTimestamp !== undefined ? { token: Buffer.alloc(0), senderTimestamp: existingEntry.senderTimestamp } : null } })
|
|
677
|
+
} catch { }
|
|
435
678
|
}
|
|
436
679
|
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
const device0Entries = devices.filter(d => d.device === 0)
|
|
440
|
-
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
441
|
-
devices.length = 0
|
|
442
|
-
const senderIdentity = isLid && meLid ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined) : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
|
|
443
|
-
// Fetch both sender and recipient devices to ensure complete enumeration
|
|
444
|
-
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
445
|
-
devices.push(...device0Entries, ...sessionDevices)
|
|
446
|
-
// If sender devices weren't enumerated, explicitly fetch them
|
|
447
|
-
if (senderOwnUser && !sessionDevices.some(d => d.user === senderOwnUser && d.device !== 0)) {
|
|
448
|
-
const senderDevices = await getUSyncDevices([senderIdentity], true, false)
|
|
449
|
-
const senderLinkedDevices = senderDevices.filter(d => d.device !== 0 && d.user === senderOwnUser)
|
|
450
|
-
if (senderLinkedDevices.length > 0) devices.push(...senderLinkedDevices)
|
|
451
|
-
}
|
|
680
|
+
if (tcTokenBuffer?.length) {
|
|
681
|
+
stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
452
682
|
}
|
|
453
|
-
}
|
|
454
683
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
684
|
+
// Fire-and-forget: issue our token to the contact after send
|
|
685
|
+
const isProtocolMsg = !!normalizeMessageContent(message)?.protocolMessage
|
|
686
|
+
if (!isProtocolMsg && shouldSendNewTcToken(existingEntry?.senderTimestamp) && !inFlightTcTokenIssuance.has(tcTokenJid)) {
|
|
687
|
+
inFlightTcTokenIssuance.add(tcTokenJid)
|
|
688
|
+
const issueTimestamp = unixTimestampSeconds()
|
|
689
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
690
|
+
const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
|
|
691
|
+
resolveIssuanceJid(destinationJid, issueToLid, getLIDForPN, getPNForLID)
|
|
692
|
+
.then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
|
|
693
|
+
.then(async () => {
|
|
694
|
+
const currentData = await authState.keys.get('tctoken', [tcTokenJid])
|
|
695
|
+
const current = currentData[tcTokenJid]
|
|
696
|
+
await authState.keys.set({ tctoken: { [tcTokenJid]: { ...current, senderTimestamp: issueTimestamp } } })
|
|
697
|
+
})
|
|
698
|
+
.catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
|
|
699
|
+
.finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
|
|
700
|
+
}
|
|
466
701
|
}
|
|
467
702
|
|
|
468
|
-
|
|
703
|
+
logger.debug({ msgId: finalMsgId }, `sending message to ${participants.length} devices`)
|
|
704
|
+
await sendNode(stanza)
|
|
705
|
+
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, finalMsgId, message)
|
|
706
|
+
|
|
707
|
+
}, activeSender)
|
|
708
|
+
|
|
709
|
+
const isSelf = areJidsSameUser(jid, meId) || (meLid && areJidsSameUser(jid, meLid))
|
|
710
|
+
const returnParticipant = (isGroup || isSelf) ? jidNormalizedUser(activeSender) : undefined
|
|
711
|
+
return {
|
|
712
|
+
key: {
|
|
713
|
+
remoteJid: jid,
|
|
714
|
+
fromMe: true,
|
|
715
|
+
id: finalMsgId,
|
|
716
|
+
participant: returnParticipant,
|
|
717
|
+
addressingMode: (isLid || (isGroup && groupAddressingMode === 'lid')) ? 'lid' : 'pn'
|
|
718
|
+
},
|
|
719
|
+
messageId: finalMsgId
|
|
720
|
+
}
|
|
721
|
+
}
|
|
469
722
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
|
|
473
|
-
])
|
|
723
|
+
const nexus = new NexusHandler(Utils, waUploadToServer, relayMessage, { logger, mediaCache: config.mediaCache, options: config.options, mediaUploadTimeoutMs: config.mediaUploadTimeoutMs, user: authState.creds.me, getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }) })
|
|
724
|
+
const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update')
|
|
474
725
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
726
|
+
const sendMessage = async (jid, content, options = {}) => {
|
|
727
|
+
const meId = authState.creds.me.id
|
|
728
|
+
const meLid = authState.creds.me?.lid
|
|
729
|
+
const { server } = jidDecode(jid)
|
|
730
|
+
const isGroup = server === 'g.us'
|
|
731
|
+
const isDestinationLid = server === 'lid'
|
|
732
|
+
const useCache = options.useCachedGroupMetadata !== false
|
|
733
|
+
const { quoted } = options
|
|
734
|
+
|
|
735
|
+
let activeSender = meId
|
|
736
|
+
let addressingMode = 'pn'
|
|
737
|
+
if (isGroup) {
|
|
738
|
+
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
|
|
739
|
+
addressingMode = groupData?.addressingMode || 'lid'
|
|
740
|
+
if (addressingMode === 'lid' && meLid) activeSender = meLid
|
|
741
|
+
} else if (isDestinationLid && meLid) {
|
|
742
|
+
activeSender = meLid
|
|
743
|
+
addressingMode = 'lid'
|
|
478
744
|
}
|
|
479
745
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
746
|
+
// Unwrap shorthand `interactive` key
|
|
747
|
+
if (content.interactive && !content.interactiveMessage) {
|
|
748
|
+
const { interactive, ...rest } = content
|
|
749
|
+
content = { ...rest, interactiveMessage: interactive }
|
|
483
750
|
}
|
|
484
751
|
|
|
485
|
-
const
|
|
752
|
+
const messageType = nexus.detectType(content)
|
|
753
|
+
if (messageType) return await nexus.processMessage(content, jid, quoted)
|
|
486
754
|
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
let additionalAlready = false
|
|
494
|
-
if (!isNewsletter && buttonType) {
|
|
495
|
-
const buttonsNode = getButtonArgs(messages)
|
|
496
|
-
const filteredButtons = getBinaryFilteredButtons(additionalNodes || [])
|
|
497
|
-
if (filteredButtons) { stanza.content.push(...additionalNodes); additionalAlready = true }
|
|
498
|
-
else stanza.content.push(buttonsNode)
|
|
755
|
+
if (content.disappearingMessagesInChat && isJidGroup(jid)) {
|
|
756
|
+
const value = typeof content.disappearingMessagesInChat === 'boolean'
|
|
757
|
+
? (content.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
|
|
758
|
+
: content.disappearingMessagesInChat
|
|
759
|
+
await groupToggleEphemeral(jid, value)
|
|
760
|
+
return
|
|
499
761
|
}
|
|
500
762
|
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// Check if token is expired
|
|
509
|
-
if (isTokenExpired(contactTcTokenData[destinationJid])) {
|
|
510
|
-
logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
|
|
511
|
-
try {
|
|
512
|
-
const freshTokens = await getPrivacyTokens([destinationJid])
|
|
513
|
-
tcTokenBuffer = freshTokens[destinationJid]?.token
|
|
514
|
-
} catch (err) {
|
|
515
|
-
logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
|
523
|
-
await sendNode(stanza)
|
|
524
|
-
if (messageRetryManager && !participant) messageRetryManager.addRecentMessage(destinationJid, msgId, message)
|
|
525
|
-
}, meId)
|
|
763
|
+
if (content.delete) {
|
|
764
|
+
const deleteKey = content.delete
|
|
765
|
+
if (!deleteKey.remoteJid || !deleteKey.id) {
|
|
766
|
+
logger.error({ deleteKey }, 'Invalid delete key: missing remoteJid or id')
|
|
767
|
+
throw new Boom('Delete key must have remoteJid and id', { statusCode: 400 })
|
|
768
|
+
}
|
|
526
769
|
|
|
527
|
-
|
|
528
|
-
|
|
770
|
+
const { server: deleteServer } = jidDecode(deleteKey.remoteJid)
|
|
771
|
+
let deleteAddressingMode = deleteServer === 'lid' ? 'lid' : 'pn'
|
|
772
|
+
if (isJidGroup(deleteKey.remoteJid)) {
|
|
773
|
+
const groupData = useCache && cachedGroupMetadata ? await cachedGroupMetadata(deleteKey.remoteJid) : undefined
|
|
774
|
+
deleteAddressingMode = groupData?.addressingMode || 'lid'
|
|
775
|
+
}
|
|
529
776
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
return age > TOKEN_EXPIRY_TTL
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const getPrivacyTokens = async (jids) => {
|
|
539
|
-
const t = unixTimestampSeconds().toString()
|
|
540
|
-
const result = await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'privacy' }, content: [{ tag: 'tokens', attrs: {}, content: jids.map(jid => ({ tag: 'token', attrs: { jid: jidNormalizedUser(jid), t, type: 'trusted_contact' } })) }] })
|
|
541
|
-
const tokens = parseTCTokens(result)
|
|
542
|
-
if (Object.keys(tokens).length > 0) await authState.keys.set({ 'tctoken': tokens })
|
|
543
|
-
return tokens
|
|
544
|
-
}
|
|
777
|
+
let normalizedParticipant = deleteKey.participant
|
|
778
|
+
if (deleteKey.fromMe || isJidGroup(deleteKey.remoteJid)) {
|
|
779
|
+
const senderJid = (deleteAddressingMode === 'lid' && meLid) ? meLid : meId
|
|
780
|
+
normalizedParticipant = jidNormalizedUser(senderJid)
|
|
781
|
+
}
|
|
545
782
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
783
|
+
content.delete = {
|
|
784
|
+
remoteJid: deleteKey.remoteJid,
|
|
785
|
+
fromMe: deleteKey.fromMe === true || deleteKey.fromMe === 'true',
|
|
786
|
+
id: deleteKey.id,
|
|
787
|
+
...(normalizedParticipant ? { participant: jidNormalizedUser(normalizedParticipant) } : {}),
|
|
788
|
+
addressingMode: deleteAddressingMode
|
|
789
|
+
}
|
|
790
|
+
logger.debug({ jid, deleteKey: content.delete }, 'processing message deletion')
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const fullMsg = await generateWAMessage(jid, content, {
|
|
794
|
+
logger,
|
|
795
|
+
userJid: jidNormalizedUser(activeSender),
|
|
796
|
+
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
797
|
+
getProfilePicUrl: sock.profilePictureUrl,
|
|
798
|
+
getCallLink: sock.createCallLink,
|
|
799
|
+
upload: waUploadToServer,
|
|
800
|
+
mediaCache: config.mediaCache,
|
|
801
|
+
options: config.options,
|
|
802
|
+
messageId: generateMessageIDV2(activeSender),
|
|
803
|
+
...options
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
const additionalAttributes = {}, additionalNodes = []
|
|
807
|
+
if (content.delete) {
|
|
808
|
+
additionalAttributes.edit = isJidGroup(content.delete.remoteJid) ? '8' : '7'
|
|
809
|
+
} else if (content.edit) {
|
|
810
|
+
additionalAttributes.edit = '1'
|
|
811
|
+
} else if (content.pin) {
|
|
812
|
+
additionalAttributes.edit = '2'
|
|
813
|
+
}
|
|
814
|
+
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
815
|
+
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
816
|
+
if (content.ai) additionalNodes.push({ tag: 'bot', attrs: { biz_bot: '1' } })
|
|
817
|
+
|
|
818
|
+
// Pre-fetch TC token for new DM contacts
|
|
819
|
+
if (!isJidGroup(jid) && !isJidStatusBroadcast(jid) && !isJidBroadcast(jid) && !content.disappearingMessagesInChat) {
|
|
820
|
+
const existingToken = await authState.keys.get('tctoken', [jid])
|
|
821
|
+
if (!existingToken[jid]) {
|
|
822
|
+
try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
|
|
823
|
+
catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
await relayMessage(jid, fullMsg.message, {
|
|
828
|
+
messageId: fullMsg.key.id,
|
|
829
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
830
|
+
additionalAttributes,
|
|
831
|
+
statusJidList: options.statusJidList,
|
|
832
|
+
additionalNodes
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
if (config.emitOwnEvents) {
|
|
836
|
+
process.nextTick(() => processingMutex.mutex(() => upsertMessage(fullMsg, 'append')))
|
|
837
|
+
}
|
|
838
|
+
return fullMsg
|
|
839
|
+
}
|
|
549
840
|
|
|
550
841
|
return {
|
|
551
842
|
...sock,
|
|
552
|
-
getPrivacyTokens, assertSessions, relayMessage,
|
|
553
|
-
|
|
843
|
+
getPrivacyTokens, issuePrivacyTokens, assertSessions, relayMessage,
|
|
844
|
+
sendReceipt, sendReceipts, nexus, readMessages, refreshMediaConn,
|
|
845
|
+
waUploadToServer, fetchPrivacySettings, sendPeerDataOperationMessage,
|
|
554
846
|
createParticipantNodes, getUSyncDevices, messageRetryManager, updateMemberLabel,
|
|
555
847
|
|
|
556
848
|
updateMediaMessage: async (message) => {
|
|
@@ -562,11 +854,14 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
562
854
|
await Promise.all([sendNode(node), waitForMsgMediaUpdate(async (update) => {
|
|
563
855
|
const result = update.find(c => c.key.id === message.key.id)
|
|
564
856
|
if (result) {
|
|
565
|
-
if (result.error)
|
|
566
|
-
|
|
857
|
+
if (result.error) {
|
|
858
|
+
error = result.error
|
|
859
|
+
} else {
|
|
567
860
|
try {
|
|
568
861
|
const media = await decryptMediaRetryData(result.media, mediaKey, result.key.id)
|
|
569
|
-
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS)
|
|
862
|
+
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
|
|
863
|
+
throw new Boom(`Media re-upload failed (${proto.MediaRetryNotification.ResultType[media.result]})`, { data: media, statusCode: getStatusCodeForMediaRetry(media.result) || 404 })
|
|
864
|
+
}
|
|
570
865
|
content.directPath = media.directPath
|
|
571
866
|
content.url = getUrlFromDirectPath(content.directPath)
|
|
572
867
|
logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful')
|
|
@@ -584,30 +879,41 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
584
879
|
const userJid = jidNormalizedUser(authState.creds.me.id)
|
|
585
880
|
const allUsers = new Set([userJid])
|
|
586
881
|
for (const id of jids) {
|
|
587
|
-
if (isJidGroup(id)) {
|
|
588
|
-
|
|
882
|
+
if (isJidGroup(id)) {
|
|
883
|
+
try { const metadata = await cachedGroupMetadata(id) || await groupMetadata(id); metadata.participants.forEach(p => allUsers.add(jidNormalizedUser(p.id))) }
|
|
884
|
+
catch (error) { logger.error(`Error getting metadata for ${id}: ${error}`) }
|
|
885
|
+
} else if (isJidUser(id)) {
|
|
886
|
+
allUsers.add(jidNormalizedUser(id))
|
|
887
|
+
}
|
|
589
888
|
}
|
|
590
|
-
|
|
889
|
+
|
|
591
890
|
const getRandomHex = () => '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')
|
|
592
891
|
const isMedia = content.image || content.video || content.audio
|
|
593
892
|
const isAudio = !!content.audio
|
|
594
893
|
const msgContent = { ...content }
|
|
595
894
|
if (isMedia && !isAudio) { if (msgContent.text) { msgContent.caption = msgContent.text; delete msgContent.text }; delete msgContent.ptt; delete msgContent.font; delete msgContent.backgroundColor; delete msgContent.textColor }
|
|
596
895
|
if (isAudio) { delete msgContent.text; delete msgContent.caption; delete msgContent.font; delete msgContent.textColor }
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const backgroundColor = (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined
|
|
600
|
-
const ptt = isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined
|
|
601
|
-
let msg, mediaHandle
|
|
896
|
+
|
|
897
|
+
let msg
|
|
602
898
|
try {
|
|
603
899
|
msg = await generateWAMessage(STORIES_JID, msgContent, {
|
|
604
900
|
logger, userJid,
|
|
605
901
|
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
606
|
-
upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts });
|
|
607
|
-
mediaCache: config.mediaCache, options: config.options,
|
|
902
|
+
upload: async (encFilePath, opts) => { const up = await waUploadToServer(encFilePath, { ...opts }); return up },
|
|
903
|
+
mediaCache: config.mediaCache, options: config.options,
|
|
904
|
+
font: !isMedia ? (content.font || Math.floor(Math.random() * 9)) : undefined,
|
|
905
|
+
textColor: !isMedia ? (content.textColor || getRandomHex()) : undefined,
|
|
906
|
+
backgroundColor: (!isMedia || isAudio) ? (content.backgroundColor || getRandomHex()) : undefined,
|
|
907
|
+
ptt: isAudio ? (typeof content.ptt === 'boolean' ? content.ptt : true) : undefined
|
|
608
908
|
})
|
|
609
909
|
} catch (error) { logger.error(`Error generating message: ${error}`); throw error }
|
|
610
|
-
|
|
910
|
+
|
|
911
|
+
await relayMessage(STORIES_JID, msg.message, {
|
|
912
|
+
messageId: msg.key.id,
|
|
913
|
+
statusJidList: Array.from(allUsers),
|
|
914
|
+
additionalNodes: [{ tag: 'meta', attrs: {}, content: [{ tag: 'mentioned_users', attrs: {}, content: jids.map(jid => ({ tag: 'to', attrs: { jid: jidNormalizedUser(jid) } })) }] }]
|
|
915
|
+
})
|
|
916
|
+
|
|
611
917
|
for (const id of jids) {
|
|
612
918
|
try {
|
|
613
919
|
const normalizedId = jidNormalizedUser(id)
|
|
@@ -622,6 +928,7 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
622
928
|
return msg
|
|
623
929
|
},
|
|
624
930
|
|
|
931
|
+
// Nexus handler shortcuts
|
|
625
932
|
sendPaymentMessage: (jid, data, quoted) => nexus.handlePayment({ requestPaymentMessage: data }, jid, quoted),
|
|
626
933
|
sendProductMessage: (jid, data, quoted) => nexus.handleProduct({ productMessage: data }, jid, quoted),
|
|
627
934
|
sendInteractiveMessage: (jid, data, quoted) => nexus.handleInteractive({ interactiveMessage: data }, jid, quoted),
|
|
@@ -634,53 +941,20 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
634
941
|
sendCarouselMessage: (jid, data, quoted) => nexus.handleCarousel({ carouselMessage: data }, jid, quoted),
|
|
635
942
|
sendCarouselProtoMessage: (jid, data, quoted) => nexus.handleCarouselProto({ carouselProto: data }, jid, quoted),
|
|
636
943
|
stickerPackMessage: (jid, data, options) => nexus.handleStickerPack(data, jid, options?.quoted),
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const fullMsg = await generateWAMessage(jid, content, {
|
|
654
|
-
logger, userJid,
|
|
655
|
-
getUrlInfo: text => getUrlInfo(text, { thumbnailWidth: linkPreviewImageThumbnailWidth, fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) }, logger, uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined }),
|
|
656
|
-
getProfilePicUrl: sock.profilePictureUrl, getCallLink: sock.createCallLink,
|
|
657
|
-
upload: waUploadToServer, mediaCache: config.mediaCache, options: config.options,
|
|
658
|
-
messageId: generateMessageIDV2(sock.user?.id), ...options
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
const additionalAttributes = {}, additionalNodes = []
|
|
662
|
-
if (content.delete) additionalAttributes.edit = isJidGroup(content.delete?.remoteJid) && !content.delete?.fromMe ? '8' : '7'
|
|
663
|
-
else if (content.edit) additionalAttributes.edit = '1'
|
|
664
|
-
else if (content.pin) additionalAttributes.edit = '2'
|
|
665
|
-
if (content.poll) additionalNodes.push({ tag: 'meta', attrs: { polltype: 'creation' } })
|
|
666
|
-
if (content.event) additionalNodes.push({ tag: 'meta', attrs: { event_type: 'creation' } })
|
|
667
|
-
|
|
668
|
-
// Auto-fetch TCToken for new contacts
|
|
669
|
-
if (!isJidGroup(jid) && !content.disappearingMessagesInChat) {
|
|
670
|
-
const existingToken = await authState.keys.get('tctoken', [jid])
|
|
671
|
-
if (!existingToken[jid]) {
|
|
672
|
-
try { await getPrivacyTokens([jid]); logger.debug({ jid }, 'fetched tctoken for new contact') }
|
|
673
|
-
catch (err) { logger.warn({ jid, err }, 'failed to fetch tctoken') }
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
await relayMessage(jid, fullMsg.message, {
|
|
678
|
-
messageId: fullMsg.key.id, useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
679
|
-
additionalAttributes, statusJidList: options.statusJidList, additionalNodes
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
if (config.emitOwnEvents) process.nextTick(() => { processingMutex.mutex(() => upsertMessage(fullMsg, 'append')) })
|
|
683
|
-
return fullMsg
|
|
684
|
-
}
|
|
944
|
+
sendMessage,
|
|
945
|
+
// Shorthand wrappers
|
|
946
|
+
sendText: (jid, text, options = {}) => sendMessage(jid, { text, ...options }, options),
|
|
947
|
+
sendImage: (jid, image, caption = '', options = {}) => sendMessage(jid, { image, caption, ...options }, options),
|
|
948
|
+
sendVideo: (jid, video, caption = '', options = {}) => sendMessage(jid, { video, caption, ...options }, options),
|
|
949
|
+
sendDocument: (jid, document, caption = '', options = {}) => sendMessage(jid, { document, caption, ...options }, options),
|
|
950
|
+
sendAudio: (jid, audio, options = {}) => sendMessage(jid, { audio, ...options }, options),
|
|
951
|
+
sendLocation: (jid, { degreesLatitude, degreesLongitude, name, url, address } = {}, options = {}) =>
|
|
952
|
+
sendMessage(jid, { location: { degreesLatitude, degreesLongitude, name, url, address }, ...options }, options),
|
|
953
|
+
sendPoll: (jid, name, pollVote = [], multiSelect = false, options = {}) =>
|
|
954
|
+
sendMessage(jid, { poll: { name, values: pollVote, selectableOptionsCount: multiSelect ? pollVote.length : 0 }, ...options }, options),
|
|
955
|
+
sendReaction: (jid, key, reaction, options = {}) => sendMessage(jid, { react: { text: reaction, key }, ...options }, options),
|
|
956
|
+
sendSticker: (jid, sticker, options = {}) => sendMessage(jid, { sticker, ...options }, options),
|
|
957
|
+
sendContact: (jid, contact, options = {}) => sendMessage(jid, { contacts: { contacts: Array.isArray(contact) ? contact : [contact] }, ...options }, options),
|
|
958
|
+
sendForward: (jid, message, options = {}) => sendMessage(jid, { forward: message, force: options.force }, options),
|
|
685
959
|
}
|
|
686
960
|
}
|