@badzz88/baileys 8.5.4 → 8.5.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/README.md +1 -1
- package/WAProto/WAProto.proto +75 -1256
- package/WAProto/index.d.ts +19729 -36122
- package/WAProto/index.js +132193 -178538
- package/engine-requirements.js +15 -7
- package/lib/Defaults/index.js +14 -22
- package/lib/Signal/Group/group_cipher.js +5 -5
- package/lib/Signal/Group/keyhelper.js +4 -4
- package/lib/Signal/Group/sender-chain-key.js +2 -2
- package/lib/Signal/Group/sender-key-distribution-message.js +1 -1
- package/lib/Signal/Group/sender-key-message.js +3 -3
- package/lib/Signal/Group/sender-key-record.js +1 -1
- package/lib/Signal/Group/sender-message-key.js +5 -5
- package/lib/Signal/libsignal.js +28 -39
- package/lib/Signal/lid-mapping.js +8 -28
- package/lib/Socket/aigroups.js +5 -5
- package/lib/Socket/business.js +11 -3
- package/lib/Socket/chats.js +319 -668
- package/lib/Socket/communities.js +89 -85
- package/lib/Socket/graphql.js +228 -435
- package/lib/Socket/groups.js +211 -132
- package/lib/Socket/index.js +7 -9
- package/lib/Socket/interactive-handler.js +60 -117
- package/lib/Socket/interop.js +175 -56
- package/lib/Socket/luxu.js +387 -0
- package/lib/Socket/managed-account.js +48 -79
- package/lib/Socket/messages-recv.js +340 -898
- package/lib/Socket/messages-send.js +160 -462
- package/lib/Socket/mex.js +53 -40
- package/lib/Socket/newsletter.js +295 -291
- package/lib/Socket/privacy.js +265 -77
- package/lib/Socket/registration.js +157 -164
- package/lib/Socket/socket.js +199 -181
- package/lib/Socket/text-router.js +83 -0
- package/lib/Socket/username.js +227 -144
- package/lib/Store/keyed-db.js +108 -0
- package/lib/Store/make-cache-manager-store.js +2 -2
- package/lib/Store/make-in-memory-store.js +141 -25
- package/lib/Types/Auth.js +3 -3
- package/lib/Types/Label.js +1 -1
- package/lib/Types/LabelAssociation.js +1 -1
- package/lib/Types/Mex.js +112 -0
- package/lib/Types/Newsletter.js +65 -96
- package/lib/Types/State.js +4 -4
- package/lib/Types/index.js +1 -1
- package/lib/Utils/auth-utils.js +57 -52
- package/lib/Utils/browser-utils.js +1 -1
- package/lib/Utils/business.js +6 -2
- package/lib/Utils/chat-utils.js +38 -102
- package/lib/Utils/command-loader.d.ts +31 -0
- package/lib/Utils/command-loader.js +100 -0
- package/lib/Utils/companion-reg-client-utils.js +42 -0
- package/lib/Utils/crypto.js +84 -54
- package/lib/Utils/curve25519-js.js +275 -0
- package/lib/Utils/decode-wa-message.js +83 -316
- package/lib/Utils/event-buffer.js +61 -37
- package/lib/Utils/generics.js +103 -157
- package/lib/Utils/group-history.js +12 -3
- package/lib/Utils/history.js +15 -15
- package/lib/Utils/index.js +5 -1
- package/lib/Utils/link-preview.js +8 -3
- package/lib/Utils/lt-hash.js +35 -2
- package/lib/Utils/make-mutex.js +1 -1
- package/lib/Utils/message-retry-manager.js +92 -27
- package/lib/Utils/messages-media.js +130 -159
- package/lib/Utils/messages.js +566 -144
- package/lib/Utils/meta-ai-msmsg.js +202 -62
- package/lib/Utils/native-bridge.js +82 -0
- package/lib/Utils/noise-handler.js +21 -19
- package/lib/Utils/offline-node-processor.js +8 -3
- package/lib/Utils/pre-key-manager.js +23 -13
- package/lib/Utils/process-message.js +106 -197
- package/lib/Utils/reporting-utils.js +2 -2
- package/lib/Utils/session-pool.d.ts +16 -0
- package/lib/Utils/session-pool.js +94 -0
- package/lib/Utils/signal.js +63 -28
- package/lib/Utils/stanza-ack.js +12 -39
- package/lib/Utils/sticker.d.ts +17 -0
- package/lib/Utils/sticker.js +145 -0
- package/lib/Utils/sync-action-utils.js +7 -4
- package/lib/Utils/tc-token-utils.js +25 -20
- package/lib/Utils/use-multi-file-auth-state.js +14 -30
- package/lib/Utils/validate-connection.js +19 -59
- package/lib/Utils/view-once-cache.d.ts +9 -0
- package/lib/Utils/view-once-cache.js +79 -0
- package/lib/WABinary/decode.js +16 -50
- package/lib/WABinary/encode.js +224 -54
- package/lib/WABinary/generic-utils.js +97 -7
- package/lib/WABinary/jid-utils.js +14 -27
- package/lib/WAM/encode.js +7 -7
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +1 -58
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +1 -7
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +5 -13
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +4 -1
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +7 -20
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +1 -1
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +2 -2
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +2 -7
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +1 -1
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +1 -2
- package/lib/WAUSync/Protocols/index.js +0 -1
- package/lib/WAUSync/USyncQuery.js +6 -77
- package/lib/WAUSync/USyncUser.js +8 -16
- package/lib/WAUSync/index.js +0 -1
- package/lib/antiban.js +854 -608
- package/lib/index.js +1 -1
- package/package.json +20 -16
|
@@ -37,6 +37,8 @@ const makeMessagesRecvSocket = config => {
|
|
|
37
37
|
signalRepository,
|
|
38
38
|
query,
|
|
39
39
|
upsertMessage,
|
|
40
|
+
getUSyncDevices,
|
|
41
|
+
createParticipantNodes,
|
|
40
42
|
resyncAppState,
|
|
41
43
|
onUnexpectedError,
|
|
42
44
|
assertSessions,
|
|
@@ -46,36 +48,33 @@ const makeMessagesRecvSocket = config => {
|
|
|
46
48
|
uploadPreKeys,
|
|
47
49
|
sendPeerDataOperationMessage,
|
|
48
50
|
messageRetryManager,
|
|
49
|
-
issuePrivacyTokens
|
|
50
|
-
getUSyncDevices,
|
|
51
|
-
createParticipantNodes,
|
|
52
|
-
newsletterServerIdCache
|
|
51
|
+
issuePrivacyTokens
|
|
53
52
|
} = sock
|
|
54
53
|
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
55
|
-
|
|
54
|
+
// Track when the socket fully opens so pending pre-connect messages are treated as history
|
|
56
55
|
const socketCreatedAt = Math.floor(Date.now() / 1000)
|
|
57
56
|
let isConnected = false
|
|
58
|
-
|
|
57
|
+
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
59
58
|
const retryMutex = (0, make_mutex_1.makeMutex)()
|
|
60
59
|
const msgRetryCache =
|
|
61
60
|
config.msgRetryCounterCache ||
|
|
62
61
|
new node_cache_1.default({
|
|
63
|
-
stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
|
|
62
|
+
stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
64
63
|
useClones: false
|
|
65
64
|
})
|
|
66
65
|
const callOfferCache =
|
|
67
66
|
config.callOfferCache ||
|
|
68
67
|
new node_cache_1.default({
|
|
69
|
-
stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER,
|
|
68
|
+
stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
|
|
70
69
|
useClones: false
|
|
71
70
|
})
|
|
72
71
|
const placeholderResendCache =
|
|
73
72
|
config.placeholderResendCache ||
|
|
74
73
|
new node_cache_1.default({
|
|
75
|
-
stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY,
|
|
74
|
+
stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
76
75
|
useClones: false
|
|
77
76
|
})
|
|
78
|
-
|
|
77
|
+
// Debounce identity-change session refreshes per JID to avoid bursts
|
|
79
78
|
const identityAssertDebounce = new node_cache_1.default({ stdTTL: 5, useClones: false })
|
|
80
79
|
let sendActiveReceipts = false
|
|
81
80
|
const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
|
|
@@ -102,8 +101,8 @@ const makeMessagesRecvSocket = config => {
|
|
|
102
101
|
logger.debug({ messageKey }, 'already requested resend')
|
|
103
102
|
return
|
|
104
103
|
} else {
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Store original message data so PDO response handler can preserve
|
|
105
|
+
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
107
106
|
await placeholderResendCache.set(messageKey?.id, msgData || true)
|
|
108
107
|
}
|
|
109
108
|
await (0, Utils_1.delay)(2000)
|
|
@@ -127,7 +126,11 @@ const makeMessagesRecvSocket = config => {
|
|
|
127
126
|
}, 8000)
|
|
128
127
|
return sendPeerDataOperationMessage(pdoMessage)
|
|
129
128
|
}
|
|
130
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Request a Waffle (Meta-account) linking nonce from the paired phone.
|
|
131
|
+
* The phone responds via a PeerDataOperationRequestResponseMessage containing
|
|
132
|
+
* a WaffleNonceFetchResponse with the nonce needed for Meta account linking.
|
|
133
|
+
*/
|
|
131
134
|
const requestWaffleNonce = async () => {
|
|
132
135
|
if (!authState.creds.me?.id) {
|
|
133
136
|
throw new boom_1.Boom('Not authenticated')
|
|
@@ -136,18 +139,27 @@ const makeMessagesRecvSocket = config => {
|
|
|
136
139
|
peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.WAFFLE_LINKING_NONCE_FETCH
|
|
137
140
|
})
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Request a Companion Canonical User nonce from the paired phone.
|
|
144
|
+
* Used during companion linking to canonicalize the user identity across devices.
|
|
145
|
+
* The phone responds with a CompanionCanonicalUserNonceFetchResponse (nonce + waFbid).
|
|
146
|
+
*
|
|
147
|
+
* @param {string} [registrationTraceId] - Optional trace ID for this registration attempt.
|
|
148
|
+
*/
|
|
149
|
+
const requestCompanionCanonicalNonce = async (registrationTraceId) => {
|
|
141
150
|
if (!authState.creds.me?.id) {
|
|
142
151
|
throw new boom_1.Boom('Not authenticated')
|
|
143
152
|
}
|
|
144
153
|
return sendPeerDataOperationMessage({
|
|
145
154
|
companionCanonicalUserNonceFetchRequest: registrationTraceId ? { registrationTraceId } : {},
|
|
146
|
-
peerDataOperationRequestType:
|
|
147
|
-
index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
|
|
155
|
+
peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_CANONICAL_USER_NONCE_FETCH
|
|
148
156
|
})
|
|
149
157
|
}
|
|
150
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Request a Companion Meta nonce from the paired phone.
|
|
160
|
+
* Used during Meta-account companion linking flow.
|
|
161
|
+
* The phone responds with a CompanionMetaNonceFetchResponse (nonce).
|
|
162
|
+
*/
|
|
151
163
|
const requestCompanionMetaNonce = async () => {
|
|
152
164
|
if (!authState.creds.me?.id) {
|
|
153
165
|
throw new boom_1.Boom('Not authenticated')
|
|
@@ -156,138 +168,57 @@ const makeMessagesRecvSocket = config => {
|
|
|
156
168
|
peerDataOperationRequestType: index_js_1.proto.Message.PeerDataOperationRequestType.COMPANION_META_NONCE_FETCH
|
|
157
169
|
})
|
|
158
170
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
logger.debug({ node }, 'mex notification with no update children')
|
|
171
|
+
// Handles mex newsletter notifications
|
|
172
|
+
const handleMexNewsletterNotification = async node => {
|
|
173
|
+
const mexNode = (0, WABinary_1.getBinaryNodeChild)(node, 'mex')
|
|
174
|
+
if (!mexNode?.content) {
|
|
175
|
+
logger.warn({ node }, 'Invalid mex newsletter notification')
|
|
165
176
|
return
|
|
166
177
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const upd = d.xwa2_notify_newsletter_on_metadata_update
|
|
187
|
-
if (upd?.id) {
|
|
188
|
-
ev.emit('newsletter-settings.update', {
|
|
189
|
-
id: upd.id,
|
|
190
|
-
update: upd.thread_metadata?.settings ?? {}
|
|
191
|
-
})
|
|
192
|
-
}
|
|
193
|
-
break
|
|
194
|
-
}
|
|
195
|
-
case 'NotificationNewsletterJoin': {
|
|
196
|
-
|
|
197
|
-
const upd = d.xwa2_notify_newsletter_on_join
|
|
198
|
-
if (upd?.id) {
|
|
199
|
-
ev.emit('newsletter-participants.update', {
|
|
200
|
-
id: upd.id,
|
|
201
|
-
author: node.attrs.from,
|
|
202
|
-
user: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
|
|
203
|
-
new_role: upd.viewer_metadata?.role ?? 'SUBSCRIBER',
|
|
204
|
-
action: 'join',
|
|
205
|
-
metadata: upd.thread_metadata
|
|
206
|
-
})
|
|
207
|
-
}
|
|
208
|
-
break
|
|
209
|
-
}
|
|
210
|
-
case 'NotificationNewsletterMuteChange': {
|
|
211
|
-
|
|
212
|
-
const upd = d.xwa2_notify_newsletter_on_mute_change
|
|
213
|
-
if (upd?.id) {
|
|
214
|
-
ev.emit('newsletter-settings.update', {
|
|
215
|
-
id: upd.id,
|
|
216
|
-
update: { mute: upd.mute }
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
break
|
|
220
|
-
}
|
|
221
|
-
case 'NotificationNewsletterUserSettingChange': {
|
|
222
|
-
|
|
223
|
-
const upd = d.xwa2_notify_newsletter_on_user_setting_change
|
|
224
|
-
if (upd?.id && upd.setting) {
|
|
178
|
+
let data
|
|
179
|
+
try {
|
|
180
|
+
data = JSON.parse(mexNode.content.toString())
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
const operation = data?.operation
|
|
186
|
+
const updates = data?.updates
|
|
187
|
+
if (!updates || !operation) {
|
|
188
|
+
logger.warn({ data }, 'Invalid mex newsletter notification content')
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
logger.info({ operation, updates }, 'got mex newsletter notification')
|
|
192
|
+
switch (operation) {
|
|
193
|
+
case 'NotificationNewsletterUpdate':
|
|
194
|
+
for (const update of updates) {
|
|
195
|
+
if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
|
|
225
196
|
ev.emit('newsletter-settings.update', {
|
|
226
|
-
id:
|
|
227
|
-
update:
|
|
197
|
+
id: update.jid,
|
|
198
|
+
update: update.settings
|
|
228
199
|
})
|
|
229
200
|
}
|
|
230
|
-
break
|
|
231
201
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
202
|
+
break
|
|
203
|
+
case 'NotificationNewsletterAdminPromote':
|
|
204
|
+
for (const update of updates) {
|
|
205
|
+
if (update.jid && update.user) {
|
|
236
206
|
ev.emit('newsletter-participants.update', {
|
|
237
|
-
id:
|
|
207
|
+
id: update.jid,
|
|
238
208
|
author: node.attrs.from,
|
|
239
|
-
user:
|
|
209
|
+
user: update.user,
|
|
240
210
|
new_role: 'ADMIN',
|
|
241
211
|
action: 'promote'
|
|
242
212
|
})
|
|
243
213
|
}
|
|
244
|
-
break
|
|
245
|
-
}
|
|
246
|
-
case 'NotificationGroupMemberLinkPropertyUpdate': {
|
|
247
|
-
|
|
248
|
-
const upd = d.xwa2_notify_group_on_prop_change
|
|
249
|
-
if (upd?.id && upd.properties?.member_link_mode !== undefined) {
|
|
250
|
-
ev.emit('groups.update', [
|
|
251
|
-
{
|
|
252
|
-
id: upd.id,
|
|
253
|
-
memberAddMode: upd.properties.member_link_mode
|
|
254
|
-
}
|
|
255
|
-
])
|
|
256
|
-
}
|
|
257
|
-
break
|
|
258
|
-
}
|
|
259
|
-
case 'NotificationGroupLimitSharingPropertyUpdate': {
|
|
260
|
-
|
|
261
|
-
const upd = d.xwa2_notify_group_on_prop_change
|
|
262
|
-
if (upd?.id && upd.properties?.limit_sharing !== undefined) {
|
|
263
|
-
ev.emit('groups.update', [
|
|
264
|
-
{
|
|
265
|
-
id: upd.id,
|
|
266
|
-
limitSharing: upd.properties.limit_sharing
|
|
267
|
-
}
|
|
268
|
-
])
|
|
269
|
-
}
|
|
270
|
-
break
|
|
271
214
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
ev.emit('groups.update', [
|
|
277
|
-
{
|
|
278
|
-
id: upd.id,
|
|
279
|
-
memberShareHistoryMode: upd.properties.member_share_group_history_mode
|
|
280
|
-
}
|
|
281
|
-
])
|
|
282
|
-
}
|
|
283
|
-
break
|
|
284
|
-
}
|
|
285
|
-
default:
|
|
286
|
-
logger.debug({ opName, from: node.attrs.from }, 'unhandled mex op')
|
|
287
|
-
}
|
|
215
|
+
break
|
|
216
|
+
default:
|
|
217
|
+
logger.info({ operation, data }, 'Unhandled mex newsletter notification')
|
|
218
|
+
break
|
|
288
219
|
}
|
|
289
220
|
}
|
|
290
|
-
|
|
221
|
+
// Handles newsletter notifications
|
|
291
222
|
const handleNewsletterNotification = async node => {
|
|
292
223
|
const from = node.attrs.from
|
|
293
224
|
const child = (0, WABinary_1.getAllBinaryNodeChildren)(node)[0]
|
|
@@ -337,9 +268,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
337
268
|
})
|
|
338
269
|
}
|
|
339
270
|
break
|
|
340
|
-
case 'message':
|
|
341
|
-
const viewCount = child.attrs.view_count !== undefined ? +child.attrs.view_count : undefined
|
|
342
|
-
const impressionCount = child.attrs.impression_count !== undefined ? +child.attrs.impression_count : undefined
|
|
271
|
+
case 'message':
|
|
343
272
|
const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(child, 'plaintext')
|
|
344
273
|
if (plaintextNode?.content) {
|
|
345
274
|
try {
|
|
@@ -352,16 +281,11 @@ const makeMessagesRecvSocket = config => {
|
|
|
352
281
|
key: {
|
|
353
282
|
remoteJid: from,
|
|
354
283
|
id: child.attrs.message_id || child.attrs.server_id,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
fromMe: false
|
|
284
|
+
fromMe: false // TODO: is this really true though
|
|
358
285
|
},
|
|
359
286
|
message: messageProto,
|
|
360
287
|
messageTimestamp: +child.attrs.t
|
|
361
288
|
}).toJSON()
|
|
362
|
-
|
|
363
|
-
if (viewCount !== undefined) fullMessage.views = viewCount
|
|
364
|
-
if (impressionCount !== undefined) fullMessage.impressions = impressionCount
|
|
365
289
|
await upsertMessage(fullMessage, 'append')
|
|
366
290
|
logger.info('Processed plaintext newsletter message')
|
|
367
291
|
} catch (error) {
|
|
@@ -369,44 +293,23 @@ const makeMessagesRecvSocket = config => {
|
|
|
369
293
|
}
|
|
370
294
|
}
|
|
371
295
|
break
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const fwdNode = (0, WABinary_1.getBinaryNodeChild)(msgNode, 'forwards_count')
|
|
382
|
-
const forwardsCount = fwdNode?.attrs?.count !== undefined ? +fwdNode.attrs.count : undefined
|
|
383
|
-
const reactionsNode = (0, WABinary_1.getBinaryNodeChild)(msgNode, 'reactions')
|
|
384
|
-
const reactions = (0, WABinary_1.getBinaryNodeChildren)(reactionsNode ?? msgNode, 'reaction').map(r => ({
|
|
385
|
-
code: r.attrs.code,
|
|
386
|
-
count: +r.attrs.count
|
|
387
|
-
}))
|
|
388
|
-
ev.emit('newsletter.live-update', {
|
|
389
|
-
id: from,
|
|
390
|
-
server_id: serverId,
|
|
391
|
-
timestamp: msgTs,
|
|
392
|
-
forwardsCount,
|
|
393
|
-
reactions
|
|
394
|
-
})
|
|
395
|
-
}
|
|
296
|
+
case 'live_updates':
|
|
297
|
+
// Live view count / engagement update
|
|
298
|
+
const liveCount = parseInt(child.attrs.count || child.content?.toString() || '0', 10)
|
|
299
|
+
ev.emit('newsletter.live-update', {
|
|
300
|
+
id: from,
|
|
301
|
+
server_id: child.attrs.message_id || child.attrs.server_id,
|
|
302
|
+
liveViewers: liveCount,
|
|
303
|
+
timestamp: child.attrs.t ? +child.attrs.t : undefined
|
|
304
|
+
})
|
|
396
305
|
break
|
|
397
|
-
|
|
398
|
-
case 'pin': {
|
|
399
|
-
const pinnedServerId = child.attrs.message_id || child.attrs.server_id
|
|
400
|
-
const isPinned = child.attrs.action !== 'unpin'
|
|
306
|
+
case 'pin':
|
|
401
307
|
ev.emit('newsletter.pin', {
|
|
402
308
|
id: from,
|
|
403
|
-
server_id:
|
|
404
|
-
pinned:
|
|
309
|
+
server_id: child.attrs.message_id || child.attrs.server_id,
|
|
310
|
+
pinned: child.attrs.action !== 'unpin'
|
|
405
311
|
})
|
|
406
|
-
|
|
407
|
-
ev.emit('newsletters.update', [{ id: from, pinnedMessage: isPinned ? pinnedServerId : null }])
|
|
408
312
|
break
|
|
409
|
-
}
|
|
410
313
|
case 'category':
|
|
411
314
|
ev.emit('newsletter-settings.update', {
|
|
412
315
|
id: from,
|
|
@@ -426,92 +329,8 @@ const makeMessagesRecvSocket = config => {
|
|
|
426
329
|
break
|
|
427
330
|
}
|
|
428
331
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const handleNewsletterStatus = async node => {
|
|
432
|
-
const { id, from, server_id, t, is_sender, offline, type, edit } = node.attrs
|
|
433
|
-
const serverId = server_id ? +server_id : undefined
|
|
434
|
-
const timestamp = t ? +t : undefined
|
|
435
|
-
const isSender = is_sender === 'true'
|
|
436
|
-
const offlineIndex = offline !== undefined ? +offline : undefined
|
|
437
|
-
|
|
438
|
-
if (id && serverId != null) {
|
|
439
|
-
newsletterServerIdCache?.set(id, serverId)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
|
|
444
|
-
const meta = metaNode
|
|
445
|
-
? {
|
|
446
|
-
...(metaNode.attrs.msg_edit_t ? { editedAt: +metaNode.attrs.msg_edit_t } : {}),
|
|
447
|
-
...(metaNode.attrs.original_msg_t ? { originalTimestamp: +metaNode.attrs.original_msg_t } : {}),
|
|
448
|
-
...(metaNode.attrs.interaction_type ? { interactionType: metaNode.attrs.interaction_type } : {}),
|
|
449
|
-
...(metaNode.attrs.parent_server_id ? { parentServerId: +metaNode.attrs.parent_server_id } : {}),
|
|
450
|
-
...(metaNode.attrs.response_server_id ? { responseServerId: +metaNode.attrs.response_server_id } : {})
|
|
451
|
-
}
|
|
452
|
-
: undefined
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const viewsNode = (0, WABinary_1.getBinaryNodeChild)(node, 'views_count')
|
|
456
|
-
const viewsCount = viewsNode?.attrs?.count !== undefined ? +viewsNode.attrs.count : undefined
|
|
457
|
-
const responsesNode = (0, WABinary_1.getBinaryNodeChild)(node, 'responses_count')
|
|
458
|
-
const responsesCount = responsesNode?.attrs?.count !== undefined ? +responsesNode.attrs.count : undefined
|
|
459
|
-
const reactionsNode = (0, WABinary_1.getBinaryNodeChild)(node, 'reactions')
|
|
460
|
-
const reactionCounts = (0, WABinary_1.getBinaryNodeChildren)(reactionsNode ?? { content: [] }, 'reaction').map(
|
|
461
|
-
r => ({ code: r.attrs.code, count: +r.attrs.count })
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
let content = null
|
|
465
|
-
let mediaType = undefined
|
|
466
|
-
|
|
467
|
-
if (type === 'reaction') {
|
|
468
|
-
const reactionNode = (0, WABinary_1.getBinaryNodeChild)(node, 'reaction')
|
|
469
|
-
content = { type: 'reaction', code: reactionNode?.attrs?.code }
|
|
470
|
-
} else if (type === 'text' || type === 'media') {
|
|
471
|
-
const plaintextNode = (0, WABinary_1.getBinaryNodeChild)(node, 'plaintext')
|
|
472
|
-
if (type === 'media') mediaType = plaintextNode?.attrs?.mediatype
|
|
473
|
-
if (edit === '7' || edit === '8') {
|
|
474
|
-
content = { type: 'revoke', edit }
|
|
475
|
-
} else if (plaintextNode?.content) {
|
|
476
|
-
try {
|
|
477
|
-
const buf = Buffer.isBuffer(plaintextNode.content)
|
|
478
|
-
? plaintextNode.content
|
|
479
|
-
: Buffer.from(plaintextNode.content)
|
|
480
|
-
const message = index_js_1.proto.Message.decode(buf).toJSON()
|
|
481
|
-
content = { type, message, ...(mediaType ? { mediaType } : {}) }
|
|
482
|
-
} catch (err) {
|
|
483
|
-
logger.error({ err }, 'Failed to decode newsletter status plaintext')
|
|
484
|
-
content = { type, raw: true, ...(mediaType ? { mediaType } : {}) }
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const ackType = type === 'reaction' ? 'reaction' : edit ? 'revoke' : type
|
|
491
|
-
await sendNode({
|
|
492
|
-
tag: 'ack',
|
|
493
|
-
attrs: { id, to: from, class: 'status', type: ackType || 'text' },
|
|
494
|
-
content: undefined
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
ev.emit('newsletter.status', {
|
|
498
|
-
id: from,
|
|
499
|
-
messageId: id,
|
|
500
|
-
serverId,
|
|
501
|
-
timestamp,
|
|
502
|
-
isSender,
|
|
503
|
-
...(offlineIndex !== undefined ? { offlineIndex } : {}),
|
|
504
|
-
...(meta ? { meta } : {}),
|
|
505
|
-
...(viewsCount !== undefined ? { viewsCount } : {}),
|
|
506
|
-
...(responsesCount !== undefined ? { responsesCount } : {}),
|
|
507
|
-
...(reactionCounts.length ? { reactionCounts } : {}),
|
|
508
|
-
content
|
|
509
|
-
})
|
|
510
|
-
}
|
|
511
332
|
const sendMessageAck = async (node, errorCode) => {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me?.id)
|
|
333
|
+
const stanza = (0, stanza_ack_1.buildAckStanza)(node, errorCode, authState.creds.me.id)
|
|
515
334
|
logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack')
|
|
516
335
|
await sendNode(stanza)
|
|
517
336
|
}
|
|
@@ -678,7 +497,15 @@ const makeMessagesRecvSocket = config => {
|
|
|
678
497
|
await query(stanza)
|
|
679
498
|
}
|
|
680
499
|
|
|
681
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Re-encrypt call key for a device that reconnected mid-call.
|
|
502
|
+
* Source: OutgoingSignalingHandler.java enc_rekey / rekeyEncryptionTask
|
|
503
|
+
*
|
|
504
|
+
* @param {string} callId - Active call ID
|
|
505
|
+
* @param {string} callFrom - JID of the call creator
|
|
506
|
+
* @param {Buffer} encryptedKeyBytes - Re-encrypted call session key bytes
|
|
507
|
+
* @param {number} [count=0] - Retry counter (0–4)
|
|
508
|
+
*/
|
|
682
509
|
const rekeyCall = async (callId, callFrom, encryptedKeyBytes, count = 0) => {
|
|
683
510
|
const stanza = {
|
|
684
511
|
tag: 'call',
|
|
@@ -707,7 +534,14 @@ const makeMessagesRecvSocket = config => {
|
|
|
707
534
|
await query(stanza)
|
|
708
535
|
}
|
|
709
536
|
|
|
710
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Join a call via an invite link.
|
|
539
|
+
* Source: OutgoingSignalingHandler.java link_join tag
|
|
540
|
+
*
|
|
541
|
+
* @param {string} callId - Call ID from the link
|
|
542
|
+
* @param {string} callCreator - JID of the call creator
|
|
543
|
+
* @param {string} linkToken - Token from the call link
|
|
544
|
+
*/
|
|
711
545
|
const joinCallLink = async (callId, callCreator, linkToken) => {
|
|
712
546
|
const stanza = {
|
|
713
547
|
tag: 'call',
|
|
@@ -730,7 +564,13 @@ const makeMessagesRecvSocket = config => {
|
|
|
730
564
|
await query(stanza)
|
|
731
565
|
}
|
|
732
566
|
|
|
733
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Query info about a call link before joining.
|
|
569
|
+
* Source: OutgoingSignalingHandler.java link_query tag
|
|
570
|
+
*
|
|
571
|
+
* @param {string} callLinkCode - The call link code to query
|
|
572
|
+
* @param {string} to - JID to send the query to
|
|
573
|
+
*/
|
|
734
574
|
const queryCallLink = async (callLinkCode, to) => {
|
|
735
575
|
const stanza = {
|
|
736
576
|
tag: 'call',
|
|
@@ -753,19 +593,19 @@ const makeMessagesRecvSocket = config => {
|
|
|
753
593
|
const { key: msgKey } = fullMessage
|
|
754
594
|
const msgId = msgKey.id
|
|
755
595
|
if (messageRetryManager) {
|
|
756
|
-
|
|
596
|
+
// Check if we've exceeded max retries using the new system
|
|
757
597
|
if (messageRetryManager.hasExceededMaxRetries(msgId)) {
|
|
758
598
|
logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing')
|
|
759
599
|
messageRetryManager.markRetryFailed(msgId)
|
|
760
600
|
return
|
|
761
601
|
}
|
|
762
|
-
|
|
602
|
+
// Increment retry count using new system
|
|
763
603
|
const retryCount = messageRetryManager.incrementRetryCount(msgId)
|
|
764
|
-
|
|
604
|
+
// Use the new retry count for the rest of the logic
|
|
765
605
|
const key = `${msgId}:${msgKey?.participant}`
|
|
766
606
|
await msgRetryCache.set(key, retryCount)
|
|
767
607
|
} else {
|
|
768
|
-
|
|
608
|
+
// Fallback to old system
|
|
769
609
|
const key = `${msgId}:${msgKey?.participant}`
|
|
770
610
|
let retryCount = (await msgRetryCache.get(key)) || 0
|
|
771
611
|
if (retryCount >= maxMsgRetryCount) {
|
|
@@ -780,12 +620,12 @@ const makeMessagesRecvSocket = config => {
|
|
|
780
620
|
const retryCount = (await msgRetryCache.get(key)) || 1
|
|
781
621
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
|
782
622
|
const fromJid = node.attrs.from
|
|
783
|
-
|
|
623
|
+
// Check if we should recreate the session
|
|
784
624
|
let shouldRecreateSession = false
|
|
785
625
|
let recreateReason = ''
|
|
786
626
|
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
787
627
|
try {
|
|
788
|
-
|
|
628
|
+
// Check if we have a session with this JID
|
|
789
629
|
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
|
|
790
630
|
const hasSession = await signalRepository.validateSession(fromJid)
|
|
791
631
|
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists)
|
|
@@ -793,7 +633,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
793
633
|
recreateReason = result.reason
|
|
794
634
|
if (shouldRecreateSession) {
|
|
795
635
|
logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry')
|
|
796
|
-
|
|
636
|
+
// Delete existing session to force recreation
|
|
797
637
|
await authState.keys.set({ session: { [sessionId]: null } })
|
|
798
638
|
forceIncludeKeys = true
|
|
799
639
|
}
|
|
@@ -802,9 +642,9 @@ const makeMessagesRecvSocket = config => {
|
|
|
802
642
|
}
|
|
803
643
|
}
|
|
804
644
|
if (retryCount <= 2) {
|
|
805
|
-
|
|
645
|
+
// Use new retry manager for phone requests if available
|
|
806
646
|
if (messageRetryManager) {
|
|
807
|
-
|
|
647
|
+
// Schedule phone request with delay (like whatsmeow)
|
|
808
648
|
messageRetryManager.schedulePhoneRequest(msgId, async () => {
|
|
809
649
|
try {
|
|
810
650
|
const requestId = await requestPlaceholderResend(msgKey)
|
|
@@ -816,7 +656,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
816
656
|
}
|
|
817
657
|
})
|
|
818
658
|
} else {
|
|
819
|
-
|
|
659
|
+
// Fallback to immediate request
|
|
820
660
|
const msgId = await requestPlaceholderResend(msgKey)
|
|
821
661
|
logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`)
|
|
822
662
|
}
|
|
@@ -838,9 +678,8 @@ const makeMessagesRecvSocket = config => {
|
|
|
838
678
|
id: node.attrs.id,
|
|
839
679
|
t: node.attrs.t,
|
|
840
680
|
v: '1',
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
error: '1'
|
|
681
|
+
// ADD ERROR FIELD
|
|
682
|
+
error: '0'
|
|
844
683
|
}
|
|
845
684
|
},
|
|
846
685
|
{
|
|
@@ -878,14 +717,17 @@ const makeMessagesRecvSocket = config => {
|
|
|
878
717
|
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
|
|
879
718
|
}, authState?.creds?.me?.id || 'sendRetryRequest')
|
|
880
719
|
}
|
|
881
|
-
|
|
720
|
+
/**
|
|
721
|
+
* Fire-and-forget tctoken re-issuance after a peer's device identity changed.
|
|
722
|
+
* Runs in parallel with the session refresh (not after it).
|
|
723
|
+
*/
|
|
882
724
|
const reissueTcTokenAfterIdentityChange = from => {
|
|
883
725
|
void (async () => {
|
|
884
726
|
const normalizedJid = (0, WABinary_1.jidNormalizedUser)(from)
|
|
885
727
|
const tcJid = await (0, tc_token_utils_1.resolveTcTokenJid)(normalizedJid, getLIDForPN)
|
|
886
728
|
const tcTokenData = await authState.keys.get('tctoken', [tcJid])
|
|
887
729
|
const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
|
|
888
|
-
if (senderTs
|
|
730
|
+
if (senderTs === null || senderTs === undefined || (0, tc_token_utils_1.isTcTokenExpired)(senderTs)) {
|
|
889
731
|
return
|
|
890
732
|
}
|
|
891
733
|
logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
|
|
@@ -933,21 +775,8 @@ const makeMessagesRecvSocket = config => {
|
|
|
933
775
|
}
|
|
934
776
|
}
|
|
935
777
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const storeParticipantLidPairs = participants => {
|
|
939
|
-
const pairs = []
|
|
940
|
-
for (const p of participants || []) {
|
|
941
|
-
if (p.phoneNumber) pairs.push({ lid: p.id, pn: p.phoneNumber })
|
|
942
|
-
else if (p.lid) pairs.push({ lid: p.lid, pn: p.id })
|
|
943
|
-
}
|
|
944
|
-
if (!pairs.length) return
|
|
945
|
-
signalRepository.lidMapping
|
|
946
|
-
.storeLIDPNMappings(pairs)
|
|
947
|
-
.catch(error => logger.debug({ error }, 'failed to store LID/PN mappings from group notification'))
|
|
948
|
-
}
|
|
949
778
|
const handleGroupNotification = (fullNode, child, msg) => {
|
|
950
|
-
|
|
779
|
+
// TODO: Support PN/LID (Here is only LID now)
|
|
951
780
|
const actingParticipantLid = fullNode.attrs.participant
|
|
952
781
|
const actingParticipantPn = fullNode.attrs.participant_pn
|
|
953
782
|
const actingParticipantUsername = fullNode.attrs.participant_username
|
|
@@ -961,7 +790,6 @@ const makeMessagesRecvSocket = config => {
|
|
|
961
790
|
msg.messageStubType = Types_1.WAMessageStubType.GROUP_CREATE
|
|
962
791
|
msg.messageStubParameters = [metadata.subject]
|
|
963
792
|
msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
|
|
964
|
-
storeParticipantLidPairs(metadata.participants)
|
|
965
793
|
ev.emit('chats.upsert', [
|
|
966
794
|
{
|
|
967
795
|
id: metadata.id,
|
|
@@ -1000,6 +828,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1000
828
|
const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`
|
|
1001
829
|
msg.messageStubType = Types_1.WAMessageStubType[stubType]
|
|
1002
830
|
const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(({ attrs }) => {
|
|
831
|
+
// TODO: Store LID MAPPINGS
|
|
1003
832
|
return {
|
|
1004
833
|
id: attrs.jid,
|
|
1005
834
|
phoneNumber:
|
|
@@ -1008,21 +837,19 @@ const makeMessagesRecvSocket = config => {
|
|
|
1008
837
|
: undefined,
|
|
1009
838
|
lid: (0, WABinary_1.isPnUser)(attrs.jid) && (0, WABinary_1.isLidUser)(attrs.lid) ? attrs.lid : undefined,
|
|
1010
839
|
username: attrs.participant_username || attrs.username || undefined,
|
|
1011
|
-
admin: attrs.type || null
|
|
1012
|
-
uuid: attrs.uuid || attrs.participant_uuid || undefined
|
|
840
|
+
admin: attrs.type || null
|
|
1013
841
|
}
|
|
1014
842
|
})
|
|
1015
843
|
if (
|
|
1016
844
|
participants.length === 1 &&
|
|
1017
|
-
|
|
1018
|
-
|
|
845
|
+
// if recv. "remove" message and sender removed themselves
|
|
846
|
+
// mark as left
|
|
1019
847
|
((0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantLid) ||
|
|
1020
848
|
(0, WABinary_1.areJidsSameUser)(participants[0].id, actingParticipantPn)) &&
|
|
1021
849
|
child.tag === 'remove'
|
|
1022
850
|
) {
|
|
1023
851
|
msg.messageStubType = Types_1.WAMessageStubType.GROUP_PARTICIPANT_LEAVE
|
|
1024
852
|
}
|
|
1025
|
-
storeParticipantLidPairs(participants)
|
|
1026
853
|
msg.messageStubParameters = participants.map(a => JSON.stringify(a))
|
|
1027
854
|
break
|
|
1028
855
|
case 'subject':
|
|
@@ -1072,51 +899,13 @@ const makeMessagesRecvSocket = config => {
|
|
|
1072
899
|
break
|
|
1073
900
|
case 'revoked_membership_requests':
|
|
1074
901
|
const isDenied = (0, WABinary_1.areJidsSameUser)(affectedParticipantLid, actingParticipantLid)
|
|
1075
|
-
|
|
1076
|
-
storeParticipantLidPairs([{ id: affectedParticipantLid, phoneNumber: affectedParticipantPn }])
|
|
1077
|
-
}
|
|
902
|
+
// TODO: LIDMAPPING SUPPORT
|
|
1078
903
|
msg.messageStubType = Types_1.WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
|
|
1079
904
|
msg.messageStubParameters = [
|
|
1080
905
|
JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
|
|
1081
906
|
isDenied ? 'revoked' : 'rejected'
|
|
1082
907
|
]
|
|
1083
908
|
break
|
|
1084
|
-
case 'sibling_link': {
|
|
1085
|
-
const linkedGroupJid = (0, WABinary_1.getBinaryNodeChild)(child, 'group')?.attrs?.jid || child.attrs?.jid
|
|
1086
|
-
ev.emit('groups.update', [
|
|
1087
|
-
{
|
|
1088
|
-
id: fullNode.attrs.from,
|
|
1089
|
-
siblingGroupLinked: linkedGroupJid || true,
|
|
1090
|
-
author: actingParticipantLid,
|
|
1091
|
-
authorPn: actingParticipantPn
|
|
1092
|
-
}
|
|
1093
|
-
])
|
|
1094
|
-
break
|
|
1095
|
-
}
|
|
1096
|
-
case 'sibling_unlink': {
|
|
1097
|
-
const unlinkedGroupJid = (0, WABinary_1.getBinaryNodeChild)(child, 'group')?.attrs?.jid || child.attrs?.jid
|
|
1098
|
-
ev.emit('groups.update', [
|
|
1099
|
-
{
|
|
1100
|
-
id: fullNode.attrs.from,
|
|
1101
|
-
siblingGroupUnlinked: unlinkedGroupJid || true,
|
|
1102
|
-
author: actingParticipantLid,
|
|
1103
|
-
authorPn: actingParticipantPn
|
|
1104
|
-
}
|
|
1105
|
-
])
|
|
1106
|
-
break
|
|
1107
|
-
}
|
|
1108
|
-
case 'clear_history': {
|
|
1109
|
-
const historyClearTimestamp = child.attrs?.t ? +child.attrs.t : (0, Date)()
|
|
1110
|
-
ev.emit('groups.update', [
|
|
1111
|
-
{
|
|
1112
|
-
id: fullNode.attrs.from,
|
|
1113
|
-
historyClearTimestamp,
|
|
1114
|
-
author: actingParticipantLid,
|
|
1115
|
-
authorPn: actingParticipantPn
|
|
1116
|
-
}
|
|
1117
|
-
])
|
|
1118
|
-
break
|
|
1119
|
-
}
|
|
1120
909
|
}
|
|
1121
910
|
}
|
|
1122
911
|
const normalizeNotificationParticipant = async (jid, groupData) => {
|
|
@@ -1250,12 +1039,21 @@ const makeMessagesRecvSocket = config => {
|
|
|
1250
1039
|
}
|
|
1251
1040
|
}
|
|
1252
1041
|
|
|
1253
|
-
|
|
1042
|
+
/**
|
|
1043
|
+
* Handle incoming interop notifications (type="interop").
|
|
1044
|
+
*
|
|
1045
|
+
* The APK emits these for:
|
|
1046
|
+
* - stella_interop_enabled / stella_ios_enabled → feature-flag toggles
|
|
1047
|
+
* - ig_professional / ig_handle / followers → Instagram profile data updates
|
|
1048
|
+
* - fbid:thread / fbid:devices → Meta thread/device association
|
|
1049
|
+
* - peer_device_presence → interop contact online/offline
|
|
1050
|
+
* - group membership changes in interop groups → add/remove/promote events
|
|
1051
|
+
*/
|
|
1254
1052
|
const handleInteropNotification = (node, child) => {
|
|
1255
1053
|
const childTag = child?.tag
|
|
1256
1054
|
const attrs = child?.attrs || {}
|
|
1257
1055
|
|
|
1258
|
-
|
|
1056
|
+
// Feature flag: server toggled stella_interop_enabled or stella_ios_enabled
|
|
1259
1057
|
if (childTag === 'feature') {
|
|
1260
1058
|
const feature = attrs.name
|
|
1261
1059
|
const enabled = attrs.value === 'true' || attrs.value === '1'
|
|
@@ -1264,7 +1062,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1264
1062
|
return
|
|
1265
1063
|
}
|
|
1266
1064
|
|
|
1267
|
-
|
|
1065
|
+
// Instagram profile data pushed for an interop contact
|
|
1268
1066
|
if (childTag === 'ig_profile') {
|
|
1269
1067
|
const contactUpdate = {
|
|
1270
1068
|
id: (0, WABinary_1.jidNormalizedUser)(node.attrs.from),
|
|
@@ -1277,28 +1075,26 @@ const makeMessagesRecvSocket = config => {
|
|
|
1277
1075
|
return
|
|
1278
1076
|
}
|
|
1279
1077
|
|
|
1280
|
-
|
|
1078
|
+
// peer_device_presence — interop contact came online or went offline
|
|
1281
1079
|
if (childTag === 'peer_device_presence') {
|
|
1282
1080
|
const jid = attrs.jid || (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
|
|
1283
1081
|
const presence = attrs.type === 'unavailable' ? 'unavailable' : 'available'
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } }, isIosInterop })
|
|
1082
|
+
logger.debug({ jid, presence }, '[interop] peer_device_presence')
|
|
1083
|
+
ev.emit('presence.update', { id: jid, presences: { [jid]: { lastKnownPresence: presence } } })
|
|
1287
1084
|
return
|
|
1288
1085
|
}
|
|
1289
1086
|
|
|
1290
|
-
|
|
1087
|
+
// fbid:thread / fbid:devices — Meta thread or device list association
|
|
1291
1088
|
if (childTag === 'fbid_thread' || childTag === 'fbid_devices') {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs, isIosInterop })
|
|
1089
|
+
logger.debug({ childTag, attrs, from: node.attrs.from }, '[interop] fbid association update')
|
|
1090
|
+
ev.emit('interop.fbid-update', { type: childTag, jid: node.attrs.from, attrs })
|
|
1295
1091
|
return
|
|
1296
1092
|
}
|
|
1297
1093
|
|
|
1298
|
-
|
|
1094
|
+
// Interop group membership changes (add / remove / promote / demote)
|
|
1299
1095
|
if (childTag === 'participants') {
|
|
1300
1096
|
const groupJid = node.attrs.from
|
|
1301
|
-
const action = attrs.type
|
|
1097
|
+
const action = attrs.type // 'add' | 'remove' | 'promote' | 'demote'
|
|
1302
1098
|
const participants = (0, WABinary_1.getBinaryNodeChildren)(child, 'participant').map(p => p.attrs.jid)
|
|
1303
1099
|
logger.info({ groupJid, action, participants }, '[interop] group participants update')
|
|
1304
1100
|
ev.emit('group-participants.update', { id: groupJid, participants, action })
|
|
@@ -1318,24 +1114,10 @@ const makeMessagesRecvSocket = config => {
|
|
|
1318
1114
|
await handleNewsletterNotification(node)
|
|
1319
1115
|
break
|
|
1320
1116
|
case 'mex':
|
|
1321
|
-
await
|
|
1117
|
+
await handleMexNewsletterNotification(node)
|
|
1322
1118
|
break
|
|
1323
1119
|
case 'w:gp2':
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const dirtyGroupJids = (0, WABinary_1.getBinaryNodeChildren)(child, 'group')
|
|
1328
|
-
.map(g => g.attrs.jid)
|
|
1329
|
-
.filter(Boolean)
|
|
1330
|
-
if (dirtyGroupJids.length) {
|
|
1331
|
-
ev.emit(
|
|
1332
|
-
'groups.update',
|
|
1333
|
-
dirtyGroupJids.map(jid => ({ id: jid }))
|
|
1334
|
-
)
|
|
1335
|
-
}
|
|
1336
|
-
break
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1120
|
+
// TODO: HANDLE PARTICIPANT_PN
|
|
1339
1121
|
const groupData = await getNotificationGroupData(node)
|
|
1340
1122
|
handleGroupNotification(node, child, result)
|
|
1341
1123
|
|
|
@@ -1348,39 +1130,26 @@ const makeMessagesRecvSocket = config => {
|
|
|
1348
1130
|
case 'encrypt':
|
|
1349
1131
|
await handleEncryptNotification(node)
|
|
1350
1132
|
break
|
|
1351
|
-
case 'devices':
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const addNode = (0, WABinary_1.getBinaryNodeChild)(node, 'add')
|
|
1356
|
-
const removeNode = (0, WABinary_1.getBinaryNodeChild)(node, 'remove')
|
|
1357
|
-
const changedNode = addNode || removeNode
|
|
1358
|
-
if (!changedNode) {
|
|
1359
|
-
logger.debug({ node }, 'devices hash refresh, no add/remove list to report')
|
|
1360
|
-
break
|
|
1361
|
-
}
|
|
1362
|
-
const isAdded = !!addNode
|
|
1363
|
-
const devices = (0, WABinary_1.getBinaryNodeChildren)(changedNode, 'device')
|
|
1364
|
-
const deviceOwnerJid = from
|
|
1133
|
+
case 'devices':
|
|
1134
|
+
const devices = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
|
|
1135
|
+
const deviceOwnerJid = child.attrs.jid || child.attrs.lid
|
|
1365
1136
|
const deviceData = devices.map(d => ({
|
|
1366
1137
|
id: d.attrs.jid,
|
|
1367
1138
|
lid: d.attrs.lid,
|
|
1368
|
-
|
|
1369
|
-
keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined,
|
|
1139
|
+
keyIndex: d.attrs.key_index ? +d.attrs.key_index : undefined,
|
|
1370
1140
|
platform: d.attrs.platform || undefined,
|
|
1371
1141
|
isCompanion: d.attrs.companion === 'true' || undefined
|
|
1372
1142
|
}))
|
|
1373
|
-
|
|
1374
|
-
(0, WABinary_1.areJidsSameUser)(
|
|
1375
|
-
(0, WABinary_1.areJidsSameUser)(
|
|
1376
|
-
|
|
1377
|
-
logger.info({ deviceData
|
|
1378
|
-
|
|
1379
|
-
if (deviceOwnerJid) {
|
|
1380
|
-
ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf
|
|
1143
|
+
if (
|
|
1144
|
+
(0, WABinary_1.areJidsSameUser)(child.attrs.jid, authState.creds.me.id) ||
|
|
1145
|
+
(0, WABinary_1.areJidsSameUser)(child.attrs.lid, authState.creds.me.lid)
|
|
1146
|
+
) {
|
|
1147
|
+
logger.info({ deviceData }, 'my own devices changed')
|
|
1148
|
+
ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: true })
|
|
1149
|
+
} else if (deviceOwnerJid) {
|
|
1150
|
+
ev.emit('devices.update', { id: deviceOwnerJid, devices: deviceData, isSelf: false })
|
|
1381
1151
|
}
|
|
1382
1152
|
break
|
|
1383
|
-
}
|
|
1384
1153
|
case 'server_sync':
|
|
1385
1154
|
const update = (0, WABinary_1.getBinaryNodeChild)(node, 'collection')
|
|
1386
1155
|
if (update) {
|
|
@@ -1388,18 +1157,13 @@ const makeMessagesRecvSocket = config => {
|
|
|
1388
1157
|
await resyncAppState([name], false)
|
|
1389
1158
|
}
|
|
1390
1159
|
break
|
|
1391
|
-
case 'picture':
|
|
1160
|
+
case 'picture':
|
|
1392
1161
|
const setPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'set')
|
|
1393
1162
|
const delPicture = (0, WABinary_1.getBinaryNodeChild)(node, 'delete')
|
|
1394
|
-
|
|
1395
|
-
if (!pictureOwnerJid) {
|
|
1396
|
-
|
|
1397
|
-
logger.debug({ node }, 'picture notification missing "from", skipping contacts.update')
|
|
1398
|
-
break
|
|
1399
|
-
}
|
|
1163
|
+
// TODO: WAJIDHASH stuff proper support inhouse
|
|
1400
1164
|
ev.emit('contacts.update', [
|
|
1401
1165
|
{
|
|
1402
|
-
id:
|
|
1166
|
+
id: (0, WABinary_1.jidNormalizedUser)(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
|
|
1403
1167
|
imgUrl: setPicture ? 'changed' : 'removed'
|
|
1404
1168
|
}
|
|
1405
1169
|
])
|
|
@@ -1416,7 +1180,6 @@ const makeMessagesRecvSocket = config => {
|
|
|
1416
1180
|
}
|
|
1417
1181
|
}
|
|
1418
1182
|
break
|
|
1419
|
-
}
|
|
1420
1183
|
case 'account_sync':
|
|
1421
1184
|
if (child.tag === 'disappearing_mode') {
|
|
1422
1185
|
const newDuration = +child.attrs.duration
|
|
@@ -1432,60 +1195,18 @@ const makeMessagesRecvSocket = config => {
|
|
|
1432
1195
|
}
|
|
1433
1196
|
})
|
|
1434
1197
|
} else if (child.tag === 'blocklist') {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const dhash = child.attrs.dhash
|
|
1443
|
-
const deviceNodes = (0, WABinary_1.getBinaryNodeChildren)(child, 'device')
|
|
1444
|
-
const keyIndexListNode = (0, WABinary_1.getBinaryNodeChild)(child, 'key-index-list')
|
|
1445
|
-
const devices = deviceNodes.map(d => ({
|
|
1446
|
-
jid: d.attrs.jid ? String(d.attrs.jid) : undefined,
|
|
1447
|
-
keyIndex: d.attrs['key-index'] ? +d.attrs['key-index'] : undefined
|
|
1448
|
-
}))
|
|
1449
|
-
logger.info({ dhash, deviceCount: devices.length }, 'account devices list synced')
|
|
1450
|
-
ev.emit('account.devices-synced', {
|
|
1451
|
-
dhash,
|
|
1452
|
-
devices,
|
|
1453
|
-
keyIndexListTimestamp: keyIndexListNode?.attrs?.ts ? +keyIndexListNode.attrs.ts : undefined,
|
|
1454
|
-
keyIndexList: keyIndexListNode?.content ? Buffer.from(keyIndexListNode.content) : undefined
|
|
1455
|
-
})
|
|
1456
|
-
}
|
|
1457
|
-
break
|
|
1458
|
-
case 'disappearing_mode': {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const newDuration = +child.attrs.duration
|
|
1462
|
-
const timestamp = +child.attrs.t
|
|
1463
|
-
logger.info({ newDuration }, 'updated account disappearing mode')
|
|
1464
|
-
ev.emit('creds.update', {
|
|
1465
|
-
accountSettings: {
|
|
1466
|
-
...authState.creds.accountSettings,
|
|
1467
|
-
defaultDisappearingMode: {
|
|
1468
|
-
ephemeralExpiration: newDuration,
|
|
1469
|
-
ephemeralSettingTimestamp: timestamp
|
|
1470
|
-
}
|
|
1198
|
+
const blocklists = (0, WABinary_1.getBinaryNodeChildren)(child, 'item')
|
|
1199
|
+
for (const { attrs } of blocklists) {
|
|
1200
|
+
const blocklist = [attrs.jid]
|
|
1201
|
+
const type = attrs.action === 'block' ? 'add' : 'remove'
|
|
1202
|
+
ev.emit('blocklist.update', { blocklist, type })
|
|
1471
1203
|
}
|
|
1472
|
-
})
|
|
1473
|
-
break
|
|
1474
|
-
}
|
|
1475
|
-
case 'contacts': {
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const updateChild = (0, WABinary_1.getBinaryNodeChild)(node, 'update')
|
|
1479
|
-
if (updateChild?.attrs?.jid) {
|
|
1480
|
-
ev.emit('contacts.update', [{ id: (0, WABinary_1.jidNormalizedUser)(updateChild.attrs.jid) }])
|
|
1481
|
-
} else {
|
|
1482
|
-
logger.debug({ node }, 'contacts list changed (hash/sync signal), no per-contact jid to update')
|
|
1483
1204
|
}
|
|
1484
1205
|
break
|
|
1485
|
-
}
|
|
1486
1206
|
case 'business':
|
|
1207
|
+
// SMB privacy / data-sharing settings sync push
|
|
1208
|
+
// (WhatsApp Web: WASmaxInBizSettingsSyncPrivacySettingRequest)
|
|
1487
1209
|
if (child?.tag === 'privacy') {
|
|
1488
|
-
|
|
1489
1210
|
ev.emit('business.privacy-settings-sync', {
|
|
1490
1211
|
jid: from,
|
|
1491
1212
|
categories: (0, WABinary_1.getBinaryNodeChildren)(child, 'category').map(c => ({
|
|
@@ -1494,37 +1215,11 @@ const makeMessagesRecvSocket = config => {
|
|
|
1494
1215
|
})),
|
|
1495
1216
|
attrs: child.attrs
|
|
1496
1217
|
})
|
|
1497
|
-
} else if (child?.tag === 'profile') {
|
|
1498
|
-
|
|
1499
|
-
ev.emit('contacts.update', [
|
|
1500
|
-
{
|
|
1501
|
-
id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid || from),
|
|
1502
|
-
businessProfileTag: child.attrs.tag
|
|
1503
|
-
}
|
|
1504
|
-
])
|
|
1505
|
-
} else if (child?.tag === 'verified_name') {
|
|
1506
|
-
|
|
1507
|
-
ev.emit('contacts.update', [
|
|
1508
|
-
{
|
|
1509
|
-
id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid || from),
|
|
1510
|
-
verifiedName: {
|
|
1511
|
-
verifiedLevel: child.attrs.verified_level,
|
|
1512
|
-
serial: child.attrs.serial,
|
|
1513
|
-
version: child.attrs.v
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
])
|
|
1517
|
-
} else if (child?.tag === 'remove') {
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (child.attrs.jid) {
|
|
1521
|
-
ev.emit('contacts.update', [{ id: (0, WABinary_1.jidNormalizedUser)(child.attrs.jid), isBusiness: false }])
|
|
1522
|
-
}
|
|
1523
1218
|
}
|
|
1524
1219
|
break
|
|
1525
1220
|
case 'hosted':
|
|
1526
|
-
|
|
1527
|
-
|
|
1221
|
+
// Coexistence (WhatsApp <-> Messenger/Instagram) onboarding/offboarding push
|
|
1222
|
+
// (WhatsApp Web: WASmaxInCoexistenceOnboarding/OffboardingNotification)
|
|
1528
1223
|
if (child?.tag === 'onboarding_status') {
|
|
1529
1224
|
ev.emit('coexistence.update', {
|
|
1530
1225
|
jid: from,
|
|
@@ -1542,28 +1237,18 @@ const makeMessagesRecvSocket = config => {
|
|
|
1542
1237
|
break
|
|
1543
1238
|
case 'link_code_companion_reg':
|
|
1544
1239
|
const linkCodeCompanionReg = (0, WABinary_1.getBinaryNodeChild)(node, 'link_code_companion_reg')
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
linkCodeCompanionReg,
|
|
1548
|
-
'primary_identity_pub'
|
|
1240
|
+
const ref = toRequiredBuffer(
|
|
1241
|
+
(0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'link_code_pairing_ref')
|
|
1549
1242
|
)
|
|
1550
|
-
const
|
|
1551
|
-
linkCodeCompanionReg,
|
|
1552
|
-
|
|
1243
|
+
const primaryIdentityPublicKey = toRequiredBuffer(
|
|
1244
|
+
(0, WABinary_1.getBinaryNodeChildBuffer)(linkCodeCompanionReg, 'primary_identity_pub')
|
|
1245
|
+
)
|
|
1246
|
+
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
|
|
1247
|
+
(0, WABinary_1.getBinaryNodeChildBuffer)(
|
|
1248
|
+
linkCodeCompanionReg,
|
|
1249
|
+
'link_code_pairing_wrapped_primary_ephemeral_pub'
|
|
1250
|
+
)
|
|
1553
1251
|
)
|
|
1554
|
-
if (
|
|
1555
|
-
refBuffer === undefined ||
|
|
1556
|
-
primaryIdentityPublicKeyBuffer === undefined ||
|
|
1557
|
-
primaryEphemeralPublicKeyWrappedBuffer === undefined
|
|
1558
|
-
) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
logger.debug({ id: node.attrs.id, type: node.attrs.type }, 'skipping empty link code companion registration')
|
|
1562
|
-
break
|
|
1563
|
-
}
|
|
1564
|
-
const ref = toRequiredBuffer(refBuffer)
|
|
1565
|
-
const primaryIdentityPublicKey = toRequiredBuffer(primaryIdentityPublicKeyBuffer)
|
|
1566
|
-
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(primaryEphemeralPublicKeyWrappedBuffer)
|
|
1567
1252
|
const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
|
|
1568
1253
|
const companionSharedKey = Utils_1.Curve.sharedKey(
|
|
1569
1254
|
authState.creds.pairingEphemeralKeyPair.private,
|
|
@@ -1638,7 +1323,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1638
1323
|
await handlePrivacyTokenNotification(node)
|
|
1639
1324
|
break
|
|
1640
1325
|
case 'security':
|
|
1641
|
-
|
|
1326
|
+
// Security notifications (compromised session, location change alerts)
|
|
1642
1327
|
const securityType = child?.tag || node.attrs.type
|
|
1643
1328
|
const securityData = {
|
|
1644
1329
|
type: securityType,
|
|
@@ -1650,7 +1335,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1650
1335
|
ev.emit('security.alert', securityData)
|
|
1651
1336
|
break
|
|
1652
1337
|
case 'identity':
|
|
1653
|
-
|
|
1338
|
+
// Identity change notifications — peer changed their identity key
|
|
1654
1339
|
const identityJid = node.attrs.from
|
|
1655
1340
|
const identityNewKey = child?.content ? Buffer.from(child.content) : undefined
|
|
1656
1341
|
ev.emit('identity.update', {
|
|
@@ -1660,7 +1345,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1660
1345
|
})
|
|
1661
1346
|
break
|
|
1662
1347
|
case 'server':
|
|
1663
|
-
|
|
1348
|
+
// Server-issued notifications (config changes, client config refresh)
|
|
1664
1349
|
const serverTag = child?.tag
|
|
1665
1350
|
if (serverTag === 'config') {
|
|
1666
1351
|
const configData = {}
|
|
@@ -1669,7 +1354,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1669
1354
|
}
|
|
1670
1355
|
ev.emit('server.config', configData)
|
|
1671
1356
|
} else if (serverTag === 'app_state_key') {
|
|
1672
|
-
|
|
1357
|
+
// Server pushed a new app-state key — trigger resync
|
|
1673
1358
|
logger.info('server pushed app state key update')
|
|
1674
1359
|
await resyncAppState(['critical_block', 'regular_high', 'regular_low'], false)
|
|
1675
1360
|
} else {
|
|
@@ -1677,7 +1362,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1677
1362
|
}
|
|
1678
1363
|
break
|
|
1679
1364
|
case 'status':
|
|
1680
|
-
|
|
1365
|
+
// Contact status (about) change notification
|
|
1681
1366
|
const statusOwner = node.attrs.from
|
|
1682
1367
|
const statusText = child?.content ? child.content.toString() : undefined
|
|
1683
1368
|
if (statusOwner && statusText !== undefined) {
|
|
@@ -1690,7 +1375,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1690
1375
|
}
|
|
1691
1376
|
break
|
|
1692
1377
|
case 'usync':
|
|
1693
|
-
|
|
1378
|
+
// USync result push from server
|
|
1694
1379
|
const usyncResults = (0, WABinary_1.getBinaryNodeChildren)(child || node, 'user')
|
|
1695
1380
|
if (usyncResults.length) {
|
|
1696
1381
|
const updates = usyncResults
|
|
@@ -1707,12 +1392,12 @@ const makeMessagesRecvSocket = config => {
|
|
|
1707
1392
|
}
|
|
1708
1393
|
break
|
|
1709
1394
|
case 'interop':
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1395
|
+
// Interop-related server notifications — covers:
|
|
1396
|
+
// stella_interop_enabled / stella_ios_enabled feature flags
|
|
1397
|
+
// ig_professional / ig_handle / followers (Instagram account data)
|
|
1398
|
+
// fbid:thread / fbid:devices (Meta thread/device references)
|
|
1399
|
+
// peer_device_presence updates
|
|
1400
|
+
// group membership changes in interop groups
|
|
1716
1401
|
handleInteropNotification(node, child)
|
|
1717
1402
|
break
|
|
1718
1403
|
}
|
|
@@ -1720,7 +1405,10 @@ const makeMessagesRecvSocket = config => {
|
|
|
1720
1405
|
return result
|
|
1721
1406
|
}
|
|
1722
1407
|
}
|
|
1723
|
-
|
|
1408
|
+
/**
|
|
1409
|
+
* In-memory cache of storage JIDs with stored tctokens, seeded from the persisted index.
|
|
1410
|
+
* Used to coalesce writes during a session; pruning always re-reads the persisted index.
|
|
1411
|
+
*/
|
|
1724
1412
|
const tcTokenKnownJids = new Set()
|
|
1725
1413
|
const tcTokenIndexLoaded = (async () => {
|
|
1726
1414
|
try {
|
|
@@ -1761,7 +1449,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1761
1449
|
const tokensNode = (0, WABinary_1.getBinaryNodeChild)(node, 'tokens')
|
|
1762
1450
|
if (!tokensNode) return
|
|
1763
1451
|
const from = (0, WABinary_1.jidNormalizedUser)(node.attrs.from)
|
|
1764
|
-
|
|
1452
|
+
// WA Web uses: senderLid ?? toLid(from) for the storage key
|
|
1765
1453
|
const senderLid =
|
|
1766
1454
|
node.attrs.sender_lid && (0, WABinary_1.isLidUser)((0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid))
|
|
1767
1455
|
? (0, WABinary_1.jidNormalizedUser)(node.attrs.sender_lid)
|
|
@@ -1800,30 +1488,31 @@ const makeMessagesRecvSocket = config => {
|
|
|
1800
1488
|
const newValue = ((await msgRetryCache.get(key)) || 0) + 1
|
|
1801
1489
|
await msgRetryCache.set(key, newValue)
|
|
1802
1490
|
}
|
|
1803
|
-
const sendMessagesAgain = async (key, ids, retryNode) => {
|
|
1491
|
+
const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
|
|
1804
1492
|
const remoteJid = key.remoteJid
|
|
1805
1493
|
const participant = key.participant || remoteJid
|
|
1806
1494
|
const retryCount = +retryNode.attrs.count || 1
|
|
1807
|
-
|
|
1495
|
+
const msgId = ids[0]
|
|
1496
|
+
// Try to get messages from cache first, then fallback to getMessage
|
|
1808
1497
|
const msgs = []
|
|
1809
1498
|
for (const id of ids) {
|
|
1810
1499
|
let msg
|
|
1811
|
-
|
|
1500
|
+
// Try to get from retry cache first if enabled
|
|
1812
1501
|
if (messageRetryManager) {
|
|
1813
1502
|
const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id)
|
|
1814
1503
|
if (cachedMsg) {
|
|
1815
1504
|
msg = cachedMsg.message
|
|
1816
1505
|
logger.debug({ jid: remoteJid, id }, 'found message in retry cache')
|
|
1817
|
-
|
|
1506
|
+
// Mark retry as successful since we found the message
|
|
1818
1507
|
messageRetryManager.markRetrySuccess(id)
|
|
1819
1508
|
}
|
|
1820
1509
|
}
|
|
1821
|
-
|
|
1510
|
+
// Fallback to getMessage if not found in cache
|
|
1822
1511
|
if (!msg) {
|
|
1823
1512
|
msg = await getMessage({ ...key, id })
|
|
1824
1513
|
if (msg) {
|
|
1825
1514
|
logger.debug({ jid: remoteJid, id }, 'found message via getMessage')
|
|
1826
|
-
|
|
1515
|
+
// Also mark as successful if found via getMessage
|
|
1827
1516
|
if (messageRetryManager) {
|
|
1828
1517
|
messageRetryManager.markRetrySuccess(id)
|
|
1829
1518
|
}
|
|
@@ -1831,16 +1520,57 @@ const makeMessagesRecvSocket = config => {
|
|
|
1831
1520
|
}
|
|
1832
1521
|
msgs.push(msg)
|
|
1833
1522
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1523
|
+
// if it's the primary jid sending the request
|
|
1524
|
+
// just re-send the message to everyone
|
|
1525
|
+
// prevents the first message decryption failure
|
|
1837
1526
|
const sendToAll = !(0, WABinary_1.jidDecode)(participant)?.device
|
|
1838
|
-
|
|
1527
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
|
|
1528
|
+
let injectedFromBundle = false
|
|
1529
|
+
if (typeof signalRepository.injectE2ESession === 'function' && typeof Utils_1.extractE2ESessionFromRetryReceipt === 'function') {
|
|
1530
|
+
const bundle = Utils_1.extractE2ESessionFromRetryReceipt(receiptNode)
|
|
1531
|
+
if (bundle) {
|
|
1532
|
+
try {
|
|
1533
|
+
await signalRepository.injectE2ESession({ jid: participant, session: bundle })
|
|
1534
|
+
injectedFromBundle = true
|
|
1535
|
+
logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle')
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
logger.warn({ error, participant }, 'failed to inject session from retry receipt')
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (!injectedFromBundle && signalRepository.getSessionInfo) {
|
|
1542
|
+
const receivedRegId = (0, WABinary_1.getBinaryNodeChildUInt)(receiptNode, 'registration', 4)
|
|
1543
|
+
if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
|
|
1544
|
+
const info = await signalRepository.getSessionInfo(participant)
|
|
1545
|
+
if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) {
|
|
1546
|
+
logger.info(
|
|
1547
|
+
{ participant, stored: info.registrationId, received: receivedRegId },
|
|
1548
|
+
'reg id mismatch on retry without bundle, deleting session'
|
|
1549
|
+
)
|
|
1550
|
+
await authState.keys.set({ session: { [sessionId]: null } })
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const BASE_KEY_CHECK_RETRY = 2
|
|
1555
|
+
if (msgId && messageRetryManager && signalRepository.getSessionInfo) {
|
|
1556
|
+
const info = await signalRepository.getSessionInfo(participant)
|
|
1557
|
+
if (info) {
|
|
1558
|
+
if (retryCount === BASE_KEY_CHECK_RETRY) {
|
|
1559
|
+
messageRetryManager.saveBaseKey(sessionId, msgId, info.baseKey)
|
|
1560
|
+
} else if (retryCount > BASE_KEY_CHECK_RETRY) {
|
|
1561
|
+
if (messageRetryManager.hasSameBaseKey(sessionId, msgId, info.baseKey)) {
|
|
1562
|
+
logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session')
|
|
1563
|
+
await authState.keys.set({ session: { [sessionId]: null } })
|
|
1564
|
+
}
|
|
1565
|
+
messageRetryManager.deleteBaseKey(sessionId, msgId)
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
// Check if we should recreate session for this retry
|
|
1839
1570
|
let shouldRecreateSession = false
|
|
1840
1571
|
let recreateReason = ''
|
|
1841
|
-
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
1572
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
|
|
1842
1573
|
try {
|
|
1843
|
-
const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
|
|
1844
1574
|
const hasSession = await signalRepository.validateSession(participant)
|
|
1845
1575
|
const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists)
|
|
1846
1576
|
shouldRecreateSession = result.recreate
|
|
@@ -1853,7 +1583,9 @@ const makeMessagesRecvSocket = config => {
|
|
|
1853
1583
|
logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry')
|
|
1854
1584
|
}
|
|
1855
1585
|
}
|
|
1856
|
-
|
|
1586
|
+
if (!injectedFromBundle) {
|
|
1587
|
+
await assertSessions([participant], true)
|
|
1588
|
+
}
|
|
1857
1589
|
if ((0, WABinary_1.isJidGroup)(remoteJid)) {
|
|
1858
1590
|
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
|
|
1859
1591
|
}
|
|
@@ -1902,59 +1634,14 @@ const makeMessagesRecvSocket = config => {
|
|
|
1902
1634
|
const items = (0, WABinary_1.getBinaryNodeChildren)(content[0], 'item')
|
|
1903
1635
|
ids.push(...items.map(i => i.attrs.id))
|
|
1904
1636
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
if (attrs.type === 'media-retry') {
|
|
1908
|
-
const mediaRetryNode =
|
|
1909
|
-
(0, WABinary_1.getBinaryNodeChild)(node, 'media-retry') ||
|
|
1910
|
-
(0, WABinary_1.getBinaryNodeChild)(node, 'media_retry')
|
|
1911
|
-
ev.emit('messages.media-retry', {
|
|
1912
|
-
ids,
|
|
1913
|
-
from: attrs.from,
|
|
1914
|
-
participant: attrs.participant,
|
|
1915
|
-
t: attrs.t,
|
|
1916
|
-
retryAttrs: mediaRetryNode?.attrs
|
|
1917
|
-
})
|
|
1918
|
-
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack media-retry receipt'))
|
|
1919
|
-
return
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
if (attrs.type === 'server-error') {
|
|
1924
|
-
const errorNode = (0, WABinary_1.getBinaryNodeChild)(node, 'error')
|
|
1925
|
-
ev.emit('messages.server-error', {
|
|
1926
|
-
ids,
|
|
1927
|
-
from: attrs.from,
|
|
1928
|
-
participant: attrs.participant,
|
|
1929
|
-
t: attrs.t,
|
|
1930
|
-
errorCode: errorNode?.attrs?.code || attrs.error_code,
|
|
1931
|
-
errorText: errorNode?.attrs?.text || errorNode?.content?.toString?.()
|
|
1932
|
-
})
|
|
1933
|
-
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack server-error receipt'))
|
|
1934
|
-
return
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (attrs.receipt_agg) {
|
|
1940
|
-
ev.emit('receipt.batched', {
|
|
1941
|
-
ids,
|
|
1942
|
-
from: attrs.from,
|
|
1943
|
-
participant: attrs.participant,
|
|
1944
|
-
type: attrs.type,
|
|
1945
|
-
t: attrs.t,
|
|
1946
|
-
receiptAgg: attrs.receipt_agg
|
|
1947
|
-
})
|
|
1948
|
-
|
|
1949
|
-
}
|
|
1950
1637
|
try {
|
|
1951
1638
|
await Promise.all([
|
|
1952
1639
|
receiptMutex.mutex(async () => {
|
|
1953
1640
|
const status = (0, Utils_1.getStatusFromReceiptType)(attrs.type)
|
|
1954
1641
|
if (
|
|
1955
1642
|
typeof status !== 'undefined' &&
|
|
1956
|
-
|
|
1957
|
-
|
|
1643
|
+
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
|
1644
|
+
// or another device of ours has read some messages
|
|
1958
1645
|
(status >= index_js_1.proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)
|
|
1959
1646
|
) {
|
|
1960
1647
|
if ((0, WABinary_1.isJidGroup)(remoteJid) || (0, WABinary_1.isJidStatusBroadcast)(remoteJid)) {
|
|
@@ -1983,7 +1670,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1983
1670
|
}
|
|
1984
1671
|
}
|
|
1985
1672
|
if (attrs.type === 'retry') {
|
|
1986
|
-
|
|
1673
|
+
// correctly set who is asking for the retry
|
|
1987
1674
|
key.participant = key.participant || attrs.from
|
|
1988
1675
|
const retryNode = (0, WABinary_1.getBinaryNodeChild)(node, 'retry')
|
|
1989
1676
|
if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
|
|
@@ -1991,7 +1678,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
1991
1678
|
try {
|
|
1992
1679
|
await updateSendMessageAgainCount(ids[0], key.participant)
|
|
1993
1680
|
logger.debug({ attrs, key }, 'recv retry request')
|
|
1994
|
-
await sendMessagesAgain(key, ids, retryNode)
|
|
1681
|
+
await sendMessagesAgain(key, ids, retryNode, node)
|
|
1995
1682
|
} catch (error) {
|
|
1996
1683
|
logger.error(
|
|
1997
1684
|
{ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' },
|
|
@@ -2090,11 +1777,11 @@ const makeMessagesRecvSocket = config => {
|
|
|
2090
1777
|
const groupJid = node.attrs.from
|
|
2091
1778
|
const communityJid = linkedParentMap[groupJid]
|
|
2092
1779
|
const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
|
|
2093
|
-
|
|
1780
|
+
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
2094
1781
|
if (encNode?.attrs.type === 'msmsg') {
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
1782
|
+
// await sendMessageAck(node, Utils_1.NACK_REASONS.MissingMessageSecret)
|
|
1783
|
+
// return
|
|
1784
|
+
// Pre-populate botMessageSecrets from store so msmsg can be decrypted after restart
|
|
2098
1785
|
if (getMessage) {
|
|
2099
1786
|
const metaNode = (0, WABinary_1.getBinaryNodeChild)(node, 'meta')
|
|
2100
1787
|
const targetId = metaNode?.attrs?.target_id
|
|
@@ -2132,7 +1819,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2132
1819
|
)
|
|
2133
1820
|
}
|
|
2134
1821
|
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
|
|
2135
|
-
|
|
1822
|
+
// store new mappings we didn't have before
|
|
2136
1823
|
if (!!alt) {
|
|
2137
1824
|
const altServer = (0, WABinary_1.jidDecode)(alt)?.server
|
|
2138
1825
|
const primaryJid = msg.key.participant || msg.key.remoteJid
|
|
@@ -2145,32 +1832,6 @@ const makeMessagesRecvSocket = config => {
|
|
|
2145
1832
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }])
|
|
2146
1833
|
await signalRepository.migrateSession(alt, primaryJid)
|
|
2147
1834
|
}
|
|
2148
|
-
} else if (msg.key.addressingMode === 'lid') {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const primaryJid = msg.key.participant || msg.key.remoteJid
|
|
2154
|
-
if ((0, WABinary_1.isLidUser)(primaryJid)) {
|
|
2155
|
-
try {
|
|
2156
|
-
const pn = await signalRepository.lidMapping.getPNForLID(primaryJid)
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
const pnJid = pn ? (0, WABinary_1.jidNormalizedUser)(pn) : ''
|
|
2161
|
-
if (pnJid) {
|
|
2162
|
-
if ((0, WABinary_1.isJidGroup)(msg.key.remoteJid)) {
|
|
2163
|
-
msg.key.participantAlt = pnJid
|
|
2164
|
-
} else {
|
|
2165
|
-
msg.key.remoteJidAlt = pnJid
|
|
2166
|
-
}
|
|
2167
|
-
logger.debug({ lid: primaryJid, pn: pnJid }, 'recovered alt JID from LID mapping store')
|
|
2168
|
-
}
|
|
2169
|
-
} catch (err) {
|
|
2170
|
-
|
|
2171
|
-
logger.warn({ err, lid: primaryJid }, 'failed to recover alt JID from LID mapping store')
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
1835
|
}
|
|
2175
1836
|
await messageMutex.mutex(async () => {
|
|
2176
1837
|
await decrypt()
|
|
@@ -2191,7 +1852,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2191
1852
|
if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
|
|
2192
1853
|
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
|
|
2193
1854
|
}
|
|
2194
|
-
|
|
1855
|
+
// message failed to decrypt
|
|
2195
1856
|
if (msg.messageStubType === index_js_1.proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
2196
1857
|
if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
|
|
2197
1858
|
if (isInteropNode) logger.warn({ id: msg.key.id }, '[interop] decrypt failed: MISSING_KEYS')
|
|
@@ -2199,8 +1860,8 @@ const makeMessagesRecvSocket = config => {
|
|
|
2199
1860
|
return sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
|
|
2200
1861
|
}
|
|
2201
1862
|
if (msg.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
2202
|
-
|
|
2203
|
-
|
|
1863
|
+
// Message arrived without encryption (e.g. CTWA ads messages).
|
|
1864
|
+
// Check if this is eligible for placeholder resend (matching WA Web filters).
|
|
2204
1865
|
const unavailableNode = (0, WABinary_1.getBinaryNodeChild)(node, 'unavailable')
|
|
2205
1866
|
const unavailableType = unavailableNode?.attrs?.type
|
|
2206
1867
|
if (
|
|
@@ -2221,18 +1882,18 @@ const makeMessagesRecvSocket = config => {
|
|
|
2221
1882
|
acked = true
|
|
2222
1883
|
return sendMessageAck(node)
|
|
2223
1884
|
}
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
1885
|
+
// Request the real content from the phone via placeholder resend PDO.
|
|
1886
|
+
// Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
|
|
1887
|
+
// and store the requestId in stubParameters[1] so users can correlate
|
|
1888
|
+
// with the incoming PDO response event.
|
|
2228
1889
|
const cleanKey = {
|
|
2229
1890
|
remoteJid: msg.key.remoteJid,
|
|
2230
1891
|
fromMe: msg.key.fromMe,
|
|
2231
1892
|
id: msg.key.id,
|
|
2232
1893
|
participant: msg.key.participant
|
|
2233
1894
|
}
|
|
2234
|
-
|
|
2235
|
-
|
|
1895
|
+
// Cache the original message metadata so the PDO response handler
|
|
1896
|
+
// can preserve key fields (LID details etc.) that the phone may omit
|
|
2236
1897
|
const msgData = {
|
|
2237
1898
|
key: msg.key,
|
|
2238
1899
|
messageTimestamp: msg.messageTimestamp,
|
|
@@ -2257,9 +1918,9 @@ const makeMessagesRecvSocket = config => {
|
|
|
2257
1918
|
})
|
|
2258
1919
|
acked = true
|
|
2259
1920
|
await sendMessageAck(node)
|
|
2260
|
-
|
|
1921
|
+
// Don't return — fall through to upsertMessage so the stub is emitted
|
|
2261
1922
|
} else {
|
|
2262
|
-
|
|
1923
|
+
// Skip retry for expired status messages (>24h old)
|
|
2263
1924
|
if ((0, WABinary_1.isJidStatusBroadcast)(msg.key.remoteJid)) {
|
|
2264
1925
|
const messageAge = (0, Utils_1.unixTimestampSeconds)() - (0, Utils_1.toNumber)(msg.messageTimestamp)
|
|
2265
1926
|
if (messageAge > Defaults_1.STATUS_EXPIRY_SECONDS) {
|
|
@@ -2274,14 +1935,14 @@ const makeMessagesRecvSocket = config => {
|
|
|
2274
1935
|
const errorMessage = msg?.messageStubParameters?.[0] || ''
|
|
2275
1936
|
const isPreKeyError = errorMessage.includes('PreKey')
|
|
2276
1937
|
logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
|
|
2277
|
-
|
|
1938
|
+
// Handle both pre-key and normal retries in single mutex
|
|
2278
1939
|
await retryMutex.mutex(async () => {
|
|
2279
1940
|
try {
|
|
2280
1941
|
if (!ws.isOpen) {
|
|
2281
1942
|
logger.debug({ node }, 'Connection closed, skipping retry')
|
|
2282
1943
|
return
|
|
2283
1944
|
}
|
|
2284
|
-
|
|
1945
|
+
// Handle pre-key errors with upload and delay
|
|
2285
1946
|
if (isPreKeyError) {
|
|
2286
1947
|
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying')
|
|
2287
1948
|
try {
|
|
@@ -2300,7 +1961,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2300
1961
|
}
|
|
2301
1962
|
} catch (err) {
|
|
2302
1963
|
logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry')
|
|
2303
|
-
|
|
1964
|
+
// Still attempt retry even if pre-key upload failed
|
|
2304
1965
|
try {
|
|
2305
1966
|
const encNode = (0, WABinary_1.getBinaryNodeChild)(node, 'enc')
|
|
2306
1967
|
await sendRetryRequest(node, !encNode)
|
|
@@ -2318,34 +1979,34 @@ const makeMessagesRecvSocket = config => {
|
|
|
2318
1979
|
}
|
|
2319
1980
|
const isNewsletter = (0, WABinary_1.isJidNewsletter)(msg.key.remoteJid)
|
|
2320
1981
|
if (!isNewsletter) {
|
|
2321
|
-
|
|
1982
|
+
// no type in the receipt => message delivered
|
|
2322
1983
|
let type = undefined
|
|
2323
1984
|
let participant = msg.key.participant
|
|
2324
1985
|
if (communityJid) {
|
|
2325
1986
|
msg.communityJid = communityJid
|
|
2326
1987
|
}
|
|
2327
1988
|
if (category === 'peer') {
|
|
2328
|
-
|
|
1989
|
+
// special peer message
|
|
2329
1990
|
type = 'peer_msg'
|
|
2330
1991
|
} else if (msg.key.fromMe) {
|
|
2331
|
-
|
|
1992
|
+
// message was sent by us from a different device
|
|
2332
1993
|
type = 'sender'
|
|
2333
|
-
|
|
1994
|
+
// need to specially handle this case
|
|
2334
1995
|
if ((0, WABinary_1.isLidUser)(msg.key.remoteJid) || (0, WABinary_1.isLidUser)(msg.key.remoteJidAlt)) {
|
|
2335
|
-
participant = author
|
|
1996
|
+
participant = author // TODO: investigate sending receipts to LIDs and not PNs
|
|
2336
1997
|
}
|
|
2337
1998
|
} else if (!sendActiveReceipts) {
|
|
2338
1999
|
type = 'inactive'
|
|
2339
2000
|
}
|
|
2340
2001
|
acked = true
|
|
2341
|
-
|
|
2002
|
+
// Pass sts from the original stanza for interop contacts (BirdyChat/Haiket)
|
|
2342
2003
|
const interopSts = (0, WABinary_1.isInteropUser)(msg.key.remoteJid) ? node.attrs.sts : undefined
|
|
2343
2004
|
await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type, interopSts)
|
|
2344
|
-
|
|
2005
|
+
// send ack for history message
|
|
2345
2006
|
const isAnyHistoryMsg = (0, Utils_1.getHistoryMsg)(msg.message)
|
|
2346
2007
|
if (isAnyHistoryMsg) {
|
|
2347
2008
|
const jid = (0, WABinary_1.jidNormalizedUser)(msg.key.remoteJid)
|
|
2348
|
-
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync')
|
|
2009
|
+
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync') // TODO: investigate
|
|
2349
2010
|
}
|
|
2350
2011
|
} else {
|
|
2351
2012
|
acked = true
|
|
@@ -2407,19 +2068,6 @@ const makeMessagesRecvSocket = config => {
|
|
|
2407
2068
|
}
|
|
2408
2069
|
}
|
|
2409
2070
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
const parseGroupCallRoster = groupInfoNode =>
|
|
2414
|
-
(0, WABinary_1.getBinaryNodeChildren)(groupInfoNode, 'user').map(userNode => ({
|
|
2415
|
-
jid: userNode.attrs.jid,
|
|
2416
|
-
state: userNode.attrs.state,
|
|
2417
|
-
userPn: userNode.attrs.user_pn,
|
|
2418
|
-
devices: (0, WABinary_1.getBinaryNodeChildren)(userNode, 'device').map(d => ({
|
|
2419
|
-
jid: d.attrs.jid,
|
|
2420
|
-
platform: d.attrs.platform
|
|
2421
|
-
}))
|
|
2422
|
-
}))
|
|
2423
2071
|
const handleCall = async node => {
|
|
2424
2072
|
try {
|
|
2425
2073
|
const { attrs } = node
|
|
@@ -2447,88 +2095,20 @@ const makeMessagesRecvSocket = config => {
|
|
|
2447
2095
|
}
|
|
2448
2096
|
}
|
|
2449
2097
|
if (status === 'offer') {
|
|
2450
|
-
|
|
2451
|
-
const audioNodes = (0, WABinary_1.getBinaryNodeChildren)(infoChild, 'audio')
|
|
2452
|
-
const silenceNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'silence')
|
|
2453
|
-
call.isVideo = !!videoNode
|
|
2098
|
+
call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(infoChild, 'video')
|
|
2454
2099
|
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']
|
|
2455
2100
|
call.groupJid = infoChild.attrs['group-jid']
|
|
2456
|
-
|
|
2457
|
-
call.isLightweight = infoChild.attrs.lightweight === '1'
|
|
2458
|
-
if (silenceNode?.attrs?.reason) {
|
|
2459
|
-
call.silenceReason = silenceNode.attrs.reason
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
if (audioNodes.length) {
|
|
2464
|
-
call.audioCodecs = audioNodes.map(n => ({ enc: n.attrs.enc, rate: n.attrs.rate ? +n.attrs.rate : undefined }))
|
|
2465
|
-
call.audioCodec = audioNodes[0].attrs.enc
|
|
2466
|
-
}
|
|
2467
|
-
if (videoNode?.attrs?.dec) {
|
|
2468
|
-
call.videoCodec = videoNode.attrs.dec
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
const encNode = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc')
|
|
2472
|
-
if (encNode?.content) {
|
|
2473
|
-
try {
|
|
2474
|
-
const encType = encNode.attrs.type || 'msg'
|
|
2475
|
-
const ciphertext = Buffer.isBuffer(encNode.content) ? encNode.content : Buffer.from(encNode.content)
|
|
2476
|
-
const decrypted = await signalRepository.decryptMessage({ jid: attrs.from, type: encType, ciphertext })
|
|
2477
|
-
const unpadded = (0, Utils_1.unpadRandomMax16)(decrypted)
|
|
2478
|
-
const callMsg = index_js_1.proto.Message.decode(unpadded)
|
|
2479
|
-
if (callMsg?.call?.callKey?.length) {
|
|
2480
|
-
call.callKey = Buffer.from(callMsg.call.callKey)
|
|
2481
|
-
}
|
|
2482
|
-
} catch (e) {
|
|
2483
|
-
logger.debug({ e }, 'failed to decrypt call enc')
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
2101
|
await callOfferCache.set(call.id, call)
|
|
2487
|
-
} else if (status === 'waiting_room_request') {
|
|
2488
|
-
call.peerJid = infoChild.attrs.from || infoChild.attrs['peer-jid'] || infoChild.attrs['user-jid']
|
|
2489
2102
|
}
|
|
2490
2103
|
const existingCall = await callOfferCache.get(call.id)
|
|
2491
|
-
|
|
2104
|
+
// use existing call info to populate this event
|
|
2492
2105
|
if (existingCall) {
|
|
2493
2106
|
call.isVideo = existingCall.isVideo
|
|
2494
2107
|
call.isGroup = existingCall.isGroup
|
|
2495
2108
|
call.callerPn = call.callerPn || existingCall.callerPn
|
|
2496
2109
|
}
|
|
2497
|
-
|
|
2498
|
-
if (status === '
|
|
2499
|
-
const stateChild = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'peer_state')
|
|
2500
|
-
call.state = stateChild?.attrs?.state || infoChild.attrs?.state
|
|
2501
|
-
} else if (status === 'group_info') {
|
|
2502
|
-
call.payload = infoChild.attrs
|
|
2503
|
-
call.participants = parseGroupCallRoster(infoChild)
|
|
2504
|
-
} else if (status === 'video_state') {
|
|
2505
|
-
const vsChild = (0, WABinary_1.getBinaryNodeChild)(infoChild, 'video_state')
|
|
2506
|
-
const enabledRaw = vsChild?.attrs?.enabled ?? infoChild.attrs?.enabled
|
|
2507
|
-
call.enabled = enabledRaw === 'true' || enabledRaw === '1'
|
|
2508
|
-
} else if (status === 'enc_rekey') {
|
|
2509
|
-
const rekeyChild =
|
|
2510
|
-
(0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc-rekey') ||
|
|
2511
|
-
(0, WABinary_1.getBinaryNodeChild)(infoChild, 'enc_rekey')
|
|
2512
|
-
if (rekeyChild?.content && Buffer.isBuffer(rekeyChild.content)) {
|
|
2513
|
-
try {
|
|
2514
|
-
call.rekeyPayload = (0, Utils_1.decodeE2eRekeyPayload)(rekeyChild.content)
|
|
2515
|
-
} catch (e) {
|
|
2516
|
-
logger.debug({ e }, 'failed to decode enc-rekey payload in handleCall')
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
|
|
2521
|
-
if (
|
|
2522
|
-
status === 'reject' ||
|
|
2523
|
-
status === 'accept' ||
|
|
2524
|
-
status === 'timeout' ||
|
|
2525
|
-
status === 'terminate' ||
|
|
2526
|
-
status === 'reject_do_not_disturb' ||
|
|
2527
|
-
status === 'mic_permission_denied' ||
|
|
2528
|
-
status === 'camera_permission_denied' ||
|
|
2529
|
-
status === 'remote_busy' ||
|
|
2530
|
-
status === 'remote_offline'
|
|
2531
|
-
) {
|
|
2110
|
+
// delete data once call has ended
|
|
2111
|
+
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
2532
2112
|
await callOfferCache.del(call.id)
|
|
2533
2113
|
}
|
|
2534
2114
|
await normalizeCallEventJids(call, infoChild)
|
|
@@ -2539,32 +2119,15 @@ const makeMessagesRecvSocket = config => {
|
|
|
2539
2119
|
await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'))
|
|
2540
2120
|
}
|
|
2541
2121
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
const CALL_STATE_TAGS = new Set([
|
|
2547
|
-
'offer',
|
|
2548
|
-
'offer_notice',
|
|
2549
|
-
'terminate',
|
|
2550
|
-
'accept',
|
|
2551
|
-
'reject',
|
|
2552
|
-
'preaccept',
|
|
2553
|
-
'accept_ack',
|
|
2554
|
-
'enc-rekey',
|
|
2555
|
-
'enc_rekey',
|
|
2556
|
-
'peer_state',
|
|
2557
|
-
'group_info',
|
|
2558
|
-
'video_state',
|
|
2559
|
-
'video_state_ack',
|
|
2560
|
-
'flow_control',
|
|
2561
|
-
'mute_v2',
|
|
2562
|
-
'waiting_room_request'
|
|
2563
|
-
])
|
|
2122
|
+
// Some accounts receive call signalling as TOP-LEVEL stanzas (<offer>, <terminate>,
|
|
2123
|
+
// <mute_v2>, <transport>, … each with a call-id) instead of wrapped in <call>.
|
|
2124
|
+
// This additively handles those: emit a 'call' event for state stanzas and ack ALL
|
|
2125
|
+
// of them (otherwise WhatsApp keeps redelivering). The <call> path above is untouched.
|
|
2126
|
+
const CALL_STATE_TAGS = new Set(['offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept'])
|
|
2564
2127
|
const handleStandaloneCallStanza = async node => {
|
|
2565
2128
|
try {
|
|
2566
2129
|
if (!CALL_STATE_TAGS.has(node.tag)) {
|
|
2567
|
-
return
|
|
2130
|
+
return // media/relay signalling (transport, video, duration, mute_v2, lobby, …): ack only
|
|
2568
2131
|
}
|
|
2569
2132
|
const { attrs } = node
|
|
2570
2133
|
const status = (0, Utils_1.getCallStatusFromNode)(node)
|
|
@@ -2580,24 +2143,9 @@ const makeMessagesRecvSocket = config => {
|
|
|
2580
2143
|
status
|
|
2581
2144
|
}
|
|
2582
2145
|
if (status === 'offer') {
|
|
2583
|
-
|
|
2584
|
-
const audioNodes = (0, WABinary_1.getBinaryNodeChildren)(node, 'audio')
|
|
2585
|
-
const silenceNode = (0, WABinary_1.getBinaryNodeChild)(node, 'silence')
|
|
2586
|
-
call.isVideo = !!videoNode
|
|
2146
|
+
call.isVideo = !!(0, WABinary_1.getBinaryNodeChild)(node, 'video')
|
|
2587
2147
|
call.isGroup = attrs.type === 'group' || !!attrs['group-jid']
|
|
2588
2148
|
call.groupJid = attrs['group-jid']
|
|
2589
|
-
call.isLightweight = attrs.lightweight === '1'
|
|
2590
|
-
if (silenceNode?.attrs?.reason) {
|
|
2591
|
-
call.silenceReason = silenceNode.attrs.reason
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
if (audioNodes.length) {
|
|
2595
|
-
call.audioCodecs = audioNodes.map(n => ({ enc: n.attrs.enc, rate: n.attrs.rate ? +n.attrs.rate : undefined }))
|
|
2596
|
-
call.audioCodec = audioNodes[0].attrs.enc
|
|
2597
|
-
}
|
|
2598
|
-
if (videoNode?.attrs?.dec) {
|
|
2599
|
-
call.videoCodec = videoNode.attrs.dec
|
|
2600
|
-
}
|
|
2601
2149
|
if (callId) {
|
|
2602
2150
|
await callOfferCache.set(callId, call)
|
|
2603
2151
|
}
|
|
@@ -2608,45 +2156,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2608
2156
|
call.isGroup = existingCall.isGroup
|
|
2609
2157
|
call.callerPn = call.callerPn || existingCall.callerPn
|
|
2610
2158
|
}
|
|
2611
|
-
|
|
2612
|
-
if (status === 'peer_state') {
|
|
2613
|
-
const stateChild = (0, WABinary_1.getBinaryNodeChild)(node, 'peer_state')
|
|
2614
|
-
call.state = stateChild?.attrs?.state || attrs?.state
|
|
2615
|
-
} else if (status === 'group_info') {
|
|
2616
|
-
call.payload = attrs
|
|
2617
|
-
call.participants = parseGroupCallRoster(node)
|
|
2618
|
-
} else if (status === 'video_state') {
|
|
2619
|
-
const vsChild = (0, WABinary_1.getBinaryNodeChild)(node, 'video_state')
|
|
2620
|
-
const enabledRaw = vsChild?.attrs?.enabled ?? attrs?.enabled
|
|
2621
|
-
call.enabled = enabledRaw === 'true' || enabledRaw === '1'
|
|
2622
|
-
} else if (status === 'enc_rekey') {
|
|
2623
|
-
const rekeyChild =
|
|
2624
|
-
(0, WABinary_1.getBinaryNodeChild)(node, 'enc-rekey') || (0, WABinary_1.getBinaryNodeChild)(node, 'enc_rekey')
|
|
2625
|
-
if (rekeyChild?.content && Buffer.isBuffer(rekeyChild.content)) {
|
|
2626
|
-
try {
|
|
2627
|
-
call.rekeyPayload = (0, Utils_1.decodeE2eRekeyPayload)(rekeyChild.content)
|
|
2628
|
-
} catch (e) {
|
|
2629
|
-
logger.debug({ e }, 'failed to decode enc-rekey payload in standalone call stanza')
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2632
|
-
} else if (status === 'mute') {
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
call.muted = node.attrs['mute-state'] === '1'
|
|
2637
|
-
}
|
|
2638
|
-
if (
|
|
2639
|
-
callId &&
|
|
2640
|
-
(status === 'reject' ||
|
|
2641
|
-
status === 'accept' ||
|
|
2642
|
-
status === 'timeout' ||
|
|
2643
|
-
status === 'terminate' ||
|
|
2644
|
-
status === 'reject_do_not_disturb' ||
|
|
2645
|
-
status === 'mic_permission_denied' ||
|
|
2646
|
-
status === 'camera_permission_denied' ||
|
|
2647
|
-
status === 'remote_busy' ||
|
|
2648
|
-
status === 'remote_offline')
|
|
2649
|
-
) {
|
|
2159
|
+
if (callId && (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate')) {
|
|
2650
2160
|
await callOfferCache.del(callId)
|
|
2651
2161
|
}
|
|
2652
2162
|
await normalizeCallEventJids(call, node)
|
|
@@ -2659,26 +2169,26 @@ const makeMessagesRecvSocket = config => {
|
|
|
2659
2169
|
}
|
|
2660
2170
|
const handleBadAck = async ({ attrs }) => {
|
|
2661
2171
|
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2172
|
+
// WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
|
|
2173
|
+
// // current hypothesis is that if pash is sent in the ack
|
|
2174
|
+
// // it means -- the message hasn't reached all devices yet
|
|
2175
|
+
// // we'll retry sending the message here
|
|
2176
|
+
// if(attrs.phash) {
|
|
2177
|
+
// logger.info({ attrs }, 'received phash in ack, resending message...')
|
|
2178
|
+
// const msg = await getMessage(key)
|
|
2179
|
+
// if(msg) {
|
|
2180
|
+
// await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
|
|
2181
|
+
// } else {
|
|
2182
|
+
// logger.warn({ attrs }, 'could not send message again, as it was not found')
|
|
2183
|
+
// }
|
|
2184
|
+
// }
|
|
2185
|
+
// error in acknowledgement,
|
|
2186
|
+
// device could not display the message
|
|
2677
2187
|
if (attrs.error) {
|
|
2678
2188
|
if (attrs.error === Utils_1.SERVER_ERROR_CODES.MissingTcToken) {
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2189
|
+
// 463 = account restricted + no tctoken for this contact.
|
|
2190
|
+
// WA Web prevents this client-side (disables compose bar).
|
|
2191
|
+
// No retry — retrying worsens the restriction by counting as another "reach out" to an unknown contact.
|
|
2682
2192
|
logger.warn(
|
|
2683
2193
|
{ msgId: attrs.id, from: attrs.from },
|
|
2684
2194
|
'error 463: account restricted or missing tctoken for contact'
|
|
@@ -2700,54 +2210,23 @@ const makeMessagesRecvSocket = config => {
|
|
|
2700
2210
|
}
|
|
2701
2211
|
}
|
|
2702
2212
|
])
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2213
|
+
// resend the message with device_fanout=false, use at your own risk
|
|
2214
|
+
// if (attrs.error === '475') {
|
|
2215
|
+
// const msg = await getMessage(key)
|
|
2216
|
+
// if (msg) {
|
|
2217
|
+
// await relayMessage(key.remoteJid!, msg, {
|
|
2218
|
+
// messageId: key.id!,
|
|
2219
|
+
// useUserDevicesCache: false,
|
|
2220
|
+
// additionalAttributes: {
|
|
2221
|
+
// device_fanout: 'false'
|
|
2222
|
+
// }
|
|
2223
|
+
// })
|
|
2224
|
+
// }
|
|
2225
|
+
// }
|
|
2716
2226
|
}
|
|
2717
2227
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
if (!from) return
|
|
2721
|
-
const isTyping = state === 'typing'
|
|
2722
|
-
ev.emit('presence.update', [
|
|
2723
|
-
{
|
|
2724
|
-
id: from,
|
|
2725
|
-
presences: {
|
|
2726
|
-
[from]: isTyping ? 'composing' : 'paused'
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
])
|
|
2730
|
-
}
|
|
2731
|
-
const handlePresence = async node => {
|
|
2732
|
-
const { from, type } = node.attrs
|
|
2733
|
-
if (!from || !type) return
|
|
2734
|
-
const presenceMap = {
|
|
2735
|
-
available: 'available',
|
|
2736
|
-
unavailable: 'unavailable'
|
|
2737
|
-
}
|
|
2738
|
-
const presence = presenceMap[type] || type
|
|
2739
|
-
ev.emit('presence.update', [
|
|
2740
|
-
{
|
|
2741
|
-
id: from,
|
|
2742
|
-
presences: {
|
|
2743
|
-
[from]: presence
|
|
2744
|
-
},
|
|
2745
|
-
lastSeen: node.attrs.t ? Number(node.attrs.t) * 1000 : undefined
|
|
2746
|
-
}
|
|
2747
|
-
])
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2228
|
+
/// processes a node with the given function
|
|
2229
|
+
/// and adds the task to the existing buffer if we're buffering events
|
|
2751
2230
|
const processNodeWithBuffer = async (node, identifier, exec) => {
|
|
2752
2231
|
ev.buffer()
|
|
2753
2232
|
await execTask()
|
|
@@ -2786,7 +2265,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2786
2265
|
const setNodeLoggerListener = () => {
|
|
2787
2266
|
return latestNodeInMemory
|
|
2788
2267
|
}
|
|
2789
|
-
|
|
2268
|
+
// recv a message
|
|
2790
2269
|
ws.on('CB:message', async node => {
|
|
2791
2270
|
nodelogger(node)
|
|
2792
2271
|
await processNode('message', node, 'processing message', handleMessage)
|
|
@@ -2795,35 +2274,10 @@ const makeMessagesRecvSocket = config => {
|
|
|
2795
2274
|
nodelogger(node)
|
|
2796
2275
|
await processNode('call', node, 'handling call', handleCall)
|
|
2797
2276
|
})
|
|
2798
|
-
|
|
2277
|
+
// additive: top-level call-signalling stanzas (some accounts send these instead of <call>)
|
|
2799
2278
|
for (const callTag of [
|
|
2800
|
-
'offer',
|
|
2801
|
-
'
|
|
2802
|
-
'terminate',
|
|
2803
|
-
'accept',
|
|
2804
|
-
'reject',
|
|
2805
|
-
'preaccept',
|
|
2806
|
-
'accept_ack',
|
|
2807
|
-
'enc-rekey',
|
|
2808
|
-
'enc_rekey',
|
|
2809
|
-
'peer_state',
|
|
2810
|
-
'group_info',
|
|
2811
|
-
'video_state',
|
|
2812
|
-
'video_state_ack',
|
|
2813
|
-
'flow_control',
|
|
2814
|
-
'transport',
|
|
2815
|
-
'video',
|
|
2816
|
-
'duration',
|
|
2817
|
-
'mute_v2',
|
|
2818
|
-
'lobby',
|
|
2819
|
-
'heartbeat',
|
|
2820
|
-
'relaylatency',
|
|
2821
|
-
'link_query',
|
|
2822
|
-
'waiting_room_request',
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
'group_update'
|
|
2279
|
+
'offer', 'offer_notice', 'terminate', 'accept', 'reject', 'preaccept',
|
|
2280
|
+
'transport', 'video', 'duration', 'mute_v2', 'lobby', 'heartbeat', 'relaylatency', 'link_query'
|
|
2827
2281
|
]) {
|
|
2828
2282
|
ws.on('CB:' + callTag, node => {
|
|
2829
2283
|
nodelogger(node)
|
|
@@ -2838,18 +2292,6 @@ const makeMessagesRecvSocket = config => {
|
|
|
2838
2292
|
nodelogger(node)
|
|
2839
2293
|
await processNode('notification', node, 'handling notification', handleNotification)
|
|
2840
2294
|
})
|
|
2841
|
-
ws.on('CB:status', async node => {
|
|
2842
|
-
nodelogger(node)
|
|
2843
|
-
await handleNewsletterStatus(node).catch(error => onUnexpectedError(error, 'handling newsletter status'))
|
|
2844
|
-
})
|
|
2845
|
-
ws.on('CB:chatstate', async node => {
|
|
2846
|
-
nodelogger(node)
|
|
2847
|
-
await handleChatstate(node).catch(error => onUnexpectedError(error, 'handling chatstate'))
|
|
2848
|
-
})
|
|
2849
|
-
ws.on('CB:presence', async node => {
|
|
2850
|
-
nodelogger(node)
|
|
2851
|
-
await handlePresence(node).catch(error => onUnexpectedError(error, 'handling presence'))
|
|
2852
|
-
})
|
|
2853
2295
|
ws.on('CB:ack,class:message', node => {
|
|
2854
2296
|
nodelogger(node)
|
|
2855
2297
|
handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))
|
|
@@ -2879,7 +2321,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2879
2321
|
return
|
|
2880
2322
|
}
|
|
2881
2323
|
nodelogger(call)
|
|
2882
|
-
|
|
2324
|
+
// missed call + group call notification message generation
|
|
2883
2325
|
if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
|
|
2884
2326
|
const msg = {
|
|
2885
2327
|
key: {
|
|
@@ -2915,7 +2357,7 @@ const makeMessagesRecvSocket = config => {
|
|
|
2915
2357
|
sendActiveReceipts = isOnline
|
|
2916
2358
|
logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`)
|
|
2917
2359
|
}
|
|
2918
|
-
|
|
2360
|
+
// Daily cleanup of expired tctokens (mirrors WA Web's CLEAN_TC_TOKENS task)
|
|
2919
2361
|
if (isOnline) {
|
|
2920
2362
|
const now = Date.now()
|
|
2921
2363
|
const DAY_MS = 24 * 60 * 60 * 1000
|