@nexustechpro/baileys 1.1.5 → 1.1.9
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/WAProto/GenerateStatics.sh +3 -0
- package/lib/Defaults/baileys-version.json +3 -0
- package/lib/Defaults/index.js +150 -128
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/libsignal.js +19 -1
- package/lib/Socket/Client/websocket.js +278 -25
- package/lib/Socket/index.js +8 -4
- package/lib/Socket/messages-recv.js +18 -7
- package/lib/Socket/messages-send.js +60 -9
- package/lib/Socket/nexus-handler.js +1 -1
- package/lib/Socket/registration.js +197 -0
- package/lib/Socket/socket.js +929 -196
- package/lib/Utils/decode-wa-message.js +6 -0
- package/lib/Utils/generics.js +1 -1
- package/lib/Utils/link-preview.js +11 -7
- package/lib/index.js +3 -4
- package/package.json +14 -7
|
@@ -2,57 +2,310 @@ import WebSocket from 'ws'
|
|
|
2
2
|
import { DEFAULT_ORIGIN } from '../../Defaults/index.js'
|
|
3
3
|
import { AbstractSocketClient } from './types.js'
|
|
4
4
|
|
|
5
|
+
// ==================== CONSTANTS ====================
|
|
6
|
+
const CONSTANTS = {
|
|
7
|
+
MIN_SEND_INTERVAL_MS: 50,
|
|
8
|
+
MAX_RECONNECT_ATTEMPTS: 5,
|
|
9
|
+
INITIAL_RECONNECT_DELAY: 1000,
|
|
10
|
+
MAX_RECONNECT_DELAY: 30000,
|
|
11
|
+
RECONNECT_BACKOFF_MULTIPLIER: 2
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ==================== WEBSOCKET CLIENT ====================
|
|
5
15
|
export class WebSocketClient extends AbstractSocketClient {
|
|
6
|
-
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments)
|
|
18
|
+
|
|
19
|
+
// Core socket
|
|
20
|
+
this.socket = null
|
|
21
|
+
|
|
22
|
+
// Message queue
|
|
23
|
+
this._queue = []
|
|
24
|
+
this._isDispatching = false
|
|
25
|
+
this._lastDispatch = 0
|
|
26
|
+
this._minSendIntervalMs = CONSTANTS.MIN_SEND_INTERVAL_MS
|
|
27
|
+
|
|
28
|
+
// Reconnection state
|
|
29
|
+
this._reconnectTimeout = null
|
|
30
|
+
this._reconnectAttempts = 0
|
|
31
|
+
this._maxReconnectAttempts = CONSTANTS.MAX_RECONNECT_ATTEMPTS
|
|
32
|
+
this._reconnectDelay = CONSTANTS.INITIAL_RECONNECT_DELAY
|
|
33
|
+
this._shouldReconnect = true
|
|
34
|
+
this._isManualClose = false
|
|
35
|
+
this._isReconnecting = false
|
|
36
|
+
}
|
|
7
37
|
|
|
38
|
+
// ==================== LOGGER ====================
|
|
39
|
+
get logger() {
|
|
40
|
+
return this.config?.logger || console
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ==================== CONNECTION STATE ====================
|
|
8
44
|
get isOpen() {
|
|
9
45
|
return this.socket?.readyState === WebSocket.OPEN
|
|
10
46
|
}
|
|
11
47
|
|
|
12
48
|
get isClosed() {
|
|
13
|
-
return this.socket
|
|
49
|
+
return !this.socket || this.socket.readyState === WebSocket.CLOSED
|
|
14
50
|
}
|
|
15
51
|
|
|
16
52
|
get isClosing() {
|
|
17
|
-
return this.socket
|
|
53
|
+
return !this.socket || this.socket.readyState === WebSocket.CLOSING
|
|
18
54
|
}
|
|
19
55
|
|
|
20
56
|
get isConnecting() {
|
|
21
57
|
return this.socket?.readyState === WebSocket.CONNECTING
|
|
22
58
|
}
|
|
23
59
|
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
// ==================== CONNECTION MANAGEMENT ====================
|
|
61
|
+
async connect() {
|
|
62
|
+
if (this.socket && !this.isClosed) {
|
|
63
|
+
this.logger.debug({ state: this.socket.readyState }, 'already connected or connecting')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.logger.debug('establishing websocket connection')
|
|
69
|
+
|
|
70
|
+
this.socket = new WebSocket(this.url, {
|
|
71
|
+
origin: DEFAULT_ORIGIN,
|
|
72
|
+
headers: this.config.options?.headers,
|
|
73
|
+
handshakeTimeout: this.config.connectTimeoutMs,
|
|
74
|
+
timeout: this.config.connectTimeoutMs,
|
|
75
|
+
agent: this.config.agent
|
|
76
|
+
})
|
|
26
77
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
handshakeTimeout: this.config.connectTimeoutMs,
|
|
31
|
-
timeout: this.config.connectTimeoutMs,
|
|
32
|
-
agent: this.config.agent
|
|
33
|
-
})
|
|
78
|
+
if (!this.socket) {
|
|
79
|
+
throw new Error('WebSocket creation failed')
|
|
80
|
+
}
|
|
34
81
|
|
|
35
|
-
|
|
82
|
+
this.socket.setMaxListeners(0)
|
|
36
83
|
|
|
37
|
-
|
|
38
|
-
|
|
84
|
+
// Forward all WebSocket events
|
|
85
|
+
const events = ['error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
|
|
86
|
+
events.forEach(e => {
|
|
87
|
+
this.socket?.on(e, (...args) => this.emit(e, ...args))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Handle close with auto-reconnect
|
|
91
|
+
this.socket.on('close', (...args) => {
|
|
92
|
+
this.emit('close', ...args)
|
|
93
|
+
if (this._shouldReconnect && !this._isManualClose) {
|
|
94
|
+
this._attemptReconnect()
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Handle successful connection
|
|
99
|
+
this.socket.on('open', () => {
|
|
100
|
+
this.logger.info('websocket connection established')
|
|
101
|
+
this._reconnectAttempts = 0
|
|
102
|
+
this._reconnectDelay = CONSTANTS.INITIAL_RECONNECT_DELAY
|
|
103
|
+
this._isReconnecting = false
|
|
104
|
+
|
|
105
|
+
// Process any queued messages
|
|
106
|
+
if (this._queue.length > 0) {
|
|
107
|
+
this.logger.debug({ queueLength: this._queue.length }, 'processing queued messages')
|
|
108
|
+
this._dispatch()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.error({ error: error.message }, 'websocket connection failed')
|
|
114
|
+
this.socket = null
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
39
117
|
}
|
|
40
118
|
|
|
41
119
|
async close() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
120
|
+
this.logger.debug('closing websocket connection (manual)')
|
|
121
|
+
|
|
122
|
+
this._isManualClose = true
|
|
123
|
+
this._shouldReconnect = false
|
|
124
|
+
this._isReconnecting = false
|
|
125
|
+
|
|
126
|
+
if (this._reconnectTimeout) {
|
|
127
|
+
clearTimeout(this._reconnectTimeout)
|
|
128
|
+
this._reconnectTimeout = null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.socket?.close?.()
|
|
46
132
|
this.socket = null
|
|
133
|
+
this._queue = []
|
|
47
134
|
}
|
|
48
135
|
|
|
136
|
+
async restart() {
|
|
137
|
+
this.logger.info('restarting websocket connection')
|
|
138
|
+
|
|
139
|
+
this._isManualClose = true
|
|
140
|
+
this._isReconnecting = false
|
|
141
|
+
|
|
142
|
+
// Force close existing connection
|
|
143
|
+
if (this.socket) {
|
|
144
|
+
await new Promise(resolve => {
|
|
145
|
+
this.socket.once('close', resolve)
|
|
146
|
+
this.socket.terminate()
|
|
147
|
+
})
|
|
148
|
+
this.socket = null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Clear queue and reset state
|
|
152
|
+
this._queue = []
|
|
153
|
+
this._reconnectDelay = CONSTANTS.INITIAL_RECONNECT_DELAY
|
|
154
|
+
this._isManualClose = false
|
|
155
|
+
this._shouldReconnect = true
|
|
156
|
+
|
|
157
|
+
// Reconnect
|
|
158
|
+
await this.connect()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ==================== RECONNECTION LOGIC ====================
|
|
162
|
+
_attemptReconnect() {
|
|
163
|
+
if (this._isReconnecting) {
|
|
164
|
+
this.logger.trace('reconnection already in progress')
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this._reconnectAttempts >= this._maxReconnectAttempts) {
|
|
169
|
+
this.logger.error(
|
|
170
|
+
{ attempts: this._reconnectAttempts, max: this._maxReconnectAttempts },
|
|
171
|
+
'max reconnect attempts reached'
|
|
172
|
+
)
|
|
173
|
+
this.emit('reconnect-failed')
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (this._reconnectTimeout) {
|
|
178
|
+
clearTimeout(this._reconnectTimeout)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this._isReconnecting = true
|
|
182
|
+
this._reconnectAttempts++
|
|
183
|
+
|
|
184
|
+
this.logger.info(
|
|
185
|
+
{
|
|
186
|
+
attempt: this._reconnectAttempts,
|
|
187
|
+
maxAttempts: this._maxReconnectAttempts,
|
|
188
|
+
delay: this._reconnectDelay
|
|
189
|
+
},
|
|
190
|
+
'attempting websocket reconnection'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
this._reconnectTimeout = setTimeout(async () => {
|
|
194
|
+
try {
|
|
195
|
+
await this.connect()
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.logger.warn({ error: error.message }, 'reconnection attempt failed')
|
|
198
|
+
this._isReconnecting = false
|
|
199
|
+
|
|
200
|
+
if (this._reconnectAttempts < this._maxReconnectAttempts) {
|
|
201
|
+
this._attemptReconnect()
|
|
202
|
+
} else {
|
|
203
|
+
this.emit('reconnect-failed')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, this._reconnectDelay)
|
|
207
|
+
|
|
208
|
+
// Exponential backoff with cap
|
|
209
|
+
this._reconnectDelay = Math.min(
|
|
210
|
+
this._reconnectDelay * CONSTANTS.RECONNECT_BACKOFF_MULTIPLIER,
|
|
211
|
+
CONSTANTS.MAX_RECONNECT_DELAY
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ==================== MESSAGE SENDING ====================
|
|
49
216
|
send(str, cb) {
|
|
50
|
-
|
|
51
|
-
|
|
217
|
+
const doSend = () => {
|
|
218
|
+
// Handle closed/closing socket
|
|
219
|
+
if (this.isClosed || this.isClosing) {
|
|
220
|
+
this.logger.warn('socket closed, attempting reconnection')
|
|
221
|
+
|
|
222
|
+
this._isManualClose = false
|
|
223
|
+
this._shouldReconnect = true
|
|
224
|
+
this._attemptReconnect()
|
|
225
|
+
this._queue.unshift(doSend) // Re-queue at front
|
|
226
|
+
|
|
227
|
+
cb?.(new Error('Socket closed, reconnecting...'))
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if socket is ready
|
|
232
|
+
if (!this.socket || !this.isOpen) {
|
|
233
|
+
cb?.(new Error('Socket not open'))
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Send the message
|
|
238
|
+
try {
|
|
239
|
+
this.socket.send(str, cb)
|
|
240
|
+
return true
|
|
241
|
+
} catch (error) {
|
|
242
|
+
this.logger.error({ error: error.message }, 'failed to send message')
|
|
243
|
+
cb?.(error)
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._queue.push(doSend)
|
|
249
|
+
this._dispatch()
|
|
250
|
+
return true
|
|
52
251
|
}
|
|
53
252
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
253
|
+
// ==================== MESSAGE QUEUE DISPATCH ====================
|
|
254
|
+
_dispatch() {
|
|
255
|
+
// Don't dispatch if already dispatching or socket not ready
|
|
256
|
+
if (this._isDispatching || (!this.isOpen && !this.isConnecting)) {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const now = Date.now()
|
|
261
|
+
const elapsed = now - this._lastDispatch
|
|
262
|
+
|
|
263
|
+
// Check if enough time has passed and queue has items
|
|
264
|
+
if (this._queue.length && elapsed >= this._minSendIntervalMs) {
|
|
265
|
+
this._isDispatching = true
|
|
266
|
+
|
|
267
|
+
const sendFn = this._queue.shift()
|
|
268
|
+
sendFn?.()
|
|
269
|
+
|
|
270
|
+
this._lastDispatch = Date.now()
|
|
271
|
+
this._isDispatching = false
|
|
272
|
+
|
|
273
|
+
// Schedule next dispatch if queue not empty
|
|
274
|
+
if (this._queue.length) {
|
|
275
|
+
const nextDelay = Math.max(0, this._minSendIntervalMs - (Date.now() - this._lastDispatch))
|
|
276
|
+
setTimeout(() => this._dispatch(), nextDelay)
|
|
277
|
+
}
|
|
278
|
+
} else if (this._queue.length) {
|
|
279
|
+
// Schedule dispatch after required interval
|
|
280
|
+
const delay = Math.max(0, this._minSendIntervalMs - elapsed)
|
|
281
|
+
setTimeout(() => this._dispatch(), delay)
|
|
282
|
+
}
|
|
57
283
|
}
|
|
58
|
-
|
|
284
|
+
|
|
285
|
+
// ==================== PUBLIC CONTROL METHODS ====================
|
|
286
|
+
disableAutoReconnect() {
|
|
287
|
+
this.logger.info('auto-reconnect disabled')
|
|
288
|
+
this._shouldReconnect = false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
enableAutoReconnect() {
|
|
292
|
+
this.logger.info('auto-reconnect enabled')
|
|
293
|
+
this._shouldReconnect = true
|
|
294
|
+
this._isManualClose = false
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Getters for external monitoring
|
|
298
|
+
get reconnectAttempts() {
|
|
299
|
+
return this._reconnectAttempts
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
get queueLength() {
|
|
303
|
+
return this._queue.length
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
get isReconnecting() {
|
|
307
|
+
return this._isReconnecting
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default WebSocketClient
|
package/lib/Socket/index.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { makeRegistrationSocket } from './registration.js';
|
|
3
3
|
import NexusHandler from './nexus-handler.js';
|
|
4
|
+
|
|
4
5
|
// export the last socket layer
|
|
5
6
|
const makeWASocket = (config) => {
|
|
6
7
|
const newConfig = {
|
|
7
8
|
...DEFAULT_CONNECTION_CONFIG,
|
|
8
9
|
...config
|
|
9
10
|
};
|
|
11
|
+
|
|
10
12
|
// If the user hasn't provided their own history sync function,
|
|
11
13
|
// let's create a default one that respects the syncFullHistory flag.
|
|
12
14
|
if (config.shouldSyncHistoryMessage === undefined) {
|
|
13
15
|
newConfig.shouldSyncHistoryMessage = () => !!newConfig.syncFullHistory;
|
|
14
16
|
}
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
return makeRegistrationSocket(newConfig);
|
|
16
19
|
};
|
|
20
|
+
|
|
17
21
|
export { NexusHandler };
|
|
18
|
-
export
|
|
19
|
-
|
|
22
|
+
export { makeWASocket };
|
|
23
|
+
export default makeWASocket;
|
|
@@ -46,12 +46,12 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
46
46
|
|
|
47
47
|
const handleMexNewsletterNotification = async (node) => {
|
|
48
48
|
const mexNode = getBinaryNodeChild(node, "mex")
|
|
49
|
-
if (!mexNode?.content) { logger.warn({ node }, "Invalid mex newsletter notification")
|
|
49
|
+
if (!mexNode?.content) { /*logger.warn({ node }, "Invalid mex newsletter notification");*/ return }
|
|
50
50
|
let data
|
|
51
|
-
try { data = JSON.parse(mexNode.content.toString()) } catch (error) { logger.error({ err: error, node }, "Failed to parse mex newsletter notification")
|
|
51
|
+
try { data = JSON.parse(mexNode.content.toString()) } catch (error) { /*logger.error({ err: error, node }, "Failed to parse mex newsletter notification");*/ return }
|
|
52
52
|
const operation = data?.operation
|
|
53
53
|
const updates = data?.updates
|
|
54
|
-
if (!updates || !operation) { logger.warn({ data }, "Invalid mex newsletter notification content")
|
|
54
|
+
if (!updates || !operation) { /*logger.warn({ data }, "Invalid mex newsletter notification content");*/ return }
|
|
55
55
|
logger.info({ operation, updates }, "got mex newsletter notification")
|
|
56
56
|
switch (operation) {
|
|
57
57
|
case "NotificationNewsletterUpdate":
|
|
@@ -459,9 +459,9 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
459
459
|
if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason }, "recreating session for outgoing retry"); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
460
460
|
} catch (error) { logger.warn({ error, participant }, "failed to check session recreation for outgoing retry") }
|
|
461
461
|
}
|
|
462
|
-
await assertSessions([participant],
|
|
462
|
+
await assertSessions([participant], false);
|
|
463
463
|
if (isJidGroup(remoteJid)) await authState.keys.set({ "sender-key-memory": { [remoteJid]: null } })
|
|
464
|
-
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, "
|
|
464
|
+
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, "preparing retry recp")
|
|
465
465
|
for (const [i, msg] of msgs.entries()) {
|
|
466
466
|
if (!ids[i]) continue
|
|
467
467
|
if (msg && (await willSendMessageAgain(ids[i], participant))) {
|
|
@@ -488,7 +488,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
488
488
|
ids.push(...items.map((i) => i.attrs.id))
|
|
489
489
|
}
|
|
490
490
|
try {
|
|
491
|
-
await Promise.all([
|
|
491
|
+
await Promise.all([processingMutex.mutex(async () => {
|
|
492
492
|
const status = getStatusFromReceiptType(attrs.type)
|
|
493
493
|
if (typeof status !== "undefined" && (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
|
|
494
494
|
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
|
|
@@ -509,7 +509,18 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
509
509
|
await sendMessagesAgain(key, ids, retryNode)
|
|
510
510
|
} catch (error) { logger.error({ key, ids, trace: error instanceof Error ? error.stack : "Unknown error" }, "error in sending message again") }
|
|
511
511
|
} else logger.info({ attrs, key }, "recv retry for not fromMe message")
|
|
512
|
-
} else
|
|
512
|
+
} else {
|
|
513
|
+
logger.info({ attrs, key, participant: key.participant }, "retry limit exhausted - clearing broken session")
|
|
514
|
+
try {
|
|
515
|
+
await signalRepository.deleteSession([key.participant])
|
|
516
|
+
logger.debug({ participant: key.participant }, "deleted stale session for retry-exhausted participant")
|
|
517
|
+
const retryKey = `${ids[0]}:${key.participant}`
|
|
518
|
+
await msgRetryCache.del(retryKey)
|
|
519
|
+
logger.debug({ retryKey }, "cleared retry count cache")
|
|
520
|
+
} catch (err) {
|
|
521
|
+
logger.error({ err, participant: key.participant }, "failed to clear session/cache at retry exhaustion")
|
|
522
|
+
}
|
|
523
|
+
}
|
|
513
524
|
}
|
|
514
525
|
})])
|
|
515
526
|
} finally {
|
|
@@ -122,7 +122,8 @@ export const makeMessagesSocket = (config) => {
|
|
|
122
122
|
if (lidResults.length > 0) { logger.trace('Storing LID maps from device call'); await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id }))) }
|
|
123
123
|
try {
|
|
124
124
|
const lids = lidResults.map(a => a.lid)
|
|
125
|
-
|
|
125
|
+
// Re-fetch sessions during device lookup to ensure fresh state
|
|
126
|
+
if (lids.length) await assertSessions(lids, false)
|
|
126
127
|
} catch (e) {
|
|
127
128
|
logger.warn({ error: e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
|
|
128
129
|
}
|
|
@@ -200,7 +201,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
200
201
|
for (const node of tokenNodes) {
|
|
201
202
|
const jid = node.attrs.jid
|
|
202
203
|
const token = node.content
|
|
203
|
-
if (jid && token) tokens[jid] = { token }
|
|
204
|
+
if (jid && token) tokens[jid] = { token, timestamp: Number(unixTimestampSeconds()) }
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
return tokens
|
|
@@ -361,8 +362,25 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
361
362
|
if (groupData) { participantsList.push(...groupData.participants.map(p => p.id)); groupAddressingMode = groupData?.addressingMode || groupAddressingMode }
|
|
362
363
|
additionalAttributes = { ...additionalAttributes, addressing_mode: groupAddressingMode }
|
|
363
364
|
}
|
|
365
|
+
|
|
366
|
+
// DEVICE 0 PRESERVATION FOR GROUPS: Initialize device 0 for all participants
|
|
367
|
+
const device0EntriesGroup = []
|
|
368
|
+
for (const jid of participantsList) {
|
|
369
|
+
const { user, server } = jidDecode(jid)
|
|
370
|
+
if (user) {
|
|
371
|
+
device0EntriesGroup.push({ user, device: 0, jid: jidEncode(user, server, 0) })
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
364
375
|
const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
|
|
365
|
-
devices
|
|
376
|
+
// Combine device 0 entries with fetched devices, avoiding duplicates
|
|
377
|
+
const deviceMap = new Map()
|
|
378
|
+
for (const d of device0EntriesGroup) deviceMap.set(`${d.user}:${d.device}`, d)
|
|
379
|
+
for (const d of additionalDevices) {
|
|
380
|
+
const key = `${d.user}:${d.device}`
|
|
381
|
+
if (!deviceMap.has(key)) deviceMap.set(key, d)
|
|
382
|
+
}
|
|
383
|
+
devices.push(...Array.from(deviceMap.values()))
|
|
366
384
|
}
|
|
367
385
|
|
|
368
386
|
if (groupData?.ephemeralDuration > 0) additionalAttributes = { ...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
|
|
@@ -383,6 +401,7 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
383
401
|
if ((!hasKey || !!participant) && !isHostedLidUser(deviceJid) && !isHostedPnUser(deviceJid) && device.device !== 99) { senderKeyRecipients.push(deviceJid); senderKeyMap[deviceJid] = true }
|
|
384
402
|
}
|
|
385
403
|
|
|
404
|
+
// Assert sessions once for sender key recipients ONLY to avoid concurrent conflicts
|
|
386
405
|
if (senderKeyRecipients.length) {
|
|
387
406
|
logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending sender key')
|
|
388
407
|
const senderKeyMsg = { senderKeyDistributionMessage: { axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage, groupId: destinationJid } }
|
|
@@ -416,10 +435,20 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
416
435
|
}
|
|
417
436
|
|
|
418
437
|
if (additionalAttributes?.category !== 'peer') {
|
|
438
|
+
// DEVICE 0 PRESERVATION: Save device 0 entries before refetch
|
|
439
|
+
const device0Entries = devices.filter(d => d.device === 0)
|
|
440
|
+
const senderOwnUser = device0Entries.find(d => d.user !== user)?.user
|
|
419
441
|
devices.length = 0
|
|
420
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
|
|
421
444
|
const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
|
|
422
|
-
devices.push(...sessionDevices)
|
|
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
|
+
}
|
|
423
452
|
}
|
|
424
453
|
}
|
|
425
454
|
|
|
@@ -471,10 +500,24 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
471
500
|
|
|
472
501
|
if (shouldIncludeDeviceIdentity) { stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) }); logger.debug({ jid }, 'adding device identity') }
|
|
473
502
|
if (additionalNodes?.length > 0 && !additionalAlready) stanza.content.push(...additionalNodes)
|
|
474
|
-
// Add TCToken support
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
503
|
+
// Add TCToken support with expiration validation
|
|
504
|
+
if (!isGroup && !isRetryResend && !isStatus) {
|
|
505
|
+
const contactTcTokenData = await authState.keys.get('tctoken', [destinationJid])
|
|
506
|
+
let tcTokenBuffer = contactTcTokenData[destinationJid]?.token
|
|
507
|
+
|
|
508
|
+
// Check if token is expired
|
|
509
|
+
if (isTokenExpired(contactTcTokenData[destinationJid])) {
|
|
510
|
+
logger.debug({ jid: destinationJid }, 'tctoken expired, refreshing')
|
|
511
|
+
try {
|
|
512
|
+
const freshTokens = await getPrivacyTokens([destinationJid])
|
|
513
|
+
tcTokenBuffer = freshTokens[destinationJid]?.token
|
|
514
|
+
} catch (err) {
|
|
515
|
+
logger.warn({ jid: destinationJid, err }, 'failed to refresh expired tctoken')
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (tcTokenBuffer) stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
|
|
520
|
+
}
|
|
478
521
|
|
|
479
522
|
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
|
|
480
523
|
await sendNode(stanza)
|
|
@@ -484,7 +527,15 @@ const relayMessage = async (jid, message, { messageId: msgId, participant, addit
|
|
|
484
527
|
return {key: {remoteJid: jid, fromMe: true, id: finalMsgId, participant: isGroup ? authState.creds.me.id : undefined}, messageId: finalMsgId}
|
|
485
528
|
}
|
|
486
529
|
|
|
487
|
-
const
|
|
530
|
+
const TOKEN_EXPIRY_TTL = 24 * 60 * 60 // 24 hours in seconds
|
|
531
|
+
|
|
532
|
+
const isTokenExpired = (tokenData) => {
|
|
533
|
+
if (!tokenData || !tokenData.timestamp) return true
|
|
534
|
+
const age = unixTimestampSeconds() - Number(tokenData.timestamp)
|
|
535
|
+
return age > TOKEN_EXPIRY_TTL
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const getPrivacyTokens = async (jids) => {
|
|
488
539
|
const t = unixTimestampSeconds().toString()
|
|
489
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' } })) }] })
|
|
490
541
|
const tokens = parseTCTokens(result)
|
|
@@ -277,7 +277,7 @@ class NexusHandler {
|
|
|
277
277
|
eventMessage: {
|
|
278
278
|
contextInfo: {
|
|
279
279
|
mentionedJid: [jid], participant: jid, remoteJid: 'status@broadcast',
|
|
280
|
-
forwardedNewsletterMessageInfo: { newsletterName: 'Nexus Events', newsletterJid: '
|
|
280
|
+
forwardedNewsletterMessageInfo: { newsletterName: 'Nexus Events', newsletterJid: '120363422827915475@newsletter', serverMessageId: 1 }
|
|
281
281
|
},
|
|
282
282
|
isCanceled: e.isCanceled || false, name: e.name, description: e.description,
|
|
283
283
|
location: e.location || { degreesLatitude: 0, degreesLongitude: 0, name: 'Location' }, joinLink: e.joinLink || '',
|